An extensible ExifReader class with customizable tag handlers





5.00/5 (32 votes)
An ExifReader class in C# that supports custom formatting and extraction. StyleCop compliant code, with demos for WPF and Windows Forms.
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.
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
struct
s 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 byte
s.
/// <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 String
s, 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