Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Photo Properties

0.00/5 (No votes)
27 Feb 2003 1  
Reads properties - such as EXIF - from graphic files.

PhotoPropertiesApp

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 options 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.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here