昨今では様々なライブラリが充実しているおかげで、バイナリファイルを読み書きする機会は減っています。しかし、時にはバイナリファイルの仕様とにらめっこしながらバイナリ操作をするという機会もあります。そこで、今回は、拡張性が高いPNG画像を例にして、Go言語でバイナリ操作する方法を確認しましょう。
テキストファイルとバイナリファイルとは?
『テキストファイル(英語: text file)』とは文字データが記述されたファイルです。これに対して『バイナリファイル(英語: binary file)』とは、文字データの範囲を超えたデータが書き込まれたファイルです。
テキストファイルは、テキストエディタで開くと内容を確認することができますが、バイナリファイル(例えば画像ファイルなど)をエディタで開くと多くの文字が文字化けしたような状態で表示されます。
なお、本連載の8回目でバイナリビューワーの作り方を紹介しましたので、これを利用してPNGファイルのバイナリデータを詳しく確認できるでしょう。
PNG画像について
さて、今回は、画像ファイルのPNGファイルを読み込み、画像ファイルの幅や高さ、色深度を調べて表示するプログラムを作ってみましょう。
実際に、PNGファイルを読むプログラムを作る前に、PNGファイルについて確認してみましょう。PNG画像は、比較的新しい画像形式で、そのファイル形式は比較的単純なものとなっています。
次の図のように、大まかに言うと、PNGシグネチャに続いてチャンクと呼ばれるデータブロックが連続して配置されるというスッキリした仕組みになっています。そして各チャンクは、チャンクサイズ、チャンクタイプ、実際のチャンクデータ、データの破損チェック用値CRC32となっています。これは、バイナリファイルのデータ形式にはよくある構造です。
そして、PNGファイルで定義されているのは、画像のヘッダ情報を表すIHDR、パレット情報を表すPLTE、イメージデータを表すIDAT、イメージ終端を表すIENDなどです。
つまり、画像に関する情報を調べるだけならば、IHDRチャンクを調べて読めば良いということになります。
Go言語でバイナリ操作をする関数を定義
それでは、さっそくGoのプログラムを作ってみましょう。PNGファイルを読み込むのにあたって、必要となるバイナリデータを手軽に操作するための関数を定義してみましょう。
ここでは、io.Readerから任意バイトを読み込む関数readNと、32ビット整数を読み込む関数readInt32の二つの関数を定義してみます。
package main
import (
"bytes"
"encoding/binary"
"io"
"io/ioutil"
)
// 手軽にnバイト読み込む関数を定義 --- (*1)
func readN(r io.Reader, n int) []byte {
buf := make([]byte, n)
cnt, err := r.Read(buf)
if err != nil || n != cnt {
return []byte{}
}
return buf
}
// 4byte整数を読む関数を定義 --- (*2)
func readInt32(r io.Reader) int {
return int(binary.BigEndian.Uint32(readN(r, 4)))
}
まず、(*1)で定義している、readN関数ですが、これは、io.Readerからnバイト読み込んで、[]byte型で返すというものです。io.Readerのread関数は、引数に与えた変数のサイズを調べて、その変数のサイズ分だけデータを読み込むというものになっています。
次に、(*2)の部分では、readInt32関数を定義していますが、ここでは、readN関数を利用して4バイト(32ビット)分読み出して、それを整数に変換して返しています。
なお、整数に変換する時に意識しなければならないのが『バイトオーダー』です。バイトオーダーとは、複数バイトから成るデータを転送したり記録する際に、どのような並び順でデータを扱うのかを表します。『ビッグエンディアン』と『リトルエンディアン』の二種類です。
例えば、0x12345678という4バイトのデータがあったとして、「12 34 56 78」の並びで扱うのがビッグエンディアン、逆さまの「78 56 34 12」の並びで扱うのがリトルエンディアンです。CPUの種類によってどちらのバイトオーダーでデータを扱うのかが異なります。
PNGファイルでは、数値はビッグエンディアンが採用されています。そこで、binary.BigEndian.Uint32関数を使うことで4バイトの[]byte型データを符号なし32ビット整数に変換します。
PNG画像の情報を調べるプログラム
上記のプログラムの続きの部分に、実際にファイルからPNGファイルを読み込んで画像サイズを調べるプログラムを見てみましょう。
func main() {
// ファイルを全部メモリに読み込む --- (*1)
buf, err := ioutil.ReadFile("tea.png")
if err != nil {
return
}
// 先頭のPNGシグネチャを取り込む --- (*2)
r := bytes.NewReader(buf)
if !bytes.Equal(readN(r, 8), []byte("\x89PNG\r\n\x1a\n")) {
println("PNGファイルではありません")
return
}
// 複数のチャンクを繰り返し読む --- (*3)
for {
// サイズ、タイプ、データ、CRCを順に読む --- (*4)
chunkLen := readInt32(r)
chunkType := string(readN(r, 4))
data := readN(r, chunkLen)
_ = readInt32(r) // CRC32
println("[CHUNK]", chunkType)
// IHDRチャンクの時 --- (*5)
if chunkType == "IHDR" {
rd := bytes.NewReader(data)
width := readInt32(rd)
height := readInt32(rd)
println("width=", width)
println("height=", height)
} else if chunkType == "IEND" {
break
}
}
}
プログラムの(*1)の部分で、PNG画像ファイル「tea.png」というファイルを読みます。そして、(*2)の部分でPNGファイルの先頭に必ずあるPNGシグネチャを調べます。PNGシグネチャは「\x89PNG\r\n\x1a\n」という文字列になっており、これと合致するかを調べます。合致しなければ、PNGファイルではないのでメッセージを表示して終了します。
(*3)の部分では、for文を用いて、繰り返しPNGファイルのチャンクを読みます。(*4)の部分で、チャンクサイズ、タイプ、データ、CRC値を順に読みます。そして、(*5)の部分でチャンクタイプがIHDRであれば、それは画像のヘッダ情報を表すチャンクなので、画像の幅と高さを読み出して表示します。もし、チャンクタイプがIHDRであれば、画像末尾を表すので、そこで繰り返しから脱出します。
プログラムを実行してみよう
ここまでのプログラムをまとめたものを、 こちらに置いてあります。プログラムをコピーし、テキストエディタに貼り付けて、ファイル「checkpng.go」という名前で保存しましょう。
それから、読んでみたいPNG画像を「tea.png」という名前でcheck.pngと同じフォルダに保存しましょう。なお、こちらにサンプル画像とプログラムを圧縮して配置してます。必要ならダウンロードして利用してください。
そして、コマンドラインから以下のコマンドを実行します。
go run checkpng.go
すると、以下のようにPNG画像の情報を表示します。
まとめ
以上、ここまでの部分で、Go言語を用いてPNG画像の情報を読み込むプログラムを紹介しました。ちなみに、Go言語には最初から"image/png"というPNG画像のためのライブラリが用意されています。これのimage.DecodeConfig関数を使えば、画像サイズを調べることができます。とは言え、今回紹介したように、構造が簡単なPNG画像を例にして、バイナリファイルの読み込み処理を作ってみるなら、バイナリ操作のとても良い練習になります。
さて、せっかくPNG画像に詳しくなったので、次回はPNG画像に暗号文を埋め込むプログラムを作ってみましょう。
自由型プログラマー。くじらはんどにて、プログラミングの楽しさを伝える活動をしている。代表作に、日本語プログラミング言語「なでしこ」 、テキスト音楽「サクラ」など。2001年オンラインソフト大賞入賞、2004年度未踏ユース スーパークリエータ、2010年 OSS貢献者章受賞。技術書も多く執筆している。