JPEGファイルから撮影情報を取得する
macOS Mojava 10.14.6 / Xcode 11.3.1 / Swift 5.0
概要
スマホやデジタルカメラで撮影した JPEGファイルには、画像データ以外に様々な情報が保存される。これらのうち撮影に関する情報(カメラの機種名、露光時間や F値、撮影日時、GPSの位置データなど)は Exifフォーマット規格(Exchangeable Image File Format)に従って JPEGファイルの一部として格納される。
本アプリケーションは JPEGファイルを解析し、撮影日時と GPS情報の2種類を抜き出して表示するものである。
操作方法
アプリケーション ExifAnalyzer を起動する。
![[image8.png]](image8.png)
Finderから JPEGファイル(複数可)をドラッグし、テーブルビューにドロップすると自動的にExif解析処理が実行される。
![[image9.png]](image9.png)
テーブルビューの表示項目
- file:ファイル名
- 作成日時:ファイルの作成日時(写真の撮影日時に等しい)
- 緯度/軽度:GPS位置情報を緯度/軽度で表示する
- 結果:処理結果を表示する。解析が成功した場合は○を、何らかの原因で失敗した場合はその理由を簡単なキーワード(内容は後述)で表示する。
- path:ファイルの格納フォルダ
JPEGファイルの構造
JPEGファイルは、ブロック化されたデータの集合体であり、複数のセグメントとイメージデータから構成される。
データはスタートマーカ(SOI)で始まり、複数のセグメント、イメージデータ本体と並び、最後はエンドマーカ(EOI)で終わる。
マーカは 2バイトのデータで、1バイト目は 0xFF、2バイト目はマーカの種類を表すコードになる。一般的には 3〜4文字の略称で表す。
セグメントは、マーカ(2バイト)、セグメント長(2バイト)、セグメントデータから構成される。
セグメントは次の2種類に大別される。
画像の表示に必須のセグメント
JPEGファイルに必須な JPEG規約で定められたセグメントで、データの圧縮方法や画質に関する情報が設定される。これがないと画像を表示することができない。以下のデータから構成される。
- DQT:量子化テーブル
- DHT:ハフマンテーブル
- SOF:フレームヘッダ
- SOS:スキャンヘッダ(イメージデータの先頭に置かれる)
APPセグメント
ユーザー(撮影機器や画像編集ソフトなどの製造者)が独自に仕様を定義しデータを格納することができる領域である。オプションであり、JPEGファイルの構成において必須な要素ではない。APP0から APP16までの 16個の APPセグメントが使用可能である。
APPセグメントの中で特に一般的なのが APP1セグメント(Exif規格)で、ここには撮影に関する種々の情報が Exifフォーマット規格に従って格納される。この規格は、日本の JEITA(電子情報技術産業協会)で制定されたものである。保存する撮影情報は多岐にわたるので、詳しくは「一般社団法人カメラ映像機器工業会」のサイトからダウンロードできる規格書を参照ください。なおこの規格は、現在は Apple Googleなど世界的な企業が採用する事実上の世界標準になっている。
セグメント構成
Exif解析処理
本アプリケーションの処理を順番に説明していく
JPEGファイルを読み込む
JPEGファイルをバイトストリームとして読み込み Dataオブジェクトを作成する。
[ ポイント ]
Dataオブジェクトは UInt8を要素とした配列に型変換することで、バイト単位の操作を行うことができる。
APP1(Exif規格)セグメントを探索する
ファイルの先頭からマーカを辿り、APP1セグメントの先頭位置を求める。
セグメントのマーカ(2バイト)の後ろのセグメント長(2バイト)は、セグメント全体のサイズをバイト数で持つ。サイズには、マーカとセグメント長の領域(計4バイト)も含む。セグメント長はセグメントのオフセットなので、これを辿っていけば、順々に並んだセグメントの先頭アドレスを順々に取得していくことができる。
セグメント並び順
JPEG規格にはセグメントの並び順のに関する規定はないので、理屈上は自由に並べることは可能。しかし一般的には、ファイルの先頭にAPPセグメントを置き、その後ろに JPEGファイル必須のセグメント(セグメント量子化テーブル等)を続け、最後にイメージデータ本体を置く。
現在では、デジタルカメラ(スマホも含む)で撮影した JPEGファイルには撮影情報として APP1セグメント(Exif規格)が必ず作成される(事実上の標準仕様といえる)。ただし、一部の機種では、歴史的経緯もあり、APP0セグメント(JFIF規格)というものと APP1セグメントの両方に持つものもある。
これらのセグメント(APP0、APP1)は JFIF規格/Exif規格により、スタートマーカ(SOI)の直後に配置することが 「推奨」されている。実際のところ、APP1を持つ JPEGファイルは、全てスタートマーカの直後にこれを置く。APP0がある場合は、番号順の並びについての推奨は特にないが、APP0 → APP1という並びになるのが普通である。
セグメントの並びがこのように固定されるのが確実なら、APP1セグメントを取得するロジックは、場所が決め打ちできるので簡単である。
しかしながら、あくまでもこれは推奨なので、並びが異なる可能性は排除できない。そのため本アプリケーションでは、セグメントの並びがどうなろうと、確実に APP1セグメントが取得できるような汎用的なロジックで実装することにした。
ファイル形式のチェック
ファイルの先頭 2バイトがスタートマーカ(SOI)であることをチェックする。そうでない場合は、JPEG形式のファイルでないのでここではじく。
APP1セグメントを取得する
スタートマーカ(SOI)の直後からセグメントを探索し、 APP1セグメント(マーカ FFE1)が見つかった時点で終了する。このあと Exifデータの解析処理に進む。
ファイルに APP1セグメントが作成されていない場合は、APP1セグメントが見つからないまま SOSマーカ(画像データの直前に置かれるスキャンヘッダ)に到達する。画像編集ソフト(Windowsペイントなど)で作成したJPEGファイルでは、このようになる。
セグメントの探索で、セグメントの先頭のマーカが JPEG規格で定義されたマーカ値と異なる場合、JPEGファイルが壊れている可能性があるので、そこで探索を終了する。マーカは、FF01 〜 FFFE の範囲になければならない。
数値の扱い
JPEGファイルの構造を解析するには、データをバイトのストリームとして読み込む必要がある。Data型のオブジェクトして読み込んだ JPEGファイルのデータは UInt型の配列として扱うことができるので、比較的操作はわかりやすい。ただし、数値(10進数、浮動小数点)の扱いには注意が必要で、2バイト以上で構成される数値は、エンディアンに応じた読み方をしないと、正しい数値を得ることができない。
JPEGファイルの数値(2バイトのセグメント長など)のエンディアンは規格によりビッグエンディアンと決まっている。一方、APP1セグメント(Exif)の中のデータは、データを作成する機器・ソフトウェアにより数値のエンディアンが異なる。データのヘッダ部にエンディアンを識別するコードが設定されるので、それを参照して数値を読み込む方法を変える必要がある。
本アプリケーションでは数値を読み込むために、IntManagerクラス のメソッドを使用する。詳細については、エンディアンの変換 を参照のこと。
APP1セグメントの構造
![[image3.png]](image3.png)
マーカとセグメント長の直後に 6バイトの Exif識別コード(ヌルで終端する文字列 "Exif")が続く。これをチェックし異なっていればエラーとする。原因はわからないがこのケースに遭遇したことはある。
識別コードの後ろには バイトオーダ、固定値(0x002A)、オフセットから構成される Tiffヘッダが続く。バイトオーダには整数のエンディアンが設定される。これは Exifセグメントデータを作成する機器・ソフトによりエンディアンが異なるためである。ビッグエンディアンの場合 0x4D4D、ビッグエンディアンの場合 0x4949 となる。エンディアンは直後オフセットの値から適用される。オフセットは(後述する)0thIFDの先頭を指す。アドレスの始点は Tiffヘッダの先頭になる。0thIFDは Tiffヘッダの後ろに続くのでオフセットの値は常に 8 になる。
Exifセグメントは 5個のIFD(Image File Directory)から構成される。0thIFD をトップレベルとした階層構造で 0thIFDは、1stIFD、ExifIFD、GPSIFDを子ノードとして持ち、ExifIFDは、互換性IFDを持つ。
0thIFDには画像に関する基本的な情報、1stIFDにはサムネイルに関する情報が格納される。ExifIFDには撮影情報、GPSIFDには位置情報が格納され、本アプリケーションの対象となる。Tiffヘッダの先頭からオフセットの値を移動した地点が最初の IFDである 0thIFDの起点になる。
IFDの構造
![[image4.png]](image4.png)
IFDは関連するデータの集まりで、複数のフィールドを持つ。IFDの最初の 2バイトがフィールドの件数になる。次IFDへのオフセットは 0thIFD から 1stIFDへの接続だけ設定される。
IFDフィールドの構造
![[image5.png]](image5.png)
フィールドは12バイト固定長で タグ、タイプ、カウント、値(または値へのオフセット)からなる。
タグは、値の種類を表すコード(2バイトの整数)である。
タイプは、データ型を表すコード(2バイトの整数)である。種類は、(1) 文字、(2) 整数、(3)分数がある。整数は符号の有無、バイト数により細分化する。分数は、分母と分子に相当する2個の整数で表され、これも整数の種類により細分化する。
カウントは値の件数である。
値のサイズは4バイトである。値が 4バイト以内であればここに設定される。4バイトを超える場合は値は別の領域に設定され、値にはそこまでのオフセット(バイト数)が入る。アドレスの起点は Tiffヘッダの先頭となる。値の必要バイト数は、ひとつの値の長さ(バイト数) × カウントとなる。
2バイト以上の整数値は、エンディアンが実行マシンのそれと異なる場合は変換が必要になる。
各項目の詳細については、一般社団法人カメラ映像機器工業会発行による規格書を参照されたい。
撮影日時取得する
日時は 19文字固定の文字列で、年月日、時分秒をコロンと空白で区切って表示する。具体的には 2024:05:11△06:31:14 という表現になる。月日や時刻が一桁の場合は先頭にゼロを埋める。△は空白文字である。
IFDの階層
![[image6.png]](image6.png)
撮影日時(Exifでは「原画像データの生成日時」と定義されるが本章では分かりやすさ優先してこう呼ぶ)は、ExifIFDというレコード群に保存されている。ExifIFDの先頭アドレスは、Tiffヘッダを起点にして、0thIFDのタグ「34665」を経由し辿ることができる。ExifIFDのタグ「36867」のレコードに影日時が保存される。
入力ファイルのデータは一つの Dataオブジェクトとして読み込まれているので、撮影日時を読み込むには、Dataオブジェクトを文字列に変換する必要がある。
まず、全体の Dataオブジェクトから、撮影日時が格納されている部分の Dataオブジェクトを抜き出す。撮影日時は ExifIFD上では 20バイトだが、最後の 1バイトはヌル終端文字列なので、19バイト分のデータだけが必要。これをバイト(UInt8)の配列として読み込み、Stringクラスのイニシャライザに指定すれば文字列を得ることができる。
GPS位置情報を取得する
緯度/経度の Exifデータは度分秒方式の「度」「分」「秒」 に対応した値が 3組の分数に書き込まれる。「度」「分」は分母が必ず1で常に整数になり、残余の小数が「秒」の分数に書き込まれる。
本アプリケーションでは、緯度/経度は「度分秒方式」と「十進度方式」の2種類を出力する。
度分秒方式は、次のような文字列(String型)に編集する。
36°4'46.098"N
"N" は北緯を示す記号、"S" は南緯、"E" は東経、"S"は西経を表す。
十進度方式は、度分秒方式の値を不動小数点(Double型)に変換する。有効値として小数点以下3桁までとする。
36.079
IFDの構造
![[image7.png]](image7.png)
GPS位置情報(撮影地点の緯度と経度)は、もし作成されていれば、GPSIFDというレコード群に保存されている。GPSIFDの先頭アドレスは、Tiffヘッダを起点にして、0thIFDのタグ「34853」を経由し辿ることができる。GPSIFD タグ「1〜4」のレコードに位置情報が保存されている。
タグ 1:緯度の南北を示す文字列
1バイト目に北緯の場合は'N'、南緯の場合は'S'が入る。2バイト目はヌル終端文字列になる。
タグ 2:緯度(度分秒)
3個の分数からなり、それぞれ度・分・秒を表す。分数(タイプ=RATIONAL)は、2個の LONG型(4バイト)の整数から構成され、それぞれが分母と分子に相当する。実際の値はオフセット先に格納される。
タグ 3:経度の東西を示す文字列
1バイト目に東経の場合は'E'、西経の場合は'W'が入る。2バイト目はヌル終端文字列になる。
タグ 4:経度(度分秒)
3個の分数からなり、それぞれ度・分・秒を表す。
具体例
例えば、緯度が 36°4'46.098"(36度 4分 46.098秒)であれば、分数の値はそれぞれ、度は 36/1、分は 4/1、秒は 46098/1000 となる。度・分の分母は 1で値は常に整数になる。秒の単位は、ほぼ全ての機種で 1/1000 であるが、現状の技術レベルでは、これ以上の精度を出す一般的なカメラは存在しない。(ちなみに 1/1000秒は 約3cm!である)
クラス一覧
JPEGファイルを解析するクラス
jpegInfoメソッドは、本アプリケーションの主要処理である。引数に解析したいJPEGファイルのパス名を指定し、そこから Exifデータの撮影日時と GPS位置情報を抜き出し、InfoRecord構造体のレコードに設定し、戻り値として返す。
メソッド宣言
InfoRecord構造体の定義
エンディアンを操作するユーティリティクラス
UIコントロールの制御
テーブルビューを制御するクラス