1. 背景
JPG形式の画像は再圧縮を繰り返すと画質が劣化する。したがって、再圧縮せずExifだけ編集したい場合、System.Windows.Media.Imaging名前空間にはおあつらえ向きのInPlaceBitmapMetadataWriterが存在する。で、早速これでJPGファイルのExifを編集しようとすると、成功しない。
その理由は既に説明されていて、これでExifのメタデータを書き込むにはそのJPGファイルのメタデータにそのための領域、Paddingが必要なのだが、カメラがJPGファイルに記録するときにはPaddingが付けられていないという問題がある。
この問題を解決するためのサンプルコード(サンプル中の"UsingInPlaceBitmapMetadataWriter"プロジェクト)も示されているので、これをベースに実用的なメソッドを仕立ててみる。
2. コーディング
流れとしては、以下のとおり。
- 画像データを開き、メタデータにPaddingを付けた上で一旦保存する。この際、画像データが再圧縮されないようにする。
- 画像データをInPlaceBitmapMetadataWriterで開き、InPlaceBitmapMetadataWriterのプロパティで公開されているフィールドであればそのプロパティを通して、あるいはSetQueryメソッドなどでフィールドのパスを直接指定して編集した後、TrySaveメソッドで保存する。
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Windows.Media.Imaging;
/// <summary>
/// Query paths for padding
/// </summary>
private static readonly List<string> queryPadding = new List<string>()
{
"/app1/ifd/PaddingSchema:Padding", // Query path for IFD metadata
"/app1/ifd/exif/PaddingSchema:Padding", // Query path for EXIF metadata
"/xmp/PaddingSchema:Padding", // Query path for XMP metadata
};
/// <summary>
/// Edit date taken field of Exif metadata of image data.
/// </summary>
/// <param name="source">Stream of source image data in JPG format</param>
/// <param name="date">Date to be set</param>
/// <returns>Byte array of outcome image data</returns>
public static Byte[] EditDateTaken(Stream source, DateTime date)
{
if (source == null)
throw new ArgumentNullException("source");
if (date == null)
throw new ArgumentNullException("date");
if (0 < source.Position)
source.Seek(0, SeekOrigin.Begin);
// Create BitmapDecoder for a lossless transcode.
var sourceDecoder = BitmapDecoder.Create(
source,
BitmapCreateOptions.PreservePixelFormat | BitmapCreateOptions.IgnoreColorProfile,
BitmapCacheOption.None);
// Check if the source image data is in JPG format.
if (!sourceDecoder.CodecInfo.FileExtensions.Contains("jpg"))
return null;
if ((sourceDecoder.Frames[0] == null) || (sourceDecoder.Frames[0].Metadata == null))
return null;
var sourceMetadata = sourceDecoder.Frames[0].Metadata.Clone() as BitmapMetadata;
// Add padding (4KiB) to metadata.
queryPadding.ForEach(x => sourceMetadata.SetQuery(x, 4096U));
using (var ms = new MemoryStream())
{
// Perform a lossless transcode with metadata which includes added padding.
var outcomeEncoder = new JpegBitmapEncoder();
outcomeEncoder.Frames.Add(BitmapFrame.Create(
sourceDecoder.Frames[0],
sourceDecoder.Frames[0].Thumbnail,
sourceMetadata,
sourceDecoder.Frames[0].ColorContexts));
outcomeEncoder.Save(ms);
// Create InPlaceBitmapMetadataWriter.
ms.Seek(0, SeekOrigin.Begin);
var outcomeDecoder = BitmapDecoder.Create(ms, BitmapCreateOptions.None, BitmapCacheOption.Default);
var metadataWriter = outcomeDecoder.Frames[0].CreateInPlaceBitmapMetadataWriter();
// Edit date taken field by accessing property of InPlaceBitmapMetadataWriter.
metadataWriter.DateTaken = date.ToString();
// Edit date taken field by using query with path string.
metadataWriter.SetQuery("/app1/ifd/exif/{ushort=36867}", date.ToString("yyyy:MM:dd HH:mm:ss"));
// Try to save edited metadata to stream.
if (metadataWriter.TrySave())
{
Debug.WriteLine("InPlaceMetadataWriter succeeded!");
return ms.ToArray();
}
else
{
Debug.WriteLine("InPlaceMetadataWriter failed!");
return null;
}
}
}
ポイントは、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メソッドで文字列にする必要があった(プロパティを通す場合は書式指定は不要)。
- なお、パス自体を書く代わりにSystem.Photo名前空間の該当するPhoto Metadata Policyの文字列(この場合はSystem.Photo.DateTakenなので、"System.Photo.DateTaken")を使うことも可能、なはずなのだが、このフィールドではうまく行かなかった。
ついでに、たまたま気づいたが、このPhoto Metadata PolicyにSystem.Photo.ISOSpeedがあるが、このパス"/app1/ifd/exif/{ushort=34855}"のタグ番号34855はExif 2.2のISOSpeedRatingsに当たる一方、Exif 2.3ではPhotographicSensitivityに変更され、新たにタグ番号34867のISOSpeedが設けられている。したがって、Exif 2.3のISOSpeedを指定したつもりで古い別のフィールドを指定することがあり得る。
一応、このメソッドの使用例。sourcePathのパスにあるJPGファイルのExifの撮影日時を変更し、outcomePathのパスに保存する。
private async Task ChangeDateTaken(string sourcePath, string outcomePath, DateTime date)
{
byte[] buff;
using (var sourceStream = File.Open(sourcePath, FileMode.Open))
using (var ms = new MemoryStream())
{
await sourceStream.CopyToAsync(ms);
buff = BitmapMetadataEditor.EditDateTaken(ms, date);
}
if (buff == null)
return;
using (var outcomeStream = File.Create(outcomePath))
{
await outcomeStream.WriteAsync(buff, 0, buff.Length);
}
}
フィールドを固定しない汎用的なメソッドにしたサンプルコード。フィールドの値の引数は型をObjectにしてあるが、それぞれ適した型にして引数に入れる必要がある。
0 件のコメント:
コメントを投稿