具有可自定义标签处理程序的扩展ExifReader类





5.00/5 (32投票s)
一个支持自定义格式和提取的 C# ExifReader 类。符合 StyleCop 规范的代码,包含 WPF 和 Windows Forms 的演示。


引言
我正在开发一个 C# 应用程序,需要一个 Exif 阅读器类,虽然我找到了不少可用的实现,包括 Code Project 上的一些,但它们都不能完全满足我的要求。所以我写了自己的类。本文描述了这个类的使用和实现,还包括几个演示项目,一个使用 Windows Forms 和 PropertyGrid,另一个使用 WPF 和日益流行的 MVVM 架构。ExifReader 项目除了 IDE 生成的 AssemblyInfo.cs 和 PropertyTagId.cs 文件(我根据多个来源(包括一些 GDI+ 头文件)以及反复试验自定义创建的)之外,100% 符合 Style Cop 规范。我不认为尝试将 Style Cop 准则应用于这两个文件是一个好主意。该类不直接从图像文件中访问 Exif 元数据,而是将该功能委托给 System.Drawing 中的 Image 类。公共接口不公开任何 System.Drawing 类型,因此该库可以在 WPF 中使用,而无需引用 System.Drawing。
给该类的读者/用户的一点说明
这个类试图涵盖我所能测试的所有已文档化和未文档化的 Exif 标签,但如果你们中有人遇到我的类无法识别的 Exif 标签的图像,请联系我并发送图像给我。一旦我有了这些图像,我就可以尝试支持这些标签。目前,如果它遇到一个未文档化的标签,它仍然会提取标签值,但你不会看到任何描述性的标签名称或任何可能需要的自定义解析/格式化。
代码是在 VS 2010 和 .NET 4.0 上编写的
所有代码,包括演示,都是在 VS 2010 RC 和 .NET 4.0 上编写和测试的。虽然我没有故意使用任何 .NET 4.0/C# 4.0 独有的功能,例如 dynamic 关键字,但我可能无意中编写了在 .NET 3.5 中无法编译的代码。但我预计大多数程序员修复或更改这些问题不会太难,但如果您遇到麻烦并且不知道该怎么做,请通过文章论坛给我发消息,我会帮助解决。我怀疑最常见的兼容性问题可能与 .NET 3.5 中 IEnumerable 的转换要求有关,因为它直到 .NET 4.0 才成为 variant 接口。
行内代码格式
本文中的行内代码片段已进行换行处理,以便于查看。源文件中的实际代码并未假设您正在使用 10 英寸上网本。*微笑*
使用 ExifReader 类
如果您正在使用 Exif 读取器类,我想可以放心地假设您熟悉 Exif 属性和 Exif 规范。所以我就不详细介绍了。对于感兴趣的人,这里有两个很好的链接
第二个链接包含了 Exif 2.2 规范。2.21 规范不能免费下载,尽管有些网站可以购买。您还会发现许多相机和手机插入了 Exif 2.21 标签,尽管 Exif 版本标签仍设置为 2.2,您还会发现 Adobe Photoshop 等应用程序插入了它们自己的自定义标签。尼康和佳能等相机制造商也有自己的专有标签。我基本上坚持使用标准标签,没有尝试破译任何公司专有标签,尽管未来的版本也可能支持这些标签。
类的接口相当简单,您只需实例化一个 ExifReader 对象,传入文件路径,然后通过调用 GetExifProperties 访问提取的 Exif 属性。以下是一些示例代码
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();
    }
}
在接下来的两个部分中,我将详细介绍 ExifProperty 类型,我将在其中讨论两个演示应用程序。本质上,它是您在使用 ExifReader 类时必须处理的唯一其他类型。如果需要,您可以通过使用 ExifProperty.ExifValue 进一步深入研究属性数据,如下所示,但这不太可能发生,除非您正在处理应用程序特有的自定义 Exif 标签类型,或者它是未处理的专有标签,例如尼康、佳能或 Adobe 的标签,在这种情况下,您可能希望直接访问这些值并进行自己的解释或格式化。
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 是一个 enum,表示已文档化的 Exif 标签属性以及一些未文档化的属性。还有一个 PropertyTagType enum,表示 Exif 数据类型。这两个枚举将在本文的后续部分进行讨论。
使用自定义格式器和提取器
ExifReader 支持标签的自定义处理,这不仅对类未自动处理的未文档化标签有用,而且对文档化标签的专门处理也很有用。您需要处理两个事件:QueryUndefinedExtractor 和 QueryPropertyFormatter,这些事件允许您指定自定义格式化对象和自定义提取器对象。请注意,在单个类中实现格式化器和提取器通常很方便,但并非必需。另请注意,如果您为文档化标签提供了自定义提取器,那么强烈建议您也实现自定义格式化器,特别是如果您更改了属性的提取数据类型。以下是一个非常简单的标签处理程序类的示例。
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) });
    }
}
这两个已实现的接口将在本文后面讨论。拥有自定义处理程序类后,您可以处理事件并设置 EventArgs 对象上的相应值。
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())
{
    . . .
}
请注意,检查 TagId 至关重要。如果您不这样做,您将把自定义格式器或提取器应用于从图像中提取的每个 Exif 标签,这必然会以灾难告终!
Windows Forms 演示应用程序
Windows Forms 演示使用 PropertyGrid 来显示各种 Exif 属性。 ExifReader 内置支持 PropertyGrid 控件,因此您需要编写的代码很少。以下是 Windows Forms 演示项目中用户编写的全部代码
/// <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");
            }
        }
    }
}
高亮行显示了我们将 ExifReader 对象与 PropertyGrid 控件关联的位置。图 2 显示了应用程序运行并选择图像时的屏幕截图。之所以能这样工作,是因为 ExifReader 类有一个自定义的 TypeDescriptionProvider,它动态地提供了 PropertyGrid 所查找的属性描述符。我在下面的实现细节部分简要地谈到了它的实现方式。从 Windows Forms 的角度来看,如果您的表单需要显示 Exif 属性,这将是最简单的方法。在表单上放置一个 PropertyGrid 控件,然后将 SelectedObject 设置为 ExifReader 实例,仅此而已。
异常处理
请注意,类型为 ExifReaderException 的异常是如何被捕获和处理的。ExifReader 的构造函数是直接抛出此异常的唯一地方。异常处理在文章中多个地方讨论过,但简而言之,如果文件路径无效、图像文件损坏或包含无效的 Exif 元数据,您通常会在此处收到此异常。一旦 ExifReader 被实例化,您将永远不会直接收到类型为 ExifReaderException 的异常。数据绑定(无论是 WinForms 还是 WPF)将主要通过属性访问器和 ToString 重写进行,因此不建议从这些位置抛出任意异常对象,这与标准的 .NET 实践相悖。从所有这些地方抛出的是类型为 InvalidOperationException 的异常,大多数编写良好的数据绑定库都知道如何正确处理它。您仍然可以在调试器中处理此异常,并通过 InvalidOperationException 对象的 InnerException 属性访问源 ExifReaderException 对象。
WPF/MVVM 演示应用程序
Josh Smith、Sacha Barber 和 Pete O'Hanlon 三位先生在 Code Project 上对 MVVM 的好评至少已有两年之久,因此我认为将其应用于 WPF 演示应用程序是一个好主意,即使整个应用程序非常简单。当然,我的实现相当平淡,缺乏我们现在在上述先生们撰写的文章和应用程序中认为理所当然的精致和优雅。虽然 View 类 100% 使用 Xaml 而没有代码隐藏并不是绝对必需的,但由于这是我的第一个 MVVM 应用程序,我特意确保没有使用代码隐藏。虽然它与 ExifReader 本身无关,但我仍将简要介绍我为了实现我想要做的事情而做的一些工作。

