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

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

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