みなさんバイナリファイルはお好きですか?僕は絶賛マイブーム中です。
バイナリファイルは人が見てもイマイチ理解しづらいために苦手意識を持つ方も少なくないのではと思います。
今回はそんな苦手意識を少しでも軽減すべく、簡単なBitmap画像を手作りしてみよう、という試みです。この記事を参考にバイナリデータを読んだり作ったりしてみることで、きっとバイナリデータの見方が変わるのではないかと思います。
では、どうぞ。
Bitmapとは
まずはBitmapというデータフォーマットについて説明しておきます。
Bitmapは、画像を表現するための(たぶん)一番単純なフォーマットです。
特に単純な白黒画像であれば、以下の参考リンクのようなフォーマット解説のページをざっと見ればなんとなく作れるような気がしてくるくらいの分かりやすさだったため、今回はこれを題材にしてみました。
参考
バイナリファイルの作り方
バイナリファイルはバイトデータの羅列でできたファイルです。1今回はBitmap形式のバイナリファイルをゼロから作ってみることで、バイナリファイルの扱いを体験します。
なお、バイナリファイルの内容を手作業で見たり修正したりする場合は通常バイナリエディタを使うのですが、今回は以前紹介したワンライナーを使って作成します。
これを使うと、まず16進数表記のバイト列をテキストファイルに記述・保存してからコマンドでバイナリファイルに変換するため、実際の記述作業は使い慣れたテキストエディタでできるメリットがあります。
また、バイナリファイルは「何バイト目から何バイト目にこの情報を記載する」というように仕様で定められた位置に、定められた桁数でデータを打ち込んでいく必要があります。上記のワンライナーを使えば、元となるテキストファイルは好きな位置で改行したりコメントを入れたりできるため、仕様に合わせて分かりやすく整形しつつ、効率的に編集することができます。
いざ、作成
今回は下の画像のように白地に黒で四角が描かれているだけの単純な画像ファイルを作成することを目指します。
Bitmapファイルのフォーマットを確認する
まずは今回作成するBitmapファイルのフォーマットを確認します。
Bitmapファイルは大雑把に
- ファイルヘッダ
- 情報ヘッダ
- パレットデータ
- 画像データ
の4つの領域から成り立ちます。
細かな仕様は先述のリンク先を参照していただければと思いますが、今回はこれらの領域をファイルの先頭から順番に16進数のバイト列で記述していくことになります。
では、Vim等テキストエディタを開いて作業開始です。
$ vi square_bitmap.txt
ファイルヘッダ
ファイルヘッダには、ファイルサイズなどファイル全体の情報を記載します。
ひとまずできあがったものを以下に記載します。
## ファイルヘッダ ##
42 4d # ファイルタイプを2バイトで指定。Bitmapは必ず'BM' (16進数で 42 4d)
00 00 00 00 # ファイルサイズを4バイトで指定。TODO: あとで
00 00 # 予約領域1を2バイトで指定。常に0
00 00 # 予約領域2を2バイトで指定。常に0
00 00 00 00 # ファイルの先頭から画像データまでのバイト数を4バイトで指定。 TODO: あとで
上から順番に説明します。
__ファイルタイプ__は、そのバイナリファイルが何の形式であるかを示すバイト列です。Bitmapの場合はASCII文字でBM
、つまり16進数表記では42 4d
となります。
__ファイルサイズ__は、今回作成するBitmapファイルのサイズを指定します。とはいえ今の段階では最終的に何バイトになるのかが分からないため、いったん0で埋めておきます。最後に測って修正します。
__予約領域__は特に使用しません。0 (16進数で00
)で埋めておきます。
__オフセット__はファイルの先頭から画像データまでのバイト数です。こちらも後で測ります。
なんだかファイルタイプ以外は全部0で埋めるだけになりましたね、、、まあ気にせず次にいきましょう。
なお、ここで注意なのですが、ファイルタイプ以外のバイト列はすべて__リトルエンディアン__で記載します。詳しい説明は省略しますが、リトルエンディアンで記述する場合、下位バイトが左にくるよう記載します。
例えば、仮にファイルサイズが5000
だった場合、16進数表記では13 88
となります。なので上記のファイルサイズ部分は4バイトになるようゼロ埋めして00 00 13 88
となるのですが、リトルエンディアンでは下の桁が左なので、88 13 00 00
となる、というワケです。
なんでこんな逆の書き方するのかと言われるといろいろと理由があるみたいなのですが、そのあたりの説明は今回は省略します。とりあえずそういうものなんだ、と理解して先に進んでください。(ちなみにしばらく作業しているとリトルエンディアンの方が分かりやすく思えてきたりするから不思議です)
情報ヘッダ
次に、情報ヘッダです。
情報ヘッダには画像の色数やサイズなど、Bitmap画像自体の情報をいろいろと埋め込んでいきます。
今回は16ピクセル×16ピクセルの白黒画像という前提で指定していきます。
以下をsquare_bitmap.txt
に追記してください。
## 情報ヘッダ ##
28 00 00 00 # 情報ヘッダサイズを4バイトで指定。
10 00 00 00 # 画像の横幅(単位はピクセル)を4バイトで指定。
10 00 00 00 # 画像の高さ(単位はピクセル)を4バイトで指定。
01 00 # プレーン数を2バイトで指定。常に1
01 00 # 色ビット数を2バイトで指定。今回は白黒2色なので1
00 00 00 00 # 圧縮形式を4バイトで指定。
00 00 00 00 # 画像データ部分のサイズを4バイトで指定。 TODO: あとで
10 00 00 00 # 横方向の解像度を4バイトで指定。
10 00 00 00 # 縦方向の解像度を4バイトで指定。
00 00 00 00 # パレット数を4バイトで指定。
00 00 00 00 # 重要色数を4バイトで指定。
記載の内容について、順番に説明します。
__情報ヘッダサイズ__は、その名の通り情報ヘッダのサイズです。情報ヘッダはBitmapの種類によって変わるため、ここでどこまでが情報ヘッダなのかをデータを読み取るプログラムに教えてあげます。今回作成するWindows形式のBitmapファイルでは情報ヘッダは40バイトですので、4桁で0埋めして(そしてリトルエンディアンで)28 00 00 00
となります。
__画像の横幅・高さ__はそのままです。幅・高さそれぞれ何ピクセルかを指定します。
__プレーン数__は、よく理解できていないですが1(2桁の16進数で01 00
)固定だそうです。その通り記載します。
__色ビット数__は画像を何ビットで表すか、という数値です。白黒など2色の場合は1ビット(0/1)で表せるため、1(2桁の16進数で01 00
)を指定します。
__圧縮形式__は、今回は割愛します。無圧縮を表す0を指定します。
__画像データサイズ__は後述する画像データ部分のサイズを指定します。まだ作ってみないと分からないため一旦保留です。
__解像度__は横、縦それぞれの1メートルあたりのドット数を指定します。なのですが、いろいろ変えてみても見た目が変わらなかったため、どのように指定するのが適切かは未調査です。とりあえず横幅、高さと同じ16(4桁の16進数で10 00 00 00
)を指定しておきます。
__パレット数__はこの画像データで利用する色の数を指定します。ただし、今回のように色ビット数に指定した通り(今回は1bitで2色)の場合は省略して0を指定することができます。
__重要色数__についてもちょっと未調査です。今回は0で問題ないようなので、0を埋めておきます。
いろいろと理解不足な感は否めないですが、今回の目的はBitmapマスターになることではないため、一旦動けばいい作戦で次に進みたいと思います。
パレットデータ
次にパレットデータを作成します。
今回は色ビット数が1bitのため、2色分のパレットデータを指定する必要があります。白と黒で作成します。
## パレットデータ ##
ff ff ff 00 # RGB指定。4バイト目は常に0。
00 00 00 00 # RGB指定。4バイト目は常に0。
これだけです。1パレット4バイトで、左3バイトが16進数表記のRGB、4バイト目が予約領域で常に0だそうです。これを白黒それぞれ指定します。RGBの値を変えれば好きな色で画像が作れそうです。
なお、ここで先に指定した色がパレット番号0、後に指定した色がパレット番号1で登録されます。それぞれの番号は後述の画像データで利用します。
画像データ
最後に画像データです。今回は白地(パレット番号0)の上に、黒(パレット番号1)で四角形を描きます。
まず、今回のように色ビット数が1の場合、画像データは0と1の2進数で作成します。
1ピクセル1bitで、縦横16ピクセルずつですので、2進数表記の画像データは以下のようになります。(見やすさのため半角スペースを入れています)
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0
0 1 0 0 0 0 0 0 0 0 0 0 0 0 1 0
0 1 0 0 0 0 0 0 0 0 0 0 0 0 1 0
0 1 0 0 0 0 0 0 0 0 0 0 0 0 1 0
0 1 0 0 0 0 0 0 0 0 0 0 0 0 1 0
0 1 0 0 0 0 0 0 0 0 0 0 0 0 1 0
0 1 0 0 0 0 0 0 0 0 0 0 0 0 1 0
0 1 0 0 0 0 0 0 0 0 0 0 0 0 1 0
0 1 0 0 0 0 0 0 0 0 0 0 0 0 1 0
0 1 0 0 0 0 0 0 0 0 0 0 0 0 1 0
0 1 0 0 0 0 0 0 0 0 0 0 0 0 1 0
0 1 0 0 0 0 0 0 0 0 0 0 0 0 1 0
0 1 0 0 0 0 0 0 0 0 0 0 0 0 1 0
0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
しかしバイナリデータは1バイト(8ビット)単位で表記する必要があるため、上記のビット列を8ビット1組のバイト列に変換します。
例えば3行目の0 1 0 0 0 0 0 0 0 0 0 0 0 0 1 0
の場合、01000000
と00000010
に分け、それぞれ16進数表記で40
と02
になります。
同じ要領で上から順番に変換していくと、結果は以下のようになります。
00 00
7f fe
40 02
40 02
40 02
40 02
40 02
40 02
40 02
40 02
40 02
40 02
40 02
40 02
7f fe
00 00
さて、これで16ピクセル×16ピクセルの白黒データができたかと思いきや、もう1つ今回気にしなければならないルールがあります。
それは、__行データはlong(4byte)の境界に揃えなければいけない__というものです。
つまり、1行分のデータは4バイトでなければならず、それに満たない部分については00
で埋める、というルールです。
上記の画像データも1行あたりが2バイト(16ビット)しかありませんので、2バイト分を00
で埋める必要があります。結果は以下のようになります。
## 画像データ ##
00 00 00 00
7f fe 00 00
40 02 00 00
40 02 00 00
40 02 00 00
40 02 00 00
40 02 00 00
40 02 00 00
40 02 00 00
40 02 00 00
40 02 00 00
40 02 00 00
40 02 00 00
40 02 00 00
7f fe 00 00
00 00 00 00
画像データはこれで完成です。
ここで、1点だけ注意すべき点があります。Bitmapでは、画像データは__左下から__指定する、ということです。今回は上下線対称な画像だったため特に気にはしていませんが、実際の画像を作成する際は、下の列からデータを埋めていく必要があります。
このあたりは実際に作って表示しながら実験してみるとわかりやすいかと思いますので、詳しくは省略します。
「TODO: あとで」を埋める
さて、ここまでできれば先ほど「TODO: あとで」として飛ばした部分を埋めることができます。
まずは__ファイルサイズ__ですが、これは現状のBitmapデータのバイト数を数えれば大丈夫です。一度紹介したワンライナーでバイナリファイルを作ってみましょう。
$ cat square_bitmap.txt | awk -F# '{print $1}' | tr -dc [0-9a-f] | xxd -r -p > square_bitmap.bmp
$ ls -l square_bitmap.bmp
-rw-r--r--@ 1 chooyan wheel 126 11 20 23:40 square_bitmap.bmp
126バイトだそうです。126は16進数で7e
ですので、4桁で0埋めして7e 00 00 00
を記載します。
次に__オフセット__です。これは全体のファイルサイズから画像データのサイズを引くことで得られます。つまり、今回は126 - (4バイト * 16行)
で、62です。16進数にすると3e
ですね。4桁で0埋めして3e 00 00 00
となります。
最後に__画像データのサイズ__ですが、これは先ほど出したとおり、4バイト * 16行で64バイトですね。16進数表記に直し、4桁で0埋めして40 00 00 00
になります。
できあがったテキストファイル
これでテキストファイルの編集は終わりです。完成したファイルはこのようになっているはずです。
$ cat square_bitmap.txt
## ファイルヘッダ ##
42 4d # ファイルタイプを2バイトで指定。Bitmapは必ず'BM' (16進数で 42 4d)
7e 00 00 00 # ファイルサイズを4バイトで指定。
00 00 # 予約領域1を2バイトで指定。常に0
00 00 # 予約領域2を2バイトで指定。常に0
3e 00 00 00 # ファイルの先頭から画像データまでのバイト数を4バイトで指定。
## 情報ヘッダ ##
28 00 00 00 # 情報ヘッダサイズを4バイトで指定。
10 00 00 00 # 画像の横幅(単位はピクセル)を4バイトで指定。
10 00 00 00 # 画像の高さ(単位はピクセル)を4バイトで指定。
01 00 # プレーン数を2バイトで指定。常に1
01 00 # 色ビット数を2バイトで指定。今回は白黒2色なので1
00 00 00 00 # 圧縮形式を4バイトで指定。
40 00 00 00 # 画像データ部分のサイズを4バイトで指定。
10 00 00 00 # 横方向の解像度を4バイトで指定。
10 00 00 00 # 縦方向の解像度を4バイトで指定。
00 00 00 00 # パレット数を4バイトで指定。
00 00 00 00 # 重要色数を4バイトで指定。
## パレットデータ ##
ff ff ff 00 # RGB指定。4バイト目は常に0。
00 00 00 00 # RGB指定。4バイト目は常に0。
## 画像データ ##
00 00 00 00
7f fe 00 00
40 02 00 00
40 02 00 00
40 02 00 00
40 02 00 00
40 02 00 00
40 02 00 00
40 02 00 00
40 02 00 00
40 02 00 00
40 02 00 00
40 02 00 00
40 02 00 00
7f fe 00 00
00 00 00 00
バイナリファイルに変換する
最後に、作成したテキストファイルを先ほどから使っているワンライナーでバイナリファイルに変換します。
$ cat square_bitmap.txt | awk -F# '{print $1}' | tr -dc [0-9a-f] | xxd -r -p > square_bitmap.bmp
生成されたバイナリファイルをod
コマンドで見てみます。
$ od -Ad -tx1 -v square_bitmap.bmp
0000000 42 4d 7e 00 00 00 00 00 00 00 3e 00 00 00 28 00
0000016 00 00 10 00 00 00 10 00 00 00 01 00 01 00 00 00
0000032 00 00 40 00 00 00 10 00 00 00 10 00 00 00 00 00
0000048 00 00 00 00 00 00 ff ff ff 00 00 00 00 00 00 00
0000064 00 00 7f fe 00 00 40 02 00 00 40 02 00 00 40 02
0000080 00 00 40 02 00 00 40 02 00 00 40 02 00 00 40 02
0000096 00 00 40 02 00 00 40 02 00 00 40 02 00 00 40 02
0000112 00 00 40 02 00 00 7f fe 00 00 00 00 00 00
0000126
まあ、よく分からないですね。とりあえず整形のための改行やコメントはきれいさっぱり消えて、126バイトのデータになっていることがわかります。
今回作成したのは画像ファイルですので、ちゃんとできているかの確認はエクスプローラーなどから確認します。フォルダを開いて、ダブルクリックしてみます。
ちゃんとできていれば、最初に掲載したキャプチャのような図が(ちっちゃく)表示されるはずです。
まとめ
というわけで、今回はBitmap画像ファイルを0から手打ちで作成してみました。
今までは手も足も出そうと思わなかったバイナリデータが、意外と仕様を見ながら手作業で作成できてしまう様子が体験できたのではないかと思います。
基本的にバイナリデータは、決まった仕様にしたがって作成されたデータを、同じ仕様にしたがって解釈・処理するプログラムが読み取ることによって役割を果たします。結局は共通の仕様が全てですので、その意味では「人間が読めればいいでしょ」的に曖昧に作られることの多いテキストファイルよりもある意味読みやすいと言えるんじゃないかと最近考えています。
おそらく他のファイルフォーマットでも要領は同じですので、ぜひいろいろと今回使ったワンライナーを使ってバイナリファイルを手書きしてみてください!
-
ホントはファイルは全てバイトデータの塊なので「バイナリファイル」と言えるのですが、その中でもテキストとして解釈できるバイナリファイルは慣習的(?)に「テキストファイル」と呼んで区別します。 ↩