读取和写入图像元数据概述

本主题概述如何使用 Windows 映像组件 (WIC) API 读取和写入嵌入在图像文件中的元数据。

本主题包含以下各节:

先决条件

若要了解本主题,应熟悉 WIC 元数据系统,如 WIC 元数据概述中所述。 还应熟悉用于读取和写入元数据的查询语言,如 元数据查询语言概述中所述。

简介

WIC 为应用程序开发人员提供了组件对象模型 (COM) 组件,用于读取和写入嵌入在图像文件中的元数据。 可通过两种方式读取和写入元数据:

  • 使用查询读取器/编写器和查询表达式查询嵌套块或块内特定元数据的元数据块。
  • 使用元数据处理程序 (元数据读取器或元数据编写器) 来访问嵌套的元数据块或元数据块中的特定元数据。

其中最简单的方法是使用查询读取器/编写器和查询表达式来访问元数据。 (IWICMetadataQueryReader) 的查询读取器用于读取元数据,而查询编写器 (IWICMetadataQueryWriter) 用于写入元数据。 这两种方法都使用查询表达式来读取或写入所需的元数据。 在后台,查询读取器 (和编写器) 使用元数据处理程序来访问查询表达式描述的元数据。

更高级的方法是直接访问元数据处理程序。 元数据处理程序是使用块读取器 (IWICMetadataBlockReader) 或块编写器 (IWICMetadataBlockWriter) 获取的。 可用的两种类型的元数据处理程序是 IWICMetadataReader) (元数据读取器和元数据编写器 (IWICMetadataWriter) 。

本主题的整个示例中使用了以下 JPEG 图像文件内容的示意图。 此关系图表示的图像是使用 Microsoft 画图 创建的;分级元数据是使用 Windows Vista 的照片库功能添加的。

包含分级元数据的 jpeg 图像插图

使用查询读取器读取 Metadadata

读取元数据的最简单方法是使用查询读取器接口 IWICMetadataQueryReader。 使用查询读取器,可以使用查询表达式读取元数据块和元数据块中的项。

可通过三种方式获取查询读取器:通过位图解码器 (IWICBitmapDecoder) ,通过其单个帧 (IWICBitmapFrameDecode) ,或者通过查询编写器 (IWICMetadataQueryWriter) 。

获取查询读取器

以下示例代码演示如何从映像工厂获取位图解码器并检索单个位图帧。 此代码还执行从解码帧获取查询读取器所需的设置工作。

IWICImagingFactory *pFactory = NULL;
IWICBitmapDecoder *pDecoder = NULL;
IWICBitmapFrameDecode *pFrameDecode = NULL;
IWICMetadataQueryReader *pQueryReader = NULL;
IWICMetadataQueryReader *pEmbedReader = NULL;
PROPVARIANT value;

// Initialize COM
CoInitialize(NULL);

// Initialize PROPVARIANT
PropVariantInit(&value);

//Create the COM imaging factory
HRESULT hr = CoCreateInstance(
    CLSID_WICImagingFactory,
    NULL,
    CLSCTX_INPROC_SERVER,
    IID_IWICImagingFactory,
    (LPVOID*)&pFactory);

// Create the decoder
if (SUCCEEDED(hr))
{
    hr = pFactory->CreateDecoderFromFilename(
        L"test.jpg",
        NULL,
        GENERIC_READ,
        WICDecodeMetadataCacheOnDemand,
        &pDecoder);
}

// Get a single frame from the image
if (SUCCEEDED(hr))
{
    hr = pDecoder->GetFrame(
         0,  //JPEG has only one frame.
         &pFrameDecode); 
}

使用映像工厂的 CreateDecoderFromFilename 方法获取test.jpg文件的位图解码器。 在此方法中,第四个参数设置为 WICDecodeOptions 枚举中的值 WICDecodeMetadataCacheOnDemand 。 这会告知解码器在需要元数据时缓存元数据;通过获取查询读取器或基础元数据读取器。 使用此选项可将流保留到快速元数据编码所需的元数据,并启用 JPEG 图像的无损解码。 或者,可以使用其他 WICDecodeOptions 值 WICDecodeMetadataCacheOnLoad,该值在加载图像后立即缓存嵌入的图像元数据。

若要获取帧的查询读取器,请对帧的 GetMetadataQueryReader 方法进行简单调用。 以下代码演示了此调用。

// Get the query reader
if (SUCCEEDED(hr))
{
    hr = pFrameDecode->GetMetadataQueryReader(&pQueryReader);
}

同样,也可以在解码器级别获取查询读取器。 对解码器的 GetMetadataQueryReader 方法的简单调用可获取解码器的查询读取器。 与帧的查询读取器不同,解码器的查询读取器读取单个帧之外的图像的元数据。 但是,这种情况并不常见,本机图像格式不支持此功能。 WIC 提供的本机图像编解码器在帧级别读取和写入元数据,即使对于 JPEG 等单帧格式也是如此。

读取元数据

在继续实际读取元数据之前,请查看以下 JPEG 文件的关系图,其中包含嵌入的元数据块和要检索的实际数据。 此图提供对图像中特定元数据块和项的标注,为每个块或项目提供元数据查询表达式。

包含元数据标注的 jpeg 图像插图

若要按名称查询嵌入的元数据块或特定项,请调用 GetMetadataByName 方法。 此方法采用查询表达式和 PROPVARIANT ,其中返回元数据项。 以下代码查询嵌套元数据块,并将 PROPVARIANT 值提供的 IUnknown 组件转换为查询读取器(如果找到)。

if (SUCCEEDED(hr))
{
    // Get the nested IFD reader
    hr = pQueryReader->GetMetadataByName(L"/app1/ifd", &value);
    if (value.vt == VT_UNKNOWN)
    {
        hr = value.punkVal->QueryInterface(IID_IWICMetadataQueryReader, (void **)&pEmbedReader);
    }
    PropVariantClear(&value); // Clear value for new query
}

查询表达式“/app1/ifd”正在查询嵌套在 App1 块中的 IFD 块。 JPEG 图像文件包含 IFD 嵌套元数据块,因此 返回 PROPVARIANT 的变量类型 (vt) VT_UNKNOWN ,指向 IUnknown 接口的指针 (punkVal) 。 然后,查询查询读取器的 IUnknown 接口。

以下代码演示基于相对于嵌套 IFD 块的新查询读取器的新查询。

if (SUCCEEDED(hr))
{
    hr = pEmbedReader->GetMetadataByName(L"/{ushort=18249}", &value);
    PropVariantClear(&value); // Clear value for new query
}

查询表达式“/{ushort=18249}”查询嵌入在标记 18249 下的 MicrosoftPhoto 分级的 IFD 块。 PROPVARIANT 值现在将包含值类型和VT_UI2数据值 50。

但是,在查询特定数据值之前,无需获取嵌套块。 例如,可以改用根元数据块和以下代码中显示的查询来获取相同的信息,而不是查询嵌套 IFD,然后查询 MicrosoftPhoto 分级。

if (SUCCEEDED(hr))
{
    hr = pQueryReader->GetMetadataByName(L"/app1/ifd/{ushort=18249}", &value);
    PropVariantClear(&value);
}

除了查询元数据块中的特定元数据项外,还可以枚举元数据块中的所有元数据项, (不包括嵌套元数据块) 中的元数据项。 若要枚举当前块中的元数据项,请使用查询读取器的 GetEnumeration 方法。 此方法获取使用当前块中的元数据项填充的 IEnumString 接口。 例如,以下代码枚举先前获取的嵌套 IFD 块的 XMP 分级和 MicrosoftPhoto 分级。

IEnumString *metadataItems = NULL;

if (SUCCEEDED(hr))
{
    hr = pEmbedReader->GetEnumerator(&metadataItems);
}

有关识别各种图像格式和元数据格式的相应标记的详细信息,请参阅 本机图像格式元数据查询

其他查询读取器方法

除了读取元数据外,还可以获取有关查询读取器的其他信息,并通过其他方式获取元数据。 查询读取器提供两种方法,用于提供有关查询读取器的信息: GetContainerFormatGetLocation

使用嵌入式查询读取器,可以使用 GetContainerFormat 来确定元数据块的类型,并且可以调用 GetLocation 来获取相对于根元数据块的路径。 以下代码查询嵌入式查询读取器的位置。

// Determine the metadata block format

if (SUCCEEDED(hr))
{
    hr = pEmbedReader->GetContainerFormat(&containerGUID);
}

// Determine the query reader's location
if (SUCCEEDED(hr))
{
    UINT length;
    WCHAR readerNamespace[100];
    hr = pEmbedReader->GetLocation(100, readerNamespace, &length);
}

调用嵌入式查询读取器的 GetContainerFormat 将返回 IFD 元数据格式 GUID。 对 GetLocation 的调用返回命名空间“/app1/ifd”;提供从中对新查询读取器执行后续查询的相对路径。 当然,前面的代码不是很有用,但它确实演示了如何使用 GetLocation 方法来查找嵌套元数据块。

使用查询编写器写入元数据

注意

本节中提供的一些代码示例未显示在编写元数据所需的实际步骤的上下文中。 若要查看工作示例上下文中的代码示例,请参阅操作方法:使用元数据重新编码图像教程。

 

用于编写元数据的main组件是查询编写器 (IWICMetadataQueryWriter) 。 使用查询编写器可以设置和删除元数据块中的元数据块和项。

与查询读取器一样,可通过三种方式获取查询编写器:通过位图编码器 (IWICBitmapEncoder) ,通过其单个帧 (IWICBitmapFrameEncode) ,或者通过快速元数据编码器 (IWICFastMetadataEncoder) 。

获取查询编写器

最常见的查询编写器适用于位图的单个帧。 此查询编写器设置并删除图像帧的元数据块和项。 若要获取图像帧的查询编写器,请调用该帧的 GetMetadataQueryWriter 方法。 以下代码演示了用于获取帧查询编写器的简单方法调用。

IWICMetadataQueryWriter &pFrameQWriter = NULL;

//Obtain a query writer from the frame.
hr = pFrameEncode->GetMetadataQueryWriter(&pFrameQWriter);

同样,还可以获取编码器级别的查询编写器。 简单调用编码器的 GetMetadataQueryWriter 方法即可获取编码器的查询编写器。 编码器的查询编写器与帧的查询编写器不同,它为单个帧之外的图像编写元数据。 但是,这种情况并不常见,本机图像格式不支持此功能。 WIC 提供的本机图像编解码器在帧级别读取和写入元数据,即使对于 JPEG 等单帧格式也是如此。

还可以直接从映像工厂 (IWICImagingFactory) 获取查询编写器。 有两种映像工厂方法返回查询编写器: CreateQueryWriterCreateQueryWriterFromReader

CreateQueryWriter 为指定的元数据格式和供应商创建查询编写器。 使用此查询编写器,可以编写特定元数据格式的元数据并将其添加到图像。 以下代码演示 CreateQueryWriter 调用以创建 XMP 查询编写器。

IWICMetadataQueryWriter *pXMPWriter = NULL;

// Create XMP block
GUID vendor = GUID_VendorMicrosoft;
hr = pFactory->CreateQueryWriter(
        GUID_MetadataFormatXMP,
        &vendor,
        &pXMPWriter);

在此示例中,友好名称 GUID_MetadataFormatXMP 用作 guidMetadataFormat 参数。 它表示 XMP 元数据格式 GUID,供应商表示 Microsoft 创建的处理程序。 或者,如果没有其他 XMP 处理程序,则可以将 NULL 作为 pguidVendor 参数传递,结果相同。 如果自定义 XMP 处理程序与本机 XMP 处理程序一起安装,则为供应商传递 NULL 将导致返回具有最低 GUID 的查询编写器。

CreateQueryWriterFromReader 类似于 CreateQueryWriter 方法,只不过它使用查询读取器提供的数据预填充新的查询编写器。 这对于在维护现有元数据或删除不需要的元数据的同时重新编码图像非常有用。 以下代码演示 CreateQueryWriterFromReader 调用。

hr = pFrameDecode->GetMetadataQueryReader(&pFrameQReader);

// Copy metadata using query readers
if(SUCCEEDED(hr) && pFrameQReader)
{
    IWICMetadataQueryWriter *pNewWriter = NULL;

    GUID vendor = GUID_VendorMicrosoft;
    hr = pFactory->CreateQueryWriterFromReader(
        pFrameQReader,
        &vendor,
        &pNewWriter);

添加元数据

获取查询编写器后,可以使用它添加元数据块和项。 若要编写元数据,请使用查询编写器的 SetMetadataByName 方法。 SetMetadataByName 采用两个参数:一个查询表达式 (wzName) 和一个指向 PROPVARIANT (pvarValue) 的指针。 查询表达式定义要设置的块或项,而 PROPVARIANT 提供要设置的实际数据值。

以下示例演示如何使用以前使用 CreateQueryWriter 方法获取的 XMP 查询编写器添加标题。

// Write metadata to the XMP writer
if (SUCCEEDED(hr))
{
    PROPVARIANT value;
    PropVariantInit(&value);

    value.vt = VT_LPWSTR;
    value.pwszVal = L"Metadata Test Image.";
   
    hr = pXMPWriter->SetMetadataByName(L"/dc:title", &value);

    PropVariantClear(&value);
}

在此示例中,值的类型 (vt) 设置为 VT_LPWSTR,指示字符串将用作数据值。 由于 的类型是字符串,因此 pwszVal 用于设置要使用的标题。 然后,使用查询表达式“/dc:title”和新设置的 PROPVARIANT 调用 SetMetadataByName。 使用的查询表达式指示应设置数码相机 (dc) 架构中的 title 属性。 请注意,表达式不是“/xmp/dc:title”;这是因为查询编写器已经特定于 XMP,并且不包含嵌入的 XMP 块,“/xmp/dc:title”会建议。

至此,实际上尚未向图像帧添加任何元数据。 只需使用数据填充查询编写器即可。 若要将查询编写器表示的元数据块添加到帧中,请再次使用查询编写器作为 PROPVARIANT 的值调用 SetMetadataByName。 这有效地将查询编写器中的元数据复制到图像帧。 以下代码演示如何将之前获取的 XMP 查询编写器中的元数据添加到帧的根元数据块。

// Get the frame's query writer and write the XMP query writer to it
if (SUCCEEDED(hr))
{
    hr = pFrameEncode->GetMetadataQueryWriter(&pFrameQWriter);

    // Copy the metadata in the XMP query writer to the frame
    if (SUCCEEDED(hr))
    {
        PROPVARIANT value;

        PropVariantInit(&value);
        value.vt = VT_UNKNOWN;
        value.punkVal = pXMPWriter;
        value.punkVal->AddRef();

        hr = pFrameQWriter->SetMetadataByName(L"/", &value);

        PropVariantClear(&value);
    }
}

在此示例中,使用 (vt) 的值 VT_UNKOWN 类型;指示 COM 接口值类型。 然后,将 xMP 查询编写器 (piXMPWriter) 用作 PROPVARIANT 的值,并使用 AddRef 方法添加对它的引用。 最后,通过调用帧的 SetMetadataByName 方法并传递表示根块和新设置的 PROPVARIANT 的查询表达式“/”来设置 XMP 查询编写器。

注意

如果帧已包含要添加的元数据块,则将添加要添加的元数据,并覆盖现有元数据。

 

删除元数据

查询编写器还允许通过调用 RemoveMetadataByName 方法删除元数据。 RemoveMetadataByName 采用查询表达式,并删除元数据块或项(如果存在)。 以下代码演示如何删除之前添加的游戏。

if (SUCCEEDED(hr))
{
    hr = pFrameQWriter->RemoveMetadataByName(L"/xmp/dc:title");
}

以下代码演示如何删除整个 XMP 元数据块。

if (SUCCEEDED(hr))
{
    hr = pFrameQWriter->RemoveMetadataByName(L"/xmp");
}

复制元数据以重新编码

注意

仅当源图像和目标图像格式相同时,本部分中的代码才有效。 编码为其他图像格式时,不能在单个操作中复制图像的所有元数据。

 

若要在将图像重新编码为相同图像格式时保留元数据,可以使用一些方法在单个操作中复制所有元数据。 其中每个操作都遵循类似的模式:每个将解码帧的元数据直接设置为要编码的新帧。

复制元数据的首选方法是使用解码帧的块读取器初始化新帧的块编写器。 以下代码演示了此方法。

if (SUCCEEDED(hr) && formatsEqual)
{
    // Copy metadata using metadata block reader/writer
    if (SUCCEEDED(hr))
    {
        pFrameDecode->QueryInterface(
            IID_IWICMetadataBlockReader,
            (void**)&pBlockReader);
    }
    if (SUCCEEDED(hr))
    {
        pFrameEncode->QueryInterface(
            IID_IWICMetadataBlockWriter,
            (void**)&pBlockWriter);
    }
    if (SUCCEEDED(hr))
    {
        pBlockWriter->InitializeFromBlockReader(pBlockReader);
    }
}

在此示例中,块读取器和块编写器分别从源帧和目标帧获取。 然后,从块读取器初始化块编写器。 这会使用块读取器的预填充元数据初始化块读取器。

复制元数据的另一种方法是使用编码器的查询编写器编写查询读取器引用的元数据块。 以下代码演示了此方法。

if (SUCCEEDED(hr) && formatsEqual)
{
    hr = pFrameDecode->GetMetadataQueryReader(&pFrameQReader);

    // Copy metadata using query readers
    if(SUCCEEDED(hr))
    {
        hr = pFrameEncode->GetMetadataQueryWriter(&pFrameQWriter);
        if (SUCCEEDED(hr))
        {
            PropVariantClear(&value);
            value.vt=VT_UNKNOWN;
            value.punkVal=pFrameQReader;
            value.punkVal->AddRef();
            hr = pFrameQWriter->SetMetadataByName(L"/", &value);
            PropVariantClear(&value);
        }
    }
}

在这里,查询读取器是从解码的帧获取的,然后用作 PROPVARIANT 的属性值,值类型设置为 VT_UNKNOWN。 获取编码器的查询编写器,并使用查询表达式“/”在根导航路径中设置元数据。 还可以在设置嵌套元数据块时使用此方法,方法是将查询表达式调整到所需位置。

同样,可以使用映像工厂的 CreateQueryWriterFromReader 方法,基于解码帧的查询读取器创建查询编写器。 此操作中创建的查询编写器将使用查询读取器的元数据预填充,然后可在帧中设置。 以下代码演示 CreateQueryWriterFromReader 复制操作。

IWICMetadataQueryWriter *pNewWriter = NULL;

GUID vendor = GUID_VendorMicrosoft;
hr = pFactory->CreateQueryWriterFromReader(
    pFrameQReader,
    &vendor,
    &pNewWriter);

if (SUCCEEDED(hr))
{
    // Get the frame's query writer
    hr = pFrameEncode->GetMetadataQueryWriter(&pFrameQWriter);
}

// Set the query writer to the frame.
if (SUCCEEDED(hr))
{
    PROPVARIANT value;

    PropVariantInit(&value);
    value.vt = VT_UNKNOWN;
    value.punkVal = pNewWriter;
    value.punkVal->AddRef();
    hr = pFrameQWriter->SetMetadataByName(L"/",&value);
}

此方法使用基于查询读取器的数据创建单独的查询编写器。 然后,在帧中设置此新查询编写器。

同样,这些复制元数据的操作仅在源图像和目标图像具有相同的格式时才起作用。 这是因为不同的图像格式将元数据块存储在不同的位置。 例如,JPEG 和 TIFF 都支持 XMP 元数据块。 在 JPEG 映像中,XMP 块位于根元数据块中,如 WIC 元数据概述中所述。 但是,在 TIFF 映像中,XMP 块嵌套在根 IFD 块中。 下图说明了 JPEG 图像与具有相同评级元数据的 TIFF 图像之间的差异。

jpeg 和 tiff 比较。

快速元数据编码

并不总是需要重新编码图像以向其写入新元数据。 还可以使用快速元数据编码器编写元数据。 快速元数据编码器可以将有限数量的元数据写入图像,而无需重新编码图像。 这是通过在某些元数据格式提供的空填充中编写新元数据来实现的。 支持元数据填充的本机元数据格式为 Exif、IFD、GPS 和 XMP。

向元数据块添加填充

在可以执行快速元数据编码之前,元数据块内必须有空间来写入更多元数据。 如果现有填充中没有足够的空间来写入新元数据,快速元数据编码将失败。 若要向图像添加元数据填充,必须重新编码图像。 如果正在填充的元数据块支持,则可以使用查询表达式添加填充,以添加任何其他元数据项的相同方式添加填充。 以下示例演示如何向 App1 块中嵌入的 IFD 块添加填充。

if (SUCCEEDED(hr))
{
    // Add metadata padding
    PROPVARIANT padding;

    PropVariantInit(&padding);
    padding.vt = VT_UI4;
    padding.uiVal = 4096; // 4KB

    hr = pFrameQWriter->SetMetadataByName(L"/app1/ifd/PaddingSchema:padding", &padding);

    PropVariantClear(&padding);
}

若要添加填充,请创建类型为 VT_UI4 的 PROPVARIANT,以及对应于要添加的填充字节数的值。 典型值为 4096 字节。 此表中包含 JPEG、TIFF 和 JPEG-XR 的元数据查询。

元数据格式 JPEG 元数据查询 TIFF、JPEG-XR 元数据查询
IFD /app1/ifd/PaddingSchema:Padding /ifd/PaddingSchema:Padding
Exif /app1/ifd/exif/PaddingSchema:Padding /ifd/exif/PaddingSchema:Padding
XMP /xmp/PaddingSchema:Padding /ifd/xmp/PaddingSchema:Padding
GPS /app1/ifd/gps/PaddingSchema:Padding /ifd/gps/PaddingSchema:Padding

 

获取快速元数据编码器

如果图像具有元数据填充,则可以使用映像工厂方法 CreateFastMetadataEncoderFromDecoderCreateFastMetadataEncoderFromFrameDecode 获取快速元数据编码器。

顾名思义, CreateFastMetadataEncoderFromDecoder 为解码器级元数据创建快速元数据编码器。 WIC 提供的本机图像格式不支持解码器级元数据,但在将来开发此类图像格式时会提供此方法。

更常见的方案是使用 CreateFastMetadataEncoderFromFrameDecode 从图像帧获取快速元数据编码器。 以下代码获取解码帧的快速元数据编码器,并更改 App1 块中的评级值。

if (SUCCEEDED(hr))
{
    IWICFastMetadataEncoder *pFME = NULL;
    IWICMetadataQueryWriter *pFMEQW = NULL;

    hr = pFactory->CreateFastMetadataEncoderFromFrameDecode(
        pFrameDecode, 
        &pFME);
}

使用快速元数据编码器

从快速元数据编码器中,可以获取查询编写器。 这使你能够使用查询表达式编写元数据,如前所述。 在查询编写器中设置元数据后,提交快速元数据编码器以完成元数据更新。 以下代码演示如何设置和提交元数据更改

    if (SUCCEEDED(hr))
    {
        hr = pFME->GetMetadataQueryWriter(&pFMEQW);
    }

    if (SUCCEEDED(hr))
    {
        // Add additional metadata
        PROPVARIANT value;

        PropVariantInit(&value);

        value.vt = VT_UI4;
        value.uiVal = 99;
        hr = pFMEQW->SetMetadataByName(L"/app1/ifd/{ushort=18249}", &value);

        PropVariantClear(&value);
    }

    if (SUCCEEDED(hr))
    {
        hr = pFME->Commit();
    }
}

如果 由于 任何原因提交失败,则需要重新编码图像,以确保将新的元数据添加到映像中。

概念性

Windows 映像组件概述

WIC 元数据概述

元数据查询语言概述

元数据扩展性概述

操作说明:使用元数据重新编码 JPEG 图像