WPF 演示比 WinForms 演示做得更多。一方面,它显示了每个提取标签的 Exif 数据类型和 Exif 标签名称。另一方面,它支持使用搜索关键字过滤结果,该关键字搜索标签名称和标签属性值。此外,我的演示应用程序还展现了我糟糕的配色方案、样式和 UI 主题。我总是对 Sacha 关于颜色和样式的非正统想法感到头疼,但我认为我在糟糕的 UI 品味方面甚至超越了 Sacha。*咧嘴笑*
ViewModel 类
此演示项目只有一个 ViewModel 类,它具有用于浏览、退出和过滤的命令。还有一个名为 ExifProperties 的公共 CollectionViewSource 属性,它返回提取的 Exif 属性。最初这是一个 ObservableCollection 属性,但我不得不将其更改为 CollectionViewSource 属性,以坚定地坚持我避免在视图类中使用代码隐藏的决定。由于我想进行过滤,这是我在 ViewModel 类中完全使用 CollectionViewSource 内置过滤支持的最简单方法。
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));
        }
    }
}
以下是浏览图像并实例化 ExifReader 的代码。所有提取的属性都添加到 ObservableCollection 属性中,并且预览图像也相应设置。
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");
        }
    }
}
请注意,与 WinForms 演示类似,这里有一个专门用于 ExifReaderException 类型的 try-catch 处理程序。对于大多数应用程序,您可能希望在此处过滤掉特定标签,或者只显示一些预选的 Exif 属性列表。无论哪种方式,几行 LINQ 就可以解决问题。我确实考虑过在 ExifReader 类中添加内置过滤支持,但后来决定这偏离了核心类的功能,并且无论如何都没有太大价值,因为用户可以在一两行代码中完成。对于演示应用程序,过滤器是基于文本的,对 Exif 属性名称以及属性值的字符串表示进行不区分大小写的搜索。
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;
        }
    }
}
View
该视图是项目中的主 Window 类(我确实说过这是一个相当粗糙的 MVVM 实现,所以不要笑太多)。有一个 ListBox 应用了数据模板,显示 Exif 属性。以下是一些部分剪切且大量换行的 Xaml 代码
<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>
我删除了多余的样式代码。代码中的绑定点在上面突出显示,属性值绑定到自身的原因是 ExifProperty 类具有 ToString 重写——这使得呈现 Exif 属性值的可读文本变得无忧无虑。事实上,Windows Forms PropertyGrid 也使用 ToString,这确保了无论您如何使用该类,都会看到一致的显示值。
我希望过滤是实时的,即当用户在文本框中键入时,过滤器应该生效。这方面最大的问题是 TextBox 没有 Command 属性,因此我需要使用代码隐藏将其文本更改事件代理到 ViewModel。我不想这样做。这又纯粹是我的一时兴起,因为 MVVM 根本不强制这样做,尽管很多人都推荐这样做。显然,您可以通过引用 Expression Blend DLL 来解决这个问题,该 DLL 支持交互触发器,您可以将其转发到命令对象,从而避免任何代码隐藏。我不想引用 Expression Blend DLL,至少对于一个简单的演示应用程序来说是这样,所以我不得不通过向 TextBox 添加一个附加行为来解决它,该行为可以接受一个命令对象。这当然是完全多余的,在实际应用程序中,我只会简单地在代码隐藏中实现。最有可能的是这样:
ViewModel viewModel = this.DataContext as ViewModel;
. . . 
private void TextChanged(. . .)
{
    viewModel.SomeCommand(. . .) ;
}
那将是在我的视图代码后台中,虽然一些纯粹主义者可能不太高兴,但我认为这肯定比引用 Expression Blend 更简单!引用 Rama Vavilala 的话:“毕竟,这些框架应该让事情变得更简单”。这是 Xaml 代码片段
<TextBox x:Name="searchTextBox" Width="165"  
  HorizontalAlignment="Left" Margin="3,0,0,0"
  Text="{Binding SearchText}" 
  local:TextChangedBehavior.TextChanged="{Binding FilterCommand}" />
