2013年08月30日

Exif データにアクセスするコードを自作してみる

はてなブックマークに登録

Exif (Exchangeable image file format) は現在出回っているほとんどのデジタルカメラや携帯電話・スマートフォン等での撮影画像のファイル形式として利用されています。画像データそのものに加え機器本体や撮影時の設定や環境など多くの情報が記録されることを活かしたソフトウェアやサービスも増えていますね。

先日ちょっとしたアプリを書いていた折に JPEG ファイル内の Exif データを参照する処理が必要になりました。初めての機会だったのでやり方を調べたところ 開発環境向けに用意されたこれこれのライブラリを使うのが常道とのことで、リファレンスを読みながらそういうコードを書きました。プログラムは期待通りに動作しその時はそれで特にどうと言うこともなかったのですが、後日別のプラットフォームで Exif データを利用するアイディアを思いつき、その環境でまた別のライブラリの使い方など調べているうちに何だか面倒になりました。

ここでのターゲットはあくまでも一介のデータ形式です。既存のソフトウェア資産を利用することは生産性の上でも効率の面でも間違った選択ではないものの、「普通のファイルに記録されたデータ」にアクセスするための道具立てにいろいろ左右されることが煩わしく感じられたのです。Exif の規格は公開されているため仕様を確認して必要なコードを自作すれば今後この形式を既存のライブラリごしに「ブラックボックス」として扱う必要はなくなるはずで、また、自作のコードであれば拡張や応用の融通が利きやすく必要なら別のプログラム言語への移植も楽でしょう。

デジタルカメラという今日ではごく身近な道具を通じてその存在を知りながら中身をずっと知らずにいた Exif のしくみに触れることはそれ自体も興味ぶかい経験でした。その一端をご紹介します。C 言語で試作したソースコード一式を記事の最後に掲載しています。

Exif の仕様

まず Exif のデータ形式の仕様をできるだけコンパクトに整理してみます。もっとも詳しく正確なのはもちろん公式の規格書ですから不明な点があればそこでの記述を参照して下さい。

規格書

電子情報技術産業協会 (JEITA) ホームページ上の「JEITA 規格総合検索」フォームから、「規格名」に "Exif" を指定して検索を実行すると「JEITA CP-3451C デジタルスチルカメラ用 画像ファイルフォーマット規格 Exif 2.3」を電子文書として閲覧できます。 ※ 2013年8月31日現在

一般社団法人 電子情報技術産業協会(JEITA) 公式サイト

全体の構造

下の図は規格書をもとに JPEG ファイル中の Exif データの構造をまとめてみたものです。要点をピックアップしてみます。

  • Exif データは JPEG ファイル内に配置可能な「アプリケーションセグメント」のひとつである APP1 セグメントに格納される
  • 当該 APP1 セグメントが Exif のセグメントであるか否かは当該セグメントのヘッダ内容で識別可能
  • Exif セグメント内に格納された数値データのバイトオーダーは big-endian, little-endian の両方があり得る
  • Exif セグメントには一件以上の IFD (Image File Directory) が含まれる
    • 0th IFD:主画像に関する付属情報を保持(Exifセグメントに必ず存在)
    • 1st IFD:主画像のサムネイルに関する情報およびサムネイル画像データを保持
    • Exif IFD:Exif 固有の付属情報を保持
    • GPS IFD:GPS 等の位置情報を保持
    • Interoperability IFD:互換性を保証するための情報を保持
  • 各 IFD は任意の数のタグフィールドを保持する
  • タグの種類は規格で定義されており各タグには識別用の番号が割り当てられている
  • 各タグの保持する値には複数の種類がありそのタイプとデータ長によって値の格納方法が規定されている

タグフィールドの扱い方

どこにどのような情報が配置されているかは上の図を追えば把握できるでしょう。構造を順に辿って最後に到着するのは具体的なデータの格納された末端のタグフィールドです。もちろんこれらが最も重要な要素であり、ここではタグの値にアクセスする方法を説明します。

フィールドの構成
一件のタグフィールドには以下の内容が含まれます
  • タグ 番号    2 バイト    所定のタグを識別するための番号
  • タイプ    2 バイト    タグの値のデータタイプを示す
  • カウント    4 バイト    値の個数
  • オフセット    4 バイト    値の格納された位置
タグ番号とそのタイプ・カウントの定義および各タグの詳細は規格書に掲載されており、所定のタグ番号とその意味との紐づけにはここでの情報を使用します。以下に記事の例を示します。
タグ名称         Field Name   タグ番号(Hex)   タイプ    カウント
Exif バージョン  ExifVersion     9000 	    UNDEFINED 	   4
色空間情報       ColorSpace      A001         SHORT        1
  :               :           :            :         :

