JPEGのExif情報をとっかかりにバイナリデータと戯れる
こんにちは。
開発チームのワイルド担当、まんだいです。
先日、スマートフォンの画像があさっての方を向くという怪奇現象に遭遇した際、Exif情報について色々調べた訳ですが、その過程でExif情報のバイナリが読めるようになったので、Exif情報の構成を解説したいと思います。
また、その過程を通じて、バイナリデータってチョロいんだという事を知ってもらえたらといいなと思っています。
Exif解析に使うもの
Exifの解析に使ったものは、インターネットで手に入ります。
以下の3つがあれば、Exif情報を読取る事は可能です。
まずは、規格の情報。
一般社団法人 電子情報技術産業協会の資料を完璧に理解できれば、余裕なのでしょうが、中々難解な書物なので、エクセルなんかを使って、一つ一つ確認していきました。
バイナリデータの参照には、Stirlingというソフトウェアを利用しました。
既にメンテナンスは終了しているようですが、この分野においてこれを超えるツールは、まだ見かけないくらい、良い作りです。
Linux環境の場合、バイナリデータを表示するコマンドとして、odというコマンドがあるので、od -x [path]で、だいたい同じようなものを見ることが出来ます。
この場合は、ダンプリストがズラーッと流れてしまうので、適宜headコマンド、tailコマンド、grepコマンドを駆使して、見たい部分だけ表示するようにします。
Exifが採用されている画像フォーマットは、JPEG、TIFF、JPEG XRとなっています。
これらのファイルには、Exifが登録されている事が多いため、サンプルはどこにでもありますね。
適当にスマートフォンで撮った写真を取り込んでみましょう。
バイナリ情報を読み解いてみる
バイナリデータは、テキスト情報ではなく、拡張子に表されるフォーマットに則ったデータの集合ですから、出だしの部分から、JPEGデータのフォーマットに沿ったデータの並び方をしています。
手元にあったJPEGファイルの先頭部分を抜き出してみました。
1 | FF D8 FF E1 6C 0F 45 78 69 66 00 00 49 49 |
JPEGファイルは、必ず「FF D8」という2バイトのデータから始まります。
JPEG業界では、SOIと呼ばれる特殊な領域で、ここからしばらく、画像のメタデータを保存する領域が続きます。
メタデータとは、画像データ以外に保持しているデータの事で、Exif情報もこの中に含まれます。
このデータは必須項目と、それ以外の撮影した機器や、編集したアプリケーションが独自に付与できるデータに分けられます。
続くデータは「FF E1」で、これは、APP1アプリケーションセグメントというメタデータのセグメントの開始を表します。
これは可変長のデータなので、続く2バイトに、APP1アプリケーションセグメントのデータ長が格納されています。
データの長さは「6C0F」バイトなので、APP1アプリケーションセグメントのデータは、6C13アドレスまでになります。
続きデータは「45 78 69 66 00 00」で、これはExif情報の有無を示しています。
今回のデータは存在するようです。
ここまでがAPP1セグメント全体に関わる情報になります。
ここから先は、付属情報で必須ではありません。
続きのデータを抜き出してみましょう。
1 | 49 49 2A 00 08 00 00 00 |
まず始まるのは、TIFFヘッダで、バイトオーダー、TIFFバージョン、0th IFDの開始位置までのオフセット情報がここで表されています。
バイトオーダーは、今回は「49 49」でリトルエンディアンでデータが並んでいる事を表します。
ビッグエンディアンの場合は「4D 4D」となります。
以降、リトルエンディアン、ビッグエンディアンの違いで、各情報の並び順が逆になりますので、Apple製品で撮影した写真がお手元にある場合は、ビッグエンディアンなので、注意が必要です。
バイトオーダーという言葉の説明は、Wikipediaが詳しいかなと思います。
また、このデータの開始位置がこの先に出てくるデータアドレスのオフセットの基準位置になりますので、割と重要なポジションです。
その次がTIFFバージョンで、「2A 00」となっています。
続いて、0th IFDというものが出てきます。IFDは、Image File Directoryの略で、付属情報のまとまりを表します。
一つのIFDの中に、幾つかのタグがまとめられているというイメージです。
データの種類によって、IFDが分けられています。
IFD内のデータはタグ毎に分けられていて、それぞれのタグは12バイトの長さと決まっています。
IFDのデータ構造は以下のようになっています。
0th IFD | タグ数 | 2バイト |
タグ1 | 12バイト | |
タグ2 | 12バイト | |
タグ3 | 12バイト | |
・・・ | ||
タグN | 12バイト | |
1st IFDへのオフセット値 | 4バイト | |
0th IFD内の値 | 可変 |
- 「タグ数」は、0th IFD内に何個のタグがあるかを表します。
- 「1st IFDへのオフセット値」は、続くIFD(0th IFDの場合は1st IFD、1st IFDの場合は2nd IFD)が始まるアドレスまでのオフセット値になります。(ここが0の場合は、次のIFDはないという意味になります)
- 「0th IFD内の値」は、各タグに割り当てられたバイト数で収まらないデータが入ります。(構造の仕組みは後ほど)
各タグの構成は以下のようになっています。
0th IFD | タグ1 | タグ番号 | 2バイト |
タイプ | 2バイト | ||
カウント | 4バイト | ||
値までのオフセット値 | 4バイト | ||
タグ2 | タグ番号 | 2バイト | |
タイプ | 2バイト | ||
カウント | 4バイト | ||
値までのオフセット値 | 4バイト |
- 「タグ番号」は、結構な種類があって、列挙しきれません。仕様書のこの辺りから始まるのを、照らし合わせていくのが、一番確実かと思いますが、結構やってられない感もあるので、Exifの取得に関係あるものは都度紹介していきます。
- 「タイプ」は、データ型です。型付けがある言語でのプログラミングを経験された方にはよく分かる、あれです。8種類しかないので、後ほどご紹介します。
- 「カウント」は、タグ内に何個のデータが収められているかを表します。
- 「値までのオフセット値」は、4バイトよりも大きいデータの場合、この中には入らない為、別途可変領域を割り当てて値を入れるようにする仕組みのため、そのデータへの場所へのオフセット値になります。例外があって、4バイト以下のデータはこのオフセット値用の部分にデータを入れてしまいます。
タイプを一覧にしてみました。
タイプによって、カウントの取り方が違うので、合わせて記載しました。
タイプの値 | 型 | 説明 | カウントの取り方 |
1 | BYTE | 8ビット符号なし整数 | |
2 | ASCII | 文字列。文字数には、文字列の終端を表す最後のNULLも含める | 「BEYOND」の場合は、「B」「E」「Y」「O」「N」「D」「\0」 → 7カウント |
3 | SHORT | 16ビット(2バイト)符号なし整数 | 5の場合は1カウント |
4 | LONG | 32ビット(4バイト)符号なし整数 | 5の場合は1カウント |
5 | RATIONAL | LONG2個分で、最初のLONGを分子、2個目のLONGを分母とする | 5/4の場合は1 |
7 | UNDEFINED | フィールドの定義により、どんな値をとってもいい8ビットのデータ | 0xFF、0x01、0x45、0x11、0xDD、で5カウント(8ビットで1カウントのため) |
9 | SLONG | 32ビット(4バイト)符号付き整数 | 5で1カウント |
10 | SRATIONAL | SLONG2古文で、最初のSLONGを分子、2個目のSLONGを分母とする | 5/4で1カウント |
表だけ見ても、イメージは沸かないと思うので、データを見ながら一つずつ見ていきましょう。
IFDを読んでみる
実際の写真データから、0th IFDの部分を抜き出してみました。
1 2 3 4 5 6 7 8 9 | 0B 00 0F 01 02 00 05 00 00 00 92 00 00 00 10 01 02 00 07 00 00 00 98 00 00 00 12 01 03 00 01 00 00 00 01 00 00 00 1A 01 05 00 01 00 00 00 A0 00 00 00 1B 01 05 00 01 00 00 00 A8 00 00 00 28 01 03 00 01 00 00 00 02 00 00 00 31 01 02 00 14 00 00 00 B0 00 00 00 32 01 02 00 14 00 00 00 C4 00 00 00 13 02 03 00 01 00 00 00 01 00 00 00 69 87 04 00 01 00 00 00 D8 00 00 00 25 88 04 00 01 00 00 00 2C 52 32 52 00 00 |
こちらのテーブルを頼りに、まずは先頭の2バイトを抜き出します。
「0B 00」の部分は、0th IFDに何個のタグが含まれているかを表す部分です。
このデータのバイトオーダーはリトルエンディアンなので、人間が読む場合は、0x000Bとなります。
つまり、11ですね。
なので、0th IFDには、11個のタグが含まれている、という事になります。
1つのタグは12バイトの固まりなので、12*11=132バイトがタグデータです。
タグを固まりごとに抜き出してみましょう。
1 2 3 4 5 6 7 8 9 10 11 | 0F 01 02 00 05 00 00 00 92 00 00 00 10 01 02 00 07 00 00 00 98 00 00 00 12 01 03 00 01 00 00 00 01 00 00 00 1A 01 05 00 01 00 00 00 A0 00 00 00 1B 01 05 00 01 00 00 00 A8 00 00 00 28 01 03 00 01 00 00 00 02 00 00 00 31 01 02 00 14 00 00 00 B0 00 00 00 32 01 02 00 14 00 00 00 C4 00 00 00 13 02 03 00 01 00 00 00 01 00 00 00 69 87 04 00 01 00 00 00 D8 00 00 00 25 88 04 00 01 00 00 00 2C 52 00 00 |
タグを要素ごとに切り出して、表にしてみます。
ついでに、タグ番号が何を表しているかを仕様書から調べて追記しています。
タグ番号 / タグ名 | タイプ | カウント | 値へのオフセット値 |
0F 01 メーカー |
02 00 ASCII |
05 00 00 00 カウント5 |
92 00 00 00 |
10 01 モデル |
02 00 ASCII |
07 00 00 00 カウント7 |
98 00 00 00 |
12 01 画像方向 |
03 00 SHORT |
01 00 00 00 カウント1 |
01 00 00 00 |
1A 01 画像の幅の解像度 |
05 00 RATIONAL |
01 00 00 00 カウント1 |
A0 00 00 00 |
1B 01 画像の高さの解像度 |
05 00 RATIONAL |
01 00 00 00 カウント1 |
A8 00 00 00 |
28 01 画像の幅と高さの解像度の単位 |
03 00 SHORT |
01 00 00 00 カウント1 |
02 00 00 00 |
31 01 ソフトウェア |
02 00 ASCII |
14 00 00 00 カウント20 |
B0 00 00 00 |
32 01 ファイル変更日時 |
02 00 ASCII |
14 00 00 00 カウント20 |
C4 00 00 00 |
13 02 YCCの画素構成(YとCの位置) |
03 00 SHORT |
01 00 00 00 カウント1 |
01 00 00 00 |
69 87 Exif IFDへのポインタ |
04 00 LONG |
01 00 00 00 カウント1 |
D8 00 00 00 |
25 88 GPS IFDへのポインタ |
04 00 LONG |
01 00 00 00 カウント1 |
2C 52 00 00 |
実際の値の取り方ですが、これにはパターンがあって、タイプがBYTE、SHORT、LONG、SLONGの場合、カウントが1の場合はオフセット部分に値が入っています。
ASCII、UNDEFINEDの場合も、カウントが4以下の場合はオフセット部分に値が入っています。
それ以外の場合は、ここを基準に、データのアドレスを計算してタイプとカウントの分だけ、データを取得します。
取得した結果が以下のようになります。
タグ名 | 値 |
メーカー | 53 6F 6E 79 00 Sony |
モデル | 53 4F 2D 30 34 48 00 SO-04H |
画像方向 | 1 |
画像の幅の解像度 | 48 00 00 00 01 00 00 00 72/1 |
画像の高さの解像度 | 48 00 00 00 01 00 00 00 72/1 |
画像の幅と高さの解像度の単位 | 2 |
ソフトウェア | 33 35 2E 30 2E 42 2E 32 2E 32 37 32 5F 30 5F 66 36 30 30 00 35.0.B.2.272_0_f600 |
ファイル変更日時 | 32 30 31 36 3A 31 30 3A 32 31 20 31 35 3A 32 30 3A 35 34 00 2016:10:21 15:20:54 |
YCCの画素構成(YとCの位置) | 1 |
Exif IFDへのポインタ | D8 00 00 00 |
GPS IFDへのポインタ | 2C 52 00 00 |
1st IFDへのオフセット値 | 32 52 00 00 |
となっています。私の携帯の機種がバレた訳ですが、たかがjpeg画像といえど、この程度の情報は簡単に取り出せるということですね。
では、Exif IFDの場所が分かったところで、本題のExif情報を見てみましょう。
Exif情報を読んでみる
手順は、0th IFDと全く同じです。Exif情報部分のバイナリを抜き出してみましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | 1C 00 9A 82 05 00 01 00 00 00 2E 02 00 00 9D 82 05 00 01 00 00 00 36 02 00 00 27 88 03 00 01 00 00 00 A0 00 00 00 00 90 07 00 04 00 00 00 30 32 32 30 03 90 02 00 14 00 00 00 3E 02 00 00 04 90 02 00 14 00 00 00 52 02 00 00 01 91 07 00 04 00 00 00 01 02 03 00 01 92 0A 00 01 00 00 00 66 02 00 00 04 92 0A 00 01 00 00 00 6E 02 00 00 07 92 03 00 01 00 00 00 05 00 00 00 08 92 03 00 01 00 00 00 00 00 00 00 09 92 03 00 01 00 00 00 10 00 00 00 0A 92 05 00 01 00 00 00 76 02 00 00 7C 92 07 00 70 4F 00 00 7E 02 00 00 90 92 02 00 07 00 00 00 EE 51 00 00 91 92 02 00 07 00 00 00 F6 51 00 00 92 92 02 00 07 00 00 00 FE 51 00 00 00 A0 07 00 04 00 00 00 30 31 30 30 01 A0 03 00 01 00 00 00 01 00 00 00 02 A0 04 00 01 00 00 00 60 17 00 00 03 A0 04 00 01 00 00 00 26 0D 00 00 05 A0 04 00 01 00 00 00 0E 52 00 00 01 A4 03 00 01 00 00 00 00 00 00 00 02 A4 03 00 01 00 00 00 00 00 00 00 03 A4 03 00 01 00 00 00 00 00 00 00 04 A4 05 00 01 00 00 00 06 52 00 00 06 A4 03 00 01 00 00 00 00 00 00 00 0C A4 03 00 01 00 00 00 00 00 00 00 00 00 00 00 |
タグ番号 | タイプ | カウント | 値へのオフセット値 / 実データ |
9A 82 露出時間 |
05 00 RATIONAL |
01 00 00 00 カウント1 |
2E 02 00 00 0A 00 00 00 40 01 00 00(10/320) |
9D 82 Fナンバー |
05 00 RATIONAL |
01 00 00 00 カウント1 |
36 02 00 00 14 00 00 00 0A 00 00 00(20/10) |
27 88 撮影感度 |
03 00 SHORT |
01 00 00 00 カウント1 |
A0 00 00 00 10 |
00 90 Exifバージョン |
07 00 UNDEFINED |
04 00 00 00 カウント4 |
30 32 32 30 0220 |
03 90 原画像データの生成日時 |
02 00 ASCII |
14 00 00 00 カウント20 |
3E 02 00 00 32 30 31 36 3A 31 30 3A 32 31 20 31 35 3A 32 30 3A 35 34(2016:10:21 15:20:54) |
04 90 ディジタルデジタルデータの作成日時 |
02 00 ASCII |
14 00 00 00 カウント20 |
52 02 00 00 32 30 31 36 3A 31 30 3A 32 31 20 31 35 3A 32 30 3A 35 34(2016:10:21 15:20:54) |
01 91 各コンポーネントの意味 |
07 00 UNDEFINED |
04 00 00 00 カウント4 |
01 02 03 00 その他(Y、Cb、Cr) |
01 92 シャッタースピード |
0A 00 SRATIONAL |
01 00 00 00 カウント1 |
66 02 00 00 F4 01 00 00 64 00 00 00(500/100) |
04 92 露光補正値 |
0A 00 SRATIONAL |
01 00 00 00 カウント1 |
6E 02 00 00 00 00 00 00 03 00 00 00(0/3) |
07 92 測光方式 |
03 00 SHORT |
01 00 00 00 カウント1 |
05 00 00 00 5(分割測光) |
08 92 光源 |
03 00 SHORT |
01 00 00 00 カウント1 |
00 00 00 00 0(不明) |
09 92 フラッシュ |
03 00 SHORT |
01 00 00 00 カウント1 |
10 00 00 00 ストロボ発光 |
0A 92 レンズ焦点距離 |
05 00 RATIONAL |
01 00 00 00 カウント1 |
76 02 00 00 A7 01 00 00 64 00 00 00(423/100) |
7C 92 メーカーノート |
07 00 UNDEFINED |
70 4F 00 00 カウント20336 |
7E 02 00 00 データ量が多いため割愛 |
90 92 DateTimeのサブセック |
02 00 ASCII |
07 00 00 00 カウント7 |
EE 51 00 00 36 38 34 37 37 32(684772) |
91 92 DateTimeOriginalのサブセック |
02 00 ASCII |
07 00 00 00 カウント7 |
F6 51 00 00 36 38 34 37 37 32(684772) |
92 92 DateTimeDegitizedのサブセック |
02 00 ASCII |
07 00 00 00 カウント7 |
FE 51 00 00 36 38 34 37 37 32(684772) |
00 A0 対応フラッシュピックスバージョン |
07 00 UNDEFINED |
04 00 00 00 カウント4 |
30 31 30 30 0100(Flashpix Format Version 1.0) |
01 A0 色空間情報 |
03 00 SHORT |
01 00 00 00 カウント1 |
01 00 00 00 sRGB |
02 A0 実効画像幅 |
04 00 LONG |
01 00 00 00 カウント1 |
60 17 00 00 5984 |
03 A0 実効画像高さ |
04 00 LONG |
01 00 00 00 カウント1 |
26 0D 00 00 3366 |
05 A0 互換性IFDへのポインタ |
04 00 LONG |
01 00 00 00 カウント1 |
0E 52 00 00 0E 52 00 00 |
01 A4 個別画像処理 |
03 00 SHORT |
01 00 00 00 カウント1 |
00 00 00 00 0 |
02 A4 露出モード |
03 00 SHORT |
01 00 00 00 カウント1 |
00 00 00 00 0(自動露出) |
03 A4 ホワイトバランス |
03 00 SHORT |
01 00 00 00 カウント1 |
00 00 00 00 0(ホワイトバランス自動) |
04 A4 デジタルズーム倍率 |
05 00 RATIONAL |
01 00 00 00 カウント1 |
06 52 00 00 64 00 00 00 64 00 00 00(100/100) |
06 A4 撮影シーンタイプ |
03 00 SHORT |
01 00 00 00 カウント1 |
00 00 00 00 0(標準) |
0C A4 被写体距離レンジ |
03 00 SHORT |
01 00 00 00 カウント1 |
00 00 00 00 0(不明) |
この後、GPS情報と続く訳ですが、GPS情報に至っては、どこで撮ったかすぐに分かってしまう訳です。
旅行先の写真などの場合は、撮った写真を地図上に表示したりすると面白いでしょうが、自宅で撮った写真がそうであると困ります。
Exif情報が無事読めて良かったなという達成感でいっぱいですが、PHPなら、read_exif_data関数で上記のデータは簡単に手に入ります。
手元にバイナリエディタとExif資料しかない場合に、この記事が参考になれば幸いです。
以上です。