Created
May 26, 2014 11:22
-
-
Save emoacht/fec367ccb7fb8ad75b02 to your computer and use it in GitHub Desktop.
Edit Exif metadata.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using System.Diagnostics; | |
using System.IO; | |
using System.Linq; | |
using System.Windows.Media.Imaging; | |
/// <summary> | |
/// Edit Exif metadata. | |
/// </summary> | |
/// <remarks> | |
/// This class is based on http://blogs.msdn.com/b/rwlodarc/archive/2007/07/18/using-wpf-s-inplacebitmapmetadatawriter.aspx | |
/// </remarks> | |
public static class BitmapMetadataEditor | |
{ | |
/// <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 | |
}; | |
public static Byte[] EditByProperty(Stream source, string propertyName, object propertyValue) | |
{ | |
return Edit(source, new Dictionary<string, object>() { { propertyName, propertyValue } }, null); | |
} | |
public static Byte[] EditByProperty(Byte[] source, string propertyName, object propertyValue) | |
{ | |
return Edit(source, new Dictionary<string, object>() { { propertyName, propertyValue } }, null); | |
} | |
public static Byte[] EditByQuery(Stream source, string queryPath, object queryValue) | |
{ | |
return Edit(source, null, new Dictionary<string, object>() { { queryPath, queryValue } }); | |
} | |
public static Byte[] EditByQuery(Byte[] source, string queryPath, object queryValue) | |
{ | |
return Edit(source, null, new Dictionary<string, object>() { { queryPath, queryValue } }); | |
} | |
public static Byte[] Edit(Stream source, string propertyName, object propertyValue, string queryPath, object queryValue) | |
{ | |
return Edit(source, new Dictionary<string, object>() { { propertyName, propertyValue } }, new Dictionary<string, object>() { { queryPath, queryValue } }); | |
} | |
public static Byte[] Edit(Byte[] source, string propertyName, object propertyValue, string queryPath, object queryValue) | |
{ | |
return Edit(source, new Dictionary<string, object>() { { propertyName, propertyValue } }, new Dictionary<string, object>() { { queryPath, queryValue } }); | |
} | |
public static Byte[] Edit(Byte[] source, Dictionary<string, object> propertySet, Dictionary<string, object> querySet, uint padding = 0) | |
{ | |
using (var ms = new MemoryStream(source)) // This stream must not be changed. | |
{ | |
return Edit(ms, propertySet, querySet, padding); | |
} | |
} | |
/// <summary> | |
/// Edit Exif metadata of image data. | |
/// </summary> | |
/// <param name="source">Stream of source image data in JPG format</param> | |
/// <param name="propertySet">Set of property names and values of InPlaceBitmapMetadataWriter</param> | |
/// <param name="querySet">Set of query paths and values of metadata</param> | |
/// <param name="padding">Padding of metadata</param> | |
/// <returns>Byte array of outcome image data</returns> | |
/// <remarks> | |
/// This method will perform a lossless transcode and so no degradation of image quality will happen. | |
/// Property names of InPlaceBitmapMetadataWriter must be correct strings. Otherwise, | |
/// an ArgumentException will be thrown. | |
/// Query paths can be path strings such as "/app1/ifd/{ushort=272}" (field for camera model) or | |
/// photo metadata policy strings such as "System.Photo.CameraModel" depending on the field. | |
/// </remarks> | |
public static Byte[] Edit(Stream source, Dictionary<string, object> propertySet, Dictionary<string, object> querySet, uint padding = 0) | |
{ | |
if (source == null) | |
throw new ArgumentNullException("source"); | |
if (0 < source.Position) | |
source.Seek(0, SeekOrigin.Begin); | |
// Create BitmapDecoder for a lossless transcode. | |
// BitmapCreateOptions (BitmapCreateOptions.PreservePixelFormat | BitmapCreateOptions.IgnoreColorProfile) | |
// and BitmapCacheOption (BitmapCacheOption.None) are 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; | |
uint paddingOld = 0; | |
uint paddingNew = padding; | |
if (padding == 0) | |
{ | |
// Get padding of the source image data. | |
// ContainsQuery method for query path for XMP metadata seems to always return false even after the image data is saved. | |
paddingOld = queryPadding.Take(2).Select(x => sourceMetadata.ContainsQuery(x) ? (uint)sourceMetadata.GetQuery(x) : 0U).Min(); | |
Debug.WriteLine("Source padding: {0}", paddingOld); | |
} | |
bool isInitial = true; | |
while (true) | |
{ | |
if (padding == 0) | |
{ | |
if (!isInitial || (paddingOld == 0)) | |
{ | |
if (paddingNew == 0) | |
paddingNew = paddingOld; | |
// Add 1KiB to padding. | |
// The original author recommended to keep padding between 1KiB and 5KiB as most metadata updates are not large. | |
paddingNew += 1024U; | |
} | |
isInitial = false; | |
} | |
if (0 < paddingNew) | |
{ | |
// Apply added padding to metadata. | |
queryPadding.ForEach(x => sourceMetadata.SetQuery(x, paddingNew)); | |
Debug.WriteLine("Added padding: {0}", (uint)sourceMetadata.GetQuery(queryPadding[0])); | |
foreach (var query in queryPadding) | |
{ | |
Debug.WriteLine("{0} -> {1}", query, sourceMetadata.ContainsQuery(query)); | |
} | |
} | |
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 by accessing property of InPlaceBitmapMetadataWriter. | |
if (propertySet != null) | |
{ | |
var properties = typeof(InPlaceBitmapMetadataWriter).GetProperties(); | |
foreach (var p in propertySet) | |
{ | |
var targetProperty = properties.SingleOrDefault(x => x.Name == p.Key); | |
if (targetProperty == null) | |
throw new ArgumentException("Property name does not match."); | |
try | |
{ | |
targetProperty.SetValue(metadataWriter, p.Value, null); | |
} | |
catch (Exception ex) | |
{ | |
Debug.WriteLine(ex); | |
} | |
} | |
} | |
// Edit by using query (SetQuery method). RemoveQuery method is also available to remove query. | |
if (querySet != null) | |
{ | |
foreach (var q in querySet) | |
{ | |
Debug.WriteLine("Before: {0}", metadataWriter.GetQuery(q.Key)); | |
metadataWriter.SetQuery(q.Key, q.Value); | |
Debug.WriteLine("After: {0}", metadataWriter.GetQuery(q.Key)); | |
} | |
} | |
// Try to save edited metadata to stream. | |
if (metadataWriter.TrySave()) | |
{ | |
Debug.WriteLine("InPlaceMetadataWriter succeeded!"); | |
return ms.ToArray(); | |
} | |
else | |
{ | |
Debug.WriteLine("InPlaceMetadataWriter failed!"); | |
if ((padding != 0) || (10240 <= paddingNew - paddingOld)) // Added padding is up to 10 KiB. | |
return null; | |
} | |
} | |
} | |
} | |
/// <summary> | |
/// Edit DateTaken 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; | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment