world2014/05/26ExifをC#から編集する写真の画像ファイルを扱うアプリを作るとき、意外と無視できない要素がExifのメタデータで、例えば画像方向(Orientation)が反映されてないとどうも気になったりする。そこでExifを読み出すようにするまではいいが、さらに一歩進んでExifを書き込もうとすると、これが意外と手がかかるという話。背景JPG形式の画像は再圧縮を繰り返すと画質が劣化する。したがって、再圧縮せずExifだけ編集したい場合、System.Windows.Media.Imaging名前空間にはおあつらえ向きのInPlaceBitmapMetadataWriterが存在する。これでJPGファイルのExifを編集しようとすると、成功しない。その理由は既に説明されていて、これでExifのメタデータを書き込むにはそのJPGファイルのメタデータにそのための領域、Paddingが必要なのだが、カメラがJPGファイルに記録するときにはPaddingが付けられていないという問題がある。InPlaceBitmapMetadataWriterこの問題を解決するためのサンプルコード(サンプル中の"UsingInPlaceBitmapMetadataWriter"プロジェクト)も示されているので、これをベースに実用的なメソッドを仕立ててみる。コーディング流れとしては、以下のとおり。画像データを開き、メタデータにPaddingを付けた上で一旦保存する。この際、画像データが再圧縮されないようにする。画像データをInPlaceBitmapMetadataWriterで開き、InPlaceBitmapMetadataWriterのプロパティで公開されているフィールドであればそのプロパティを通して、あるいはSetQueryメソッドなどでフィールドのパスを直接指定して編集した後、TrySaveメソッドで保存する。例として、撮影日時(DateTaken)を編集するようにしたコードは以下のとおり。入力はI/Oバウンドなメソッドに合わせやすいStreamで、出力は加工しやすいByte配列で。}}ポイントは、BitmapDecoder.CreateメソッドでBitmapCreateOptionsにBitmapCreateOptions.PreservePixelFormatとBitmapCreateOptions.IgnoreColorProfileを指定し、BitmapCacheOptionにBitmapCacheOption.Noneを指定することで(36-39行目)、これで再圧縮が行われなくなる。Paddingのサイズは4KiBを指定しているが(51行目)、元記事によればたいていの書き込みで1-5KiBの範囲で収まるとのことなので、余っていると思う。Exifの編集は、BitmapCreateOptions.IgnoreColorProfileのプロパティを通したもの(74行目)と、フィールドのパスを直接指定したもの(77行目)で、これはどちらかでいい。それぞれの場合の留意事項として、プロパティを通す場合そもそもプロパティで公開されているフィールドは限られていて、その中でも機能しないものがある(例えばTitleはうまく行かなかった)。パスを直接指定する場合フィールドの値について、SetQueryメソッドの引数はObjectなので何でも入れられるが、その前に型をきちんと合わせる必要がある。型が文字列や数値の場合は別に難しくないが、DateTimeの場合、この例のように書式指定したToStringメソッドで文字列にする必要があった(プロパティを通す場合は書式指定は不要)。Policyの文字列(この場合はSystem.Photo.DateTakenなので、"System.Photo.DateTaken")を使うことも可能、なはずなのだが、このフィールドではうまく行かなかった。2.3ではPhotographicSensitivityに変更され、新たにタグ番号34867のISOSpeedが設けられている。2.3のISOSpeedを指定したつもりで古い別のフィールドを指定することがあり得る。いう次第で、実際に使うフィールドごとに関係資料を見つつ確認していく地道な作業が必要だと思う。一応、このメソッドの使用例。sourcePathのパスにあるJPGファイルのExifの撮影日時を変更し、outcomePathのパスに保存する。}}フィールドを固定しない汎用的なメソッドにしたサンプルコード。フィールドの値の引数は型をObjectにしてあるが、それぞれ適した型にして引数に入れる必要がある。Frameworkにはこれを簡単に調べる方法が用意されていない。よって、間接的に対応する方法が色々編み出されてきたが、直接的に調べるAPIも(P/Invokeにはなるが)存在する。目的を果たせれば方法は何でもいいが、直接、ストレートにできる方法があるならその方がいいと思う。APIを利用する方法だが、これはVista以降でのみ使用可。したがって従来なら少し二の足を踏むところだが、晴れてXPのことは考えなくてよくなったので(一応)、これも一つの福音と言えよう。ラッパークラス具体的な方法は既に説明されていて、必要なことは全部書いてある。再起動マネージャとジェネリックメソッドのコンパイルこれをもう少し使いやすい形にしてみる。処理としては、大まかに以下の4段階。Managerセッションを開始する(RmStartSession)対象のファイルをセッションに登録する(RmRegisterResource)対象のファイルを使用中のプロセスをセッションから取得する(RmGetList)セッションを終了する(RmEndSession)これを行うラッパークラスは以下のとおり。}}EnumerateProcessesUsingFilesメソッド中の3段階目のRmGetListがポイントで、使用中のプロセスの情報を格納するためのRM_PROCESS_INFO構造体の配列の長さをどう決めるかが問題になる。使用中のプロセスが存在しない場合、配列はそのままでよく、ERROR_SUCCESSが返ってくるので、そのまま抜ける。使用中のプロセスが存在する場合、配列の長さが足りず、ERROR_MORE_DATAが返ってくるので、224と225行のように配列を整え直した上でループさせ、再度RmGetListを実行する。この間に終了しているプロセスがあればArgumentExceptionが出るが、それはtry-catchで潰す。プロセスが増えていれば、同じことの繰り返し。このクラスを含むサンプルアプリ。[修正]IsProcessesUsingFileメソッドの中でプロセスをDisposeするように修正し、合わせてEnumerateProcessesUsingFilesメソッドをIEnumerable<Process>を返すように変更した。返ってきたList<Process>を使って如何ようにも処理は可能だが、注意点としてこのメソッドの前後の短い間にもプロセスの状況は変わる可能性があるので、例えばファイルに書き込む前の事前チェックに使う場合でもtry-catchで例外に備えるべき必要性は変わらない。Communitywww.flickr.comコメントについてコメントは承認制です。承認まで時間がかかるときがあります。感想、間違いの指摘など歓迎します。間違いについては、その場所と内容を明記してください。エントリと関係がないもの、不適当と思われるものは承認しません。プロフィールEMOGitHub詳しくないプロフィール当てにならないカスタマイズ。