ドナドナされるプログラマのメモ

Windows用アプリのプログラミングメモ

MozJpegGUIの更新: ベンチマーキング

メタデータコピーによる変換時間増加量がどの程度のものなのか、ベンチマークをとってみた。

  • 計測対象
    624枚のpngファイル (計1GB); SSDに保存
  • 結果
    メタデータのコピーなし:59.462秒
    メタデータのコピーあり:60.213秒
    すなわち、0.751秒の増加。png1枚あたり1.2ミリ秒の時間増加。

うん、これは気にしなくていいレベルだな。常時コピーとしよう。

MozJpegGUIの更新: 残ってるTODO

そうだ、忘れないうちにToDoを書いておかないと。

TODO:

  1. 新規追加した箇所について、エラーメッセージの多言語化
  2. 元画像がJPEGだった場合はGDI+によるメタデータコピーをしない
  3. メタデータコピーのチェックボックスを消す(常時コピーする)

MozJpegGUIの更新: とりあえずGDI+でお茶を濁す

前回書いた通り、PNG/JPEG間にはメタデータ互換性がまったく無いことがわかった。となると、変換が必須だ。そして、自前で書くのは間違いなく難しい。そこで、車輪の再発明はせずに既存のものを使うことにする。候補は2つ。

  1. GDI+のImageクラスおよびその派生
    Windows標準ライブラリなので使うのは楽。C++との相性は良い。ディスク上のファイルを介さずに使えそう(後述)。
  2. exiftools( https://exiftool.org/ )
    Perlで書かれており、C++との連携はちょっと大変かも。起動に時間がかかるらしい(公式ドキュメントより)。ディスクを介さずにやり取りできるか不明。

メタデータの扱いはexiftoolsのほうが優秀そうだけど、起動に時間がかかるというのはMozJpegGUIのデザインと合わない。一方、GDI+を使う方はその点大丈夫そうだ。また、IStreamを介したLoad/Saveができるのも大きい。これならメモリ上の操作だけで済み、高速になるからだ。すなわち、以下の手順とすれば一時ファイルを使わずに操作できる。

  1. 読み込み用メモリストリーム(memory stream)の作成
    MozJpegがメモリ上に作成した画像データを、SHCreateMemStream()を使ってそのままメモリストリーム化する。
    SHCreateMemStream 関数 (shlwapi.h) - Win32 apps | Microsoft Learn
  2. Bitmapオブジェクトの生成
    Bitmapオブジェクトのコンストラクタに、先程生成したメモリストリームを渡す。

    Bitmap::Bitmap(IN IStream,IN BOOL) (gdiplusheaders.h) - Win32 apps | Microsoft Learn

  3. メタデータ(実際はExif関連のみ?)の書き込み
    SetPropertyItem()を使ってメタデータをBitmapオブジェクトに設定する。

    Image::SetPropertyItem (gdiplusheaders.h) - Win32 apps | Microsoft Learn

  4. 書き込み用メモリストリームの作成
    SHCreateMemStream()を使って書き込み用の空っぽメモリストリームを作る。
  5. Jpeg画像の保存
    BitmapオブジェクトのSave()を使って、書き込み用メモリストリームにJpeg画像を保存する。これはメモリストリームなので、ファイル書き込みは発生しない。

    Image::Save(IN IStream,IN const CLSID,IN const EncoderParameters) (gdiplusheaders.h) - Win32 apps | Microsoft Learn

  6. 書き込み用メモリストリームからのデータ読み出し
    Seek()にてアクセス位置をストリーム先頭に移動したあと、Read()にてストリームから書き込み用配列へデータをコピーする。

    IStream::Seek (objidl.h) - Win32 apps | Microsoft Learn

    ISequentialStream::Read (objidl.h) - Win32 apps | Microsoft Learn 

  7. 読み込み用メモリストリームや書き込み用メモリストリームの廃棄
    読み込み用メモリストリームや書き込み用メモリストリームは古式ゆかしいCOMオブジェクトなので、Release()する。

一方、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変換ツール)。思ったよりもおおごとになってしまった・・・

MozJpegGUIの更新: そもそもPNGとJPGでメタデータの互換性がない!?

githubのissueに、メタデータのコピーに失敗するPNGのサンプルがアップロードされた。

github.com