■ Exif バージョン ExifVersion
 本規格での対応バージョンを示す。このフィールドが存在しなければ,本規格に準拠していない
 と判断される。本規格に準拠する場合には,4Byte の ASCII "0230" を記録しなければならない。
 Type が UNDEFINED のため最後に NULL は記録してはならない。
    Tag     =  36864 (9000.H)
    Type    =  UNDEFINED
    Count   =  4
    Default =  "0230"

■      :
    :
タイプについて

タグの値には以下のタイプがあります。値の取得方法は後述のカウント・オフセットのルールとの組合せで決まります。

  • 1 = BYTE      8 ビット符号なし整数
  • 2 = ASCII     NULL 文字で終端する ASCII 文字列。ASCII のカウントは NULL 文字分を含む
  • 3 = SHORT     16 ビット符号なし整数
  • 4 = LONG     32 ビット符号なし整数
  • 5 = RATIONAL     LONG 2 個で表現する値。ひとつめの LONG は分子、ふたつめは分母を表す
  • 7 = UNDEFINED     任意のバイト列
  • 9 = SLONG     32 ビット符号つき整数
  • 10 = SRATIONAL     SLONG 2 個で表現する値。ひとつめの LONG は分子、ふたつめは分母を表す
カウントについて

カウントは「値の個数」を示します。「バイト数の合計」を表すものではありません。以下に例を示します。

  • ASCII タイプの値 "Apple\0" のカウントは 6
  • SHORT タイプの値 10 のカウントは 1
  • LONG タイプの値 10 のカウントは 1
  • LONG タイプの値 10, 11 のカウントは 2
  • RATIONAL タイプの値 3/5 のカウントは 1 (※ 2 ではない)
  • SRATIONAL タイプの値 3/5, 1/2 のカウントは 2
  • UNDEFINED タイプの値 0x03, 0x21, 0x00, 0xFF のカウントは 4
オフセットについて

4 バイトのオフセット領域にはタグの値が記録されている位置(上図中の★が起点)が保持されています。ただし、値が 4 バイト以内で表現できる場合はその値がオフセット領域に直書きされます。4 バイト以内の値の例を以下に示します。

  • BYTE タイプの値 1    -> 1 バイト
  • BYTE タイプの値 1, 3, 5, 7    -> 4 バイト
  • ASCII タイプの値 "abc\0"    -> 4 バイト
  • SHORT タイプの値 10    -> 2 バイト
  • SHORT タイプの値 10, 11    -> 4 バイト
  • LONG タイプの値 10    -> 4 バイト
  • UNDEFINED タイプの値 0x03, 0x21, 0xFF    -> 3 バイト
さらに、値の長さが 4 バイト未満の場合、値はオフセット領域に「左詰め」で格納されます。以下の要領です。

■ BYTE タイプの値 1    

  0   1   2   3
┌─┬─┬─┬─┐
│01│00│00│00│ データのバイトオーダーがビッグエンディアンの場合
└─┴─┴─┴─┘
┌─┬─┬─┬─┐
│01│00│00│00│ データのバイトオーダーがリトルエンディアンの場合
└─┴─┴─┴─┘

■ SHORT タイプの値 1, 3     

  0   1   2   3
┌─┬─┬─┬─┐
│00│01│00│03│ データのバイトオーダーがビッグエンディアンの場合
└─┴─┴─┴─┘
┌─┬─┬─┬─┐
│01│00│03│00│ データのバイトオーダーがリトルエンディアンの場合
└─┴─┴─┴─┘

データの実例

図は Exif セグメントを含むある JPEG ファイル冒頭部分の HEX ダンプイメージです。上述のデータ構造とデータ形式を踏まえてここから読み取られる 0th IFD の内容を併記しています。

試作したコード

C 言語で書いたソースコード一式を以下で公開しています。

ソースコード: GitHub - exif

