アカベコマイリ

HEAR NOTHING SEE NOTHING SAY NOTHING

WPF で Google Map & GeoTag その 5

November 11, 2009開発.NET, BitmapMetadata, GeoTag, IPTC, WPF, XMP

WPF で Google Maps API & GeoTag を扱うシリーズその 5。

今回は Google Map の Geocoder から取得した住所情報を画像に保存してみる。このシリーズでやりたかったことを達成する。これまで作成したプログラムについてソフトウェアとしての体裁 (安全性やデザインなど) を整えたら簡易 GPS 編集ツールとして Software へ公開したいと考えている。

サンプル プログラム

前回のプログラムからの主な変更点は、以下のようになる。

  • Geocoder から取得した住所情報を XMP として画像に書き込む
  • EXIF への GPS 書き込みで上手くいっていなかった部分をまとめて修正
  • 撮影日時が無い場合は、それをデジタル化日時などど共に書き込む

サンプル プログラムのプロジェクト一式は以下。ビルドには Visual Studio 2008 SP1、プログラムの実行には NET Framework 3.5 SP1 とネットワーク接続された環境が必要。

このプログラムは対象となる画像を書き換えるため、動作を試される場合はテスト用の画像を用意する事を推奨します。

BitmapMetadata.SetQuery による IPTC 書き込み

以前の記事で Geocoder から取得した住所を都市整形済みの住所にわけて取得するところまでは実装できていたので、これを IPTC として記録する方法を調べていた。

サンプルの Address クラスから抜粋すると以下の処理で IPTC へ追加可能。SetQuery の第 2 引数に渡しているデータは System.String となる。

/// <summary>
/// 現在の Address を IPTC としてメタデータへ書き込みます。
/// </summary>
/// <param name="metadata">画像のメタデータを表すオブジェクト。</param>
private void WriteIptc( BitmapMetadata metadata )
{
    metadata.SetQuery( Address.IptcPathContentLocationName, this.FormatedAddress );

    // 国
    if( String.IsNullOrEmpty( this.Country ) )
    {
        metadata.RemoveQuery( Address.IptcPathCountry );
    }
    else
    {
        metadata.SetQuery( Address.IptcPathCountry, this.Country );
    }

    // 都道府県や州
    if( String.IsNullOrEmpty( this.ProvinceState ) )
    {
        metadata.RemoveQuery( Address.IptcPathProvinceState );
    }
    else
    {
        metadata.SetQuery( Address.IptcPathProvinceState, this.ProvinceState );
    }

    // 都市
    if( String.IsNullOrEmpty( this.City ) )
    {
        metadata.RemoveQuery( Address.IptcPathCity );
    }
    else
    {
        metadata.SetQuery( Address.IptcPathCity, this.City );
    }
}

private const string IptcPathContentLocationName = "/app13/irb/8bimiptc/iptc/content location name";
private const string IptcPathCountry = "/app13/irb/8bimiptc/iptc/country\\/primary location name";
private const string IptcPathProvinceState = "/app13/irb/8bimiptc/iptc/province\\/state";
private const string IptcPathCity = "/app13/irb/8bimiptc/iptc/city";

ただしこの方法でマルチバイト文字を書き込むと XnView では表示されるが ViewNX で文字化けが発生する。

Stirling から画像を開いて当該部分を調べたところ SetQuerySystem.String を書き込むと文字エンコーディングは UTF-8 となることが判明。ViewNX は UTF-8 の IPTC が読めないようだ。ViewNX 上で撮影場所などを編集するとマルチバイト文字のエンコーディングは Shift-JIS となっていた。

XnView は UTF-8 と Sift-JIS の両方に対応していたけれど XnVIew 上で編集・保存すると Shift-JIS で記録される。そこで UTF-8 を Shift-JIS 化したバイト配列を書き込む方法を考えた。

BitmapMetadata.SetQueryBitmapMetadataBlob を利用することでバイト配列を直に書き込める。よって Encoding.UTF-8 から Encoding.Default のバイト配列に変換したデータを書き込んだところ未サポートのデータだという例外が発生。BitmapMetadataBlob を利用せずバイト配列を直に指定しても駄目だった。

更なる調査のため Bitmapmetadata.SetQuery の実装を確認したら、このクエリがバイト配列や BitmapMetadataBlob を受け付けていないことだけが分かった。.NET Framework や WPF に属するコードについては以下で紹介されている方法により追跡できる。