そこで、さっそく解析ソフトを作って、どんなメタデータがコピーできていないのかを調べてみた。そして絶望した。

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の更新: メタデータコピーの方針

MozJpegGUI 1.3にメタデータコピー機能を追加するための方針について。

まず、MozJpegライブラリにメタデータ指定機能があるかどうかを調査した。・・・うーん、元jpegにあるメタデータをコピーできるのに、メタデータの外部指定機能がなさそう?

次に、画像変換後にWin32APIを使ってメタデータを書き込む方法を調査した。GDI+のImageやBitmapを使えばできるようだけど、困ったことに読み込みや保存に実ファイルが必要。しかも、読み込み元と保存先は別ファイルにする必要がある。ということは、保存が今まで1回だったのが、2回に増えてしまう。メモリ上に一時ファイル(tempファイル)を作成するなどの迂回策が必要そうだ。

最後に、MozJpegライブラリに手を加えることで対応できるか調査した。cjpeg.cの/* Copy metadata */以下にてメタデータ保存要否をマーカー毎に判断しているようだ。ということは、マーカーデータを必要に応じ生成してあげればいいのね。大変そうだけど。

以上をまとめると、以下な感じかな。

  • メタデータをGDI+等で読み取り、MozJpegの引数指定でメタデータを保存:そのような引数はなさそうなのでNG
  • メタデータをGDI+にて後処理で保存:ファイル保存回数が1増えるので、避けたい
  • MozJpegライブラリに、GDI+で読み取ったメタデータを保存する機能を追加する:できなくはないけど難易度高い?

3番目の案が処理一番早くなるけど・・・どうしようかな。以下、今後の調査のためのメモ。

JPEG メタデータ仕様

dev.exiv2.org

hp.vector.co.jp

MozJpegGUIの更新: メタデータのコピー状況

MozJpegGUI 1.3は、メタデータのコピーについてMozJpeg任せにしている。そこで、GDI+を利用する簡単なメタデータ表示ソフトを作りメタデータのコピー状況を確認してみた。

まずはサンプルデータの収集。プロが撮った写真や、プロが公開している画像だとexifがたいてい削除されていると思うので素人が撮った写真を探す。まずはTwitterを漁ってみたが画像アップロード時にexifを削除する仕様らしく色情報テーブル、輝度情報テーブル、ICC profile程度しか残っていなかった。さすが素人仕様、ちゃんと対策しているのね。となると・・・そこいらへんが甘そうな素人向けシステムは・・・はてなブログかな。というわけで漁ってみたところ、ビンゴ。jpegpngexifデータが残っていることを確認した。

サンプルデータを入手できたので、MozJpegGUI 1.3での変換前後でどう変わるか調べてみる。まずは風景写真のjpg。

風景写真jpgの変換前メタデータ

風景写真jpgの変換後メタデータ

ファイルサイズが1/5になっているので、変換は間違いなく実行できている。かつ、少なくともGDI+で認識できるデータはすべてコピーできているようだ。

次に、pngを試してみる。はてなブログから拾ってきたやつはタグ0x5110, 0x5111, 0x5112を共通で有し、画像によって0x131をもってたり0x13e, 0x13f, 0x132をもっていたりしてた。一方でMSPaintを使ってjpgをpngで保存した場合、0x303, 0x301, 0x110, 0x131, 0x132, 0x9003と多数保存されていた。とりあえず傾向を見れればいいので、風景写真をMSPaintでpng保存したもので比較してみる。

風景写真pngの変換前メタデータ

風景写真pngの変換後メタデータ

うん、がっつり消えてるなあ。最後に、gif。これもMSPaintが一番残してくれるっぽい?

風景写真gifの変換前メタデータ

風景写真gifの変換後メタデータ

gifもがっつり消えている。

というわけで、まとめるとjpeg->jpegなら(少なくともGDI+が認識できる)メタデータは消えていないが、png->jpegやgif->jpegでは多くのメタデータが消えてしまうようだ。

さて、どうやって速度を落とさずにメタデータを保持しようかな。

MozJpegGUIの更新

MozJpegGUIの更新に着手。目標は、

このうち、ライブラリのバージョンアップは完了。速度は変わらず。

メタデータの維持はどうやろうかなあ。GDI+を使うのが王道っぽいけど、Save()時に再エンコードされそうでこわい。