Windows Imaging Component 基础知识

来自网络:http://dev.21tx.com/2009/02/09/14033.html

 

目录

  入门

  解码图像

  编码图像

  WIC 图像工厂

  使用流

  通过 WPF 使用 WIC

  下一步是什么?

  Microsoft® Windows® Imaging Component (WIC) 是用于编码、解码和操控图像的可扩展框架。WIC 最初是为 Windows Vista® 和 Windows Presentation Foundation (WPF) 而设计的,但现在,不仅 Windows Vista 和 Microsoft .net Framework 3.0 及更高版本附带此框架,而且它还是 Windows XP 和 Windows Server® 2003 的一个下载项,可供于本机应用程序使用。

  作为支持 WPF 的多个功能强大的本机框架之一,本文中所说的 WIC 是用于实现 System.Windows.Media.Imaging 命名空间的框架。但是,它也非常适合于以 C++ 编写的本机应用程序,因为它提供了通过一组 COM 接口呈现的简单但功能强大的 API。

  WIC 使用一组可扩展的图像编解码器支持多种图像格式。每个编解码器支持一种不同的图像格式,并且通常同时提供编码器和解码器。WIC 包括用于所有主要图像格式的一组内置编解码器,这些格式包括 PNG、JPEG、GIF、TIFF、HD Photo (HDP)、ICO,当然还有 Windows BMP。

  HDP 可能是唯一您没有听过的格式。它最初称为 Windows Media Photo 且是配合 Windows Vista 开发的,用于克服现有格式的一些限制并提供更好的性能和更高的图像质量。如需有关 HDP 的更多信息,请查看 microsoft.com/whdc/xps/wmphoto.mspx 上的规范。幸运地是,WIC 可很好地支持这一新图像格式,因此应用程序不必知道格式的具体细节即可使用它们。

  本月,我将向您展示如何使用 WIC 来编码和解码不同的图像格式以及其间的多项事宜。下一次,我将讲述一些更高级的功能,并向您展示如何使用自己的图像编解码器来扩展 WIC。

  入门

  WIC API 包含 COM 接口、函数、结构和错误代码,以及标识各种编解码器、容器和格式的 GUID。需要的所有声明均包括在 wincodec.h 和 wincodecsdk.h 头文件中,这些文件是 Windows SDK 的一部分(Visual Studio® 2008 中附带这些文件)。您还必须链接到 WindowsCodecs.lib 库,该库提供了您可能需要的各种定义。可将以下代码添加到项目的预编译头文件中以使其完全可用:

#include <wincodec.h>
#include <wincodecsdk.h>
#pragma comment(lib, "WindowsCodecs.lib")

  由于 WIC API 主要包含的是 COM 接口,因此我使用活动模板库 (ATL) CComPtr 类来处理接口指针的创建和管理。如果要执行相同的操作,还需包括定义 CComPtr 模板类的 atlbase.h 头文件:

#include <atlbase.h>

  WIC API 还使用 COM 库,因此使用此 API 的任何线程均必须调用 CoInitializeEx 函数。

  最后,WIC API 使用 HRESULT 来描述错误。本文中的示例使用 HR 宏来清晰地识别方法在何处返回需要检查的 HRESULT。可将它替换为自己的错误处理策略—由它引发异常或是您自己返回 HRESULT。

  解码图像

  解码器由 IWICBitmapDecoder 接口表示。WIC 提供了多种用于创建解码器对象的方法,但可仅使用特定解码器的 CLSID 来创建一个实例。以下示例为 TIFF 图像创建了一个解码器:

CComPtr<IWICBitmapDecoder> decoder;
HR(decoder.CoCreateInstance(CLSID_WICTiffDecoder));

  图 1 列出了 WIC 所包含的编解码器以及可用于创建不同解码器的 CLSID。创建完解码器后,需使用包含像素和可选元数据(它们使用解码器可理解的格式)的流对其进行初始化:

Figure1内置 WIC 编解码器的 CLSID

 

格式 解码器 编码器
BMP CLSID_WICBmpDecoder CLSID_WICBmpEncoder
PNG CLSID_WICPngDecoder CLSID_WICPngEncoder
ICO CLSID_WICIcoDecoder 不可用
JPEG CLSID_WICJpegDecoder CLSID_WICJpegEncoder
GIF CLSID_WICGIfDecoder CLSID_WICGifEncoder
TIFF CLSID_WICTiffDecoder CLSID_WICTiffEncoder
HDP CLSID_WICWmpDecoder CLSID_WICWmpEncoder

 