全世界の住所を記録する事を考えたら UTF-8 の方がよいだろう。しかし IPTC の文字コードは統一されていないようなので ANSI (日本語版 Windows なら Shift-JIS) を採用してプログラムの実行環境で切り替えるのが安全そう。というわけで IPTC 書き込みは実装だけ残し、今回のプログラムでは使用しない事にした。

XMP として住所情報を書き込む

Pant.NET で新規作成したメタデータを全く持たない画像を用意して ViewNX から撮影場所などを記録したところ、これらは XMP として書かれている事がわかった。文字コードも UTF-8。この情報は XnView の画面下部にある画像情報部分から XMP というタブを開く事で調べられる。XMP は XML 形式でデータを保存しているので、タグ名から BitmapMetadata.SetQuery に指定する為のクエリを類推できる。

以上を踏まえ XMP として住所情報を書き込んだところ無事に ViewNX で表示できた。実装としては前述の IPTC 書き込みメソッドで指定していたクエリを変更しただけである。以下に XMP 用のクエリだけ抜粋する。

private const string XmpPathContentLocationName = "/xmp/Iptc4xmpCore:Location";
private const string XmpPathCountry = "/xmp/photoshop:Country";
private const string XmpPathProvinceState = "/xmp/photoshop:State";
private const string XmpPathCity = "/xmp/photoshop:City";

/xmp/ で開始して XnView で見た XMP の XML タグ名を続ければ 、それがそのままクエリとなる。XML としての体裁、例えば名前空間の指定などは BitmapMetadata.SetQuery が面倒を見てくれるようだ。

この方法で書き込んだ画像と ViewNX で編集した画像の XMP を比較すると、データの並びやインデントなどは異なるけれど XML としてのデータは一致していた。

