JPEGファイルから撮影情報を取得する

macOS Swift

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種類を抜き出して表示する。

操作方法

アプリケーションを起動する。
[image8.png]
Finderから JPEGファイル(複数可)をドラッグし、テーブルビューにドロップする。Exif情報が格納されていれば撮影日時とGPS情報の位置データを表示する。
[image9.png]

JPEGファイルの構造

JPEGファイルは、ブロック化されたデータの集合体であり、複数のセグメントパラメータとイメージデータから構成される。セグメントパラメータは、画像の表示に必要な圧縮方法や画質に関する情報を設定する領域とファイルを作成するソフトや機器が自由に設定できるAPPセグメントと呼ばれる領域に大別される。撮影データ、GPSの位置情報などを保存する Exifデータは APPセグメントのひとつである。
[image2.png]
JPEGファイルは、スタートマーカ(SOI)で始まり、直後に複数のセグメントが続く。セグメントが終わるとスキャンヘッダマーカ(SOS)が置かれ、その後ろにイメージデータが出力され、最後はエンドマーカ(EOI)で終わる。
各セグメントは先頭に 2バイトのマーカと 2バイトのセグメント長を持つ。マーカは セグメントの種類を表すコードで、1バイト目が必ず 0xFF、2バイト目が種別を表すコードになる。マーカは 3〜4文字の略称で表すことが多い。上記の SOI, EOI, SOS がそれにあたる。マーカの後ろの長さは、セグメントパラメタの長さプラス 4バイトになる。
JPEGファイルの規格は、ITU-T勧告 T.81 および JIS X 4301 に定義されており、マーカの一覧やセグメントの構造はここで確認できる。なお、"jpeg format" で検索すれば、これらを分かりやすく解説したサイトがいくつもあるので、参考にしていただきたい。

Exif解析処理

JPEGファイルを読み込む

JPEGファイルをバイトストリームとして読み込み Dataオブジェクトを作成する。
[ ポイント ]
Dataクラスのオブジェクトは符号なし 1バイト整数(UInt8)の配列と等値である。

Exif(APP1)セグメントを探索する

ファイルの先頭 2バイトがスタートマーカ(SOI)でなければ JPEGファイルではないので終了する。3バイト目を起点として、イメージデータの起点であるスキャンヘッダマーカ(SOS)が現れるまでマーカを順次に追っていき、APP1マーカ(0xFFE1)を探索する。
マーカの直後にセグメント長が 2バイトの整数としてセットされている。次のセグメントはセグメントの先頭からセグメント長を加えた地点になる。 なお、整数はビッグエンディアンとして扱うこと。

データを切り出す

slice関数の戻り値は Dataオブジェクト(UInt8 の配列)になる。
JPEGファイルのセグメントの並びは、確認できた限りでは、スタートマーカの直後にアプリケーション用セグメントが置かれ(APP0 または APP1)、その後ろに画像を表示するために必須なフレームヘッダ(SOF)、量子化テーブル(DQT)、ハフマンテーブル(DHT)等のセグメントが続き、最後にスキャンヘッダ(SOS)の後ろにイメージデータが置かれるというものであった。ただし、セグメントの並び自体は、仕様として決まってはいないようだ。
正常な JPEGファイルでも Exifデータが存在しない場合がある。画像編集ソフトで JPEGファイルを作成すると APP1セグメント(Exifフォーマット)の代わりに APP0セグメント(JFIFフォーマット)が作られる場合がある。ここには JFIFという規格の画像情報が格納される。JFIFに撮影情報などを付加したものが Exifになったらしいので、大雑把に JFIFは Exifのスーパーセットと言っていいのだろうか。JFIFの仕様はあまり出回っていないようなので、よくわからない。
なお別途紹介するが、画像ファイルのサイズを変更するアプリケーションを自作したが、これで JPEGファイルを変換すると、APP1(Exif)のセグメントが消えて、APP0(JFIF)のセグメントが作成される。画像を表示することについては問題ないが、撮影情報は消えてしまう。(要確認!)

APP1セグメントの構造

[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]
IFDは関連するデータの集まりで、複数のフィールドを持つ。IFDの最初の 2バイトがフィールドの件数になる。次IFDへのオフセットは 0thIFD から 1stIFDへの接続だけ設定される。

IFDフィールドの構造

[image5.png]
フィールドは12バイト固定長で タグ、タイプ、カウント、値(または値へのオフセット)からなる。
タグは、値の種類を表すコード(2バイトの整数)である。
タイプは、データ型を表すコード(2バイトの整数)である。種類は、(1) 文字、(2) 整数、(3)分数がある。整数は符号の有無、バイト数により細分化する。分数は、分母と分子に相当する2個の整数で表され、これも整数の種類により細分化する。
カウントは値の件数である。
値のサイズは4バイトである。値が 4バイト以内であればここに設定される。4バイトを超える場合は値は別の領域に設定され、値にはそこまでのオフセット(バイト数)が入る。アドレスの起点は Tiffヘッダの先頭となる。値の必要バイト数は、ひとつの値の長さ(バイト数) × カウントとなる。
2バイト以上の整数値は、エンディアンが実行マシンのそれと異なる場合は変換が必要になる。
各項目の詳細については、一般社団法人カメラ映像機器工業会発行による規格書を参照されたい。

撮影日時取得する

[image6.png]
撮影日時は、Exifの定義では、原画像データの生成日時(DateTimeOriginal)になる。これは、ExifIFDに値が保存されてる。
ExifIFDには、Tiffヘッダを起点にして、0thIFDを経由し ExifIFDに至る。0thIFDのタグ=34665のフィールドの値に ExifIFDへのオフセットが設定されている。Tiffヘッダの先頭アドレスにオフセットを足した値が、ExifIFDの先頭アドレスとなる。
ExifIFDのタグ=36867のフィールドに原画像データの生成日時が格納された領域へのオフセットが格納される。日時は ASCII文字 20バイトで、"2024:05:11△06:31:14△" のような形式となる。Dataオブジェクト(バイト配列)を文字列に変換するには、Stringクラスのイニシャライザ init(decoding: as: ) を使用する。

GPS位置情報を取得する

[image7.png]
GPS位置情報(撮影地点の緯度と経度)は GPSIFDに設定される。0thIFDのタグ=34853 のフィールドの値に GPSIFDへのオフセットが設定されている。Tiffヘッダの先頭アドレスにオフセットを足した場所が GPSIFDの先頭アドレスとなる。
GPSIFDのタグの 1〜4 のフィールドにデータが個々に設定されている。
タグ 1:緯度の南北を示す文字列
北緯の場合は'N'、南緯の場合は'S'が入る。
タグ 2:緯度(60進数表示)
3個の分数(RATIONAL)からなり、それぞれ度・分・秒を表す。値はオフセット先に格納される。
タグ 3:経度の東西を示す文字列
東経の場合は'E'、西経の場合は'W'が入る。
タグ 4:経度(60進数表示)
3個の分数(タイプ=RATIONAL)からなり、それぞれ度・分・秒を表す。値はオフセット先に格納される。
緯度/経度の値は、"度"、"分" は整数値、"秒" は少数値になる。
"度"=36/1、"分"=5/1、"秒"=10110/1000 → 36°5'10.11"

クラス一覧

UIコントロールの制御
テーブルビューを制御するクラス
JPEGファイルを解析する本体処理。jpegInfoメソッドは引数に指定された JPEGファイルを解析し、Exifデータから撮影日時と GPS位置情報を抜き出し返す。
整数と Dataオブジェクトの相互変換を行うユーティリティ関数