Skip to content

Instantly share code, notes, and snippets.

@emoacht
Created May 26, 2014 11:22
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save emoacht/fec367ccb7fb8ad75b02 to your computer and use it in GitHub Desktop.
Save emoacht/fec367ccb7fb8ad75b02 to your computer and use it in GitHub Desktop.
Edit Exif metadata.
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