Introduction
I always knew that the image JPEGs from my digital camera contained more than just the image. I even researched the topic, learning about the innards of a JPEG file and the digital camera's file format standard entitled EXIF (exchangeable image file format). EXIF places metadata "tags" within a JPEG file to indicate various camera settings and picture taking conditions that occurred while creating the photo.
Unfortunately, this metadata was only available through complex, low-level byte manipulation of the image's raw data. A few products existed to access this data (including Windows XP), but not as a simple API call.
However, with the release of Microsoft .NET's GDI+ class, access to the metadata tags became simple. A PropertyItem
class was available to access any of the (public) tags in a file. With just a few lines of code I thought I would be able to get to the hidden information about the exposure rate or flash setting for my favorite pictures.
Not So Easy After All
Then, realization set in -- the PropertyItem
class only provides access to raw data values. Tag name and description strings are not available. And to add to the frustration, the values are only available in byte format; the tag's type and length is supplied, but no transformations are available. To illustrate the problem, here is an example of a PropertyItem
instance:
PropertyItem
Id = 33434
Len = 8
Type = 5
Value = 0A-00-00-00-E8-03-00-00
So... what does this data mean? How can we find out that 33434 is the EXIF ExposureTime tag? And, how can we decipher 0A-00-00-00-E8-03-00-00 into a readable output of 1/100?
To answer these questions, I created the PhotoProperties dll to provide a simple way to obtain the hidden image properties in a readable format -- an image file is analyzed and XML text is returned.
PhotoPropertiesApp is also included as a sample front-end Windows application that uses the PhotoProperties library.
The PhotoProperties Library
A Little More Research
Microsoft's Visual C++ .NET includes a GdiplusImaging.h header file containing definitions of all of the PropertyItem
IDs. For example the EXIF ExposureTime tag is defined as:
#define PropertyTagExifExposureTime 0x829A
But this barely helped the problem. I then looked at the EXIF specification document (see http://it.jeita.or.jp/jhistory/document/standard/exif_eng/jeida49eng.htm) and found the data I needed.
ExposureTime
Exposure time, given in seconds (sec).
Tag = 33434 (829A.H)
Type = RATIONAL
Count = 1
Default = none
However, there was still some missing data. The header file had a number of tag definitions that were not part of the EXIF specification. Further research on the web got me to the "Property Item Descriptions " page in the MSDN Library. Though there were a few differences between the two documents, I finally found all of the pieces I needed to create a solution.
An XML Metadata Resource
To provide a concise method of accessing this new information I decided to store it in an XML file. This way I could easily add/modify/delete the information without changing any of the code.
The XML data consists of tagMetadata
elements, each containing various attributes and sub elements such as id
, category
, name
, description
, and possibly formatInstr
or valueOptions
. For example,
<tagMetadata id="33434" category="EXIF">
<name>ExposureTime</name>
<description>Exposure time, measured in seconds.</description>
<formatInstr>FRACTION</formatInstr>
</tagMetadata>
...
<tagMetadata id="37377" category="EXIF">
<name>ShutterSpeed</name>
<description>Shutter speed. The unit is the Additive System of Photographic
Exposure (APEX) value.</description>
</tagMetadata>
...
<tagMetadata id="37383" category="EXIF">
<name>MeteringMode</name>
<description>Metering mode.</description>
<valueOptions>
<option key="0" value="unknown" />
<option key="1" value="Average" />
<option key="2" value="CenterWeightedAverage" />
<option key="3" value="Spot" />
<option key="4" value="MultiSpot" />
<option key="5" value="Pattern" />
<option key="6" value="Partial" />
<optionRange from="7" to="254" value="reserved" />
<option key="255" value="other" />
</valueOptions>
</tagMetadata>
The id
, category
, name
, and description
definitions are obvious. The formatInstr
, when provided, contains an additional formatting instruction to be applied to the PropertyItem value. The valueOptions
, when provided, contains pretty-print results that can be obtained from the value. The use of formatInstr
and valueOptions
will be explained further along in this page.
The Library's Public Methods
The PhotoProperties library contains three key public methods: Initialize
, Analyze
, and WriteXml
.
The Initialize method
The Initialize
method uses the XmlTextReader
and XmlSerializer
functions to read the XML file and deserialize the data into appropriate C# classes. More directly, the XML tagMetadata
elements are deserialized into a collection of PhotoTagMetadata
items accessible by id values.
Deserializing the tagMetadata
To deserialize an XML file, there must be a set of classes that provide a direct mapping between XML elements and attributes and .NET constructs. The easiest way to create these classes is to use the XML Schema Definition Tool (Xsd.exe).
For example, the photoMetadata
element in the PhotoMetadata.xsd schema
<xs:element name="photoMetadata">
<xs:complexType>
<xs:sequence>
<xs:element name="tagMetadata" type="TagMetadata" minOccurs="0"
maxOccurs="unbounded" />
</xs:sequence>
<xs:attribute name="category" type="xs:string" use="required" />
</xs:complexType>
</xs:element>
was generated into the following photoMetadata
class.
[System.Xml.Serialization.XmlTypeAttribute(
Namespace="http://tempuri.org/PhotoMetadata.xsd")]
[System.Xml.Serialization.XmlRootAttribute(
Namespace="http://tempuri.org/PhotoMetadata.xsd", IsNullable=false)]
public class photoMetadata {
[System.Xml.Serialization.XmlElementAttribute("tagMetadata")]
public TagMetadata[] tagMetadata;
[System.Xml.Serialization.XmlAttributeAttribute()]
public string category;
}
As you can see, there are one-to-one mappings between the XML photoMetadata
element and C# class, the XML tagMetadata
sequence and C# array, and the XML category
attribute and C# field.
The array of tagMetadata
objects was not the form I needed. I needed a collection that could accessed by their id value -- a Hashtable
. Luckily, as long as certain guidelines are followed, functionality can be added and modified without breaking the mappings. These guidelines include:
- Names can be changed as long as the original XML item name is supplied in the attribute name.
- Public fields can be added as long as the XmlIgnoreAttribute attribute is provided.
- Since only public fields are used in XML serialization, addition of non-public fields require no added attributes.
- Field accessors can be used in place of mapped fields.
So the above class was changed to:
[XmlTypeAttribute(Namespace="http://tempuri.org/PhotoMetadata.xsd")]
[XmlRootAttribute("photoMetadata",
Namespace="http://tempuri.org/PhotoMetadata.xsd", IsNullable=false)]
public class PhotoMetadata {
private Hashtable _tagMetadataCollection = new Hashtable();
[XmlIgnoreAttribute()]
public Hashtable TagMetadataCollection {
get { return _tagMetadataCollection; }
}
[XmlElementAttribute("tagMetadata")]
public PhotoTagMetadata[] TagMetadata {
get {
if (_tagMetadataCollection.Count == 0)
return null;
PhotoTagMetadata[] tagArray =
new PhotoTagMetadata[_tagMetadataCollection.Count];
_tagMetadataCollection.Values.CopyTo(tagArray, 0);
return tagArray;
}
set {
if (value == null)
return;
PhotoTagMetadata[] tagArray = (PhotoTagMetadata[])value;
_tagMetadataCollection.Clear();
foreach(PhotoTagMetadata tag in tagArray) {
_tagMetadataCollection[tag.Id] = tag;
}
}
}
[XmlAttributeAttribute("category")]
public string category;
}
In this format, the sequence of XML tagMetadata
elements are auto magically transformed into a collection of PhotoTagMetadata
objects accessed by the object's id value.
Utilizing An XML Resource
The PhotoMetadata
XML data could be deployed as an additional file along with the PhotoProperties.dll library. But that would require the user to make sure it was always copied with the library. A much more elegant method would store the XML file as a resource within the library assembly. Borrowing some of the code from Paul DiLascia's MOTLib.NET "goodies" (http://www.dilascia.com/motlib), I was able to store the XML file as a resource in the assembly. The Initialize
method also allows the use of an external XML file if needed; the external file must be valid based upon the PhotoMetadata.xsd schema.
The Analyze method
In a nutshell, the Analyze
method loops through the tag properties in an image file and converts the byte data values into a collection of formatted strings. It wasn't quite that simple, though.
The PropertyTagFormat Class
The PropertyItem
object contains four fields:
Id
, the tag identifier;
Len
, the length of the data in bytes;
Type
, one of the following eight property types:
Id |
Name |
Description |
1 |
BYTE |
An 8-bit unsigned integer. |
2 |
ASCII |
An 8-bit byte containing one 7-bit ASCII code. The final byte is terminated with NULL. |
3 |
SHORT |
A 16-bit (2-byte) unsigned integer. |
4 |
LONG |
A 32-bit (4-byte) unsigned integer. |
5 |
RATIONAL |
Two LONGs. The first LONG is the numerator and the second LONG expresses the denominator. |
7 |
UNDEFINED |
An 8-bit byte that can take any value depending on the field definition. |
9 |
SLONG |
A 32-bit (4-byte) signed integer (2's complement notation). |
10 |
SRATIONAL |
Two SLONGs. The first SLONG is the numerator and the second SLONG is the denominator. |
Value
, the property value (in byte format).
The PropertyTagFormat
's FormatValue
method uses a PropertyItem
and its associated PhotoTagMetadata
object to convert the data to a formatted string.
public static string FormatValue(PropertyItem propItem,
PhotoTagMetadata tagMetadata) {
if (propItem == null)
return String.Empty;
FormatInstr formatInstr;
if (tagMetadata != null && tagMetadata.FormatInstrSpecified == true)
formatInstr = tagMetadata.FormatInstr;
else
formatInstr = FormatInstr.NO_OP;
string strRet;
switch (propItem.Type) {
case PropertyTagTypeByte:
strRet = FormatTagByte(propItem, formatInstr);
break;
case PropertyTagTypeASCII:
strRet = FormatTagAscii(propItem, formatInstr);
break;
case PropertyTagTypeShort:
strRet = FormatTagShort(propItem, formatInstr);
break;
case PropertyTagTypeLong:
strRet = FormatTagLong(propItem, formatInstr);
break;
case PropertyTagTypeRational:
strRet = FormatTagRational(propItem, formatInstr);
break;
case PropertyTagTypeUndefined:
strRet = FormatTagUndefined(propItem, formatInstr);
break;
case PropertyTagTypeSLong:
strRet = FormatTagSLong(propItem, formatInstr);
break;
case PropertyTagTypeSRational:
strRet = FormatTagSRational(propItem, formatInstr);
break;
default:
strRet = "";
break;
}
return strRet;
}
A Format Example
Once again, let's look at the ExposureTime
example with the following PropertyItem
values:
PropertyItem
Id = 33434
Len = 8
Type = 5
Value = 0A-00-00-00-E8-03-00-00
The Type
value of 5 indicates a RATIONAL type. Based on the FormatValue
method, the FormatTagRational
method is called.
private const int BYTEJUMP_LONG = 4;
private const int BYTEJUMP_RATIONAL = 8;
private const string DOUBLETYPE_FORMAT = "0.0####";
private static string FormatTagRational(PropertyItem propItem,
FormatInstr formatInstr) {
string strRet = "";
for (int i = 0; i < propItem.Len; i = i + BYTEJUMP_RATIONAL) {
System.UInt32 numer = BitConverter.ToUInt32(propItem.Value, i);
System.UInt32 denom = BitConverter.ToUInt32(propItem.Value, i
+ BYTEJUMP_LONG);
if (formatInstr == FormatInstr.FRACTION) {
UFraction frac = new UFraction(numer, denom);
strRet += frac.ToString();
}
else {
double dbl;
if (denom == 0)
dbl = 0.0;
else
dbl = (double)numer / (double)denom;
strRet += dbl.ToString(DOUBLETYPE_FORMAT);
}
if (i + BYTEJUMP_RATIONAL < propItem.Len)
strRet += " ";
}
return strRet;
}
The length of a rational value is 8 bytes (BYTEJUMP_RATIONAL
). In this case, the Len
field value indicates a single rational value.
A PropertyItem
Rational consists of two unsigned 4-byte integers, the numerator and the denominator. The BitConverter
class converts arrays of bytes to and from base data types; in this case, arrays of four bytes into 32-bit unsigned integers. The byte array "0A-00-00-00-E8-03-00-00" is converted into numer=10 and denom=1000.
In most cases, the numerator would be divided by the denominator; the result being formatted into the string "0.01". However, the ExposureTime
's associated PhotoTagMetadata
value contains an additional formatting instruction:
PhotoTagMetadata
Id = 33434
Category = "EXIF"
Name = "ExposureTime"
Description = "Exposure time, measured in seconds."
FormatInstr = 1 (FRACTION)
This FormatInstr
value instructs the method to return the value as a fraction string. But, how do we convert "10/1000" into "1/100"?
The Fraction and UFraction Classes
If you remember back to high school: you can reduce a fraction to its lowest terms by dividing the numerator and the denominator by their greatest common denominator (gcd). Unfortunately, there is no Reduce or GCD function provided by the .NET framework. So I create an ad hoc Fraction
and UFraction
class to provide this functionality.
public class UFraction {
private UInt32 _numer;
private UInt32 _denom;
public UFraction(UInt32 numer, UInt32 denom) {
_numer = numer;
_denom = denom;
}
public override string ToString() {
UInt32 numer = _numer;
UInt32 denom = (_denom == 0) ? 1 : _denom;
Reduce(ref numer, ref denom);
string result;
if (numer == 0)
result = "0";
else if (denom == 1)
result = numer + "";
else
result = numer + "/" + denom;
return result;
}
private static void Reduce(ref UInt32 numer, ref UInt32 denom) {
if (numer != 0) {
UInt32 common = GCD(numer, denom);
numer = numer / common;
denom = denom / common;
}
}
private static UInt32 GCD(UInt32 num1, UInt32 num2) {
while (num1 != num2)
if (num1 > num2)
num1 = num1 - num2;
else
num2 = num2 - num1;
return num1;
}
}
So in this case, the values, 10 and 1000 are presented as "1/100".
The WriteXml method
The WriteXml
method returns the analysis data as a specified XML output (based on the PhotoPropertyOutput.xsd schema). Each tag item is returned as a <tagDatum>
element containing the item's id
, category
, name
, description
, and value
. For example,
<tagDatum id="37434" category="EXIF">
<name>ExposureTime</name>
<description>Exposure time, measured in seconds.</description>
<value>1/100</value>
</tagDatum>
A prettyPrintValue
may also exist if the associated PhotoTagMetadata
contained a valueOptions
value. The MeteringMode
tag is such an example:
<tagDatum id="37383" category="EXIF">
<name>MeteringMode</name>
<description>Metering mode.</description>
<value>5</value>
<prettyPrintValue>Pattern</prettyPrintValue>
</tagDatum>
PrettyPrint Options
In a number of cases, the formatted value that is obtained from the analysis phase is not enough. A value of 5 for a MeteringMode
tag is not particularly useful to a viewer. That is why the valueOptions
element exists; It provides a pretty-print value for an internal value.
The MeteringMode
tagMetadata
contains a number of option
s that match various values.
<tagMetadata id="37383" category="EXIF">
<name>MeteringMode</name>
<description>Metering mode.</description>
<valueOptions>
<option key="0" value="unknown" />
<option key="1" value="Average" />
<option key="2" value="CenterWeightedAverage" />
<option key="3" value="Spot" />
<option key="4" value="MultiSpot" />
<option key="5" value="Pattern" />
<option key="6" value="Partial" />
<optionRange from="7" to="254" value="reserved" />
<option key="255" value="other" />
</valueOptions>
</tagMetadata>
Through key matching, the value of 5 can be presented as "Pattern". The <tagDatum>
element would include the prettyPrintValue
value, "Pattern".
<tagDatum id="37383" category="EXIF">
<name>MeteringMode</name>
<description>Metering mode.</description>
<value>5</value>
<prettyPrintValue>Pattern</prettyPrintValue>
</tagDatum>
An XSLT Transform
The XML output also includes an optional XSLT instruction to transform the XML output into a more readable format. The default XSLT instruction uses the provided PhotoPropertyOutput.xslt file to transform the XML into an HTML file.
Using The PhotoProperties Library
Now that I created the PhotoProperties library, I wanted to create an application to use the library. PhotoPropertiesApp is a Windows application that uses the PhotoProperties library to analyze a selected image file. The XML result is presented in two views. The top view presents the XML result in a simple Textbox
. The bottom view parses the XML result into a detailed ListView
. The ListView
can be sorted based upon Tag Id
, Category
, Name
, or Value
data. When a tag row is selected, the tag's description is presented in the lower window. The pretty-print value is presented in the Value
column when available.