CComPtr<IStream> stream;
// Create stream object here...
HR(decoder->Initialize(
 stream,
 WICDecodeMetadataCacheOnDemand));

  我将在本文后面的内容中讨论流,但 IStream 只是许多 API 使用的传统 COM 流接口,包括我在 2007 年 4 月出版的《MSDN® 杂志》(msdn.microsoft.com/msdnmag/issues/07/04/XML) 中所介绍的 XmlLite 分析器。

  Initialize 方法的第二个参数介绍您希望解码器如何从流中读取图像信息。WICDecodeMetadataCacheOnDemand 指示解码器应仅在需要时从流中读取图像信息。如果图像格式恰好包含或支持多个帧,则此标记将特别有用。与此相对的标记是 WICDecodeMetadataCacheOnLoad,它指示解码器应立即缓存所有图像信息。然后,对解码器的所有后续请求都应直接从内存执行。它对于托管代码具有更大的意义,我将在稍后进行介绍。

  初始化解码器后,即可自由查询解码器以获取信息。您最可能询问的是构成图像的一组帧。帧是包含像素的实际位图。可将图像格式看作一个帧容器。正如我所提到的,有些图像格式支持多个帧。

  GetFrameCount 函数用于确定图像中的帧数:

UINT frameCount = 0;
HR(decoder->GetFrameCount(&frameCount));

  给定帧数后,即可使用 GetFrame 方法来检索单个帧:

for (UINT index = 0; index < frameCount; ++index)
{
  CComPtr<IWICBitmapFrameDecode> frame;
  HR(decoder->GetFrame(index, &frame));
}

  GetFrame 所返回的 IWICBitmapFrameDecode 接口是从代表只读位图的 IWICBitmapSource 接口派生而来的。IWICBitmapFrameDecode 提供与帧相关的信息,如元数据和色彩配置文件。IWICBitmapSource 提供位图的大小和分辨率、像素格式以及其他可选特性(如色彩表)。IWICBitmapSource 还提供 CopyPixels 方法,此方法可用于从位图中实际读取像素。

  可使用 GetSize 方法获取以像素表示的帧尺寸:

UINT width = 0;
UINT height = 0;
HR(frame->GetSize(&width, &height));

  并且可使用 GetResolution 方法获取以每英寸点数 (dpi) 表示的帧分辨率:

double dpiX = 0;
double dpiY = 0;
HR(frame->GetResolution(&dpiX, &dpiY));

  尽管分辨率对于像素本身没有影响,但在使用逻辑坐标系统(如 WPF 所使用的)时,它的确会影响图像的显示效果。

  最后一个重要帧属性是像素格式。像素格式描述像素在内存中的布局,并且还暗示支持的颜色或颜色空间的范围。GetPixelFormat 方法返回像素格式:

GUID pixelFormat = { 0 };
HR(frame->GetPixelFormat(&pixelFormat));

  像素格式定义为 GUID,其名称非常清楚地描述了内存布局。例如,GUID_WICPixelFormat24bppRGB 格式表示每个像素使用 24 位(3 字节)存储,其中每个颜色通道 1 个字节。此外,红色 (R)、绿色 (G) 和蓝色 (B) 字母的顺序表示从最不重要到最重要的字节顺序。例如,GUID_WICPixelFormat32bpPBGRA 格式表示每个像素使用 32 位(4 字节)存储,其中每个颜色通道 1 个字节,阿尔法通道 1 个字节。在此示例中,通道的顺序是蓝色 (B) 通道最不重要,阿尔法 (A) 通道最重要。

  可使用 CopyPixels 方法来检索实际的像素。

HRESULT CopyPixels(
 const WICRect* rect,
 UINT stride,
 UINT bufferSize,
 BYTE* buffer);

  rect 参数指定位图中要复制的一个矩形。可将此参数设置为 0,此时会复制整个位图。我马上会谈到跨距。buffer 和 bufferSize 参数指示将像素写到何处以及可用空间有多大。

  跨距是位图比较麻烦的一个方面。跨距是扫描线之间的字节数。一般而言,构成位图像素的位会被打包成行。单个行的长度应足够存储一行位图像素。跨距是一行的长度(以字节为单位),向上取整为最接近的 DWORD(4 个字节)。从而允许每像素少于 32 位 (bpp) 的位图占用更少的内存,同时仍提供良好的性能。可使用以下函数来计算指定位图的跨距:

