>

2011年2月23日水曜日

C#でEXIFのDateTimeOriginalのみを高速に取得する

C#ではJPEG画像のEXIFを読み込むための方法としてPropertyItemsを使用する方法が用意されているがこのライブラリは汎用的なために撮影日時といったような一つの値だけが欲しい時などは非常に遅くて不便。

なのでC#でJPEGの撮影日時(DateTimeOriginal)のみを高速に取得するコードを書いた。
ただし次の制約があります。
  • 簡単のため撮影日時はJPEGファイルの先頭から1000バイト以内にあることを前提としている
  • 例外処理は行っていない
  • 基本的に動くことだけ確認して細かいところは見直していない

public static string ReadExifDateTime(string path)
{
    byte[] jpegBytes = new byte[1000];
    using (BinaryReader reader = new BinaryReader(File.OpenRead(path)))
    {
        reader.Read(jpegBytes, 0, 1000);
    }

    bool isLittleEndian = false;
    if (jpegBytes[12] == 'I')
    {
        isLittleEndian = true;
    }

    int ifd0Offset = ReadInt(jpegBytes, 16, 4, isLittleEndian) + 12;
    int ifd0DirCount = ReadInt(jpegBytes, ifd0Offset, 2, isLittleEndian);

    int subIfdOffset = 0;
    for (int i = 0; i < ifd0DirCount; i++)
    {
        int ifd0Tag = ReadInt(jpegBytes, ifd0Offset + 2 + 12 * i, 2, isLittleEndian);
        if (ifd0Tag == 0x8769)
        {
            subIfdOffset = ReadInt(jpegBytes, ifd0Offset + 2 + 12 * i + 8, 4, isLittleEndian) + 12;
            break;
        }
    }
    if (subIfdOffset == 0)
    {
        return null;
    }

    int subIfdDirCount = ReadInt(jpegBytes, subIfdOffset, 2, isLittleEndian);
    int dateTimeOffset = 0;
    for (int i = 0; i < subIfdDirCount; i++)
    {
        int ifd0Tag = ReadInt(jpegBytes, subIfdOffset + 2 + 12 * i, 2, isLittleEndian);
        if (ifd0Tag == 0x9003)
        {
            dateTimeOffset = ReadInt(jpegBytes, subIfdOffset + 2 + 12 * i + 8, 4, isLittleEndian) + 12;
            break;
        }
    }
    if (dateTimeOffset == 0)
    {
        return null;
    }

    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < 20; i++)
    {
        sb.Append((char)jpegBytes[dateTimeOffset + i]);
    }

    return sb.ToString();
}

private static int ReadInt(byte[] bytes, int offset, int count, bool isLittleEndian)
{
    int retVal = 0;

    if (isLittleEndian)
    {
        for (int i = offset + count - 1; i >= offset; i--)
        {
            retVal = (retVal << 8) + bytes[i];
        }
    }
    else
    {
        for (int i = offset; i < offset + count; i++)
        {
            retVal = (retVal << 8) + bytes[i];
        }
    }

    return retVal;
}

上記コードをPropertyItemsを使って読み込む場合と速度の比較のためにHDD上のJPEG(1,590,029 バイト)の撮影日時を100回読み込むのにかかる時間を計測した結果

上記コードの場合: 15ms
PropertyItemsを使用した場合: 29395ms

とPropertyItemsを使用した場合に比べて2000倍くらい速いことが確認できる。EXIFの解析って大変なんですね。 なお、PropertyItemsを使用して取得するコードとしては以下を使用した。

public static string ReadExifDateTimeUsingPropertyItems(string path)
{
    //画像を読み込む
    System.Drawing.Bitmap bmp = new System.Drawing.Bitmap(path);

    for (int i = 0; i < bmp.PropertyItems.Length; i++)
    {
        System.Drawing.Imaging.PropertyItem pi = bmp.PropertyItems[i];
        if (pi.Id == 36867 && pi.Type == 2)
        {
            return Encoding.ASCII.GetString(pi.Value);
            break;
        }
    }
    return null;
}