アカベコマイリ

HEAR NOTHING SEE NOTHING SAY NOTHING

WM/Picture のデータ構造

いま作っているメタデータ編集ライブラリで画像データをサポートしようとしている。

ID3v2 の場合は PIC または APIC、ASF は WM/Picture がこれに該当する。ID3v2 は ID3.org にデータ構造が公開されているので、そのとおり編集すればよい。ASF の場合、Windows Media Format SDK における WM/Picture の説明を読むとデータ形式は WM_PICTURE 構造体になるそうだ。

ちなみにデータメンバは以下のように定義されている。

typedef struct _WMPicture {
  LPWSTR pwszMIMEType;
  BYTE   bPictureType;
  LPWSTR pwszDescription;
  DWORD  dwDataLen;
  BYTE * pbData;
}WM_PICTURE;

Windows Media Format SDK は ID3v2 もサポートしているためかメンバ名やデータの並びは APIC によく似ている。しかし Windows Media Player 11 (以下、WMP) で画像をつけたファイルをバイナリ エディタで開いてみたところ、WM/Picture に該当する部分は以下のようになっていた。

03 D7 14 00 00 69 00 6D 00 61 00 67 00 65 00 2F 
00 6A 00 70 00 65 00 67 00 00 00 C6 30 B9 30 C8
30 B8 30 E3 30 B1 30 C3 30 C8 30 00 00 FF D8 FF
E0 00 10 4A 46 49 46 00 01 01 ...

MIME から開始されていることを期待したが、それは 5 バイト目の 69 からで、この例では image/jpeg となる。その後は NULL 終端を挟んで 27 バイト名から説明文 テストジャケット と NULL 終端、最後は JPEG のバイナリ データになっていた。

複数の WMA ファイルを用意して WMP の拡張タグ エディタ上で画像ファイル、種類、説明を何パターンかに分けて追加してみる。そしてバイナリ エディタで比較検証してみたところ先頭の 5 バイトはどうやら bPictureTypedwDataLen を表しているようだ。bPictureType が BYTE で 1 バイト、dwDataLen は DWORD なので 4 バイトになる。

前述の例では bPictureType が 3 なので WMP でいうところの「カバー (前面)」、dwDataLen は 5,335 バイトになる。本当に正しいかを検証するため以下のようなクラスを作成してみた。

/// <summary>
/// WM/Picture の内容を表します。
/// </summary>
public class WmPicture
{
    /// <summary>
    /// インスタンスを初期化します。
    /// </summary>
    /// <param name="bytes">WM/Picture の内容を格納したバイト配列。</param>
    public WmPicture( byte[] bytes )
    {
        // 種別とサイズ
        this.Type = bytes[ 0 ];
        var size = BitConverter.ToUInt32( bytes, 1 );

        // MIME と説明文
        var position = 5;
        this.Mime        = this.ReadString( bytes, ref position );
        this.Description = this.ReadString( bytes, ref position );

        // 画像
        this.Data = new byte[ size ];
        Array.Copy( bytes, position, this.Data, 0, size );
    }

    /// <summary>
    /// バイト配列の指定位置から、UTF16LE 文字列を NULL 終端まで読み込みます。
    /// </summary>
    /// <param name="bytes">バイト配列</param>
    /// <param name="position">読み込み開始位置。読み終えた後は NULL 終端の直後を指します。</param>
    /// <returns>読み込んだ文字列。</returns>
    private string ReadString( byte[] bytes, ref int position )
    {
        // 終端の検索 ( 終端直前の文字が後続バイトにゼロを使用する場合も考慮する )
        var index = this.BytesIndexOf( bytes, StringTerminateBytes, position );
        if( bytes[ index + StringTerminateBytes.Length ] == 0 ) { ++index; }

        var str = Encoding.Unicode.GetString( bytes, position, index - position );
        position = index + StringTerminateBytes.Length;

        return str;
    }

    /// <summary>
    /// バイト配列内から指定されたバイト配列を探します。
    /// </summary>
    /// <param name="bytes">バイト配列。</param>
    /// <param name="token">検索するバイト配列。</param>
    /// <param name="index">検索を開始する位置。</param>
    /// <returns>バイト配列の見つかった位置を示すインデックス。未検出の場合は -1。</returns>
    /// <exception cref="ArgumentNullException">bytes または token が null 参照です。</exception>
    private int BytesIndexOf( byte[] bytes, byte[] token, int index )
    {
        // 検索対象なし
        if( bytes == null || token == null ) { throw new ArgumentNullException( "\"bytes\" or \"token\" is null." ); }

        // 検索できるデータ指定ではないので未検出として扱う
        if( bytes.Length == 0 || token.Length == 0 || token.Length > ( bytes.Length - index ) ) { return -1; }

        var max = bytes.Length - ( token.Length + 1 );
        for( var i = index; i < max; ++i )
        {
            for( var j = 0; j < token.Length; ++j )
            {
                if( bytes[ i + j ] != token[ j ] )
                {
                    break;
                }
                else if( j == token.Length - 1 )
                {
                    return i;
                }
            }
        }

        return -1;
    }

    /// <summary>
    /// 画像データを示すバイト配列を取得します。
    /// </summary>
    public byte[] Data { get; private set; }

    /// <summary>
    /// 画像の説明を取得します。
    /// </summary>
    public string Description { get; private set; }

    /// <summary>
    /// 画像の MIME ( Multipurpose Internet Mail Extension ) 情報を取得します。
    /// </summary>
    public string Mime { get; private set; }

    /// <summary>
    /// 画像の種別を取得します。
    /// </summary>
    public int Type { get; private set; }

    /// <summary>
    /// 文字列の終端を表すバイト配列。
    /// </summary>
    private static readonly byte[] StringTerminateBytes = { 0, 0 };
}

そしてバイナリ エディタを利用して WM/Picture 部分のデータを C:\test\picture.dump というファイルとして保存し、以下のコードを実行してみた。

var path = @"C:\test\picture.dmp";
using( var dumpFile = new FileStream( path, FileMode.Open, FileAccess.Read ) )
{
    var bytes = new byte[ dumpFile.Length ];
    dumpFile.Read( bytes, 0, bytes.Length );

    var picture = new WmPicture( bytes );
    Debug.WriteLine( String.Format( "Type = {0}", picture.Type        ) );
    Debug.WriteLine( String.Format( "MIME = {0}", picture.Mime        ) );
    Debug.WriteLine( String.Format( "Desc = {0}", picture.Description ) );

    path = @"C:\test\picture.jpg";
    using( var image = new FileStream( path, FileMode.CreateNew, FileAccess.ReadWrite ) )
    {
        image.Write( picture.Data, 0, picture.Data.Length );
    }
}

すると Visual Studio の出力ウィンドウには以下の出力が得られ、

<br />Type = 3
MIME = image/jpeg
Desc = テストジャケット

C:\test\picture.jpg という JEPG ファイルが保存されることを確認できた。画像としても破損は見られず Explorer や Windows フォト ギャラリーなどで表示できる。ちなみに WMP から PNG も追加してみたのだが、なぜかデータは JPEG になっていた。

今回の内容は個人的なハックによるもので妥当性は保証できない。しかし複数パターンで検証して大丈夫だったのでライブラリの画像取得はこれをベースに実装しようと思う。