我没有处理 TextChanged 事件,而是通过附加行为处理它,并将其路由到 ViewModel 中的 FilterCommand 命令对象。以下是附加行为的代码
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);
        }
    }
}
没有什么复杂的,只是一个基本的附加行为实现。高亮显示的代码显示了我们如何代理到实际的命令实现。
ExifReader 类参考
正如我之前提到的,除了那个枚举源文件外,ExifReader 的其余代码都完全符合 StyleCop 规范,因此我必须为每个方法、属性和字段添加 XML 注释,包括 private 字段。因此,代码在很大程度上是自文档化的。再次强调,下面的代码片段为了便于查看而进行了换行,所以一些注释行会显得很不自然地分成多行。
ExifReader 类
/// <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();
}
早些时候,我有一个带有 Image 参数的构造函数重载,但它被移除了,这样 WPF 项目就可以引用 DLL,而无需引用 System.Drawing DLL。
QueryPropertyFormatterEventArgs 类
/// <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; }
}
QueryUndefinedExtractorEventArgs 类
/// <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; }
}
我考虑过将上述两个 EventArgs 类统一为一个 generic class,但后来决定保留它们作为单独的类,因为它们都用于 public event 处理程序,我认为清晰度比紧凑性更重要。
ExifProperty 类
/// <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);
}
IExifValue 接口
    /// <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; }
    }
