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
-- サンプルコードもあります。