ひとりアドベントカレンダー2015 第19日目 〜 PHPer でもバイナリをいじってみたい その4

  • このエントリーをはてなブックマークに追加
  • Pocket

19日目です。

「PHPer でもバイナリをいじってみたい」の第4回めをお送りします。第3回で調べた pack()  や unpack()  という関数を使っていよいよ実際にバイナリファイルを操作してみたいと思います。

 

ビットマップ画像を操作する

操作する対象として、比較的単純な構造を持つ「Bitmap ファイル」を選びました。画像ファイルの一種ですが、基本的に圧縮がかかっておらずピクセルがそのままの形でファイル内に並んでいます。そのためピクセルの値をいじって画像を変換したりが容易に行なえます。また、画像なので学習する上で操作結果を目で見て確認しやすいという利点もあります。

素材としては、このブログのロゴを Bitmap 形式に変換したものを使います:

logo_blog

Bitmap ファイルフォーマット

バイナリファイルを操作するためにはそのファイルがどのようなフォーマットになっているかを知る必要があります。今回、Btimap ファイルのフォーマットについてはこちらのサイトを参考にしました:

Bitmap ファイルフォーマット) http://www.umekkii.jp/data/computer/file_format/bitmap.cgi

 今回作成したプログラムを貼っておきますので必要に応じて参照しながらお読みください:

https://gist.github.com/maple-nishiyama/d27e362bff5766862908

ファイルヘッダの読み取り

では、ファイルの頭から読んでいきましょう。参考サイトによるとファイルの先頭にはまず14バイトのファイルヘッダ(Bitmap File Header)と呼ばれる部分があるようです。

この部分はさらに、

  • ファイルタイプ(bfType: 2バイト)
  • ファイルサイズ(bfSize: 4バイト)
  • 予約領域1(bfReserved1: 2バイト)
  • 予約領域2(bfReserved2: 2バイト)
  • ファイル先頭から画像データまでのオフセット(bfOffBits: 4バイト)

から成り立っています。これを読み取って PHP の変数に入れるには unpack()  関数を使って次のようにすればよいですね:

 注意点として、一番先頭の2バイトは 'BM' という文字列を表すバイト(マジックナンバーと呼ばれる)であるので 'a2' という文字列を読み取るフォーマット文字を使っていることと、この2バイト以外はすべての数値はリトルエンディアンでエンコーディングされているのでフォーマット文字もリトルエンディアンのもの( 'v' や 'V' など)にするということを挙げておきます。

これを実行すると以下のようにヘッダーが読み取れました:

 ここからファイルサイズが75,294バイトであることと、ピクセルデータが開始するところまでファイルの先頭から54バイトあるということがわかります。

情報ヘッダの読み取り

ファイルヘッダが14バイトでピクセルデータの開始は55バイト目からなのでその間に40バイトのデータがありますね。この部分は情報ヘッダ(Bitmap Information Header) と呼ばれます。Bitmap では必ず40バイトというわけではなく、幾つかの種類があって長さはそれによって異なるそうです。どの種類の情報ヘッダでも必ず最初の4バイトにヘッダの長さが書いてあるのでまずはそれを読みます:

 実行結果は

 となり、情報ヘッダの長さは期待通り40バイトと記されていることがわかります。参考サイトによると40バイトの長さを持つ情報ヘッダは BITMAPINFOHEADER というWindows で主に使われる形式とのことで、そのフォーマットに従って各種値を読んでいきます(各値の意味や長さは参考サイトを見てください):

 情報ヘッダ40バイトのうち bcSize 分の4バイトはすでに読んだので残り36バイトを読むことに注意しましょう。結果は

 となりました。画像の(幅) x (高さ)が 227 x 110 ピクセルであることや、1ピクセルの大きさが24ビットであることなどが読み取れます。24ビットはRGBでそれぞれ8ビット( = 1バイト) ずつ使っているということですね。

 

ピクセルデータの読み取りと変換

いよいよピクセルデータそのものを読み取っていきます。今回は練習として、読み取ったピクセルデータを変換して元画像を白黒画像にし、ファイルに書き出すことを考えてみます。書き出す画像のヘッダは元画像と同じで、ピクセルデータ部分のみが変わるはずなのでまずヘッダをコピーしておきます:

 バイナリデータは文字列型であるので結合にはドット演算子を用いればよいですね。また、変換後のピクセルデータを保持しておく配列も確保しておきます:

 実際にピクセルデータを読みだし、白黒に変換するコードは以下のようになります:

ピクセルのデータは実際の画像の縦横と同じ順に並んでいるのですが、行のデータを4バイトの倍数に合わせる必要があります。 今回の画像は横幅が 227 ピクセルであり、1ピクセルは3バイト使うので 227 * 3 = 681 バイトが1行あたりのデータ長です。これは4で割ると1だけ余ってしまうので4の倍数にするには残り3バイトを0で埋めておく必要があります。これが上のコードの「パディングの処理」に当たります。

それを踏まえれば残りの部分でやっていることは単純です。1ピクセル( = 3バイト)のデータを読み込んで1バイトずつ $r, $g, $b  という変数に代入します。それの平均を取ったものを白黒画像の明るさと解釈し、変換後のピクセルデータとして保持するというわけです。ここで PHP の数値をバイナリ文字列に戻すのに pack()  関数を使用しています。

変換した画像の書き出し

変換処理ができたのでコピーしておいたヘッダと変換後のピクセルデータを結合してファイルに書き出します:

 上でも述べたようにバイナリデータの結合は文字列の結合でよいので簡単ですね。書きだされたファイルは上手く白黒になっていました:

new_log_blog

まとめ

前回までに見たバイナリデータの操作方法を使って実際に Bitmap ファイルを読み取り、加工してみました。PHP でもこのようにバイナリデータを自由に操作できることがわかりましたね。

ただ、一般にバイナリプログラミングは大量の計算が必要になる(今回もピクセルごとに値を計算していますね)ので PHP でやるのは速度の面でどうしても不利になり、実用性はイマイチかもしれませんね・・・(ここまで4回も使っておきながら!)

PHP バイナリプログラミング編はこれにていったん終了としたいと思います。

  • このエントリーをはてなブックマークに追加
  • Pocket

SNSでもご購読できます。

コメントを残す

*