In the first post of this Exif series, I talked about a technique for
loading Exif properties as fast as GDI+ is able to do.
The second post was dealing with
adding new properties to an empty image.
Now it's time to look more precisely at the Exif properties. How is a
PropertyItem structured ?
The final goal of this post is to convert the raw data exposed by GDI+ (and the
.Net Framework) into more useable data types.
An Exif property consists in the following information:
-
An identifier, determining the "meaning" of the property.
-
A data type, indicating how the data are represented.
-
A length (the number of bytes the property data contains).
-
A value which is a raw array of bytes.
The Exif property data type is a 16-bit integer, but it's quite simple to build
an enum type that maps the values to a more comprehensive type :
///
<summary>
/// Defines types for Exif Properties.
/// </summary>
public enum ExifPropertyType
{
/// <summary>
/// Specifies that the value
data member is an array of bytes.
/// </summary>
Byte = 1,
/// <summary>
/// Specifies that the value
data member is a null-terminated ASCII string.
/// </summary>
/// <remarks>If
you set <see
cref="PropertyItem.Type">PropertyItem.Type</see>
to <see cref="PropertyItem.Type"/>,
you should set the length data member to the length of the string including the
NULL terminator. For example, the string HELLO would have a length of 6.</remarks>
Ascii = 2,
/// <summary>
/// Specifies that the value
data member is an array of signed short (16-bit) integers.
/// </summary>
UInt16 = 3,
/// <summary>
/// Specifies that the value
data member is an array of unsigned long (32-bit) integers.
/// </summary>
UInt32 = 4,
/// <summary>
/// Specifies that the value
data member is an array of pairs of unsigned long integers. Each pair
represents a fraction; the first integer is the numerator and the second
integer is the denominator.
/// </summary>
URational = 5,
/// <summary>
/// Specifies that the value
data member is an array of bytes that can hold values of any data type.
/// </summary>
Raw = 7,
/// <summary>
/// Specifies that the value
data member is an array of signed long (32-bit) integers.
/// </summary>
Int32 = 9,
/// <summary>
/// Specifies that the value
data member is an array of pairs of signed long integers. Each pair represents
a fraction; the first integer is the numerator and the second integer is the
denominator.
/// </summary>
Rational = 10,
}
. . .
As you can see in the code above, most of the data types are simple and can be
directly mapped to .Net data types, excepted for rational and unsigned rational
values. We need to create a .Net representation for each of them :
///
<summary>
/// Represents a signed rational number.
/// </summary>
public class Rational
{
private
int numerator;
private int denominator;
/// <summary>
/// Initializes a new instance
of the <see cref="Rational"/>
class.
/// </summary>
/// <param
name="numerator">The numerator of the
rational number.</param>
/// <param
name="denominator">The denominator of
the rational number.</param>
public Rational(
int numerator,
int denominator) {
this.numerator
= numerator;
this.denominator = denominator;
}
/// <summary>
/// Gets the numerator of the
rational number.
/// </summary>
public int Numerator
{
}
/// <summary>
/// Gets the denominator of the
rational number.
/// </summary>
public int Denominator
{
get {
return
denominator;
}
}
/// <summary>
/// Gets the floating-point
value of the rational number.
/// </summary>
public double Value {
get
{
return (double)
numerator/(double) denominator;
}
}
/// <exclude/>
public override
string ToString() {
return
Value.ToString();
}
}
. . .
///
<summary>
/// Represents an unsigned rational number.
/// </summary>
public class URational
{
private
uint numerator;
private uint denominator;
/// <summary>
/// Initializes a new instance
of the <see cref="Rational"/>
class.
/// </summary>
/// <param
name="numerator">The numerator of the
rational number.</param>
/// <param
name="denominator">The denominator of
the rational number.</param>
public URational(
uint
numerator,
uint denominator) {
this.numerator
= numerator;
this.denominator = denominator;
}
/// <summary>
/// Gets the numerator of the
rational number.
/// </summary>
public uint Numerator
{
}
/// <summary>
/// Gets the denominator of the
rational number.
/// </summary>
public uint Denominator
{
get {
return
denominator;
}
}
/// <summary>
/// Gets the floating-point
value of the rational number.
/// </summary>
public double Value {
get
{
return (double)
numerator/(double) denominator;
}
}
/// <exclude/>
public override
string ToString() {
return
Value.ToString();
}
}
. . .
According to the Exif
specification, properties may contain arrays of values. Our conversion
tool must consequently have the following signature :
public
static Array FromPropertyItem(PropertyItem propertyItem) {}
The conversion code is quite simple (a switch... case construct branching
on different code depending on the data type).
When converting raw data into integers, just take care that Exif properties are
stored as big endian values.
#region
References
using System;
using System.Drawing.Imaging;
using System.Runtime.InteropServices;
using System.Text;
#endregion
namespace ExifNative {
///
<summary>
/// Summary description for
ExifConvert.
/// </summary>
public sealed
class ExifConvert {
private
ExifConvert() {}
/// <summary>
/// Converts a property item to
an array of objects.
/// </summary>
/// <param
name="propertyItem">The property item
to convert.</param>
/// <returns>An
array of <see cref="object"/>
items.</returns>
public static Array
FromPropertyItem(PropertyItem propertyItem) {
ExifPropertyType
type = (ExifPropertyType) propertyItem.Type;
switch (type) {
case
ExifPropertyType.Raw:
// The value
represents raw data (a single byte[] value)
return new
byte[][] {propertyItem.Value};
case ExifPropertyType.Ascii:
//
The value represents an array of strings separated by \0 characters
string stringValue =
Encoding.ASCII.GetString(propertyItem.Value, 0, propertyItem.Len - 1);
return stringValue.Split('\0');
case ExifPropertyType.Byte:
//
The value represents an array of bytes
return propertyItem.Value;
case ExifPropertyType.UInt16:
//
The value represents an array of unsigned 16-bit integers.
int ushortCount = propertyItem.Len/ushortSize;
ushort[] ushortResult =
new
ushort[ushortCount];
for (
int i = 0; i
< ushortCount; i++)
ushortResult[i] =
ReadUInt16(propertyItem.Value, i * ushortSize);
return ushortResult;
case ExifPropertyType.Int32:
//
The value represents an array of signed 32-bit integers.
int intCount = propertyItem.Len/intSize;
int[] intResult =
new
int[intCount];
for (
int i = 0; i
< intCount; i++)
intResult[i] =
ReadInt32(propertyItem.Value, i * intSize);
return intResult;
case ExifPropertyType.UInt32:
//
The value represents an array of unsigned 32-bit integers.
int uintCount = propertyItem.Len/uintSize;
uint[] uintResult =
new
uint[uintCount];
for (
int i = 0; i
< uintCount; i++)
uintResult[i] =
ReadUInt32(propertyItem.Value, i * uintSize);
return uintResult;
case ExifPropertyType.Rational:
//
The value represents an array of signed rational numbers
// Numerator is an Int32 value, denominator a UInt32 value.
int rationalCount = propertyItem.Len/rationalSize;
Rational[] rationalResult =
new Rational[rationalCount];
for (
int i = 0; i
< rationalCount; i++)
rationalResult[i] =
new Rational(
ReadInt32(propertyItem.Value,
i * rationalSize),
ReadInt32(propertyItem.Value, i * rationalSize + intSize));
return rationalResult;
case ExifPropertyType.URational:
//
The value represents an array of signed rational numbers
// Numerator and denominator are UInt32 values.
int urationalCount =
propertyItem.Len/rationalSize;
URational[] urationalResult =
new URational[urationalCount];
for (
int i = 0; i
< urationalCount; i++)
urationalResult[i] =
new URational(
ReadUInt32(propertyItem.Value,
i * urationalSize),
ReadUInt32(propertyItem.Value, i * urationalSize + uintSize));
return urationalResult;
default:
return
null;
}
}
#region Static Fields
private static
readonly int ushortSize =
Marshal.SizeOf(
typeof (
ushort));
private static
readonly int intSize = Marshal.SizeOf(
typeof
(
int));
private static
readonly int uintSize = Marshal.SizeOf(
typeof
(
uint));
private static
readonly int rationalSize = 2 *
Marshal.SizeOf(
typeof (
int));
private static
readonly int urationalSize = 2 *
Marshal.SizeOf(
typeof (
uint));
#endregion
#region Private Helpers
private static
ushort ReadUInt16(
byte[] buffer,
int offset) {
return
(
ushort) (
((ushort)
buffer[offset] +
((ushort) buffer[offset + 1] << 8)));
}
private static
int ReadInt32(
byte[] buffer,
int offset) {
return
(
int) (
((uint)
buffer[offset] +
((uint) buffer[offset + 1] << 8) +
((uint) buffer[offset + 2] << 16) +
((int) buffer[offset + 3] << 24)));
}
private static
uint ReadUInt32(
byte[] buffer,
int offset) {
return
(
uint) (
((uint)
buffer[offset] +
((uint) buffer[offset + 1] << 8) +
((uint) buffer[offset + 2] << 16) +
((uint) buffer[offset + 3] << 24)));
}
#endregion
}
}
. . .
The following code is a sample console application that make use of the code
above to load an image and display the content of Exif properties.
[STAThread]
private static
void Main(
string[] args) {
using(Image
image = Image.FromFile(args[0])) {
Console.WriteLine(
"{0}
: {1} properties", args[0], image.PropertyItems.Length);
foreach (PropertyItem propertyItem
in image.PropertyItems) {
Array result =
ExifConvert.FromPropertyItem(propertyItem);
Console.WriteLine(
"Property #{0}, type : {1}, {2}
value(s)", propertyItem.Id,
(ExifPropertyType)propertyItem.Type, result.Length);
foreach (
object
value in result)
Console.WriteLine("\t{0}",
value);
Console.WriteLine();
}
}
}