正常使用不需要访问 ExifProperty.ExifValue 属性,但它用于自定义解释未文档化或专有标签。请注意 IExifValue 接口如何实际返回 ValueType 属性的 System.Type。我这样设计是因为 IExifValue 表示 ExifReader 类内部如何表示值,而不是本机 Exif 数据的实际基于字节的表示。用户仍然可以通过 ExifProperty.ExifDatatype 访问与属性关联的 Exif 数据类型。
PropertyTagType 枚举
/// <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
}
PropertyTagId 枚举
/// <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>
这太长了,无法在此处全部列出,请参阅源代码。我将在下一节中简要讨论我在整个 Exif 提取过程中如何使用此 enum。我在此处添加了一个特殊值,以表示我无法识别的标签,我选择了可能的最大 16 位值,希望它不会与某些专有 Exif 格式使用的实际 Exif 标签值冲突。
[EnumDisplayName("Unknown Exif Tag")]
UnknownExifTag = 0xFFFF
我最初是将整数值转换为 enum,因为 WinForms 及其 PropertyGrid 不会抱怨无效的 enum 值,而 ToString 只会返回数字字符串值,这都非常正常。严格来说,WPF 也没有抱怨,但我在输出窗口中看到了一个第一次机会异常,并追溯到这样一个事实:默认的 WPF 数据绑定使用 PresentationFramework 中的 internal 类 SourceDefaultValueConverter,当它收到超出 enum 范围的 enum 值时,它会抛出 ArgumentException。因此,我决定为不在枚举中的值使用 UnknownExifTag enum 值。哦,我选择了 0xFFFF 而不是 0,因为 GpsVer 已经使用了 0!您仍然可以通过使用返回 int 的 RawExifTagId 属性获取原始 Tag-Id。
ExifReaderException 类
/// <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);
}
如果您实现自定义格式化器或提取器,则不应抛出 ExifReaderException,而应抛出标准异常或特定于您的应用程序的自定义异常。ExifReader 类将处理该异常并重新抛出 ExifReaderException,该异常将您抛出的异常作为 InnerException,因此内部构造函数(除了为序列化而保留的公共默认构造函数)除外。
实现细节
源代码注释详尽,因此任何有兴趣大致了解代码的人都应该浏览源代码。在本节中,我将简要介绍基本设计,以及我认为对部分读者会感兴趣的任何内容。正如引言中所述,我使用 System.Drawing Image 类从支持的图像文件中获取 Exif 信息。
private void InitializeExifProperties()
{
    this.exifproperties = new List<ExifProperty>();
    foreach (var propertyItem in this.imageFile.PropertyItems)
    {
        this.exifproperties.Add(new ExifProperty(propertyItem, this));
    }
}
InitializeExifProperties 方法为 Image 类返回的每个 PropertyItem 创建一个 ExifProperty 实例。在深入了解 ExifProperty 内部发生的事情之前,我想描述一下用于获取 Exif 属性的可读显示值的 IExifPropertyFormatter 接口。
/// <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 有一个 propertyFormatter 字段,类型为 IExifPropertyFormatter,它在构造函数中初始化。
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;
    }
}
ExifProperty 构造函数在父 ExifReader 类上调用 QueryForCustomPropertyFormatter,以查看是否有用户指定的属性格式器可用。
internal IExifPropertyFormatter QueryForCustomPropertyFormatter(
    int tagId)
{
    QueryPropertyFormatterEventArgs eventArgs = 
      new QueryPropertyFormatterEventArgs(tagId);
    this.FireQueryPropertyFormatter(eventArgs);
    return eventArgs.PropertyFormatter;
}
如果没有提供自定义格式器,则通过 ExifPropertyFormatterProvider 类获取属性格式器。
通过自定义枚举属性指定属性格式化器
ExifPropertyFormatterProvider 是一个类,它为给定的 Exif 标签返回一个 IExifPropertyFormatter。它在 PropertyTagId 枚举中查找自定义属性,以确定需要使用哪个属性格式化器。以下是 enum 值如何指定属性格式化器的几个示例。
[ExifPropertyFormatter(typeof(ExifExposureTimePropertyFormatter))]
ExifExposureTime = 0x829A,
[ExifPropertyFormatter(typeof(ExifFNumberPropertyFormatter), 
          ConstructorNeedsPropertyTag = true)]