UINT GetStride(
 const UINT width, // image width in pixels
 const UINT bitCount) { // bits per pixel
 ASSERT(0 == bitCount % 8);
 const UINT byteCount = bitCount / 8;
 const UINT stride = (width * byteCount + 3) & ~3;
 ASSERT(0 == stride % sizeof(DWORD));
 return stride;
}

  除此之外,可按如下调用 CopyPixels 方法,假定帧代表一个 32 bpp 位图:

const UINT stride = GetStride(width, 32);
CAtlArray<BYTE> buffer;
VERIFY(buffer.SetCount(stride * height));
HR(frame->CopyPixels(
 0, // entire bitmap
 stride,
 buffer.GetCount(),
 &buffer[0]));

  我的示例使用的是 ATL CAtlArray 集合类来分配缓存,当然您可以使用所喜欢的任何存储。要更有效地处理更大的位图,可多次调用 CopyPixels 以读取位图的不同部分。

  编码图像

  编码器由 IWICBitmapEncoder 接口表示。与解码器一样,WIC 提供了多种用于创建编码器的方法,但可仅使用特定编码器的 CLSID 来创建它。例如,以下代码为 PNG 图像创建了一个编码器:

CComPtr<IWICBitmapEncoder> encoder;
HR(encoder.CoCreateInstance(CLSID_WICPngEncoder));

  图 1 列出了可用于创建 WIC 所包含的各种编码器的 CLSID。创建完编码器后,需使用将最终接收已编码像素和可选元数据的流对其进行初始化:

CComPtr<IStream> stream;
// Create stream object here...
HR(encoder->Initialize(
 stream,
 WICBitmapEncoderNoCache));

  Initialize 方法的第二个参数就没那么有趣了,因为 WICBitmapEncoderNoCache 是目前支持的唯一标记。

  初始化编码器后,现在开始添加帧。CreateNewFrame 方法会创建一个新帧,然后可对其进行配置并向其写入像素:

CComPtr<IWICBitmapFrameEncode> frame;
CComPtr<IPropertyBag2> properties;
HR(encoder->CreateNewFrame(
 &frame,
 &properties));

  CreateNewFrame 返回代表新帧的 IWICBitmapFrameEncode 接口和 IPropertyBag2 接口。后者为可选项,可用于指定任何特定于编码器的属性,如 JPEG 的图像质量或 TIFF 的压缩算法。例如,以下显示了可如何设置 JPEG 图像的图像质量:

PROPBAG2 name = { 0 };
name.dwType = PROPBAG2_TYPE_DATA;
name.vt = VT_R4;
name.pstrName = L"ImageQuality";
CComVariant value(0.75F);
HR(properties->Write(
 1, // property count
 &name,
 &value));

  图像质量的值必须在 0.0(表示可能的最低质量)和 1.0(表示可能的最高质量)之间。

  设完编码器属性后,需先调用 Initialize 方法再进行配置和写入帧:

HR(frame->Initialize(properties));

  下一步是为新帧设置尺寸和像素格式,然后才能向它写入像素:

HR(frame->SetSize(width, height));
GUID pixelFormat = GUID_WICPixelFormat24bppBGR;
HR(frame->SetPixelFormat(&pixelFormat));
ASSERT(GUID_WICPixelFormat24bppBGR == pixelFormat);

  SetPixelFormat 参数为一个 [in, out] 参数。在输入时,它指定所需的像素格式。在输出时,它包含所支持的最接近像素格式。这通常不是问题,除非格式是在运行时设置的,可能基于另一位图的像素格式。

  使用 WritePixels 方法将像素写入到帧中,如下所示:

HRESULT WritePixels(
 UINT lineCount,
 UINT stride,
 UINT bufferSize,
 BYTE* buffer);

  lineCount 参数指定将写入的像素行数。这意味着可多次调用 WritePixels 来写入整个帧。stride 参数指示如何将缓存中的像素打包到行中。我已在上一小节中介绍了如何计算跨距。

  在一次或多次调用 WritePixels 来写入整个帧之后,需通过调用帧的 Commit 方法来告诉编码器帧已做好准备。并且,在提交了构成图像的所有帧后,需通过调用编码器的 Commit 方法来告诉编码器图像已做好被保存的准备。

  到此为止,我已介绍完编码和解码图像的基础知识。在介绍后续内容之前,我想通过一个简单的示例来将其表达清楚。图 2 显示一个 CopyIconToTiff 函数,它展示了如何读取构成图标的各个位图,以及如何将它们复制到多帧 TIFF 图像。

