In .Net, handling a picture's Exif properties seems, at first sight, to be a very simple job.
Since System.Drawing is only a wrapper around GDI+, loading Exif information from an image file is as simple as the following :
public static PropertyItem[] GetExifProperties(
string fileName) {
using(Image image = Image.FromFile(fileName))
return image.PropertyItems;
}
Unfortunately, this method requires to load - and validate - all image data in order to access the Exif properties. For instance, if you want to retrieve the thumbnail contained in the Exif properties, you first have to load and uncompress the entire image file.
This problem has been workarounded in the .Net Framework 1.1 Service Pack 1, and now you can process as follow :
public static PropertyItem[] GetExifProperties(
string fileName) {
using (FileStream stream =
new FileStream(fileName, FileMode.Open, FileAccess.Read))
using (Image image = Image.FromStream(stream,
/* useEmbeddedColorManagement = */ true,
/* validateImageData = */ false))
return image.PropertyItems;
}
However, since there is only a small share of PixVillage users that have the .Net Framework 1.1 Service Pack 1 installed, I eventually have implemented a custom ExifLoader helper class that use Interop to load Exif properties.
The first step is to load the image using the GDI+ Flat API and get the Exif properties raw data :
using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;
/// <summary>
/// Contains helper methods for loading Exif properties./// </summary>public sealed class ExifLoader {
private ExifLoader() {}
///
<summary>/// Retrieves Exif properties from a file./// </summary>/// <param name="fileName">The name of the Exif image file.</param>/// <returns>An array of <see cref="PropertyItem"/> objects containing the Exif properties.</returns>public static PropertyItem[] GetExifProperties(
string fileName) {
// Loads the image file using the GDI+ Flat APIIntPtr imageHandle = IntPtr.Zero;
if (GdipLoadImageFromFile(fileName,
out imageHandle)!=0)
throw new InvalidOperationException();
int count;
int totalSize;
IntPtr data = IntPtr.Zero;
try {
// Retrieves the number of properties the image containsif (GdipGetPropertyCount(imageHandle,
out count)!=0)
throw new InvalidOperationException();
// Gets the total size, in bytes, of the properties
if (GdipGetPropertySize(imageHandle,
out totalSize,
ref count)!=0)
throw new InvalidOperationException();
// There is no available properties...
if (count == 0 || totalSize == 0)
return new PropertyItem[0];
// Loads all properties into memory.
data = Marshal.AllocHGlobal(totalSize);
if (GdipGetAllPropertyItems(imageHandle, totalSize, count, data)!=0)
throw new InvalidOperationException();
// Convert the properties buffer into an array of PropertyItem objects.
return ExifPropertyItem.ConvertFromMemory(data, count);
}
finally {
if (data != IntPtr.Zero)
Marshal.FreeHGlobal(data);
// Releases the GDI+ image.
GdipDisposeImage(imageHandle);
}
}
ExifPropertyItem Helper Class// GDI+ Interop[DllImport(
"gdiplus.dll", CharSet=CharSet.Unicode, ExactSpelling=
true)]
private static extern int GdipLoadImageFromFile(
string filename,
out IntPtr image);
[DllImport(
"gdiplus.dll", CharSet=CharSet.Unicode, ExactSpelling=
true)]
private static extern int GdipDisposeImage(IntPtr image);
[DllImport(
"gdiplus.dll", CharSet=CharSet.Unicode, ExactSpelling=
true)]
private static extern int GdipGetPropertyCount(IntPtr image,
out int count);
[DllImport(
"gdiplus.dll", CharSet=CharSet.Unicode, ExactSpelling=
true)]
private static extern int GdipGetPropertySize(IntPtr image,
out int totalSize,
ref int count);
[DllImport(
"gdiplus.dll", CharSet=CharSet.Unicode, ExactSpelling=
true)]
private static extern int GdipGetAllPropertyItems(IntPtr image,
int totalSize,
int count, IntPtr buffer);
}
. . .
Then, we have to decode the buffer filled by GdipGetAllPropertyItems. This is done using a private inner class, as follow :
#region ExifPropertyItem Helper Class
/// <summary>
/// Represents an helper class for marshaling Exif property items./// </summary>[StructLayout(LayoutKind.Sequential)]
private class ExifPropertyItem {
// Public members used for marshaling.public int id = 0;
public int len = 0;
public short type = 0;
public IntPtr
value = IntPtr.Zero;
/// <summary>/// Initializes a new instance of the <see cref="ExifPropertyItem"/> class./// </summary>/// <remarks>Used only by the <see cref="Marshal.PtrToStructure"/> method.</remarks>public ExifPropertyItem() {}
/// <summary>/// Gets the value of the property./// </summary>public byte[] Value {
get {
byte[] buffer =
new byte[len];
if (len > 0)
Marshal.Copy(value, buffer, 0, len);
return buffer;
}
}
/// <summary>/// Converts a buffer into an array of PropertyItem objects./// </summary>/// <param name="propertyData">The properties data buffer.</param>/// <param name="count">The number of properties in the buffer.</param>/// <returns>An array of <see cref="PropertyItem"/> objects.</returns>public static PropertyItem[] ConvertFromMemory(IntPtr propertyData,
int count) {
PropertyItem[] itemArray =
new PropertyItem[count];
for (
int i = 0; i < count; i++) {
// Marshals the buffer into a ExifPropertyItem structure.
ExifPropertyItem item = (ExifPropertyItem)Marshal.PtrToStructure(propertyData, typeof(ExifPropertyItem));
// Creates the PropertyItem from the data.
itemArray[i] = Create(item.type, item.id, item.len, item.Value);
// Seek to next property.
propertyData = (IntPtr)(((long)propertyData) + Marshal.SizeOf(typeof(ExifPropertyItem)));
}
return itemArray;
}
/// <summary>/// Creates a PropertyItem from its components./// </summary>/// <param name="type">The property type.</param>/// <param name="tag">The property tag.</param>/// <param name="len">The length of the property.</param>/// <param name="value">The property value.</param>/// <returns></returns>private static PropertyItem Create(
short type,
int tag,
int len,
byte[]
value){
PropertyItem item;
// Loads a PropertyItem from a Jpeg image stored in the assembly as a resource.Assembly assembly = Assembly.GetExecutingAssembly();
using(Stream emptyBitmapStream = assembly.GetManifestResourceStream(
"ExifLoader.empty.jpg"))
using(Image empty = Image.FromStream(emptyBitmapStream))
item = empty.PropertyItems[0];
// Copies the data to the property item.
item.Type = type;
item.Len = len;
item.Id = tag;
item.Value =
new byte[
value.Length];
value.CopyTo(item.Value, 0);
return item;
}
}
#endregion
. . .
Note that in the class above, we load a jpeg file from resources into memory. This file is named "empty.jpg" but the resource is referenced as "ExifLoader.empty.jpg" since "ExifLoader" is the name of the assembly.
Since the PropertyItem class does not provide a public initializer to create a property "from scratch", we have to use an existing property item.