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

An extensible ExifReader class with customizable tag handlers

0.00/5 (No votes)
29 Mar 2010 1  
An ExifReader class in C# that supports custom formatting and extraction. StyleCop compliant code, with demos for WPF and Windows Forms.

Figure 1: WPF-MVVM Demo App

Figure 2: Windows Forms Demo App

Introduction

I needed an Exif reader class for a C# application I was working on, and though I found quite a few implementations available including a few on The Code Project, none of them fully suited my requirements. So I wrote my own class. This article describes the use and implementation of this class, and also includes a couple of demo projects, one using Windows Forms and the PropertyGrid, and another using WPF and the growingly popular MVVM architecture. The ExifReader project is 100% Style Cop compliant except for the IDE generated AssemblyInfo.cs and the PropertyTagId.cs file which I custom created from multiple sources including some GDI+ header files, as well as trial and error. I didn't think it would be a great idea to try and apply Style Cop guidelines to either of those files. The class does not directly access Exif metadata from image files, and instead delegates that functionality to the Image class from System.Drawing. The public interface does not expose any System.Drawing types and so this library can be used from WPF without needing to reference System.Drawing.

Note to readers/users of this class

This class tries to cover as many of the documented and undocumented Exif tags as I could test on, but if any of you encounter images that have Exif tags that are not recognized by my class, I request you to kindly contact me and send me the images. Once I have the images, I can attempt to support those tags too. Right now, if it encounters an undocumented tag it will still extract the tag value, but you won't see any descriptive tag-name or any custom parsing/formatting that may be required.

Code was written on VS 2010 and .NET 4.0

All the code, including the demos, have been written and tested on VS 2010 RC and .NET 4.0. While I have not intentionally used any .NET 4.0/C# 4.0 only feature like say the dynamic keyword, I may have inadvertently written code that may not compile in .NET 3.5. But I don't expect any of those to be hard to fix or change for most programmers, but if you run into trouble and can't figure out what to do, please ping me via the article forum and I'll help fix it. I suspect the most common compatibility issues would be with regard to IEnumerable<T> cast requirements in .NET 3.5, since it became a variant interface only in .NET 4.0.

Inline code formatting

The inline code snippets in this article have been wrapped for easier viewing. The actual code in the source files don't assume that you are using a 10 inch Netbook. *grin*

Using the ExifReader class

If you are using an Exif reader class, I guess it's safe to assume that you are familiar with Exif properties and the Exif specification. So I won't go into the details. For those who are interested, here are two good links:

The second link includes the Exif 2.2 specification. The 2.21 specification is not available for free download, though there are sites where you can purchase it. You will also find that many cameras and phones insert Exif 2.21 tags even though the Exif version tag is still set to 2.2, and you will also find that applications like Adobe Photoshop insert their own custom tags. Camera manufacturers like Nikon and Canon have their own proprietary tags too. I have basically stuck to the standard tags and have not attempted to decipher any of the company proprietary tags, though it's possible that a future version may support those too.

The class interface is fairly trivial, all you do is instantiate an ExifReader object passing a file path, and then access the extracted Exif properties with a call to GetExifProperties. Here's some example code:

internal class Program
{
    internal static void Main(string[] args)
    {
        ExifReader exifReader = new ExifReader(
            @"C:\Users\Public\Pictures\Sample Pictures\Jellyfish.jpg");        

        foreach (ExifProperty item in exifReader.GetExifProperties())
        {
            Console.WriteLine("{0} = {1}", item.ExifPropertyName, item);
        }

        Console.ReadKey();
    }
}

The ExifProperty type will be described in a little more detail in the next two sections where I talk about the two demo applications. Essentially it's the only other type you'll have to deal with when using the ExifReader class. You can further dig into the property data by using ExifProperty.ExifValue if you need to as shown below, but it's an unlikely scenario unless you are dealing with a custom Exif tag type unique to your application, or if it's an unhandled proprietary tag as those from Nikon, Canon, or Adobe, in which case you may want to access the values directly and do your own interpretation or formatting.

ExifProperty copyRight = exifReader.GetExifProperties().Where(
    ep => ep.ExifTag == PropertyTagId.Copyright).FirstOrDefault();

if (copyRight != null)
{
    Console.WriteLine(copyRight.ExifValue.Count);
    Console.WriteLine(copyRight.ExifValue.ValueType);

    foreach (var value in copyRight.ExifValue.Values)
    {
        Console.WriteLine(value);                    
    }
}

PropertyTagId is an enum that represents the documented Exif-Tag properties as well as a few undocumented ones. There is also a PropertyTagType enum that represents the Exif data type. Both these enumerations will be discussed in later sections of the article.

Using custom formatters and extractors

The ExifReader supports custom handling of tags, which is useful not just for undocumented tags that are not automatically handled by the class, but also for specialized handling of documented tags. There are two events QueryUndefinedExtractor and QueryPropertyFormatter that you need to handle, and these events allow you to specify a custom formatter object as well as a custom extractor object. Note that it's usually convenient but not necessary to implement both the formatter and the extractor in a single class. Also be aware that if you provide a custom extractor for a documented tag, then it's highly recommended that you also implement a custom formatter, specially if you change the extracted data type of the property. Here's an example of a very simple tag handler class.

class MyCustomExifHandler 
  : IExifPropertyFormatter, IExifValueUndefinedExtractor
{
    public string DisplayName
    {
        get { return "My Display Name"; }
    }

    public string GetFormattedString(IExifValue exifValue)
    {
        return String.Concat("Formatted version of ", 
          exifValue.Values.Cast<string>().First());
    }

    public IExifValue GetExifValue(byte[] value, int length)
    {
        string bytesString = String.Join(" ", 
          value.Select(b => b.ToString("X2")));
        return new ExifValue<string>(
          new[] { String.Concat("MyValue = ", bytesString) });
    }
}

Both the implemented interfaces will be discussed later in this article. Once you have a custom handler class, you can handle the events and set the appropriate values on the EventArgs object.

ExifReader exifReader = new ExifReader(@". . .");

MyCustomExifHandler exifHandler = new MyCustomExifHandler();

exifReader.QueryUndefinedExtractor += (sender, e) => 
{
    if (e.TagId == 40093)
    {
        e.UndefinedExtractor = exifHandler;
    }
};

exifReader.QueryPropertyFormatter += (sender, e) =>
{
    if (e.TagId == 40093)
    {
        e.PropertyFormatter = exifHandler;
    }
};

foreach (ExifProperty item in exifReader.GetExifProperties())
{
    . . .
}

Notice how it's of extreme importance to check the TagId. If you do not do this, you will be applying your custom formatter or extractor to every single Exif tag that's extracted from the image and that's guaranteed to end in disaster!

Demo app for Windows Forms

The Windows Forms demo uses a PropertyGrid to show the various Exif properties. The ExifReader has built-in support for the PropertyGrid control, so there is very little code you need to write. Here's the entire user-written code in the Windows Forms demo project:

/// <summary>
/// Event handler for Browse button Click.
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">Event arguments.</param>
private void ButtonBrowse_Click(object sender, EventArgs e)
{
    using (OpenFileDialog dialog = new OpenFileDialog()
        {
            Filter = "Image Files(*.PNG;*.JPG)|*.PNG;*.JPG;"
        })
    {
        if (dialog.ShowDialog() == DialogResult.OK)
        {
            try
            {
                ExifReader exifReader = new ExifReader(dialog.FileName);
                this.propertyGridExif.SelectedObject = exifReader;
                this.pictureBoxPreview.ImageLocation = dialog.FileName;
            }
            catch (ExifReaderException ex)
            {
                MessageBox.Show(ex.Message, "ExifReaderException was caught");
            }
        }
    }
}

The highlighted line shows where we associate the ExifReader object with the PropertyGrid control. Figure 2 shows a screenshot of what the app would look like when run and an image is selected. The reason this works this way is that the ExifReader class has a custom TypeDescriptionProvider which dynamically provides the property descriptors that the PropertyGrid looks for. I've talked a little about how this has been implemented down below in the Implementation Details section. From a Windows Forms perspective, if you have a form where you need to display the Exif properties, this would be the simplest way to go about doing it. Drop a PropertyGrid control on the form and then set the SelectedObject to an ExifReader instance, and that's all you need to do.

Handling exceptions

Notice how an exception of type ExifReaderException is caught and handled. The ExifReader's constructor is the only place where this exception is directly thrown. Exception handling is discussed in multiple places in the article, but in brief, you typically get this exception here if a file path is invalid or an image file is corrupt or has invalid Exif metadata. Once the ExifReader is instantiated, you will never get a direct exception of type ExifReaderException. The data binding (whether in WinForms or WPF) will primarily be via property accessors and the ToString override, and thus throwing arbitrary exception objects from these locations is not recommended and quite against standard .NET practices. From all these places, what's thrown is an exception of type InvalidOperationException which the majority of well-written data binding libraries know how to handle correctly. You can still handle this exception in the debugger and access the source ExifReaderException object via the InnerException property of the InvalidOperationException object.

Demo app for WPF/MVVM

Three gentlemen by the names of Josh Smith, Sacha Barber, and Pete O'Hanlon have been saying good things about MVVM here on The Code Project for at least a couple of years now, and so I thought it would be a good idea to apply it for the WPF demo app, even though the entire app is of a very simplistic nature. Of course my implementation is rather plain and lacks the finesse and poise that we now take for granted in articles and applications written by the afore mentioned gentlemen. While it's not an absolute requisite that the View class is 100% Xaml with zero code-behind, since this was my first MVVM app, I went out of my way to make sure that there is no code-behind in use. While it's not connected with the ExifReader per se, I will still briefly run through what I did to do what I wanted to do.

Figure 3: WPF-MVVM app with filter applied

The WPF demo does a little more than the WinForms demo. For one thing, it shows the Exif data type and the Exif tag name for each extracted tag. And then for another, it supports the ability to filter results using a search keyword that searches the tag name as well as the tag property value. And then there's also the fact that my demo app portrays my absolutely horrid sense of color schemes, styles, and UI themes. I always give Sacha a hard time regarding his rather unorthodox ideas on colors and styles, but I think I've surpassed even Sacha in poor UI taste. *grin*

