メタデータコピーによる変換時間増加量がどの程度のものなのか、ベンチマークをとってみた。
- 計測対象
624枚のpngファイル (計1GB); SSDに保存 - 結果
メタデータのコピーなし:59.462秒
メタデータのコピーあり:60.213秒
すなわち、0.751秒の増加。png1枚あたり1.2ミリ秒の時間増加。
うん、これは気にしなくていいレベルだな。常時コピーとしよう。
前回書いた通り、PNG/JPEG間にはメタデータ互換性がまったく無いことがわかった。となると、変換が必須だ。そして、自前で書くのは間違いなく難しい。そこで、車輪の再発明はせずに既存のものを使うことにする。候補は2つ。
メタデータの扱いはexiftoolsのほうが優秀そうだけど、起動に時間がかかるというのはMozJpegGUIのデザインと合わない。一方、GDI+を使う方はその点大丈夫そうだ。また、IStreamを介したLoad/Saveができるのも大きい。これならメモリ上の操作だけで済み、高速になるからだ。すなわち、以下の手順とすれば一時ファイルを使わずに操作できる。
Bitmap::Bitmap(IN IStream,IN BOOL) (gdiplusheaders.h) - Win32 apps | Microsoft Learn
Image::SetPropertyItem (gdiplusheaders.h) - Win32 apps | Microsoft Learn
IStream::Seek (objidl.h) - Win32 apps | Microsoft Learn
ISequentialStream::Read (objidl.h) - Win32 apps | Microsoft Learn
一方、GDI+には懸念点もあった。保存時に画像データの再エンコードをしないか?だ。もし再エンコードしてしまうようなら、MozJpegGUIの意味がない。そこで、MozJpegのデフォルト挙動(元画像のメタデータを全コピー)と、それに加えGDI+によりメタデータを複写した場合とでファイルのバイナリ値を比較した。
何しろバイナリデータなので直接の比較が困難なので、テキストデータ(値を16進文字表記にしたものを1バイト1行で書いたもの)に変換し、それをWinMergeでテキスト比較した。結果は以下の左側に見える通り、合計3領域で食い違い(細い3本のオレンジ線)はあったものの大半では一致していた。
Jpegのファイル構造を解析したところ、最初の領域における差分はエンディアンの違いによるものだった。その後そこそこ大きな一致領域があるが、これはタグID: 0x927Cのメーカーノート領域。なんと20kバイトもありやがります。敵だ。そして2番目の差分領域はやはりエンディアンの違いによるメタデータ差異。最後の差分領域はGDI+では存在しない部分で、サムネイルデータの直後に存在した。この領域はどこからも参照されておらず、ゴミデータに見える。試しにASCII変換したらRalpha,32.3.+.0.403_0_f500という文字列が出てきた。これはタグ0x131:Softwareと同じ内容だが、なぜここにあるのかは不明。
まとめると、出てきた差分はエンディアン起因か、ゴミデータ起因のもので画像は完全一致していた。うん、これならGDI+をつかってメタデータをコピーしても大丈夫だな。
それにしても、今回は補助ツールを2個も作ってしまった(画像のメタデータ解析ツール、バイナリ->ASCII変換ツール)。思ったよりもおおごとになってしまった・・・
githubのissueに、メタデータのコピーに失敗するPNGのサンプルがアップロードされた。
そこで、さっそく解析ソフトを作って、どんなメタデータがコピーできていないのかを調べてみた。そして絶望した。
PNGにおいて、画像を含む各種データはチャンクに格納される。各チャンクの種別はChunk Typeを読むことでわかる(PNGのファイルフォーマットは下記公式仕様を参照した)。
http://www.libpng.org/pub/png/spec/1.2/PNG-Structure.html
そして、今回issueとして上がっていた「画像変換時にメタデータをコピーしたい」において、コピーを希望されていたのはtEXtチャンク内に保存されたテキストだった。tEXtチャンクには「キーワード」(任意の79文字以下)と「テキスト」(任意文字数)のペアを1つ保存することができ、今回のissueのサンプルではキーワード:「prompt」「workflow」の2つのチャンクが存在し、それぞれに長大なテキストが存在した。
これをJPGにコピーするには、同様のテキスト保存機能か、そのキーワードに相当するタグが必要だ。しかし、JPGの仕様書をいくら見ても任意のテキストキーワードを保存する機能は無い。また、AIのpromptやworkflowに対応するようなタグも当然無い。すなわち、当該メタデータをPNGからJPGにコピーすることはできない。
どうしたものか。コメントセグメントにでも保存するか?
MozJpegGUI 1.3にメタデータコピー機能を追加するための方針について。
まず、MozJpegライブラリにメタデータ指定機能があるかどうかを調査した。・・・うーん、元jpegにあるメタデータをコピーできるのに、メタデータの外部指定機能がなさそう?
次に、画像変換後にWin32APIを使ってメタデータを書き込む方法を調査した。GDI+のImageやBitmapを使えばできるようだけど、困ったことに読み込みや保存に実ファイルが必要。しかも、読み込み元と保存先は別ファイルにする必要がある。ということは、保存が今まで1回だったのが、2回に増えてしまう。メモリ上に一時ファイル(tempファイル)を作成するなどの迂回策が必要そうだ。
最後に、MozJpegライブラリに手を加えることで対応できるか調査した。cjpeg.cの/* Copy metadata */以下にてメタデータ保存要否をマーカー毎に判断しているようだ。ということは、マーカーデータを必要に応じ生成してあげればいいのね。大変そうだけど。
以上をまとめると、以下な感じかな。
3番目の案が処理一番早くなるけど・・・どうしようかな。以下、今後の調査のためのメモ。
MozJpegGUI 1.3は、メタデータのコピーについてMozJpeg任せにしている。そこで、GDI+を利用する簡単なメタデータ表示ソフトを作りメタデータのコピー状況を確認してみた。
まずはサンプルデータの収集。プロが撮った写真や、プロが公開している画像だとexifがたいてい削除されていると思うので素人が撮った写真を探す。まずはTwitterを漁ってみたが画像アップロード時にexifを削除する仕様らしく色情報テーブル、輝度情報テーブル、ICC profile程度しか残っていなかった。さすが素人仕様、ちゃんと対策しているのね。となると・・・そこいらへんが甘そうな素人向けシステムは・・・はてなブログかな。というわけで漁ってみたところ、ビンゴ。jpegとpngでexifデータが残っていることを確認した。
サンプルデータを入手できたので、MozJpegGUI 1.3での変換前後でどう変わるか調べてみる。まずは風景写真のjpg。
ファイルサイズが1/5になっているので、変換は間違いなく実行できている。かつ、少なくともGDI+で認識できるデータはすべてコピーできているようだ。
次に、pngを試してみる。はてなブログから拾ってきたやつはタグ0x5110, 0x5111, 0x5112を共通で有し、画像によって0x131をもってたり0x13e, 0x13f, 0x132をもっていたりしてた。一方でMSPaintを使ってjpgをpngで保存した場合、0x303, 0x301, 0x110, 0x131, 0x132, 0x9003と多数保存されていた。とりあえず傾向を見れればいいので、風景写真をMSPaintでpng保存したもので比較してみる。
うん、がっつり消えてるなあ。最後に、gif。これもMSPaintが一番残してくれるっぽい?
gifもがっつり消えている。
というわけで、まとめるとjpeg->jpegなら(少なくともGDI+が認識できる)メタデータは消えていないが、png->jpegやgif->jpegでは多くのメタデータが消えてしまうようだ。
さて、どうやって速度を落とさずにメタデータを保持しようかな。
MozJpegGUIの更新に着手。目標は、
https://github.com/nibasya/MozjpegGUI/issues/1
Copy metadata when converting images · Issue #3 · nibasya/MozjpegGUI · GitHub
このうち、ライブラリのバージョンアップは完了。速度は変わらず。