Figure2CopyIconToTiff

HRESULT CopyIconToTiff(
  IStream* sourceStream,
  IStream* targetStream) {
 // Prepare the ICO decoder
 CComPtr<IWICBitmapDecoder> decoder;
 HR(decoder.CoCreateInstance(CLSID_WICIcoDecoder));
 HR(decoder->Initialize(
  sourceStream,
  WICDecodeMetadataCacheOnDemand));
 // Prepare the TIFF encoder
 CComPtr<IWICBitmapEncoder> encoder;
 HR(encoder.CoCreateInstance(CLSID_WICTiffEncoder));
 HR(encoder->Initialize(
  targetStream,
  WICBitmapEncoderNoCache));
 UINT frameCount = 0;
 HR(decoder->GetFrameCount(&frameCount));
 for (UINT index = 0; index < frameCount; ++index) {
  // Get the source frame info
  CComPtr<IWICBitmapFrameDecode> sourceFrame;
  HR(decoder->GetFrame(index, &sourceFrame));
  UINT width = 0;
  UINT height = 0;
  HR(sourceFrame->GetSize(&width, &height));
  GUID pixelFormat = { 0 };
  HR(sourceFrame->GetPixelFormat(&pixelFormat));
  // Prepare the target frame
  CComPtr<IWICBitmapFrameEncode> targetFrame;
  HR(encoder->CreateNewFrame(
   &targetFrame,
   0)); // no properties
  HR(targetFrame->Initialize(0)); // no properties
  HR(targetFrame->SetSize(width, height));
  HR(targetFrame->SetPixelFormat(&pixelFormat));
  // Copy the pixels and commit frame
  HR(targetFrame->WriteSource(sourceFrame, 0));
  HR(targetFrame->Commit());
 }
 // Commit image to stream
 HR(encoder->Commit());
 return S OK;
}

  在该示例中,我通过利用 WritePixels 方法的替代项进一步简化了流程。不是先从源帧中复制像素然后将其写入到目标帧中,而是使用 WriteSource 方法来从给定的 IWICBitmapSource 接口直接读取像素。由于 IWICBitmapFrameDecode 接口是从 IWICBitmapSource 派生而来,因而提供了一个完善的解决方案

  WIC 图像工厂

  WIC 提供了一个图像工厂来创建各种与 WIC 相关的对象。它通过 IWICImagingFactory 接口公开,并且可按如下所示进行创建:

CComPtr<IWICImagingFactory> factory;
HR(factory.CoCreateInstance(CLSID_WICImagingFactory));

  我已展示了如果特定图像格式给定了指出实现的 CLSID,该如何创建解码器。当然,正如您可能猜想到,如果不必指定特定实现,或者可以硬编码图像的格式,那它会更有用途。

  幸运的是,WIC 提供了解决方案。在创建解码器之前,WIC 可检查给定流有无可标识图像格式的模式。一旦找到最佳的匹配项,它会创建适当的解码器并使用相同流对其进行初始化。CreateDecoderFromStream 方法提供了这一功能:

CComPtr<IWICBitmapDecoder> decoder;
HR(factory->CreateDecoderFromStream(
 stream,
 0, // vendor
 WICDecodeMetadataCacheOnDemand,
 &decoder));

  第二个参数标识解码器的供应商。它是可选项,如果您偏好特定供应商的编解码器,这个参数就会派上用场。请记住,它仅是一个提示,如果特定供应商并未安装适合的解码器,则仍会选择一个解码器,而不管供应商是谁。

  IWICImagingFactory 还提供 CreateDecoderFromFilename 和 CreateDecoderFromFileHandle 方法,它们分别提供到文件和文件句柄的路径。也可以不指定 CLSID 或流,而是指出图像格式来创建解码器。CreateDecoder 方法正好可以完成这项工作:

CComPtr<IWICBitmapDecoder> decoder;
HR(factory->CreateDecoder(
 GUID_ContainerFormatIco,
 0, // vendor
 &decoder));