The ViewModel class

There's only one ViewModel class for this demo project, and it has commands for browse, exit, and filter. There is also a public CollectionViewSource property called ExifProperties which returns the extracted Exif properties. Initially this was an ObservableCollection<ExifProperty> property but I had to change it to a CollectionViewSource property to adamantly stick with my decision to avoid code-behind in the view class. Since I wanted to do filtering, this was the simplest way I could use the built-in filtering support of the CollectionViewSource entirely inside the ViewModel class.

internal class MainViewModel : INotifyPropertyChanged
{
    private ICommand browseCommand;

    private ICommand exitCommand;

    private ICommand filterCommand;

    private string searchText = String.Empty;

    private ObservableCollection<ExifProperty> exifPropertiesInternal 
        = new ObservableCollection<ExifProperty>();

    private CollectionViewSource exifProperties = new CollectionViewSource();

    private ImageSource previewImage;

    public MainViewModel()
    {
        exifProperties.Source = exifPropertiesInternal;
        exifProperties.Filter += ExifProperties_Filter;
    }

    public CollectionViewSource ExifProperties
    {
        get { return exifProperties; }
    }

    public ICommand BrowseCommand
    {
        get
        {
            return browseCommand ?? 
              (browseCommand = new DelegateCommand(BrowseForImage));
        }
    }

    public ICommand ExitCommand
    {
        get
        {
            return exitCommand ?? 
              (exitCommand = new DelegateCommand(
                () => Application.Current.Shutdown()));
        }
    }

