Top > presen > PHP > binary
TITLE: PHP でバイナリプログラミング
http://events.php.gr.jp/events/show/96

- (追記 2013/10/31) この後で発表した資料の方が分かり易いです。 > http://www.slideshare.net/yoyayoya1/php-10133775

#contents

* はじめに [#v18439f5]

PHP でバイナリ処理の話をあまり聞かないので、あえてニッチな所を狙って発表させて頂きます。

- キーワード: Binary、Format, Byte,
- デモ: JPEGからGPS抜き出し pure PHP で)

PHP というより、バイナリ入門といった要素が強いですが、
多分…、いつかお役に立つ日が来ると思います。どうか、ご容赦ください。

** 一応、自己紹介 [#l413a7de]

- http://d.hatena.ne.jp/yoya/ でプログラミングしてて困った事とか書いてます。
- 一昨年位まで、数十万ユーザ規模の携帯サイトでアプリ開発をしていました。
--  その時に、PHP で主に動画や画像のフォーマットを弄るお仕事をしていました。フレームワークとかよく知りません。
- 今も、一応 PHP でお仕事してます。あと、C 言語もたまに使います。

** まずは、バイナリの定義 [#vd6a4a1e]

- Wikipedia より
 通常バイナリとテキストは対比して用いられる。
 テキストとはデータの内容すべてを人間が読んで理解できる (human-readable) 表現形式を指し、
 バイナリとはそうでない表現形式を指すことが多い。

なので、本発表では、バイナリファイルの事を、
 テキストエディタで開いて読めない文字とか記号が表示されるようなファイル。
という事にしておきます。

** バイナリの実例 [#z1197160]

 % hexdump -C aria.gif
 00000000  47 49 46 38 39 61 c8 00  96 00 f7 00 00 00 00 00  |GIF89a..........|
 00000010  ff ff ff 96 53 58 29 1b  1c e6 b0 b8 b2 69 76 37  |....SX)......iv7|
 00000020  26 29 d6 96 a1 cb c6 c7  34 1c 22 48 31 38 2b 21  |&)......4."H18+!|
 <略>
GIF ファイルですね。
 $ hexdump -C kuriboo.png
 00000000  89 50 4e 47 0d 0a 1a 0a  00 00 00 0d 49 48 44 52  |.PNG........IHDR|
 00000010  00 00 00 c0 00 00 00 e0  08 06 00 00 00 55 70 69  |.............Upi|
 00000020  31 00 00 00 04 73 42 49  54 08 08 08 08 7c 08 64  |1....sBIT....|.d|
 <略>
PNG です。

先頭の4文字を見るとファイルの種類が大体分かるようになっていて、その後ろにはよく分からないデータが続いてます。

普通はここで読むのを諦めるのですが、このよく分からないデータを PHP で解釈して、欲しいデータを抜き出す方法について説明します。

* PHP とバイナリ [#e5d1b7d7]

 PHP の string 型でバイナリ処理が簡単に出来るよ!

というのが今回、紹介する Tips の肝です。

(注) PHP6 では UTF-8 対応したり取りやめたりと怪しいので、今回の発表はとりあえず、PHP5(PHP4 も多分大丈夫) only での話しだと思ってください。

* 本当に PHP の string 型でバイナリ処理できるの? [#d76edda9]

まずは確認。

** \0 は? [#ta2f5d12]

C言語が典型ですが、\0 を文字列の(最後を表す)終端マークとして使う処理系も結構あるので、
string型をバイナリデータとして使う場合、間に \0 が入る事で途中で切れないか。

- 簡単に確認

 $s = "This is TEST\n";
 $s{3} = "\0";
 echo strlen($s)."\n";
 echo $s;

 13
 Thi is TEST

- 確認完了 \0 で途切れない。

** 8bitスルー? [#e87fe524]

- 1文字は 1byte で、1byte は 8bit で構成されますが、
- US-ASCII は 7bit で表現できるので、先頭1bit が特別扱いされたりしない?

  bit 表現
 |--------+
 |XNNNNNNN| この X の bit を特別扱いされる懸念。
 +--------+

 $s = ' ';
 $s{3} = 'A';
 $s{4} = chr(ord('A') | 0x80); // chr, ord は後で説明します
 var_dump(bin2hex($s));

 string(10) "20202041c1"

- 8bit スルーです。日本語とかも入れられるし当然ですね。

** バイナリを取り込んでそのまま出力 [#q17ca2f5]

- でも、細かい事考えなくても、バイナリファイルを入力して、何も変えずに出力して同じデータが生成されれば、OKですよね。

 $data = file_get_contents($argv[1]);
 echo $data;

 % php echo.php saitama.jpg > output.dat
 % md5sum saitama.jpg output.dat
 06f741dca38937df3702f6759aead28b  saitama.jpg
 06f741dca38937df3702f6759aead28b  output.dat

- 入力をそのまま出力して、同じデータになりました。
- 後は、この途中でデータを分割したり結合したり、入れ替えたりすれば編集できる事になります。

* byte処理 [#y43196b3]

- String 関数が使えます http://php.net/manual/ja/ref.strings.php
--  strlen (データサイズを調べる。呼ぶたびに文字数を数えたりしないので安心) ☆
--  substr (データから一部を抜き出す) ☆
--  substr_replace (データの一部を入れ替える)
--  strrev (データの前後を逆にする) Endian 処理に便利 ☆
--  chr 数値 => 文字(バイナリ1byte) ☆
--  ord 文字(バイナリ1byte) => 数値 ☆
--  bin2hex ダンプするのに便利です ☆

これだけ分かれば大丈夫。(今回の発表では、☆のついた関数が出てきます)
pack 関連は次回発表の機会があれば…

さて、JPEG で試してみます。

** JPEG の解析 [#pac8673a]

- 目的、JPEG から GPS 情報を抜き出す

*** 初めの一歩 [#fb1c2d0c]

- まずファイルを開いてみる

 % hexdump -C aria.jpg
 00000000  ff d8 ff e0 00 10 4a 46  49 46 00 01 01 01 00 60  |......JFIF.....`|
 00000010  00 60 00 00 ff e1 00 22  45 78 69 66 00 00 49 49  |.`....."Exif..II|
 00000020  2a 00 08 00 00 00 01 00  00 51 04 00 01 00 00 00  |*........Q......|
 <略>

- 見てても法則が分からないので「JPEG フォーマット 仕様」でググってみる
-- http://siisise.net/jpeg.html#format

 マーカ・コード  	長さ  	データ
 FFxx(16bit) 	16bit 	可変サイズ
 
- ''16bit は 2 byteに相当''。ffxx で区切るらしい。

 00000000  ff d8 ff e0 00 10 ...
           ~~~~~ ~~~~~~~~~~~~~~~
           SOI   APP0

- 眺めていると、何となくピンと来るはず。

*** JPEG を marker で分割してみる [#u317c98f]

- この ffxx のマークで分割してみます。

- まず、バイナリの先頭から Byte を切り出すクラスを作成
 class ByteSteam {
     private $_data;
     private $_cursor;
     function getBytes($size); // バイナリ列を取り出す
      function getValue($size); // 数値として取り出す
      function getCursor();     // カーソル位置を知る
 }
- 内部的に、何処まで読んだか(cursor) を覚えておいて、そこから substr でデータを切り出すだけのクラス

- marker 一個目
 $jpegdata = file_get_contents($argv[1]); // 引数で指定したファイル取り込み
 $bs = new ByteStream($jpegdata);
 $marker = $bs->getBytes(2); // 先頭2バイト取り出す
 var_dump(bin2hex($marker)); // 16進ダンプ

 ffd8
- marker 逐次処理
-- SOI(Start of Image) と EIO(End of Image) はマーカー(2 bytes) だけ
-- APP0, APP1 等、殆どのマーカーは、マーカー(2 bytes) に続いて、長さ(2 bytes)、その後ろにデータ(長さ - 2) が続く
-- SOS は EOI の直前まで続く (途中で RST という別のタグがあるが、今回は中にまとめちゃう)

 function marker($a, $b) { return chr($a).chr($b); }

 while($marker = $bs->getBytes(2)) {
     switch ($marker) {
      case marker(0xFF, 0xD8): // SOS
        $length = $data = null;
        break;
      case marker(0xFF, 0xE0): // APP0
      case marker(0xFF, 0xE1): // APP1
        $length = $bs->getValue(2);
        $data = $bs->getBytes($length - 2);
        break;
      case marker(0xFF, 0xDA): // SOS include RST
        $length = strlen($jpegdata) - $bs->getCursor() - 2;
        $data = $bs->getBytes($length);
        break;
      case marker(0xFF, 0xD9): // EOI
        $length = $data = null;
        $done = true;
        break;
 }

- 分割できた
 string(4) "ffd8"
 string(4) "ffe1"
 string(4) "ffdb"
 string(4) "ffc0"
 string(4) "ffc4"
 string(4) "ffda"
 string(4) "ffd9"

- http://siisise.net/jpeg.html#format で対応を見ると

 string(4) "ffd8" SOI (開始マーカ)
 string(4) "ffe1" APP1 (情報)
 string(4) "ffdb" DQT (量子化テーブル定義)
 string(4) "ffc0" SOF0 標準DCT圧縮 
 string(4) "ffc4" DHT (ハフマンテーブル情報)
 string(4) "ffda" SOS エンコードされたイメージデータ (DRI 含む)
 string(4) "ffd9" EOI (終了マーカ)

*** 欲しい情報を調べる [#qbdaee18]

iPhone で写真を撮ると、GPS 情報が付くらしいので、抽出してみよう。

- 「JPEG GPS フォーマット 仕様」でググる
-- http://detail.chiebukuro.yahoo.co.jp/qa/question_detail/q1217494967
-- Exif というフォーマットに入っている事が分かった。
- 「JPEG Exif フォーマット 仕様」でググる
--  http://hp.vector.co.jp/authors/VA032610/JPEGFormat/AboutExif.htm
--  http://www.exif.org/Exif2-2.PDF

- APP1 に入っているらしい。
- Exif に入っているらしい。
- ディレクトリ構造らしい
- GPSInfo というタグらしい
- Little Endian を使う事もあるらしい。

*** Little Endian ? [#m706de1a]

- バイナリを16進で見た並び => 対応する整数値
-- Big Endian
       バイトの並び    => 対応する整数値
        00 00 00 01  =>  0x01 => 1
        12 34 56 78  =>  0x12345678 =>  305419896
-- Little Endian
       バイトの並び    => 対応する整数値
       01 00 00 00   =>  0x01 => 1
       12 34 56 78   =>  0x78563412 => 2018915346

- クラスを改造 $order_le フラグを追加
    function getValue($size, $order_le = false) {
        $data = $this->getBytes($size);
        if ($order_le) {
            $data = strrev($data); // ☆ big endian を little endian の並びにする
        }
        $value = 0;
        for ($i = 0; $i < $size; $i++) {
            $value <<= 8;
            $value += ord($data[$i]);
        }
        return $value;
    }

*** Exif の分解 [#qc02f3ab]

- JPEG を分解するついでに $jpeg_chunk 配列に格納しておく
 while($marker = $bs->getBytes(2)) {
     switch ($marker) {
       case marker(0xFF, 0xD8): // SOS
   <略>
     $jpeg_chunk[] = array(
        'marker' => $marker,
        'length' => $length,
        'data' => $data);

- $jpeg_chunk 配列から APP1 で Exif のデータを抜き出す

 foreach ($jpeg_chunk as $chunk) {
     if ($chunk['marker'] == marker(0xFF, 0xE1)) {
         $data = $chunk['data'];
         if (substr($data, 0, 4) == 'Exif') {

- Exif データを分解

 $exif_data = substr($data, 4); // Exif の文字列より後ろのデータ
 $bs_exif = new ByteStream($exif_data);
 // Exif 冒頭のタグ
 $tag = $bs_exif->getBytes(2);
 $tiff_header = $bs_exif->getBytes(2);
 if ($data == 'MM') {
     $order_le = false; // モトローラ形式(big endian)
 } else if ($data == 'II') {
     $order_le = true; // インテル形式(little endian)
 }
 $tiff_id = $bs_exif->getBytes(2);
 $pointer_to_0th_IFD = $bs_exif->getValue(4, $order_le);
 // IDF の分解
 $exif_tag_num = $bs_exif->getValue(2, $order_le);
 for ($i=0; $i< $exif_tag_num; $i++) {
     $exif_tag  = $bs_exif->getBytes(2);
     $exif_type = $bs_exif->getValue(2, $order_le);
     $exif_number = $bs_exif->getValue(4, $order_le);
     $exif_offset = $bs_exif->getValue(4, $order_le);
 }
 $pointer_to_next_IFD = $bs_exif->getValue(2, $order_le);

*** GPSInfo の分解 [#h2948c7f]

- GPS は GPS Info というタグに入っているらしい
- 「JPEG Exif GPSInfo フォーマット 仕様」でググる
--  http://www2.airnet.ne.jp/~kenshi/exif.html

- GPSInfo を分解
 if ($exif_tag == exif_tag(0x88, 0x25)) { // 0x8825: GPS Info
     // 2 = tiff header(MM or II) offset
     $bs_gps = new ByteStream($exif_data, $exif_offset + 2);
     $gps_tag_num = $bs_gps->getValue(2, $order_le); // while
     for ($j=0; $j< $gps_tag_num; $j++) {
         $gps_tag  = $bs_gps->getBytes(2);
         $gps_type = $bs_gps->getValue(2, $order_le);
         $gps_number = $bs_gps->getValue(4, $order_le);
         $gps_offset = $bs_gps->getBytes(4); // no offset but data
     }
- 結果

 XXX: tiff header=MM
 XXX: tiff id=002a
 XXX: 0th IFD pointer(8)
 XXX exif tag(010f,2, 6, 146)
 XXX exif tag(0110,2, 11, 152)
 XXX exif tag(0112,3, 1, 65536)
 XXX exif tag(011a,5, 1, 164)
 XXX exif tag(011b,5, 1, 172)
 XXX exif tag(0128,3, 1, 131072)
 XXX exif tag(0131,2, 6, 180)
 XXX exif tag(0132,2, 20, 186)
 XXX exif tag(0213,3, 1, 65536)
 XXX exif tag(8769,4, 1, 206)
 XXX exif tag(8825,4, 1, 544)
 XXX: gps_tag_num=7
 XXX gps tag(0001,2, 2, N)
 XXX gps tag(0002,5, 3, 634)
 XXX gps tag(0003,2, 2, E)
 XXX gps tag(0004,5, 3, 658)
 XXX gps tag(0007,5, 3, 682)
 XXX gps tag(0010,2, 2, T)
 XXX gps tag(0011,5, 1, 706)
 XXX: pointer_to_next_IFD=0


- N は北緯、E は東経、他は座標値等々

* bit処理は… [#mda8635b]

- 今回時間的に収まらなかったので、もし要望があれば次回に発表します。

* 今回のまとめ [#oa4951d1]

- バイナリは大抵 TLC 構造か、難しくてもディレクトリ構造で、今回は両方処理しました。

- TLC 構造
                     ↓実際のデータ
 +-------------------------+
 |type | length | contents |
 +-------------------------+
                 <--length-->

- ディレクトリ構造
                  <-count この例だと 2->
 +----------------------------------+------------------------+
 |type | count | offset1 | offset 2 | type  data | type data |
 +-----------------------+----------+------------------------+
                   |         |        ↑            ↑
                   |         +--------+------------+
                   +------------------+

- この構造を知っていれば、大抵のバイナリファイルは解析できます。(多分)

* 蛇足的な Tips [#o4b9d5e0]

- PHP の閉じタグ ?> は使わない。?> の後ろに改行やゴミ文字があった場合に、テキストなら最後にゴミが付くだけで大きな問題になりにくいが、バイナリだと致命的。
- $s = '' 等、文字列でも文字数が0 だと、$s{3} = 'A' とした場合に、array([3] => "A"); のように配列になる。$s = ' ' 等としとくと、string(4) "   A" のように思った通りいく。足りない分は 0x20(スペース) で padding されるのに注意
- C言語なら文字列から1文字抜き出してASCIIコードとして使えるが、PHP の場合は文字は ord() で ASCII コード値に変換して、文字に戻すときは chr() を通す必要がある。

* 次回予告 [#j877f5ab]

- もし、次回の要望があればですが。Flash SWF の書き換えについて発表したいです。
- Flash SWF は bit処理と signed の組み合わせがあったりと、敷居が思ったより高い為、今回の発表から外しました。
- Google検索: yoya swfed

* あ… [#ef8e53a6]
- ここまで頑張っておいて何ですが…
 /php-5.2.9/ext/exif
- http://php.net/manual/ja/function.exif-read-data.php
 var_dump(exif_read_data($argv[1]));
- !!!!!!!!

* 以上です [#n4d6f199]

ありがとうございました。

* 日記でまとめ [#xa0d5d8e]

- http://d.hatena.ne.jp/yoya/20100511/phpstudy
-- サンプルコードもあります。

Reload   Diff   Front page List of pages Search Recent changes Backup Referer   Help   RSS of recent changes