[EnumDisplayName("F-Stop")]
ExifFNumber = 0x829D,
ExifPropertyFormatter 是一个可以应用于 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);
    }
}
以下是 ExifPropertyFormatterProvider 类如何使用此属性来返回适当的属性格式化器。
/// <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);
    }
}
如果找到属性,则返回该属性关联的属性格式化器。如果没有匹配项,则返回基本格式化器。
CachedAttributeExtractor 工具类
请注意我是如何使用 CachedAttributeExtractor 工具类来提取自定义属性的。这是一个方便的小类,它不仅使提取属性变得容易,而且还缓存属性以供后续访问。
/// <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;
    }
}
创建 Exif 值
在深入探讨各种属性格式器如何实现之前,我将向您介绍 Exif 值本身是如何创建的。有一个 InitializeExifValue 方法被调用来为 ExifProperty 实例创建 IExifValue 对象。
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));
    }
}
就像代码在构造函数中首先检查自定义属性格式器一样,这里它通过调用父 ExifReader 上的 QueryForCustomUndefinedExtractor 来检查自定义提取器。
internal IExifValueUndefinedExtractor QueryForCustomUndefinedExtractor(
    int tagId)
{
    QueryUndefinedExtractorEventArgs eventArgs = 
        new QueryUndefinedExtractorEventArgs(tagId);
    this.FireQueryUndefinedExtractor(eventArgs);
    return eventArgs.UndefinedExtractor;
}
如果没有提供自定义提取器,则对未定义的数据类型调用 ExifValueCreator.CreateUndefined,对已知数据类型调用 ExifValueCreator.Create。您还应该注意 ExifReaderException 是如何被捕获并重新抛出为 InvalidOperationException 的。这是因为 InitializeExifValue 由 ExifProperty.ExifValue 属性(如下所示)调用,并且属性不应抛出任何任意异常。WinForms 和 WPF 数据绑定都可以正确处理类型为 InvalidOperationException 的异常,因此会捕获并重新抛出。
public IExifValue ExifValue
{
    get
    {
        return this.exifValue ?? this.InitializeExifValue();
    }
}
ExifValueCreator 类
ExifValueCreator 是一个用于创建不同类型 ExifValue 对象的工厂类。它声明了一个具有以下签名的 private delegate。
/// <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);
它还有一个内置的 Exif 数据类型到其相应创建方法的映射。
/// <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 }
};
Create 方法根据传入的 Exif 数据类型获取相应的方法,然后调用委托并返回其返回值。
/// <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);
    }
}
ExifReader 使用的数据类型
在 PropertyTagType enum 中定义的大多数 Exif 数据类型在 .NET 类型系统中都有对应的匹配项。例如,ASCII Exif 类型将是 System.String,Long 将是 32 位无符号整数(C# 中的 uint),而 SLong 将是 32 位有符号整数(C# 中的 int)。但也有两种类型没有直接的 .NET 等效项,即 Rational 和 SRational,我编写了一个简单的 Rational32 struct 来表示这两种类型。我不想为有符号和无符号版本使用单独的 struct,尽管 .NET 框架中就是这样做的(例如:UInt32/Int32、UInt64/Int64)。为了方便这一点,我还编写了一个简单的 struct 叫做 CommonInt32,它可以以一种大部分透明的方式高效地表示 int 或 uint,这样调用者就不必过度担心正在访问的是哪种类型。
/// <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();
    }
}
通过使用 StructLayout 的 LayoutKind.Explicit 并对 int 和 uint 字段使用 FieldOffset(0),struct 不需要比它需要的更多空间。这是一种不必要的优化,最初并非出于优化原因,而是因为我尝试了一种更透明的 struct 来映射 int 和 uint,但最终我改变了代码的其他区域,因此不再需要它。我决定保留重叠的 FieldOffset 方案,因为我已经编写了代码。Rational32 struct 使用 CommonInt32 字段来表示有符号或无符号数字,从而避免为此使用两种单独的类型。
/// <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);
    }
}
ExifValueCreator 中的创建方法
我有一些泛型方法,它们从给定的 byte 数组中创建 IExifValue。
/// <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);
}  
各个创建方法只会调用上述两个方法之一。以下是几个示例。
/// <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 是一个 泛型 类,可用于实例化特定类型的 IExifValue。如果您编写自定义 Exif 标签处理程序,您可以使用此类别,而无需从头开始实现 IExifValue 类。
/// <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(); }
    }
}
内置的未定义类型提取器
CreateUndefined 方法用于为未定义的数据类型创建 IExifValue。
/// <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 是一个类,它为给定的标签 ID 获取一个提取器对象。它通过查看 PropertyTagId enum 中的 ExifValueUndefinedExtractor 属性来完成此操作。这类似于 ExifPropertyFormatterProvider 查找 ExifPropertyFormatter 属性的方式。
[ExifValueUndefinedExtractor(typeof(ExifFileSourceUndefinedExtractor))]
[EnumDisplayName("File Source")]
ExifFileSource = 0xA300,
[ExifValueUndefinedExtractor(typeof(ExifSceneTypeUndefinedExtractor))]
[EnumDisplayName("Scene Type")]
ExifSceneType = 0xA301,
以下是 ExifValueUndefinedExtractorAttribute 类的定义方式。
/// <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);
    }
}
以下是 ExifValueUndefinedExtractorProvider 的代码,它像 ExifPropertyFormatterProvider 一样使用 CachedAttributeExtractor 工具类。
/// <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();
    }
}
以下是几个未定义的属性提取器(其他示例请参见项目源代码)。
/// <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 });
    }
}
上述 SimpleUndefinedExtractor 类是未定义属性标签的默认处理程序,如果没有可用的内置提取器,并且用户也没有提供自定义提取器,则会使用它。
内置格式化器
ExifReader 附带了二十多个属性格式化器,涵盖了最常用的 Exif 标签。正如我在介绍中提到的,如果您遇到 ExifReader 无法处理的常见 Exif 标签,如果您能向我发送几张带有该特定 Exif 标签的测试图像,我将很乐意添加支持。或者,您可以为其编写自定义处理程序。我将列出一些内置格式化器,以便您了解,希望查看所有格式化器的人可以查看项目源代码。
/// <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());
    }
}
这是一个相当简单的格式器。ISO 速度由一个无符号的 short 值表示。所以我们所做的就是提取该值并将其格式化为标准的 ISO 显示字符串。
/// <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));
    }
}
此格式器用于 GpsLatitude 和 GpsLongitude Exif 标签。这里有三个 Rational 值,您可以看到 Rational32 类的符号透明性意味着我们不需要特别检查有符号与无符号数字。
/// <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));            
    }
}
这个与 ISO 速度格式化器类似,不同之处在于它是一个有理数据类型,并且还需要一些数学运算和一些格式化工作。在实现这些格式化器时,我很快发现它们中的绝大多数都需要将数值数据映射到字符串表示。为了方便这一点,我编写了一个基类来处理映射,这样实现类就不必重复大量的代码查找逻辑。
/// <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;
    }
}
以下是格式化器类如何从上述类派生的示例。
/// <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;
    }
}
支持 Windows Forms 属性网格
我添加此支持的原因是我的假设是,在许多情况下,WinForms 应用程序只是想在一个表单中显示所有 Exif 属性。最快的方法是使用属性网格控件。支持 PropertyGrid 相当简单。ExifReaderTypeDescriptionProvider 类充当 ExifReader 类的 TypeDescriptionProvider。
/// <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);
    }
}
上面返回的 ExifReaderCustomTypeDescriptor 实例为 ExifReader 类实现了一个自定义 TypeDescriptor。
/// <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());
    }
}
对于读取器返回的每个 Exif 属性,都会创建一个 ExifPropertyPropertyDescriptor 并添加到属性列表中。正是 ExifPropertyPropertyDescriptor 为 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();
    }
    . . .    
}
在部分剪切的代码清单中,我突出显示了重要区域。我们使用 ExifProperty.ExifPropertyName 作为属性名称,并将属性类型设置为 String。最后,对于属性值,我们调用 ExifProperty 成员上的 ToString()。我曾考虑指定实际数据类型而不是将它们全部设为 String,但由于这是一个读取器类,并且在可预见的未来没有写入支持的计划,我认为不值得为此付出努力。显然,PropertyGrid 支持是一种便利,而不是必需品,并且有更灵活的方式来使用该类,如 WPF 演示应用程序所示。
如果您能读到这里,非常感谢,请随时通过本文附带的论坛提出您的批评和其他反馈。
历史
- 2010 年 3 月 28 日 - 文章首次发布