    public ICommand FilterCommand
    {
        get
        {
            return filterCommand ?? 
              filterCommand = new DelegateCommand(
                () => exifProperties.View.Refresh()));
        }
    }

    public ImageSource PreviewImage
    {
        get
        {
            return previewImage;
        }

        set
        {
            if (previewImage != value)
            {
                previewImage = value;
                FirePropertyChanged("PreviewImage");
            }
        }
    }

    public string SearchText
    {
        get
        {
            return searchText;
        }

        set
        {
            if (searchText != value)
            {
                searchText = value;
                FirePropertyChanged("SearchText");
            }
        }
    }

    private void BrowseForImage()
    {
        // <snipped>
    }

    private void ExifProperties_Filter(object sender, FilterEventArgs e)
    {
        // <snipped>
    }

    public event PropertyChangedEventHandler PropertyChanged;

    private void FirePropertyChanged(string propertyName)
    {
        PropertyChangedEventHandler handler = PropertyChanged;

        if (handler != null)
        {
            handler(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

Here's the code where the image is browsed to, and the ExifReader instantiated. All the extracted properties are added to the ObservableCollection<ExifProperty> property, and the preview image is appropriately set.

private void BrowseForImage()
{
    OpenFileDialog fileDialog = new OpenFileDialog()
        {
            Filter = "Image Files(*.PNG;*.JPG)|*.PNG;*.JPG;"
        };

    if (fileDialog.ShowDialog().GetValueOrDefault())
    {
        try
        {
            ExifReader exifReader = new ExifReader(fileDialog.FileName);
            exifPropertiesInternal.Clear();
            this.SearchText = String.Empty;
            foreach (var item in exifReader.GetExifProperties())
            {
                exifPropertiesInternal.Add(item);
            }

            this.PreviewImage = new BitmapImage(new Uri(fileDialog.FileName));
        }
        catch (ExifReaderException ex)
        {
            MessageBox.Show(ex.Message, "ExifReaderException was caught");
        }
    }
}

Note how similar to the WinForms demo, there's a try-catch handler specifically for an exception of type ExifReaderException. For most apps, you may want to filter out specific tags at this point, or maybe only show some pre-selected list of Exif properties. Either way there's nothing there that a couple of lines of LINQ won't solve. I did consider adding built-in support for filtering within the ExifReader class but then decided it deviates from the core class functionality, and does not really have a lot of value anyway since the user can do that in one or two lines of code. For the demo app, the filter is text-based and does a case insensitive search on the Exif property name as well as the property value's string representation.

private void ExifProperties_Filter(object sender, FilterEventArgs e)
{
    ExifProperty exifProperty = e.Item as ExifProperty;
    if (exifProperty == null)
    {
        return;
    }

    foreach (string body in new[] { 
      exifProperty.ExifPropertyName, 
      exifProperty.ExifTag.ToString(), exifProperty.ToString() })
    {
        e.Accepted = body.IndexOf(
          searchText, StringComparison.OrdinalIgnoreCase) != -1;

        if (e.Accepted)
        {
            break;
        }
    }
}

The View

The view is the main Window class in the project (I did say this was a rather crude MVVM implementation, so don't smirk too much). There's a ListBox with a data template applied that displays the Exif properties. Here's some partially snipped, and heavily word wrapped Xaml code:

<ListBox Grid.Column="1" ItemsSource="{Binding ExifProperties.View}" 
     Background="Transparent" TextBlock.FontSize="11"
     ScrollViewer.HorizontalScrollBarVisibility="Disabled" 
     VerticalContentAlignment="Stretch" HorizontalAlignment="Stretch">

    . . .

    <ListBox.ItemTemplate>
        <DataTemplate>

            <dropShadow:SystemDropShadowChrome CornerRadius="20,20,0,0">
                <StackPanel Orientation="Vertical" Margin="5" Width="240" 
                  Background="Transparent">

                    <Border CornerRadius="20,20,0,0" Background="#FF0D3C83" 
                      x:Name="topPanel">
                        <StackPanel Orientation="Horizontal" Margin="6,0,0,0">
                            <TextBlock x:Name="titleText" 
                              Text="{Binding ExifPropertyName}" 
                              Foreground="White" FontWeight="Bold" />
                        </StackPanel>
                    </Border>

                    <StackPanel Orientation="Vertical" 
                      Background="#FF338DBE" x:Name="mainPanel">
                        <StackPanel Orientation="Horizontal">
                            <TextBlock Text="Exif Tag: " 
                              FontWeight="Bold" MinWidth="100" />
                            <TextBlock Text="{Binding ExifTag}" />
                        </StackPanel>
                        <StackPanel Orientation="Horizontal">
                            <TextBlock Text="Exif Datatype: " 
                              FontWeight="Bold" MinWidth="100" />
                            <TextBlock Text="{Binding ExifDatatype}" />
                        </StackPanel>
                        <StackPanel Orientation="Horizontal">
                            <TextBlock Text="Property Value: " 
                              FontWeight="Bold" MinWidth="100" />
                            <TextBlock Text="{Binding}" />
                        </StackPanel>
                    </StackPanel>

                </StackPanel>
            </dropShadow:SystemDropShadowChrome>

            . . .

        </DataTemplate>
    </ListBox.ItemTemplate>

    <ListBox.ItemsPanel>
        <ItemsPanelTemplate>
            <WrapPanel />
        </ItemsPanelTemplate>
    </ListBox.ItemsPanel>
</ListBox>

I've removed the superfluous styling code. The binding points in code are highlighted above, and the reason the property value binds to self is that the ExifProperty class has a ToString override - which makes it trouble-free to render readable text for the Exif property values. In fact the Windows Forms PropertyGrid uses ToString too and this ensures that you'll see consistent display values irrespective of how you use the class.

I wanted the filtering to be live, as in the filter should take effect as the user types into the text box. The big problem with this is that the TextBox does not have a Command property, and thus I'd need to use code-behind to proxy its text changed event to the ViewModel. And I did not want to do that. Once again this was purely whimsical behavior on my part since MVVM does not mandate this at all, though a lot of people do recommend it. Apparently, you can get around this by referencing an Expression Blend DLL, which has support for interaction triggers that you can forward to a command object, thereby avoiding any code-behind. I didn't want to reference an Expression Blend DLL, not for a simple demo app anyway and so I was forced to work around it by adding an attached behavior to the TextBox that could take a command object. This was total over-kill of course, and in a real world app I'd simply do it in code-behind. Most likely something like :

ViewModel viewModel = this.DataContext as ViewModel;

. . . 

private void TextChanged(. . .)
{
    viewModel.SomeCommand(. . .) ;
}

That'd be in my view's code-behind, and while some purists may not be too happy, I think it's definitely simpler than referencing Expression Blend! And to paraphrase Rama Vavilala's words : "after all, these frameworks are supposed to make things simpler". Here's the Xaml code snippet:

<TextBox x:Name="searchTextBox" Width="165"  
  HorizontalAlignment="Left" Margin="3,0,0,0"
  Text="{Binding SearchText}" 
  local:TextChangedBehavior.TextChanged="{Binding FilterCommand}" />

Instead of handling the TextChanged event, I handle it via the attached behavior and route it to the FilterCommand command object in the ViewModel. Here's the code for the attached behavior:

internal class TextChangedBehavior
{
    public static DependencyProperty TextChangedCommandProperty 
        = DependencyProperty.RegisterAttached(
          "TextChanged", 
          typeof(ICommand), 
          typeof(TextChangedBehavior),
          new FrameworkPropertyMetadata(
            null, 
            new PropertyChangedCallback(
              TextChangedBehavior.TextChangedChanged)));

    public static void SetTextChanged(TextBox target, ICommand value)
    {
        target.SetValue(TextChangedBehavior.TextChangedCommandProperty, 
          value);
    }

    public static ICommand GetTextChanged(TextBox target)
    {
        return (ICommand)target.GetValue(TextChangedCommandProperty);
    }

    private static void TextChangedChanged(
      DependencyObject target, DependencyPropertyChangedEventArgs e)
    {
        TextBox element = target as TextBox;

        if (element != null)
        {
            if (e.NewValue != null)
            {
                element.TextChanged += Element_TextChanged;
            }
            else
            {
                element.TextChanged -= Element_TextChanged;
            }
        }
    }

    static void Element_TextChanged(object sender, TextChangedEventArgs e)
    {
        TextBox textBox = (TextBox)sender;
        BindingExpression bindingExpression = textBox.GetBindingExpression(
          TextBox.TextProperty);

        if (bindingExpression != null)
        {                
            bindingExpression.UpdateSource();
        } 

        ICommand command = GetTextChanged(textBox);

        if (command.CanExecute(null))
        {
            command.Execute(null);
        }
    }
}

Nothing complicated there, just a basic attached behavior implementation. The highlighted code shows where we proxy to the actual command implementation.

ExifReader Class Reference

As I already mentioned, except for that one enum source file, the rest of the ExifReader code is fully StyleCop compliant, and thus I had to add XML comments for every method, property, and field, including private ones. So the code is basically self documenting for the most part. Once again the code snippets below are line wrapped for viewing, so some of the comment lines will seem rather awkwardly split up into multiple lines.

The ExifReader class

/// <summary>
/// This is the implementation of the ExifReader class that 
/// reads EXIF data from image files.
/// It partially supports the Exif Version 2.2 standard.
/// </summary>
[TypeDescriptionProvider(typeof(ExifReaderTypeDescriptionProvider))]
public class ExifReader
{
    /// <summary>
    /// List of Exif properties for the image.
    /// </summary>
    private List<ExifProperty> exifproperties;

    /// <summary>
    /// The Image object associated with the image file
    /// </summary>
    private Image imageFile;

    /// <summary>
    /// Initializes a new instance of the ExifReader class based on a file path.
    /// </summary>
    /// <param name="imageFileName">Full path to the image file</param>
    public ExifReader(string imageFileName);

    /// <summary>
    /// Occurs when the class needs to query for a property formatter
    /// </summary>
    public event EventHandler<
      QueryPropertyFormatterEventArgs> QueryPropertyFormatter;

    /// <summary>
    /// Occurs when the class needs to query for an undefined extractor
    /// </summary>
    public event EventHandler<
      QueryUndefinedExtractorEventArgs> QueryUndefinedExtractor;

    /// <summary>
    /// Returns a read-only collection of all the Exif properties
    /// </summary>
    /// <returns>The Exif properties</returns>
    public ReadOnlyCollection<ExifProperty> GetExifProperties();

    /// <summary>
    /// Checks to see if a custom property formatter is available
    /// </summary>
    /// <param name="tagId">The tag Id to check for a formatter</param>
    /// <returns>An IExifPropertyFormatter or null 
    /// if there's no formatter available</returns>
    internal IExifPropertyFormatter QueryForCustomPropertyFormatter(int tagId);

    /// <summary>
    /// Checks to see if a custom undefined extractor is available
    /// </summary>
    /// <param name="tagId">The tag Id to check for an extractor</param>
    /// <returns>An IExifValueUndefinedExtractor or null 
    /// if there's no formatter available</returns>
    internal IExifValueUndefinedExtractor 
      QueryForCustomUndefinedExtractor(int tagId);

    /// <summary>
    /// Fires the QueryPropertyFormatter event
    /// </summary>
    /// <param name="eventArgs">Args data for the 
    /// QueryPropertyFormatter event</param>
    private void FireQueryPropertyFormatter(
      QueryPropertyFormatterEventArgs eventArgs);

    /// <summary>
    /// Fires the QueryUndefinedExtractor event
    /// </summary>
    /// <param name="eventArgs">Args data for the 
    /// QueryUndefinedExtractor event</param>
    private void FireQueryUndefinedExtractor(
      QueryUndefinedExtractorEventArgs eventArgs);

    /// <summary>
    /// Initializes the Exif properties for the associated image file
    /// </summary>
    private void InitializeExifProperties();
}

Earlier on, I had a constructor overload that took an Image argument but it was removed so that WPF projects can reference the DLL without needing to reference the System.Drawing DLL.

The QueryPropertyFormatterEventArgs class

/// <summary>
/// Provides data for the QueryPropertyFormatter event
/// </summary>
public class QueryPropertyFormatterEventArgs : EventArgs
{
    /// <summary>
    /// Initializes a new instance of the 
    /// QueryPropertyFormatterEventArgs class.
    /// </summary>
    /// <param name="tagId">The tag Id to query 
    /// a property formatter for</param>
    public QueryPropertyFormatterEventArgs(int tagId);

    /// <summary>
    /// Gets or sets the associated property formatter
    /// </summary>
    public IExifPropertyFormatter PropertyFormatter { get; set; }

    /// <summary>
    /// Gets the associated tag Id
    /// </summary>
    public int TagId { get; private set; }
}

The QueryUndefinedExtractorEventArgs class

/// <summary>
/// Provides data for the QueryUndefinedExtractor event
/// </summary>
public class QueryUndefinedExtractorEventArgs : EventArgs
{
    /// <summary>
    /// Initializes a new instance of the 
    /// QueryUndefinedExtractorEventArgs class.
    /// </summary>
    /// <param name="tagId">The tag Id to query a 
    /// property formatter for</param>
    public QueryUndefinedExtractorEventArgs(int tagId);

    /// <summary>
    /// Gets or sets the associated property formatter
    /// </summary>
    public IExifValueUndefinedExtractor UndefinedExtractor { get; set; }

    /// <summary>
    /// Gets the associated tag Id
    /// </summary>
    public int TagId { get; private set; }
}

I considered unifying the above two EventArgs classes into a single generic class, but then decided to retain them as separate classes since they are both used in public event handlers where I feel clarity is more important than compactness.

The ExifProperty class

/// <summary>
/// Represents an Exif property.
/// </summary>
public class ExifProperty
{
    /// <summary>
    /// The PropertyItem associated with this object.
    /// </summary>
    private PropertyItem propertyItem;

    /// <summary>
    /// The IExifValue associated with this object.
    /// </summary>
    private IExifValue exifValue;

    /// <summary>
    /// The IExifPropertyFormatter for this property.
    /// </summary>
    private IExifPropertyFormatter propertyFormatter;

    /// <summary>
    /// Set to true if this object represents an unknown property tag
    /// </summary>
    private bool isUnknown;

    /// <summary>
    /// Set to true if this object has a custom property formatter
    /// </summary>
    private bool hasCustomFormatter;

    /// <summary>
    /// The parent ExifReader that owns this ExifProperty object
    /// </summary>
    private ExifReader parentReader;

    /// <summary>
    /// Initializes a new instance of the ExifProperty class.
    /// It's marked internal  as it's not intended to be 
    /// instantiated independently outside of the library.
    /// </summary>
    /// <param name="propertyItem">The PropertyItem to 
    /// base the object on</param>
    /// <param name="parentReader">The parent ExifReader</param>
    internal ExifProperty(PropertyItem propertyItem, ExifReader parentReader);

    /// <summary>
    /// Gets the IExifValue for this property
    /// </summary>
    public IExifValue ExifValue;

    /// <summary>
    /// Gets the descriptive name of the Exif property
    /// </summary>
    public string ExifPropertyName;

    /// <summary>
    /// Gets a category name for the property.
    /// Note: This is not part of the Exif standard 
    /// and is merely for convenience.
    /// </summary>
    public string ExifPropertyCategory;

    /// <summary>
    /// Gets the Exif property tag Id for this property
    /// </summary>
    public PropertyTagId ExifTag;

    /// <summary>
    /// Gets the Exif data type for this property
    /// </summary>
    public PropertyTagType ExifDatatype;

    /// <summary>
    /// Gets the raw Exif tag. For unknown tags this will not
    /// match the value of the ExifTag property.
    /// </summary>
    public int RawExifTagId;

    /// <summary>
    /// Override for ToString
    /// </summary>
    /// <returns>Returns a readable string representing 
    /// the Exif property's value</returns>
    public override string ToString();

    /// <summary>
    /// Gets the formatted string using the property formatter
    /// </summary>
    /// <returns>The formatted string</returns>
    private string GetFormattedString();

    /// <summary>
    /// Initializes the exifValue field.
    /// </summary>
    /// <returns>The initialized exifValue</returns>
    private IExifValue InitializeExifValue();

    /// <summary>
    /// Returns an ExifReaderException set with the current property formatter
    /// </summary>
    /// <param name="ex">Inner exception object</param>
    /// <returns>The ExifReaderException object</returns>
    private ExifReaderException GetExifReaderException(Exception ex);
}

The IExifValue interface

    /// <summary>
    /// This interface represents an Exif property value
    /// </summary>
    public interface IExifValue
    {
        /// <summary>
        /// Gets the type of the Exif property value or values
        /// </summary>
        Type ValueType { get; }

        /// <summary>
        /// Gets the number of values
        /// </summary>
        int Count { get; }

        /// <summary>
        /// Gets a type-unsafe collection of values of a specific 
        /// Exif tag data type
        /// </summary>
        IEnumerable Values { get; }
    }

Normal usage would not require accessing the ExifProperty.ExifValue property, but this is provided for custom interpretation of undocumented or proprietary tags. Note how the IExifValue interface actually returns a System.Type for the ValueType property. I designed it thus since the IExifValue represents how the value is represented internally by the ExifReader class and not an actual byte-based representation of the native Exif data. The user can still access ExifProperty.ExifDatatype to get the Exif data type associated with the property.

The PropertyTagType enumeration

/// <summary>
/// Defines the various Exif property tag type values
/// </summary>
public enum PropertyTagType
{
    /// <summary>
    /// An 8-bit unsigned integer
    /// </summary>
    Byte = 1,

    /// <summary>
    /// A NULL terminated ASCII string
    /// </summary>
    ASCII = 2,

    /// <summary>
    /// A 16-bit unsigned integer
    /// </summary>
    Short = 3,

    /// <summary>
    /// A 32-bit unsigned integer
    /// </summary>
    Long = 4,

    /// <summary>
    /// Two LONGs. The first is the numerator and the 
    /// second the denominator
    /// </summary>
    Rational = 5,

    /// <summary>
    /// An 8-bit byte that can take any value depending on 
    /// the field definition
    /// </summary>
    Undefined = 7,

    /// <summary>
    /// A 32-bit signed integer 
    /// </summary>
    SLong = 9,

    /// <summary>
    /// Two SLONGs. The first SLONG is the numerator and 
    /// the second the denominator
    /// </summary>
    SRational = 10
}

The PropertyTagId enumeration

/// <summary>
/// Defines the common Exif property Ids
/// </summary>
/// <remarks>
/// This is not a comprehensive list since there are several 
/// non-standard Ids in use (example those from Adobe)
/// </remarks>
public enum PropertyTagId
{
    GpsVer = 0x0000,

    GpsLatitudeRef = 0x0001,
    
    . . .
    
    <snipped>

This is too long to list here in its entirety, so please refer to the source code. I do discuss a little about how I've used this enum in the whole Exif extraction process in the next section. I have added one special value here to represent tags that I do not recognize, and I've chosen the largest 16 bit value possible that I hope will not coincide with an actual Exif tag value used by some proprietary Exif format.

[EnumDisplayName("Unknown Exif Tag")]
UnknownExifTag = 0xFFFF

I was originally casting the integer value to the enum because WinForms and its PropertyGrid did not complain about invalid enum values, and ToString would just return the numeric string value which was all perfectly okay. Strictly speaking WPF did not complain either, but I saw a first-chance exception in the output window and traced it down to the fact that the default WPF data binding uses the internal class SourceDefaultValueConverter from PresentationFramework which was throwing an ArgumentException when it got an enum value that was outside the enum range. So I decided to go ahead with an UnknownExifTag enum value for values that were not in the enumeration. Oh, and I chose 0xFFFF instead of 0 because GpsVer has already used 0! You can still get the original Tag-Id by using the RawExifTagId property which returns an int.

The ExifReaderException class

/// <summary>
/// Represents an exception that is thrown whenever the ExifReader catches
/// any exception when applying a formatter or an extractor.
/// </summary>
[Serializable]
public class ExifReaderException : Exception
{
    /// <summary>
    /// Initializes a new instance of the ExifReaderException class.
    /// </summary>
    public ExifReaderException();

    /// <summary>
    /// Initializes a new instance of the ExifReaderException class 
    /// with the specific arguments
    /// </summary>
    /// <param name="innerException">The source exception</param>
    internal ExifReaderException(Exception innerException);

    /// <summary>
    /// Initializes a new instance of the ExifReaderException class 
    /// with the specific arguments
    /// </summary>
    /// <param name="message">The error message for the exception</param>
    /// <param name="innerException">The source exception</param>
    internal ExifReaderException(string message, Exception innerException);

    /// <summary>
    /// Initializes a new instance of the ExifReaderException class 
    /// with the specific arguments
    /// </summary>
    /// <param name="innerException">The source exception</param>
    /// <param name="propertyFormatter">The property formatter if any</param>
    internal ExifReaderException(Exception innerException, 
      IExifPropertyFormatter propertyFormatter);

    /// <summary>
    /// Initializes a new instance of the ExifReaderException class 
    /// with the specific arguments
    /// </summary>
    /// <param name="innerException">The source exception</param>
    /// <param name="undefinedExtractor">
    /// The undefined extractor if any</param>
    internal ExifReaderException(Exception innerException,  
      IExifValueUndefinedExtractor undefinedExtractor);

    /// <summary>
    /// Initializes a new instance of the ExifReaderException class 
    /// with the specific arguments
    /// </summary>
    /// <param name="innerException">The source exception</param>
    /// <param name="propertyFormatter">The property formatter if any</param>
    /// <param name="undefinedExtractor">The undefined extractor if any</param>
    internal ExifReaderException(Exception innerException, 
      IExifPropertyFormatter propertyFormatter, 
      IExifValueUndefinedExtractor undefinedExtractor);

    /// <summary>
    /// Initializes a new instance of the ExifReaderException class 
    /// with the specific arguments
    /// </summary>
    /// <param name="message">The error message for the exception</param>
    /// <param name="innerException">The source exception</param>
    /// <param name="propertyFormatter">The property formatter if any</param>
    /// <param name="undefinedExtractor">The undefined extractor if any</param>
    internal ExifReaderException(string message, 
      Exception innerException, 
      IExifPropertyFormatter propertyFormatter, 
      IExifValueUndefinedExtractor undefinedExtractor);

    /// <summary>
    /// Gets the property formatter used at the time of exception
    /// </summary>
    public IExifPropertyFormatter PropertyFormatter { get; private set; }

    /// <summary>
    /// Gets the undefined extractor used at the time of exception
    /// </summary>
    public IExifValueUndefinedExtractor UndefinedExtractor 
      { get; private set; }

    /// <summary>
    /// Sets info into the SerializationInfo object
    /// </summary>
    /// <param name="info">The serialized object data on the exception 
    /// being thrown</param>
    /// <param name="context">Contaisn context info</param>
    public override void GetObjectData(
      SerializationInfo info, StreamingContext context);
}

If you implement custom formatters or extractors, you should not throw an ExifReaderException, instead you should throw a standard exception or even a custom exception specific to your application. The ExifReader class will handle that exception and re-throw an ExifReaderException which will have your thrown exception as the InnerException, and hence the internal constructors (barring the default constructor which is left public for serialization).

Implementation Details

The source code is profusely commented and so anyone interested in getting a broad understanding of the code should probably browse through the source code. In this section I'll briefly talk about the basic design and also anything that I think will be interesting to a percentage of readers. As mentioned in the introduction, I use the System.Drawing Image class to get the Exif info out of a supported image file.

private void InitializeExifProperties()
{
    this.exifproperties = new List<ExifProperty>();

    foreach (var propertyItem in this.imageFile.PropertyItems)
    {
        this.exifproperties.Add(new ExifProperty(propertyItem, this));
    }
}

The InitializeExifProperties method creates an ExifProperty instance for every PropertyItem returned by the Image class. Before I go into what happens inside ExifProperty, I'd like to describe the IExifPropertyFormatter interface that is used to get a readable display-value for an Exif property.

/// <summary>
/// This interface defines how a property value is formatted for display
/// </summary>
public interface IExifPropertyFormatter
{
    /// <summary>
    /// Gets a display name for this property
    /// </summary>
    string DisplayName { get; }    
    
    /// <summary>
    /// Gets a formatted string for a given Exif value
    /// </summary>
    /// <param name="exifValue">The source Exif value</param>
    /// <returns>The formatted string</returns>
    string GetFormattedString(IExifValue exifValue);
}

ExifProperty has a field propertyFormatter of type IExifPropertyFormatter which is initialized in the constructor.

internal ExifProperty(PropertyItem propertyItem, ExifReader parentReader)
{
    this.parentReader = parentReader;
    this.propertyItem = propertyItem;
    this.isUnknown = !Enum.IsDefined(typeof(PropertyTagId), 
      this.RawExifTagId);

    var customFormatter = 
      this.parentReader.QueryForCustomPropertyFormatter(this.RawExifTagId);

    if (customFormatter == null)
    {
        this.propertyFormatter = 
          ExifPropertyFormatterProvider.GetExifPropertyFormatter(this.ExifTag);
    }
    else
    {
        this.propertyFormatter = customFormatter;
        this.hasCustomFormatter = true;
    }
}

The ExifProperty constructor calls QueryForCustomPropertyFormatter on the parent ExifReader class to see if there is an user specified property formatter available.

internal IExifPropertyFormatter QueryForCustomPropertyFormatter(
    int tagId)
{
    QueryPropertyFormatterEventArgs eventArgs = 
      new QueryPropertyFormatterEventArgs(tagId);
    this.FireQueryPropertyFormatter(eventArgs);
    return eventArgs.PropertyFormatter;
}

If there is no custom formatter provided, then a property formatter is obtained via the ExifPropertyFormatterProvider class.

Specifying property formatters via a custom enum attribute

ExifPropertyFormatterProvider is a class that returns an IExifPropertyFormatter for a given Exif tag. It looks for a custom attribute in the PropertyTagId enumeration to figure out what property formatter needs to be used. Here're a couple of examples of how an enum value might specify a property formatter.

[ExifPropertyFormatter(typeof(ExifExposureTimePropertyFormatter))]
ExifExposureTime = 0x829A,

[ExifPropertyFormatter(typeof(ExifFNumberPropertyFormatter), 
          ConstructorNeedsPropertyTag = true)]
[EnumDisplayName("F-Stop")]
ExifFNumber = 0x829D,

ExifPropertyFormatter is a custom attribute that can be applied to an enum.

/// <summary>
/// An attribute used to specify an IExifPropertyFormatter for Exif Tag Ids 
/// </summary>
[AttributeUsage(AttributeTargets.Field)]
internal class ExifPropertyFormatterAttribute : Attribute
{
    /// <summary>
    /// The IExifPropertyFormatter object
    /// </summary>
    private IExifPropertyFormatter exifPropertyFormatter;

    /// <summary>
    /// The type of the IExifPropertyFormatter
    /// </summary>
    private Type exifPropertyFormatterType;

    /// <summary>
    /// Initializes a new instance of the ExifPropertyFormatterAttribute class
    /// </summary>
    /// <param name="exifPropertyFormatterType">
    /// The type of the IExifPropertyFormatter</param>
    public ExifPropertyFormatterAttribute(Type exifPropertyFormatterType)
    {
        this.exifPropertyFormatterType = exifPropertyFormatterType;
    }

    /// <summary>
    /// Gets or sets a value indicating whether the constructor 
    /// for the property formatter
    /// needs to be passed the property tag as an argument. 
    /// </summary>
    public bool ConstructorNeedsPropertyTag { get; set; }

    /// <summary>
    /// Gets the IExifPropertyFormatter
    /// </summary>
    /// <param name="args">Optional arguments</param>
    /// <returns>The IExifPropertyFormatter</returns>
    public IExifPropertyFormatter GetExifPropertyFormatter(
        params object[] args)
    {
            return this.exifPropertyFormatter ??
                (this.exifPropertyFormatter = 
                  Activator.CreateInstance(
                  this.exifPropertyFormatterType, 
                  args) as IExifPropertyFormatter);
    }
}

And here's how the ExifPropertyFormatterProvider class uses this attribute to return the appropriate property formatter.

/// <summary>
/// This class provides appropriate IExifPropertyFormatter 
/// objects for Exif property values
/// </summary>
public static class ExifPropertyFormatterProvider
{
    /// <summary>
    /// Gets an IExifPropertyFormatter for the specific tagId
    /// </summary>
    /// <param name="tagId">The Exif Tag Id</param>
    /// <returns>An IExifPropertyFormatter</returns>
    internal static IExifPropertyFormatter GetExifPropertyFormatter(
        PropertyTagId tagId)
    {
        ExifPropertyFormatterAttribute attribute = 
          CachedAttributeExtractor<PropertyTagId, 
            ExifPropertyFormatterAttribute>.Instance.GetAttributeForField(
              tagId.ToString());

        if (attribute != null)
        {
            return attribute.ConstructorNeedsPropertyTag ? 
              attribute.GetExifPropertyFormatter(tagId) : 
              attribute.GetExifPropertyFormatter();
        }

        return new SimpleExifPropertyFormatter(tagId);
    }
}

If the attribute is found, the attribute's associated property formatter is returned. If there is no match, then a basic formatter is returned.

The CachedAttributeExtractor utility class

Notice how I've used the CachedAttributeExtractor utility class to extract the custom attribute. It's a handy little class that not only makes it easy to extract attributes but also caches the attributes for later access.

/// <summary>
/// A generic class used to retrieve an attribute from a type, 
/// and cache the extracted values for future access.
/// </summary>
/// <typeparam name="T">The type to search on</typeparam>
/// <typeparam name="A">The attribute type to extract</typeparam>
internal class CachedAttributeExtractor<T, A> where A : Attribute
{
    /// <summary>
    /// The singleton instance
    /// </summary>
    private static CachedAttributeExtractor<T, A> instance 
      = new CachedAttributeExtractor<T, A>();

    /// <summary>
    /// The map of fields to attributes
    /// </summary>
    private Dictionary<string, A> fieldAttributeMap 
      = new Dictionary<string, A>();

    /// <summary>
    /// Prevents a default instance of the CachedAttributeExtractor 
    /// class from being created.
    /// </summary>
    private CachedAttributeExtractor()
    {
    }

    /// <summary>
    /// Gets the singleton instance
    /// </summary>
    internal static CachedAttributeExtractor<T, A> Instance
    {
        get { return CachedAttributeExtractor<T, A>.instance; }
    }

    /// <summary>
    /// Gets the attribute for the field
    /// </summary>
    /// <param name="field">Name of the field</param>
    /// <returns>The attribute on the field or null</returns>
    public A GetAttributeForField(string field)
    {
        A attribute;

        if (!this.fieldAttributeMap.TryGetValue(field, out attribute))
        {
            if (this.TryExtractAttributeFromField(field, out attribute))
            {
                this.fieldAttributeMap[field] = attribute;
            }
            else
            {
                attribute = null;
            }
        }

        return attribute;
    }

    /// <summary>
    /// Get the attribute for the field 
    /// </summary>
    /// <param name="field">Name of the field</param>
    /// <param name="attribute">The attribute</param>
    /// <returns>Returns true of the attribute was found</returns>
    private bool TryExtractAttributeFromField(
        string field, out A attribute)
    {
        var fieldInfo = typeof(T).GetField(field);
        attribute = null;

        if (fieldInfo != null)
        {
            A[] attributes = fieldInfo.GetCustomAttributes(
                typeof(A), false) as A[];
            if (attributes.Length > 0)
            {
                attribute = attributes[0];
            }
        }

        return attribute != null;
    }
}

Creating the Exif values

Before going into how the various property formatters are implemented, I'll take you through how the Exif values themselves are created. There's an InitializeExifValue method that's called to create the IExifValue object for an ExifProperty instance.

private IExifValue InitializeExifValue()
{
    try
    {
        var customExtractor = 
          this.parentReader.QueryForCustomUndefinedExtractor(
            this.RawExifTagId);
        if (customExtractor != null)
        {
            return this.exifValue = customExtractor.GetExifValue(
              this.propertyItem.Value, this.propertyItem.Len);
        } 

        return this.exifValue = 
          this.ExifDatatype == PropertyTagType.Undefined ?
            ExifValueCreator.CreateUndefined(
              this.ExifTag, 
              this.propertyItem.Value, 
              this.propertyItem.Len) :
            ExifValueCreator.Create(
              this.ExifDatatype, 
              this.propertyItem.Value, 
              this.propertyItem.Len);
    }
    catch (ExifReaderException ex)
    {
        throw new InvalidOperationException(
"An ExifReaderException was caught. See InnerException for more details",
          ex);
    }
    catch (Exception ex)
    {
        throw new InvalidOperationException(
"An ExifReaderException was caught. See InnerException for more details",
            new ExifReaderException(ex, this.propertyFormatter, null));
    }
}

Just as the code first checked for a custom property formatter in the constructor, here it checks for a custom extractor by calling QueryForCustomUndefinedExtractor on the parent ExifReader.

internal IExifValueUndefinedExtractor QueryForCustomUndefinedExtractor(
    int tagId)
{
    QueryUndefinedExtractorEventArgs eventArgs = 
        new QueryUndefinedExtractorEventArgs(tagId);
    this.FireQueryUndefinedExtractor(eventArgs);
    return eventArgs.UndefinedExtractor;
}

If there's no custom extractor provided, ExifValueCreator.CreateUndefined is called for undefined data types, and ExifValueCreator.Create is called for well-known data types. You should also note how the ExifReaderException is caught and re-thrown in an InvalidOperationException. This is because InitializeExifValue is called by the ExifProperty.ExifValue property (shown below) and properties are not expected to throw any arbitrary exception. Both WinForms and WPF databinding can correctly handle exceptions of type InvalidOperationException, hence the catch and re-throw.

public IExifValue ExifValue
{
    get
    {
        return this.exifValue ?? this.InitializeExifValue();
    }
}

The ExifValueCreator class

The ExifValueCreator is a factory class for creating different types of ExifValue objects. It declares a private delegate with the following signature.

/// <summary>
/// Delegate that creates the appropriate Exif value of a specific type
/// </summary>
/// <param name="value">Array of bytes representing the value or values
/// </param>
/// <param name="length">Number of values or length of an ASCII string value
/// </param>
/// <returns>The Exif value or values</returns>
private delegate IExifValue CreateExifValueDelegate(
    byte[] value, int length);

It also has a built-in map of Exif data types to their corresponding creation methods.

/// <summary>
/// Delegate map between Exif tag types and associated creation methods
/// </summary>
private static Dictionary<PropertyTagType, CreateExifValueDelegate> 
  createExifValueDelegateMap = 
    new Dictionary<PropertyTagType, CreateExifValueDelegate>()
{
    { PropertyTagType.ASCII, CreateExifValueForASCIIData },
    { PropertyTagType.Byte, CreateExifValueForByteData },
    { PropertyTagType.Short, CreateExifValueForShortData },
    { PropertyTagType.Long, CreateExifValueForLongData },            
    { PropertyTagType.SLONG, CreateExifValueForSLongData },            
    { PropertyTagType.Rational, CreateExifValueForRationalData },            
    { PropertyTagType.SRational, CreateExifValueForSRationalData }
};

The Create method gets the appropriate method based on the passed in Exif data type and invokes the delegate and returns its return value.

/// <summary>
/// Creates an ExifValue for a specific type
/// </summary>
/// <param name="type">The property data type</param>
/// <param name="value">An array of bytes representing the value or 
/// values</param>
/// <param name="length">A length parameter specifying the number of 
/// values or the length of a string for ASCII string data</param>
/// <returns>An appropriate IExifValue object</returns>
internal static IExifValue Create(
    PropertyTagType type, byte[] value, int length)
{
    try
    {
        CreateExifValueDelegate createExifValueDelegate;
        if (createExifValueDelegateMap.TryGetValue(
            type, out createExifValueDelegate))
        {
            return createExifValueDelegate(value, length);
        }

        return new ExifValue<string>(new[] { type.ToString() });
    }
    catch (Exception ex)
    {
        throw new ExifReaderException(ex);
    }
}

Data types used by the ExifReader

Most of the Exif data types that are defined in the PropertyTagType enum have corresponding matches in the .NET type system. For example, the ASCII Exif type would be a System.String, a Long would be a 32 bit unsigned integer (a uint in C#), and an SLong would be a 32 bit signed integer (an int in C#). But there are also two types that don't have direct .NET equivalents, Rational and SRational, and I wrote a simple Rational32 struct that represents either of those types. I didn't want separate structs for the signed and unsigned versions even though that's what's done in the .NET framework (examples : UInt32/Int32, UInt64/Int64). To facilitate that, I also wrote a simple struct called CommonInt32 that can efficiently represent either an int or an uint in a mostly transparent fashion so the caller need not unduly worry about what type is being accessed.

/// <summary>
/// A struct that can efficiently represent either an int or an uint
/// </summary>
[StructLayout(LayoutKind.Explicit)]
public struct CommonInt32 : IEquatable<CommonInt32>
{
    /// <summary>
    /// Integer value
    /// </summary>
    [FieldOffset(0)]
    private int integer;

    /// <summary>
    /// Unsigned integer value
    /// </summary>
    [FieldOffset(0)]
    private uint uinteger;

    /// <summary>
    /// True if this is a signed value
    /// </summary>
    [FieldOffset(4)]
    private bool isSigned;

    /// <summary>
    /// Initializes a new instance of the CommonInt32 struct
    /// </summary>
    /// <param name="integer">The int value</param>
    public CommonInt32(int integer)
        : this()
    {
        this.integer = integer;
        this.isSigned = true;
    }

    /// <summary>
    /// Initializes a new instance of the CommonInt32 struct
    /// </summary>
    /// <param name="uinteger">The uint value</param>
    public CommonInt32(uint uinteger)
        : this()
    {
        this.uinteger = uinteger;
        this.isSigned = false;
    }

    /// <summary>
    /// Gets a value indicating whether the value of 
    /// this struct is signed or not
    /// </summary>
    public bool IsSigned
    {
        get
        {
            return this.isSigned;
        }
    }

    /// <summary>
    /// Explicit operator overload to int
    /// </summary>
    /// <param name="commonInt">Source object to convert</param>
    /// <returns>The int value</returns>
    public static explicit operator int(CommonInt32 commonInt)
    {
        return commonInt.integer;
    }

    /// <summary>
    /// Explicit operator overload to uint
    /// </summary>
    /// <param name="commonInt">Source object to convert</param>
    /// <returns>The uint value</returns>
    public static explicit operator uint(CommonInt32 commonInt)
    {
        return commonInt.uinteger;
    }

    /// <summary>
    /// Override for Equals
    /// </summary>
    /// <param name="obj">Object to check equality with</param>
    /// <returns>True if they are equal</returns>
    public override bool Equals(object obj)
    {
        return obj is CommonInt32 && this.Equals((CommonInt32)obj);
    }

    /// <summary>
    /// Tests if this instance is equal to another
    /// </summary>
    /// <param name="other">The other instance</param>
    /// <returns>True if they are equal</returns>
    public bool Equals(CommonInt32 other)
    {
        if (this.isSigned != other.isSigned)
        {
            return false;
        }

        return this.isSigned ?
            this.integer.Equals(other.integer) :
            this.uinteger.Equals(other.uinteger);
    }

    /// <summary>
    /// Override for GetHashCode
    /// </summary>
    /// <returns>Returns a hash code for this object</returns>
    public override int GetHashCode()
    {
        return this.isSigned ? 
          this.integer.GetHashCode() : this.uinteger.GetHashCode();
    }
}

By using a StructLayout of LayoutKind.Explicit and by using FieldOffset(0) for both the int and the uint field, the struct will not need any more space than it needs. This is a gratuitous optimization and was not originally done for optimization reasons, but because I was attempting an even more transparent struct to map between int and uint, but eventually I changed other areas of my code and thus did not need that anymore. I decided to keep the overlapping FieldOffset scheme in since I had already written the code. The Rational32 struct uses CommonInt32 fields to represent either a signed number or an unsigned number, and thereby avoids having two separate types for this.

/// <summary>
/// Struct that represents a rational number
/// </summary>
public struct Rational32 
  : IComparable, IComparable<Rational32>, IEquatable<Rational32>
{
    /// <summary>
    /// Separator character for the string representation
    /// </summary>
    private const char SEPARATOR = '/';

    /// <summary>
    /// The numerator
    /// </summary>
    private CommonInt32 numerator;
    
    /// <summary>
    /// The denominator
    /// </summary>
    private CommonInt32 denominator;        

    /// <summary>
    /// Initializes a new instance of the Rational32 struct for signed use
    /// </summary>
    /// <param name="numerator">The numerator</param>
    /// <param name="denominator">The denominator</param>
    public Rational32(int numerator, int denominator)
        : this()
    {
        int gcd = Rational32.EuclidGCD(numerator, denominator);

        this.numerator = new CommonInt32(numerator / gcd);
        this.denominator = new CommonInt32(denominator / gcd);
    }

    /// <summary>
    /// Initializes a new instance of the Rational32 struct for unsigned use
    /// </summary>
    /// <param name="numerator">The numerator</param>
    /// <param name="denominator">The denominator</param>
    public Rational32(uint numerator, uint denominator)
        : this()
    {
        uint gcd = Rational32.EuclidGCD(numerator, denominator);

        this.numerator = new CommonInt32(numerator / gcd);
        this.denominator = new CommonInt32(denominator / gcd);
    }

    /// <summary>
    /// Gets the numerator
    /// </summary>
    public CommonInt32 Numerator
    {
        get { return this.numerator; }
    }

    /// <summary>
    /// Gets the denominator
    /// </summary>
    public CommonInt32 Denominator
    {
        get { return this.denominator; }
    }

    /// <summary>
    /// Explicit conversion operator to double
    /// </summary>
    /// <param name="rational">The source object</param>
    /// <returns>A double value representing the source object</returns>
    public static explicit operator double(Rational32 rational)
    {
        return rational.denominator.IsSigned ?
            (int)rational.denominator == 0 ? 
              0.0 : 
                (double)(int)rational.numerator / 
                (double)(int)rational.denominator :
            (uint)rational.denominator == 0 ? 
              0.0 : 
                (double)(uint)rational.numerator / 
                (double)(uint)rational.denominator;
    }

    /// <summary>
    /// Operator overload for GreaterThan
    /// </summary>
    /// <param name="x">Left value</param>
    /// <param name="y">Right value</param>
    /// <returns>True if the left instance is greater than the right 
    /// instance</returns>
    public static bool operator >(Rational32 x, Rational32 y)
    {
        return (double)x > (double)y;
    }

    /// <summary>
    /// Operator overload for GreaterThanOrEqual
    /// </summary>
    /// <param name="x">Left value</param>
    /// <param name="y">Right value</param>
    /// <returns>True if the left instance is greater than or equal to 
    /// the right instance</returns>
    public static bool operator >=(Rational32 x, Rational32 y)
    {
        return (double)x >= (double)y;
    }

    /// <summary>
    /// Operator overload for LesserThan
    /// </summary>
    /// <param name="x">Left value</param>
    /// <param name="y">Right value</param>
    /// <returns>True if the left instance is lesser than the right 
    /// instance</returns>
    public static bool operator <(Rational32 x, Rational32 y)
    {
        return (double)x < (double)y;
    }

    /// <summary>
    /// Operator overload for LesserThanOrEqual
    /// </summary>
    /// <param name="x">Left value</param>
    /// <param name="y">Right value</param>
    /// <returns>True if the left instance is lesser than or equal to 
    /// the right instance</returns>
    public static bool operator <=(Rational32 x, Rational32 y)
    {
        return (double)x <= (double)y;
    }

    /// <summary>
    /// Override for ToString
    /// </summary>
    /// <returns>The string representation</returns>
    public override string ToString()
    {
        return this.denominator.IsSigned ?
            String.Format("{0} {1} {2}", (int)this.numerator, 
              Rational32.SEPARATOR, (int)this.denominator) :
            String.Format("{0} {1} {2}", (uint)this.numerator, 
              Rational32.SEPARATOR, (uint)this.denominator);
    }

    /// <summary>
    /// Override for Equals
    /// </summary>
    /// <param name="obj">Object to check equality with</param>
    /// <returns>True if they are equal</returns>
    public override bool Equals(object obj)
    {
        return obj is Rational32 && this.Equals((Rational32)obj);
    }

    /// <summary>
    /// Tests if this instance is equal to another
    /// </summary>
    /// <param name="other">The other instance</param>
    /// <returns>True if they are equal</returns>
    public bool Equals(Rational32 other)
    {
        return this.numerator.Equals(other.numerator) && 
          this.denominator.Equals(other.denominator);
    }

    /// <summary>
    /// Override for GetHashCode
    /// </summary>
    /// <returns>Returns a hash code for this object</returns>
    public override int GetHashCode()
    {
        int primeSeed = 29;
        return unchecked((this.numerator.GetHashCode() + primeSeed) * 
          this.denominator.GetHashCode());
    }

    /// <summary>
    /// Compares this instance with an object
    /// </summary>
    /// <param name="obj">An object to compare with this instance</param>
    /// <returns>Zero of equal, 1 if greater than, and -1 if less than 
    /// the compared to object</returns>
    public int CompareTo(object obj)
    {
        if (obj == null)
        {
            return 1;
        }

        if (!(obj is Rational32))
        {
            throw new ArgumentException("Rational32 expected");
        }

        return this.CompareTo((Rational32)obj);
    }

    /// <summary>
    /// Compares this instance with another
    /// </summary>
    /// <param name="other">A Rational32 object to compare with this 
    /// instance</param>
    /// <returns>Zero of equal, 1 if greater than, and -1 if less than 
    /// the compared to object</returns>
    public int CompareTo(Rational32 other)
    {
        if (this.Equals(other))
        {
            return 0;
        }

        return ((double)this).CompareTo((double)other);
    }

    /// <summary>
    /// Calculates the GCD for two signed ints
    /// </summary>
    /// <param name="x">First signed int</param>
    /// <param name="y">Second signed int</param>
    /// <returns>The GCD for the two numbers</returns>
    private static int EuclidGCD(int x, int y)
    {
        return y == 0 ? x : EuclidGCD(y, x % y);
    }

    /// <summary>
    /// Calculates the GCD for two unsigned ints
    /// </summary>
    /// <param name="x">First unsigned int</param>
    /// <param name="y">Second unsigned int</param>
    /// <returns>The GCD for the two numbers</returns>
    private static uint EuclidGCD(uint x, uint y)
    {
        return y == 0 ? x : EuclidGCD(y, x % y);
    }
}

Creation methods in ExifValueCreator

I have a couple of generic methods which create an IExifValue from a given array of bytes.

/// <summary>
/// Generic creation method
/// </summary>
/// <typeparam name="T">The data type of the value data</typeparam>
/// <param name="value">Bytes representing the data</param>
/// <param name="length">Number of bytes</param>
/// <param name="converterFunction">Function that converts from bytes 
/// to a specific data type</param>
/// <returns>Exif value representing the generic data type</returns>
private static IExifValue CreateExifValueForGenericData<T>(
  byte[] value, int length, 
  Func<byte[], int, T> converterFunction) where T : struct
{
    int size = Marshal.SizeOf(typeof(T));
    return CreateExifValueForGenericData(
      value, length, size, converterFunction);
}
/// <summary>
/// Generic creation method
/// </summary>
/// <typeparam name="T">The data type of the value data</typeparam>
/// <param name="value">Bytes representing the data</param>
/// <param name="length">Number of bytes</param>
/// <param name="dataValueSize">Size of each data value</param>
/// <param name="converterFunction">Function that converts from bytes 
/// to a specific data type</param>
/// <returns>Exif value representing the generic data type</returns>
private static IExifValue CreateExifValueForGenericData<T>(
  byte[] value, int length, int dataValueSize, 
  Func<byte[], int, T> converterFunction) where T : struct
{
    T[] data = new T[length / dataValueSize];

    for (int i = 0, pos = 0; i < length / dataValueSize; 
        i++, pos += dataValueSize)
    {
        data[i] = converterFunction(value, pos);
    }

    return new ExifValue<T>(data);
}  

The individual creation methods will merely call one of the above two methods. Here are a couple of examples.

/// <summary>
/// Creation method for SRational values
/// </summary>
/// <param name="value">Bytes representing the SRational data</param>
/// <param name="length">Number of bytes</param>
/// <returns>Exif value representing the Rational data</returns>
private static IExifValue CreateExifValueForSRationalData(
    byte[] value, int length)
{
    return CreateExifValueForGenericData(
        value, 
        length,
        sizeof(int) * 2,
        (bytes, pos) => new Rational32(
            System.BitConverter.ToInt32(bytes, pos), 
            System.BitConverter.ToInt32(bytes, pos + sizeof(int))));
}

/// <summary>
/// Creation method for long values
/// </summary>
/// <param name="value">Bytes representing the long data</param>
/// <param name="length">Number of bytes</param>
/// <returns>Exif value representing the long data</returns>
private static IExifValue CreateExifValueForLongData(
    byte[] value, int length)
{
    return CreateExifValueForGenericData(value, length, 
      (bytes, pos) => System.BitConverter.ToUInt32(bytes, pos));
}        

ExifValue<T> is a generic class that can be used to instantiate an IExifValue of a specific type. If you write custom Exif tag handlers you can use this class instead of implementing an IExifValue class from scratch.

/// <summary>
/// This class represents an Exif property value (or values)
/// </summary>
/// <typeparam name="T">The type of the Exif property value</typeparam>
public class ExifValue<T> : IExifValue
{
    /// <summary>
    /// Array of values
    /// </summary>
    private T[] values;

    /// <summary>
    /// Initializes a new instance of the ExifValue class.
    /// </summary>
    /// <param name="values">Array of Exif values</param>
    public ExifValue(T[] values)
    {
        this.values = values;
    }

    /// <summary>
    /// Gets the type of the Exif property value or values
    /// </summary>
    public Type ValueType
    {
        get { return typeof(T); }
    }

    /// <summary>
    /// Gets the number of values
    /// </summary>
    public int Count
    {
        get { return this.values.Length; }
    }

    /// <summary>
    /// Gets a type-unsafe collection of values of a 
    /// specific Exif tag data type
    /// </summary>
    public IEnumerable Values
    {
        get { return this.values.AsEnumerable(); }
    }
}

Built-in extractors for undefined types

The CreateUndefined method is used to create an IExifValue for an undefined data type.

/// <summary>
/// Creates an ExifValue for an undefined value type
/// </summary>
/// <param name="tagId">The tag Id whose value needs to 
/// be extracted</param>
/// <param name="value">An array of bytes representing the value or 
/// values</param>
/// <param name="length">The number of bytes</param>
/// <returns>An appropriate IExifValue object</returns>
internal static IExifValue CreateUndefined(
    PropertyTagId tagId, byte[] value, int length)
{
    var extractor = 
      ExifValueUndefinedExtractorProvider.GetExifValueUndefinedExtractor(
        tagId);

    try
    {
        return extractor.GetExifValue(value, length);
    }
    catch (Exception ex)
    {
        throw new ExifReaderException(ex, extractor);
    }
}

ExifValueUndefinedExtractorProvider is a class that gets an extractor object for a given tag Id. It does this by looking at the PropertyTagId enum for ExifValueUndefinedExtractor attributes. This is similar to how ExifPropertyFormatterProvider looks for ExifPropertyFormatter attributes.

[ExifValueUndefinedExtractor(typeof(ExifFileSourceUndefinedExtractor))]
[EnumDisplayName("File Source")]
ExifFileSource = 0xA300,

[ExifValueUndefinedExtractor(typeof(ExifSceneTypeUndefinedExtractor))]
[EnumDisplayName("Scene Type")]
ExifSceneType = 0xA301,

Here is how the ExifValueUndefinedExtractorAttribute class is defined.

/// <summary>
/// An attribute used to specify an IExifValueUndefinedExtractor for 
/// Exif Tags with undefined data value types
/// </summary>
[AttributeUsage(AttributeTargets.Field)]
internal class ExifValueUndefinedExtractorAttribute : Attribute
{
    /// <summary>
    /// The IExifValueUndefinedExtractor object
    /// </summary>
    private IExifValueUndefinedExtractor undefinedExtractor;

    /// <summary>
    /// The type of the IExifValueUndefinedExtractor
    /// </summary>
    private Type undefinedExtractorType;

    /// <summary>
    /// Initializes a new instance of the 
    /// ExifValueUndefinedExtractorAttribute class
    /// </summary>
    /// <param name="undefinedExtractorType">
    /// The type of the IExifValueUndefinedExtractor</param>
    public ExifValueUndefinedExtractorAttribute(Type undefinedExtractorType)
    {
        this.undefinedExtractorType = undefinedExtractorType;
    }

    /// <summary>
    /// Gets the IExifValueUndefinedExtractor
    /// </summary>
    /// <returns>The IExifValueUndefinedExtractor</returns>
    public IExifValueUndefinedExtractor GetUndefinedExtractor()
    {
        return this.undefinedExtractor ??
            (this.undefinedExtractor = 
              Activator.CreateInstance(this.undefinedExtractorType) 
              as IExifValueUndefinedExtractor);
    }
}

And here's the code for ExifValueUndefinedExtractorProvider, which uses the CachedAttributeExtractor utility class just as the ExifPropertyFormatterProvider did.

/// <summary>
/// This class provides appropriate IExifValueUndefinedExtractor objects
///  for Exif property values with undefined data
/// </summary>
public static class ExifValueUndefinedExtractorProvider
{
    /// <summary>
    /// Gets an IExifValueUndefinedExtractor for the specific tagId
    /// </summary>
    /// <param name="tagId">The Exif Tag Id</param>
    /// <returns>An IExifValueUndefinedExtractor</returns>
    internal static IExifValueUndefinedExtractor 
      GetExifValueUndefinedExtractor(PropertyTagId tagId)
    {
        ExifValueUndefinedExtractorAttribute attribute = 
          CachedAttributeExtractor<PropertyTagId, 
            ExifValueUndefinedExtractorAttribute>.Instance.GetAttributeForField(
            tagId.ToString());

        if (attribute != null)
        {
            return attribute.GetUndefinedExtractor();
        }

        return new SimpleUndefinedExtractor();
    }
}

Here're a couple of undefined property extractors (please see the project source for other examples).

/// <summary>
/// A class that extracts a value for the Exif File Source property
/// </summary>
internal class ExifFileSourceUndefinedExtractor : IExifValueUndefinedExtractor
{
    /// <summary>
    /// Gets the Exif Value
    /// </summary>
    /// <param name="value">Array of bytes representing 
    /// the value or values</param>
    /// <param name="length">Number of bytes</param>
    /// <returns>The Exif Value</returns>
    public IExifValue GetExifValue(byte[] value, int length)
    {
        string fileSource = value.FirstOrDefault() == 3 ? "DSC" : "Reserved";
        return new ExifValue<string>(new[] { fileSource });
    }
}
/// <summary>
/// Does not attempt to translate the bytes and merely returns 
/// a string representation
/// </summary>
internal class SimpleUndefinedExtractor : IExifValueUndefinedExtractor
{
    /// <summary>
    /// Gets the Exif Value
    /// </summary>
    /// <param name="value">Array of bytes representing 
    /// the value or values</param>
    /// <param name="length">Number of bytes</param>
    /// <returns>The Exif Value</returns>
    public IExifValue GetExifValue(byte[] value, int length)
    {
        string bytesString = String.Join(" ", 
          value.Select(b => b.ToString("X2")));
        return new ExifValue<string>(new[] { bytesString });
    }
}

The SimpleUndefinedExtractor class above is the default handler for undefined property tags and gets used if there is no built-in extractor available and the user has not provided a custom extractor either.

Built in formatters

The ExifReader comes with over a couple of dozen property formatters which cover the most commonly accessed Exif tags. As I mentioned in the introduction, if you encounter a common Exif tag that is not handled by the ExifReader, I'll be glad to add support for that if you can send me a couple of test images with that particular Exif tag. Alternatively you can write a custom handler for it. I will list a few of the built-in formatters just so you get an idea, and people who want to see them all can look at the project source code.

/// <summary>
/// An IExifPropertyFormatter specific to the ISO property
/// </summary>
internal class ExifISOSpeedPropertyFormatter : IExifPropertyFormatter
{
    /// <summary>
    /// Gets a display name for this property
    /// </summary>
    public string DisplayName
    {
        get
        {
            return "ISO Speed";
        }
    } 

    /// <summary>
    /// Gets a formatted string for a given Exif value
    /// </summary>
    /// <param name="exifValue">The source Exif value</param>
    /// <returns>The formatted string</returns>
    public string GetFormattedString(IExifValue exifValue)
    {
        var values = exifValue.Values.Cast<ushort>();
        return values.Count() == 0 ? 
          String.Empty : String.Format("ISO-{0}", values.First());
    }
}

This is a fairly simple formatter. The ISO speed is represented by a single unsigned short value. So all we do is extract that value and format it to a standard ISO display string.

/// <summary>
/// An IExifPropertyFormatter specific to the Gps 
/// Latitude and Longitude properties
/// </summary>
internal class GpsLatitudeLongitudePropertyFormatter 
    : SimpleExifPropertyFormatter
{
    /// <summary>
    /// Initializes a new instance of the 
    /// GpsLatitudeLongitudePropertyFormatter class.
    /// </summary>
    /// <param name="tagId">The associated PropertyTagId</param>
    public GpsLatitudeLongitudePropertyFormatter(PropertyTagId tagId)
        : base(tagId)
    {            
    }

    /// <summary>
    /// Gets a formatted string for a given Exif value
    /// </summary>
    /// <param name="exifValue">The source Exif value</param>
    /// <returns>The formatted string</returns>
    public override string GetFormattedString(IExifValue exifValue)
    {
        var values = exifValue.Values.Cast<Rational32>();
        if (values.Count() != 3)
        {
            return String.Empty;
        }

        return String.Format("{0}; {1}; {2}", 
          (double)values.ElementAt(0), 
          (double)values.ElementAt(1), 
          (double)values.ElementAt(2));
    }
}

This formatter is used by both the GpsLatitude and the GpsLongitude Exif tags. Here there are three Rational values, and you can see how the sign-transparency of the Rational32 class means we don't need to particularly check for a signed versus unsigned number.

/// <summary>
/// An IExifPropertyFormatter specific to the Exif shutter-speed property
/// </summary>
internal class ExifShutterSpeedPropertyFormatter : IExifPropertyFormatter
{
    . . .

    /// <summary>
    /// Gets a formatted string for a given Exif value
    /// </summary>
    /// <param name="exifValue">The source Exif value</param>
    /// <returns>The formatted string</returns>
    public string GetFormattedString(IExifValue exifValue)
    {
        var values = exifValue.Values.Cast<Rational32>();

        if (values.Count() == 0)
        {
            return String.Empty;
        }

        double apexValue = (double)values.First();
        double shutterSpeed = 1 / Math.Pow(2, apexValue);

        return shutterSpeed > 1 ?
            String.Format("{0} sec.", (int)Math.Round(shutterSpeed)) :
            String.Format("{0}/{1} sec.", 1, (int)Math.Round(1 / shutterSpeed));            
    }
}

This one's similar to the ISO-speed formatter except it's a rational data type and also needs a little math and some little formatting work applied. As I was implementing these formatters I quickly found that a vast majority of them required mapping from a numeric data value to a string representation. To facilitate that, I wrote a base class that will handle the mapping, so that implementing classes need not duplicate a lot of the code lookup logic.

/// <summary>
/// An IExifPropertyFormatter base implementation for formatters that 
/// use a basic dictionary to map values to names
/// </summary>
/// <typeparam name="VTYPE">The type of the value that maps to the 
/// string</typeparam>
internal abstract class GenericDictionaryPropertyFormatter<VTYPE> 
    : IExifPropertyFormatter
{
    /// <summary>
    /// Gets a display name for this property
    /// </summary>
    public abstract string DisplayName { get; }

    /// <summary>
    /// Gets a formatted string for a given Exif value
    /// </summary>
    /// <param name="exifValue">The source Exif value</param>
    /// <returns>The formatted string</returns>
    public string GetFormattedString(IExifValue exifValue)
    {
        var values = exifValue.Values.Cast<VTYPE>();

        return this.GetStringValueInternal(values.FirstOrDefault());
    }

    /// <summary>
    /// Gets a dictionary that maps values to named strings
    /// </summary>
    /// <returns>The mapping dictionary</returns>
    protected abstract Dictionary<VTYPE, string> GetNameMap();

    /// <summary>
    /// Gets the reserved string for values not in the dictionary
    /// </summary>
    /// <returns>The reserved string</returns>
    protected virtual string GetReservedValue()
    {
        return "Reserved";
    }

    /// <summary>
    /// Returns an Exif Light Source from a VTYPE value
    /// </summary>
    /// <param name="value">The VTYPE value</param>
    /// <returns>The string value</returns>
    private string GetStringValueInternal(VTYPE value)
    {
        string stringValue;

        if (!this.GetNameMap().TryGetValue(value, out stringValue))
        {
            stringValue = this.GetReservedValue();
        }

        return stringValue;
    }
}

Here's an example of how a formatter class derives from the above class.

/// <summary>
/// An IExifPropertyFormatter specific to the Metering Mode property
/// </summary>
internal class ExifMeteringModePropertyFormatter 
    : GenericDictionaryPropertyFormatter<ushort>
{
    /// <summary>
    /// Map of metering modes to their unsigned short representations
    /// </summary>
    private Dictionary<ushort, string> meteringModeMap 
      = new Dictionary<ushort, string>()
    {
        { 0, "Unknown" },
        { 1, "Average" },
        { 2, "Center-Weighted" },
        { 3, "Spot" },
        { 4, "Multi-Spot" },
        { 5, "Pattern" },
        { 6, "Partial" },
        { 255, "Other" }
    };

    /// <summary>
    /// Gets a display name for this property
    /// </summary>
    public override string DisplayName
    {
        get { return "Metering Mode"; }
    }

    /// <summary>
    /// Gets a dictionary that maps values to named strings
    /// </summary>
    /// <returns>The mapping dictionary</returns>
    protected override Dictionary<ushort, string> GetNameMap()
    {
        return this.meteringModeMap;
    }
}

Support for the Windows Forms property-grid

The reason I added this support was because of my presumption that there will be quite a few scenarios where a WinForms app merely wants to show all the Exif properties in a form. And the quickest way to do it would be with a property grid control. Supporting the PropertyGrid was fairly straightforward. The ExifReaderTypeDescriptionProvider class acts as a TypeDescriptionProvider for the ExifReader class.

/// <summary>
/// Implements a TypeDescriptionProvider for ExifReader
/// </summary>
internal class ExifReaderTypeDescriptionProvider : TypeDescriptionProvider
{
    /// <summary>
    /// The default TypeDescriptionProvider to use
    /// </summary>
    private static TypeDescriptionProvider defaultTypeProvider = 
      TypeDescriptor.GetProvider(typeof(ExifReader));
    
    . . .

    /// <summary>
    /// Gets a custom type descriptor for the given type and object.
    /// </summary>
    /// <param name="objectType">The type of object for which to retrieve 
    /// the type descriptor</param>
    /// <param name="instance">An instance of the type.</param>
    /// <returns>Returns a custom type descriptor</returns>
    public override ICustomTypeDescriptor GetTypeDescriptor(
        Type objectType, object instance)
    {
        ICustomTypeDescriptor defaultDescriptor = 
          base.GetTypeDescriptor(objectType, instance);

        return instance == null ? defaultDescriptor : 
          new ExifReaderCustomTypeDescriptor(defaultDescriptor, instance);
    }
}

The ExifReaderCustomTypeDescriptor instance returned above implements a custom TypeDescriptor for the ExifReader class.

/// <summary>
/// Implements a CustomTypeDescriptor for the ExifReader class
/// </summary>
internal class ExifReaderCustomTypeDescriptor : CustomTypeDescriptor
{
    /// <summary>
    /// List of custom fields
    /// </summary>
    private List<PropertyDescriptor> customFields 
      = new List<PropertyDescriptor>();

    /// <summary>
    ///  Initializes a new instance of the ExifReaderCustomTypeDescriptor class.
    /// </summary>
    /// <param name="parent">The parent custom type descriptor.</param>
    /// <param name="instance">Instance of ExifReader</param>
    public ExifReaderCustomTypeDescriptor(
      ICustomTypeDescriptor parent, object instance)
        : base(parent)
    {
        ExifReader exifReader = (ExifReader)instance;
        this.customFields.AddRange(
          exifReader.GetExifProperties().Select(
            ep => new ExifPropertyPropertyDescriptor(ep)));
    }

    /// <summary>
    /// Returns a collection of property descriptors for the object 
    /// represented by this type descriptor.
    /// </summary>
    /// <returns>A collection of property descriptors</returns>
    public override PropertyDescriptorCollection GetProperties()
    {
        return new PropertyDescriptorCollection(
          base.GetProperties().Cast<PropertyDescriptor>().Union(
            this.customFields).ToArray());
    }

    /// <summary>
    /// Returns a collection of property descriptors for the object 
    /// represented by this type descriptor.
    /// </summary>
    /// <param name="attributes">Attributes to filter on</param>
    /// <returns>A collection of property descriptors</returns>
    public override PropertyDescriptorCollection GetProperties(
        Attribute[] attributes)
    {
        return new PropertyDescriptorCollection(
          base.GetProperties(attributes).Cast<PropertyDescriptor>().Union(
            this.customFields).ToArray());
    }
}

For every Exif property the reader returns, a ExifPropertyPropertyDescriptor is created and added to the property list. It's ExifPropertyPropertyDescriptor which provides the property name, property value, property type etc. for the PropertyGrid.

/// <summary>
/// Implements a PropertyDescriptor for ExifProperty 
/// that returns the descriptive
/// string representation of the property's current value.
/// </summary>
internal class ExifPropertyPropertyDescriptor : PropertyDescriptor
{
    /// <summary>
    /// Initializes a new instance of the ExifPropertyPropertyDescriptor class.
    /// </summary>
    /// <param name="exifProperty">The ExifProperty to use with 
    /// this instance</param>
     public ExifPropertyPropertyDescriptor(ExifProperty exifProperty)
        : base(exifProperty.ExifPropertyName, new Attribute[1] 
            { new CategoryAttribute(exifProperty.ExifPropertyCategory) })
    {
        this.ExifProperty = exifProperty;
    }       
    
    /// <summary>
    /// Gets the ExifProperty associated with this instance
    /// </summary>
    public ExifProperty ExifProperty { get; private set; }

    /// <summary>
    /// Gets the type of the component this property is bound to
    /// </summary>
    public override Type ComponentType
    {
        get { return typeof(ExifReader); }
    }

     . . .

    /// <summary>
    /// Gets the type of the property.
    /// </summary>
    public override Type PropertyType
    {
        get { return typeof(string); }
    }

    . . .

    /// <summary>
    /// Gets the current value of the property
    /// </summary>
    /// <param name="component">The associated component</param>
    /// <returns>The property value</returns>
    public override object GetValue(object component)
    {
        return this.ExifProperty.ToString();
    }

    . . .    
}

In the partially snipped code listing, I have highlighted the important areas. We use ExifProperty.ExifPropertyName as the name of the property, and set the type of the property as a String. And finally for the property value, we call ToString() on the ExifProperty member. I thought of specifying actual data types instead of making them all Strings, but since this is a reader class and there is no plan for writer-support in the foreseeable future, I didn't think it worth the effort. And obviously PropertyGrid support is a convenience and not a necessity, and there are far more flexible ways to use the class as shown in the WPF demo application.

Thank you if you made it this far, and please feel free to pass on your criticism and other feedback via the forum attached to this article.

History

  • March 28, 2010 - Article first published

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