65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 WPF 类枚举图像文件中的所有元数据标签

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.80/5 (4投票s)

2010年3月17日

CPOL

8分钟阅读

viewsIcon

47052

本文介绍了如何使用WPF迭代图像中的所有元数据标签。

引言

读取和写入位图图像元数据标签是WPF中最棘手的部分之一。图像文件中的元数据是位图文件中“图像数据”的数据,通常由硬件采集设备写入:例如,数码相机或扫描仪。这就是所谓的客观元数据,它记录有关图像的历史事实,例如拍照时镜头的f/stop设置。但也有可以称为主观元数据标签的,这些标签通常在图像“拍摄”后由软件工具写入。主观元数据标签描述图像的主观特征,例如拍摄者认为的主题或质量评级。本文仅限于读取图像元数据,特别是迭代图像文件中的所有元数据标签。

如果您熟悉在GDI+中执行此操作的简便性,您可能会对WPF图像类的方法和属性感到困惑。这可能并不明显!但是,一旦您理解了WPF成像类的层次结构,一切都会变得清晰。正是因为这种技术有些晦涩,我才选择写这篇文章。读完本文,您将完全理解如何做到这一点,以及您需要做些什么来克服您将从WPF类中检索到的元数据“查询字符串”格式的限制。

背景

图像中的元数据在图像文件的元数据部分中以分层方式组织。因此,可以通过类似于文件系统文件夹路径的内容来访问特定的元数据标签。例如,在JPEG文件中,包含镜头焦距信息的路径如下:

/app1/ifd/exif/{ushort=33434}

上述字符串对WPF的BitmapMetadata类而言是一个查询字符串,可以作为GetQuery()方法的输入。

路径的第一个组件是图像文件元数据部分中段的物理名称。第二和第三个组件是**app1**段中的段的符号名称。示例中的第四个组件是原子元数据项的数字ID,该ID恰好标识了一个在元数据规范标准中被赋予符号名称ExposureTime的标签,也称为熟悉的摄影术语中的快门速度。但物理数字ID在元数据中是一个无符号16位整数。

每个数字ID都与一个在标准中定义的格式的元数据相关联。例如,曝光时间由一个无符号64位整数指定,该整数由一个32位无符号分子和一个32位无符号分母组成,它们共同表示以秒为单位的快门速度,例如,快门速度为0.4秒则为2/5。

尽管在查询字符串中,WPF程序通常使用上述示例这样的符号表达式,但在元数据块的内部,第一个组件(示例中的**app1**)之后的所有组件都表示为无符号16位整数。我们的示例在物理上由以下查询字符串标识:

/app1/{ushort=0}/{ushort=34665}/{ushort33434}

符号字符串或此原始查询字符串都可以作为BitmapMetadata.GetQuery()方法的输入,以获取图像拍摄时的快门速度。通常,程序员不会使用原始查询字符串进行查询,而只会使用符号查询字符串。但是,迭代图像文件中所有元数据项的机制只返回原始查询字符串,这就是为什么如果您想执行此迭代,就需要了解这种原始查询字符串格式。

通常,在使用WPF读取图像元数据时,程序员会预先知道图像文件元数据部分中的符号路径。但是,如果您有兴趣探索图像文件中的每个元数据项,您将只能从该过程返回原始查询字符串,并且您必须使用某种翻译表将原始查询字符串转换为符号查询字符串。

您可以使用BitmapMetadata类来枚举图像文件中的所有元数据标签。BitmapMetadata类以一种方式实现了IEnumerable<String>接口,使得类实例中的所有图像元数据标签都可以被枚举。枚举出的某些标签本身代表BitmapMetadata实例,因此可以使用递归过程来枚举图像中的所有元数据标签,无论它们嵌套在BitmapMetadata实例层次结构中有多深。

Using the Code

元数据标签的枚举始于图像文件的BitmapDecoder。您可以使用BitmapDecoder的静态函数Create()创建一个BitmapDecoder实例,并传递一个已打开以供读取的文件Stream。然后,您可以使用BitmapDecoder.Frames属性,这是一个ReadOnlyCollection<BitmapFrame>的实例。BitmapFrame有一个Metadata属性,这是一个ImageMetadata的实例。ImageMetadata是所有与成像相关的API的元数据操作的抽象基类。要访问单个元数据标签,您必须将其转换为BitmapMetadata实例。如上所述,除了派生自ImageMetadata之外,BitmapMetadata还实现了IEnumerable<String>接口。这正是使您能够枚举图像文件中记录的所有元数据标签的原因。

BitmapMetadata实例中枚举出的每个String都是上面描述的原始查询字符串,可用作BitmapMetadata.GetQuery()实例函数的输入。GetQuery()返回一个Object,它是一个元数据查询读取器的实例。元数据查询读取器可能是以下两种情况之一:

  1. 它可能是另一个BitmapMetadata实例,在这种情况下,您可以迭代其中的所有字符串,并对它们中的每一个执行GetQuery()。通过递归地执行此操作,您可以枚举BitmapFrame.Metadata对象中的所有查询字符串。
  2. 它也可能是查询字符串的可显示标签值,例如,JPEG文件的Orientation标签的原始查询字符串如下:
    /app1/{ushort=0}/{ushort=274}

    **Orientation**标签的值实际上是Int16类型。

在上述解释之后,我们可以查看一些实际捕获图像文件中所有元数据的代码。首先,我们必须从图像文件中获取BitmapMetadata对象。我们可以使用以下代码做到这一点,假设ImagePath是图像文件(如TIFF或JPEG文件)的完整文件系统路径:

RawMetadataItems = new List<RawMetadataItem>();
using (Stream fileStream = File.Open(ImagePath, FileMode.Open))
{
    BitmapDecoder decoder = BitmapDecoder.Create(fileStream, 	
        BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.None);
    CaptureMetadata(decoder.Frames[0].Metadata, string.Empty);
}

CaptureMetadata()是我一个简单的递归过程,用于迭代图像文件中的所有元数据。它使用我的RawMetadataItem类来接收每次迭代的原始查询字符串及其对应的值,并将它们添加到类型为RawMetadataItem的通用List中,如下所示:

class RawMetadataItem
{
    public String location;
    public Object value;
}
List<RawMetadataItem> RawMetadataItems { get; set; }

下面是简短而精炼的递归CaptureMetadata()函数:

private void CaptureMetadata(ImageMetadata imageMetadata, string query)
{
    BitmapMetadata bitmapMetadata = imageMetadata as BitmapMetadata;

    if (bitmapMetadata != null)
    {
        foreach (string relativeQuery in bitmapMetadata)
        {
            string fullQuery = query + relativeQuery;
            object metadataQueryReader = bitmapMetadata.GetQuery(relativeQuery);
            RawMetadataItem metadataItem = new RawMetadataItem();
            metadataItem.location = fullQuery;
            metadataItem.value = metadataQueryReader;
            RawMetadataItems.Add(metadataItem);
            BitmapMetadata innerBitmapMetadata = metadataQueryReader as BitmapMetadata;
            if (innerBitmapMetadata != null)
            {
                CaptureMetadata(innerBitmapMetadata, fullQuery);
            }
        }
    }
}

在我的程序中,我迭代RawMetadataItems以提取location查询字符串和value,并在XceedDataGridControl中显示。但是,显示的数据是原始格式,不太有信息量。因此,我在显示DataGridControl的窗口中有一个选项,可以将location列从原始格式翻译成对摄影师更有信息量的“熟化”格式。我使用一个简单的.NET通用Dictionary来执行此翻译。

熟化格式更准确的描述是“过度熟化”,因为我已经超越了WPFBitmapMetadata.GetQuery()所能理解的范围,并将标签ID翻译成了元数据规范中指定的符号字段名。我这样做是因为我的应用程序是针对摄影师而非程序员的,摄影师对特定元数据标签的语义 far more interested,而不是您会传递给GetQuery()方法的内容。

Dictionary是通过大量研究元数据规范构建起来的,所有这些规范都可以通过互联网搜索获得。以下是我用来构建用于在原始和(过度)熟化查询字符串之间进行翻译的字典的代码示例:Native Image Format Metadata Queries.

以下是我用来构建字典以在原始和(过度)熟化查询字符串之间进行翻译的代码摘录:

static Dictionary<string, string> TiffLocationDictionary { get; set; }
static Dictionary<string, string> JpegLocationDictionary { get; set; }
static string TiffRawXmpTag { get { return "/ifd/{ushort=700}"; } }
static string TiffCookedXmpTag { get { return "/ifd/xmp"; } }
static string TiffRawPhotoshopTag { get { return "/ifd/{ushort=34377}"; } }
static string TiffCookedPhotoshopTag { get { return "/ifd/Photoshop"; } }
static string JpegRawPhotoshopTag { get { return "/app13/{ushort=0}"; } }
static string JpegCookedPhotoshopTag { get { return "/Photoshop/irb"; } }

然后,在我的static构造函数中,我使用类似这样的代码初始化了字典:

JpegLocationDictionary = new Dictionary<string, string>();
TiffLocationDictionary = new Dictionary<string, string>();
Dictionary<string, string> j = JpegLocationDictionary;
Dictionary<string, string> t = TiffLocationDictionary;
            
// Generate JpegLocationDictionary with literal strings.
j.Add("/app0", "/JPEGFileInterchangeFormat");
j.Add("/app0/{ushort=0}", "/JPEGFileInterchangeFormat/Version");
j.Add("/app0/{ushort=1}", "/JPEGFileInterchangeFormat/Units");
j.Add("/app0/{ushort=2}", "/JPEGFileInterchangeFormat/XPixelDensity");
j.Add("/app0/{ushort=3}", "/JPEGFileInterchangeFormat/YPixelDensity");
j.Add("/app0/{ushort=4}", "/JPEGFileInterchangeFormat/Xthumbnail");
j.Add("/app0/{ushort=5}", "/JPEGFileInterchangeFormat/Ythumbnail");
j.Add("/app0/{ushort=6}", "/JPEGFileInterchangeFormat/ThumbnailData");
j.Add("/app1", "/app1");
j.Add("/app1/{ushort=0}", "/app1/ifd");
j.Add("/app1/{ushort=0}/{ushort=254}", "/app1/ifd/NewSubfileType");
j.Add("/app1/{ushort=0}/{ushort=255}", "/app1/ifd/SubfileType");
j.Add("/app1/{ushort=0}/{ushort=256}", "/app1/ifd/OriginalImageWidth");

等等,等等。这个字典中有数百个条目,随着我继续研究我的图像并从beta测试人员那里接收图像,我不断地扩大字典。大多数摄影师在使用我的应用程序探索相机放入他们图像的所有元数据标签时感到惊讶:数量实在太多了!即使如此,元数据对图像文件大小的贡献通常只占图像文件总大小的一小部分。元数据可能只有5K字节,而图像数据本身可能高达数百K字节,甚至数兆字节。

关注点

理解如何迭代位图图像中的所有元数据的关键在于认识到BitmapMetadata类实现了IEnumerable<String>接口,如上所述。

历史

  • 2010年3月17日:初始发布
© . All rights reserved.