HR(decoder->Initialize(
 stream,
 WICDecodeMetadataCacheOnDemand));

  与此类似,CreateEncoder 方法为特定图像格式创建编码器时也不必考虑其实现,如下所示:

CComPtr<IWICBitmapEncoder> encoder;
HR(factory->CreateEncoder(
 GUID_ContainerFormatBmp,
 0, // vendor
 &encoder));
HR(encoder->Initialize(
 stream,
 WICBitmapEncoderNoCache));

  图 3 列出了标识独立于实现的图像格式的 GUID,也称为容器格式。

Figure3容器格式 GUID

 

格式 GUID
BMP GUID_ContainerFormatBmp
PNG GUID_ContainerFormatPng
ICO GUID_ContainerFormatIco
JPEG GUID_ContainerFormatJpeg
GIF GUID_ContainerFormatGif
TIFF GUID_ContainerFormatTiff
HDP GUID_ContainerFormatWmp

 

  使用流

  可随时提供任何有效的 IStream 实现以用于 WIC。例如,可使用我在 XmlLite 这篇文章中所介绍的 CreateStreamOnHGlobal 或 SHCreateStreamOnFile 函数,也可以编写您自己的实现。WIC 还提供非常方便且灵活的 IStream 实现。

  通过使用我在上一小节中介绍的图像工厂,可按如下所示创建未初始化的流对象:

CComPtr<IWICStream> stream;
HR(factory->CreateStream(&stream));

  IWICStream 接口继承自 Istream,并提供了多种方法来将流与不同的存储支持相关联。例如,可使用 InitializeFromFilename 来创建特定文件所支持的流:

HR(stream->InitializeFromFilename(
 L"file path",
 GENERIC_READ));

  也可使用 InitializeFromIStreamRegion 将某个流创建为另一个流的子集,或使用 InitializeFromMemory 在一个内存块上创建流。

  通过 WPF 使用 WIC

  正如我已提到的,WIC 提供了作为 WPF 图像处理功能基础的框架。System.Windows.Media.Imaging 命名空间中定义了各种图像处理类。为向您展示从托管代码使用是多么地简单,图 4 显示了原本在图 2 中的 CopyIconToTiff 函数,它使用 WPF 包装类以 C# 重新进行了编写。

Figure4以 C# 和 WPF 重新编写的 CopyIconToTiff

static void CopyIconToTiff(Stream sourceStream,
              Stream targetStream) {
 IconBitmapDecoder decoder = new IconBitmapDecoder(
  sourceStream,
  BitmapCreateOptions.None,
  BitmapCacheOption.OnDemand);
 TiffBitmapEncoder encoder = new TiffBitmapEncoder();
 foreach (BitmapFrame frame in decoder.Frames) {
  encoder.Frames.Add(frame);
 }
 encoder.Save(targetStream);
}

  BitmapCacheOption.OnDemand 值对应于本机代码中使用的 WICDecodeMetadataCacheOnDemand 解码器选项。另一个 BitmapCacheOption.OnLoad 值则对应 WICDecodeMetadataCacheOnLoad 解码器选项。

  我已介绍了当解码器将图像信息读入内存时,这些选项分别有何影响。但是,在托管代码中处理这些选项时,还有一个应了解的副作用。考虑一下指定 BitmapCacheOption.OnDemand 时可能发生什么情况。解码器会持有对基础流的引用,并且可能在创建位图解码器对象后的某个时间从其中执行读取。它的前提是流仍然可用。您需要注意,应用程序不会过早地关闭流。因此需要管理流的生存期,以便不会在解码器完成使用之前将其关闭。

  它不会影响本机代码,因为 IStream 接口为标准 COM 接口,其生存期是由引用计数控制的。应用程序可释放对它的所有引用,但只要有必要,解码器都会保存一个引用。并且,仅当释放了所有接口指针后才会关闭流。

  下一步是什么?

  WIC 提供了一个功能极其强大且灵活的框架,它可满足您的各种图像处理需求。通过使用一组丰富的编解码器和一个简单的 API,您即可立刻开始利用它的多项功能。

  在下一专栏中,我将探讨 WIC 所提供的一些更高级的功能。我将向您展示如何开发自己的编解码器,并详细地说明注册和发现过程,包括模式匹配功能。到时,我还会更正一个内置编解码器中的一个限制。

你可能感兴趣的:(component)