Windows 用実行形式 (x86)


  • 現在のバージョンには以下の機能があります
    • 所定の JPEG ファイルの Exif セグメント中の一部またはすべての IFD の内容をダンプ表示
    • 指定された IFD タイプ+タグ番号に該当するタグフィールドの情報を取得
    • 所定の JPEG ファイルから Exif セグメントを除去した新しい JPEG ファイルを出力

  • exif.c, exif.h の二本が実体で sample_main.c は関数呼び出しのサンプルです。
    サンプルプログラムのビルドは以下の要領です
    • gcc の場合:  gcc -o exif sample_main.c exif.c
    • Microsoft Visual C++ の場合:  cl.exe /o exif sample_main.c exif.c

  • 標準ライブラリ関数のみを使っているため多くの環境での利用が可能です。手元では以下の組合せでの動作を確認しました
    • Windows XP 32bit + Microsoft Visual C++ でビルドした 32bit バイナリ
    • Windows 7 64bit + Microsoft Visual C++ でビルドした 64bit バイナリ
    • Redhat Linux 32bit + 32bit 版 gcc でビルドしたバイナリ
    • Mac OS X 64bit + 64bit 版 gcc でビルドしたバイナリ
  • 動作確認用の test.jpg をサンプルプログラムで処理した結果を以下に引用します
$ ./exif test.jpg

[test.jpg] createIfdTableArray: result=4

{0TH IFD}
 - Make: [Apple]
 - Model: [iPod touch]
 - Orientation: 1
 - XResolution: 72/1
 - YResolution: 72/1
 - ResolutionUnit: 2
 - Software: [6.1.4]
 - DateTime: [2013:09:01 09:49:00]
 - YCbCrPositioning: 1
 - ExifIFDPointer: 206
 - GPSInfoIFDPointer: 576

{EXIF IFD}
 - ExposureTime: 1/30
 - FNumber: 12/5
 - ExposureProgram: 2
 - PhotographicSensitivity: 400
 - ExifVersion: 0 2 2 1
 - DateTimeOriginal: [2013:09:01 09:49:00]
 - DateTimeDigitized: [2013:09:01 09:49:00]
 - ComponentsConfiguration: 0x01 0x02 0x03 0x00
 - ShutterSpeedValue: 4035/821
 - ApertureValue: 4845/1918
 - BrightnessValue: 2234/1113
 - MeteringMode: 5
 - Flash: 32
 - FocalLength: 77/20
 - FlashPixVersion: 0 1 0 0
 - ColorSpace: 1
 - PixelXDimension: 960
 - PixelYDimension: 720
 - SensingMethod: 2
 - ExposureMode: 0
 - WhiteBalance: 0
 - FocalLengthIn35mmFormat: 32
 - SceneCaptureType: 0

{GPS IFD} -- GPS 経由で得られた撮影地点の座標情報
 - GPSLatitudeRef: [S]
 - GPSLatitude: 69/1 17/100 0/1
 - GPSLongitudeRef: [E]
 - GPSLongitude: 39/1 35/100 0/1
 - GPSAltitudeRef: 0
 - GPSAltitude: 6151/470
 - GPSTimeStamp: 0/1 48/1 3921/100

{1ST IFD} -- 埋め込みサムネイルの情報 〜 サムネイル画像データへ続く
 - Compression: 6
 - XResolution: 72/1
 - YResolution: 72/1
 - ResolutionUnit: 2
 - JPEGInterchangeFormat: 840
 - JPEGInterchangeFormatLength: 8648

0th IFD : Model = [iPod touch]
Exif IFD : DateTimeOriginal = [2013:09:01 09:49:00]
GPS IFD : GPSLatitude = 69/1 17/100 0/1
removeExifSegmentFromJPEGFile: result=1

$ ./exif _noexif.jpg
[_noexif.jpg] does not seem to contain the Exif segment.


(tanabe)
klab_gijutsu2 at 14:55│Comments(2)TrackBack(0)win | mac

トラックバックURL

この記事へのコメント

1. Posted by odaba   2017年06月24日 19:42
上記、URLでQtでexitデータをアクセスするコードを公開しました。本コードを移植しました。DSAS開発者の皆様のお陰です。ありがとうございます。
2. Posted by tanabe   2017年06月26日 07:25
odabaさん こんにちは。記事がお役に立ち幸いです。

この記事にコメントする

名前:
URL:
  情報を記憶: 評価: 顔   
 
 
 
Blog内検索
Archives
このブログについて
DSASとは、KLab が構築し運用しているコンテンツサービス用のLinuxベースのインフラです。現在5ヶ所のデータセンタにて構築し、運用していますが、我々はDSASをより使いやすく、より安全に、そしてより省力で運用できることを目指して、日々改良に勤しんでいます。
このブログでは、そんな DSAS で使っている技術の紹介や、実験してみた結果の報告、トラブルに巻き込まれた時の経験談など、広く深く、色々な話題を織りまぜて紹介していきたいと思います。
最新コメント