デジカメで撮影した結果として、大量のjpegやら、Rawファイル(うちは、CanonのKissDNなのでCR2)が溜まってくる。
これらのファイルには、撮影時刻やら、撮影時のレンズ、絞りなどの各種カメラ情報が埋まっている。
これらのカメラ情報はEXIFという形式で埋まっているとのことで、一般的には、右クリックのプロパティやら、その他専用のツールを使って読みだすのだが、ふと思い立って、バイナリからEXIFを読んでみたのでそのメモ。
EXIFは、おおもとの考え方がTIFFフォーマットから来ているので、まず、TIFFの読み方から。
CIPA DC-008-2012 デジタルスチルカメラ用画像ファイルフォーマット規格Exif 2.3
ちなみに、CanonのKissDNのRAWファイルフォーマットCR2は中身がTIFFの構造を取るので、この読み方をすれば、大体読める。
サイズ(byte) | コメント |
8バイト | ヘッダー |
任意 | … |
任意 | IFD |
任意 | … |
任意 | IFD |
任意 | … |
TIFFは複数の画像を一つのファイルに含めることができる。一つの画像をIFD(Image File Directory)の中に埋め込む。 ヘッダーやIFDの最後に4バイトで次のIFDへのオフセットが埋まる形で、リストを形成し、IFDの間には任意のデータ領域を確保する。
ヘッダー
サイズ(byte) | コメント |
2 | “MM”(0x4D4D) : Big Endian または “II”(0x4949) : Little Endian |
2 | バージョン番号 (0x002A) |
4 | 最初のIFDへのオフセット |
MMはMotorolaのM、IIはIntelのI。このヘッダーで以降の数値をBig Endianで読むか、Little Endianで読むかが決定される。
バージョン番号は、0x002Aで固定。
ヘッダーの最後に4バイトで次のIFDへのオフセットが埋め込まれる。オフセットは、ヘッダーの先頭(すなわちファイルの先頭)からのバイト数となる。
IFD (Image File Directory)
サイズ(byte) | コメント |
2 | エントリーの数 |
12 x n | エントリー x 12 |
4 | 次のIFDへのオフセット |
ひとつの画像を表す
エントリーの数は、バイト数ではなくて数。
一つのエントリーは12バイト固定長。
最後に4バイトで次のIFDへのオフセットが埋め込まれる。オフセットは、ヘッダーの先頭(すなわちファイルの先頭)からのバイト数となる。もし次のIFDがない場合、すなわち現在のIFDが最後のIFDの場合、0x00000000が埋められる。
エントリー
サイズ(byte) | コメント |
2 | タグ |
2 | 値の型(タイプ) |
4 | 値の数(カウント) |
4 | 値または値へのオフセット |
12バイトで一つのエントリーを表す。
タグはこのエントリーがなんの値を示すのかの種別を表す。たとえば、このエントリーは画像の幅を表す(0x0100)とか、このエントリーは撮影日時を表す(0x9003)とか。。
詳細は、TIFF 6.0 Specification (PDF: 385k)などに記載されている。また、EXIF規格のCIPA DC-008-2012でも調べることができる。
値の型(タイプ)は以下のとおり
Exifで用いるタイプは以下のとおりである。(CIPA DC-008-2012) 4.6.2 IFDの構造より
値 | 型(タイプ) | コメント |
1 | BYTE | 8 ビット符号無し整数。 |
2 | ASCII | 一つの 7 ビット ASCII コードを納めた 8 ビットバイト。最後のバイトは NULL で終端する。ASCII のカウントは NULL も含めた値とする。 |
3 | SHORT | 16 ビット(2 バイト)符号無し整数。 |
4 | LONG | 32 ビット(4 バイト)符号無し整数。 |
5 | RATIONAL | LONG2個。最初のLONGは分子、2個目のLONGは分母を表す。 |
7 | UNDEFINED | フィールドの定義により、どんな値をとってもよい8ビットバイト。 |
9 | SLONG | 32 ビット(4 バイト)符号付き整数(2 の補数表現)。 |
10 | SRATIONAL | SLONG2個。最初のSLONGは分子、2個目のSLONGは分母を表す。 |
TIFFの方は(TIFF 6.0 Specification (PDF: 385k)) Image File Directory より
1,2,3,4,5を定義し、それ以外に、6,7,8,9,10,11,12をTIFF6.0で新しく定義する。
値 | 型(タイプ) | コメント |
6 | SBYTE | An 8-bit signed (twos-complement) integer. |
7 | UNDEFINED | An 8-bit byte that may contain anything, depending on the definition of the field. |
8 | SSHORT | A 16-bit (2-byte) signed (twos-complement) integer. |
9 | SLONG | A 32-bit (4-byte) signed (twos-complement) integer. |
10 | SRATIONAL | Two SLONG’s: the first represents the numerator of a fraction, the second the denominator. |
11 | FLOAT | Single precision (4-byte) IEEE format. |
12 | DOUBLE | Double precision (8-byte) IEEE format. |
ここで扱いがめんどくさいのが、RATIONALとSRATIONAL。いわゆる分数。C++やC#に分数の型がないので自前で用意する必要がある。
ASCIIに関しては、7ビットASCIIコードと記載されているので、日本語は通らない(通さない)規格と思われる。
値の数(カウント)
値の個数。カウントはバイト数の合計ではないので注意が必要である。例えば SHORT(16 ビット)の値ひとつの場合には、2Byte であるがカウントは“1”である。
(CIPA DC-008-2012) 4.6.2 IFDの構造より
分数(RATIONALとSRATIONAL)も同様。めんどくさい。
値または値へのオフセット
TIFFヘッダの先頭から値本体の記録位置へのオフセットを記 する。ただし、値が 4Byte に納まる場合には、値そのものを記録する。値が 4Byte より小さいときは、4Byte のエリアに左詰で、つまりバイトオフセットの小さい 領域から値を納める。例えば、ビッグエンディアン形式でタイプが SHORT、値が 1 の場合には、00010000.H を記録する。
(CIPA DC-008-2012) 4.6.2 IFDの構造より
この文章が最初何を言っているのかわからんかった。要するに、
値が 4Byte の領域におさまるときは、4バイトの領域におさめる。おさまらない場合は、別領域に値をおさめ、その領域へのオフセットを4バイトの領域におさめる。
4バイトの領域におさめるときは、4Byte のエリアに左詰で、つまりバイトオフセットの小さい 領域から値を納める。例えば、ビッグエンディアン形式でタイプが SHORT、値が 1 の場合には、00010000.H を記録する。
ということのようだ。
以上で、TIFFの基本はおしまい。
で、実際のTIFF画像の場合、0x0111 StripOffsetsに、画像データのStripごとのオフセットが埋まっているので、こいつをちまちまアクセスして画像データを取り出す。
Exif情報の本体は、タグ=34665 (0x8769)に埋められている。ここには、オフセットが埋まっていて、そのオフセットの先に、Exif IFDが始まる。このExif IFDもただのIFDと同じ読み方で読める。タグはEXIF用に拡張されているので、EXIF規格のCIPA DC-008-2012を参照する。
同様に、GPS情報は、タグ=34853(0x8825)に埋められているし、互換性IFDとして、タグ=40965(0xA005)にも情報が埋まる。
また、タグによっては、値のフォーマットが規定されている場合があるので、ちゃんと規格書を参照するべし。
例えば、日時
ファイル変更日時 DateTime
画像の作成された日付と時間。本規格では、ファイル変更日時として用いる。フォーマットは “YYYY:MM:DD HH:MM:SS”。時間は 24 時間表示し、日付と時間の間に空白文字を 1 つ埋める。日時 不明の場合は、コロン“:”以外の日付・時間の文字部を空白文字[20.H]で埋めるか、または、すべてを空白文字で埋めるべきである。文字列の長さは、NULL を含み 20Byte である。記載が無いときは不明として扱う。
(CIPA DC-008-2012) 4.6.2 IFDの構造より
などなど
ここまで知っていればとりあえず、TIFF内に埋まっている、EXIF情報、その他属性情報を引っ張り出せる。
その他、注意する点として、
- 文字列はASCIIのみっぽい。
- 日時時刻は文字列で埋まる。時差に関する情報がない。
次に、JPEG内に埋まっているEXIFの読み方
JPEGのファイルフォーマットを理解している人向けに、ぶっちゃけて書いてしまえば、
SOIの直後にAPP1を入れ、ここにMMまたはIIから始まるTIFFを画像情報抜きで埋め込む。
そんだけ。
極端な方法でいけば、JPEGの場合、最初のバイトが、0xFF,0xD8(SOI)として、ファイルの先頭から12バイト進んだところから、TIFFとして読みこめば読めてしまう。
具体的には、
値 | バイト数 | コメント |
0xFF,0xD8 | 2 | SOI (画像開始) |
0xFF,0xE1 | 2 | APP1 (アプリケーションデータセグメント1) |
2 | Lp パラメータ長 | |
0x45,0x78,0x69,0x66 | 4 | Exif |
0x0,0x0 | 2 | Null 2個 |
と並んだあとに、TIFFヘッダーのMMかIIが来る。
APP1はSOIの直後にこなければならないとのことなので、この並びでほぼ読める。
ポイントとしては、TIFF内のオフセットの表現は、ファイルの先頭からのバイト数ではなくて、TIFFが始まるポイントからの相対オフセットになっているので、注意。TIFFのヘッダが12バイト目から始まるのであれば、TIFFのオフセットとして埋まっている値に、12バイトを足せばファイルの先頭からの距離となる。