Comments from WordPress

  • モリタ 2011-10-23T02:51:40Z

    こんにちは。はじめまして。

    Exifについて調べていて、このブログを見つけました。 C#、Visual Studio 2010、Windows7で使っています。 ファイル名に使っている★★★★★をWindwos7の写真の評価に反映したいと考えています。

    たとえば、

    ★★★★★0001.jpg

    というファイル名の画像ファイルの評価に、★★★★★をつけたいのです。

    この場合、逆なら簡単で、

    PropertyItem item = null;
    
    item = bmp.GetPropertyItem(0x4746);
    byte[] messageBytes = removeLastNull(item.Value);
    string stars = messageBytes[0].ToString();

    で取得できるのですが、逆の場合、Exifのロスレス書き込みをする必要があって、苦しんでいます。 突然で大変恐縮なのですが、Exifのロスレス書き込みについて、アドバイスいただけないでしょうか。

  • akabeko akabeko 2011-10-23T09:10:55Z

    @モリタさん ★について「Windwos7の写真の評価」と書かれていますが、これは EXIF ではなく XMP のデータではないでしょうか。

    Explorer で JPEG を選択し、ウィンドウ下部にある★から、適当な数を設定して XnView やバイナリエディタで XMP 部分を見ると、xmp:Rating と MicrosoftPhoto:Rating が更新されることを確認できます。

    よって、★を反映したい場合は、XMP として書き込む必要があると思われます。なお、xmp:Rating は 1 ~ 5、MicrosoftPhoto:Rating のほうは 1、25、50、75、100 の範囲で★を表します。

  • モリタ 2011-10-28T03:13:04Z

    akabekoさん、早速お返事ありがとうございました。

    Exifだと思ったのは次のような理由です。

    (1)Windows7のエクスプローラでjpegファイルを選択し評価に☆を加えて保存します。
    (2)Exif読取り君 Version 1.60(http://enrai.matrix.jp/exif.html)

    で表示したところ、0x4726に5、0x4729に99の値が入っていました。

    0x4746 (1~5)
    0x4749 (1~99)

    いずれも単に、数値が入っていて、C#の場合は、PropertyItemで取得できることは確認できています。 おっしゃるとおりで、MicrosoftPhoto:Patingには、1、25、50、75、99の値が入っています。

    ご示唆いただいたXnViewで確認したところ、

    599

    のように値が入っていました。

    で、そうだとすると、XMPとして書き込むことが必要になります。わたしが、困っているのは、JPEG(Exif or XMP)へのロスレスの書き込み部分です。

    XMPで、xmpのデータを作る場合には、uuidを生成する(or取得する)ことも必要になりますね。 そこについて、もしすでにC#でお作りになっていらっしゃったら、ご示唆いただけないかと。

    Bitmap bmp = new System.Drawing.Bitmap(path);
    
    PropertyItem item = null;
    
    item = bmp.GetPropertyItem(0x4746);
    item = bmp.GetPropertyItem(0x4749);
    
    //ここでitemに0x4746/0x4749の値を設定。現在できていない。
    
    //xmp形式にする必要あり?
    
    bmp.SetPropertyItem(item);
    
    SaveJPEG(bmp, path);
    
    bmp.Dispose();

    というコードの場合、jpegを開いて再エンコードしてしまうので、Exifを単独で書き込みする方法を知りたいと思っています。

  • akabeko akabeko 2011-10-28T15:17:32Z

    まず、EXIF の規格としては Rating を格納する場所は用意されていないはずです。バイナリエディタで一つずつ値を解析してゆくと、以下のページで解説されている内容と一致することが確認できます。

    Exif file format:
    http://park2.wakwak.com/~tsuruzoh/Computer/Digicams/exif.html

    Rating をサポートするメタデータで代表的なものは IPTC か XMP です。WPF を利用可能であれば、この記事で紹介している方法により更新できます。その場合、メタデータだけの更新となります。なお、BitmapMetadata で扱えるクエリは以下に公開されています。

    Native Image Format Metadata Queries:
    http://msdn.microsoft.com/en-us/library/windows/desktop/ee719904(v=vs.85).aspx

    XMP の場合、単なる XML なので固定的なデータ形式ではなく、自由なタグを定義可能です。タグへの対応は、アプリ次第となります。MicrosoftPhoto:Rating も、その名前から分かるとおり Microsoft が決めたタグであり、XMP にそういう定義があるわけではありません。ただ、Explorer がこれに対応しているので、代表的な写真系アプリなら、大抵は読み書きできると思いますが。

    なお、WPF を利用せずにメタデータだけ更新したいなら、自力でバイナリ編集する必要があると思います。データの構造と格納すべき値が自明なら、その箇所だけ伸張・編集するような処理を実装することになるでしょう。

  • モリタ 2011-10-29T13:43:24Z

    ひょっとして、こんな感じかと思ったのですが、違ったみたいです。

    string XmpPathWindows7Rating = "xmp/MicrosoftPhoto:Rating";
    metadata.RemoveQuery(XmpPathWindows7Rating);//この行でエラーになります。
    metadata.SetQuery(XmpPathWindows7Rating, 99);
  • モリタ 2011-10-29T09:19:03Z

    akabekoさん、早速お返事ありがとうございました。

    WpfGEOTaggerは、たいへん読みやすいソースで、うまく処理部分を分離できて、WPFで次のようにしてみて、Ratingを変更できることを確認できました。

    あとは、MicrosoftPhoto:Ratingだけなのですが、これはどこを読んで、どこを書き換えたらよいのでしょう?

    ご示唆いただけるとうれしく。

    private void button1_Click(object sender, RoutedEventArgs e){
        using (var streamIn = new MemoryStream(File.ReadAllBytes(fileName)))
        {
            var decorder = new JpegBitmapDecoder(streamIn, BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.None);
            using (var streamOut = File.Open(fileName, System.IO.FileMode.Open, System.IO.FileAccess.ReadWrite))
            {
                var encorder = new JpegBitmapEncoder();
                var frame = BitmapFrame.Create(decorder.Frames[0]);
                encorder.Frames.Add(frame);
    
                var metadata = frame.Metadata as BitmapMetadata;
                metadata.Rating++;
    
                encorder.Frames.Add(BitmapFrame.Create(frame, null, metadata, null));
                encorder.Save(streamOut);
            }
        }
    }
  • akabeko akabeko 2011-11-01T13:25:31Z

    XMP の場合、XML としてのパスで値を指すため、その階層や名前空間を適切に指定する必要がありそうです。

    このあたりは、XnView で XML の構造を眺めつつ、試行錯誤してゆくのが分かりやすいと思います。また、BitmapMetadata.RemoveQuery でエラーになるとのことですが、それはエラーではなく例外ではないでしょうか?

    BitmapMetadata.RemoveQuery メソッド (System.Windows.Media.Imaging)
    http://msdn.microsoft.com/ja-jp/library/system.windows.media.imaging.bitmapmetadata.removequery.aspx

    を読むと戻り値は void です。また、パラメータが null または読み取り専用のプロパティに対して実行したとき、例外を生じるとあります。提示されたコードを見るに、前者はありえないので、パスが不適切なため、他の読み取り専用プロパティと誤読されているか、本当に読み取り専用 ( 考えにくいですが... ) なのかもしれません。

    なお、、ここまでのコメントを読む限り、既にゴール寸前まで到達しているように見えますので、私からの回答はここまでにしておこうと思います。一連のやりとりは、私の勉強にもなりました。

    コメント、ありがとうございます。