O Microsoft® Windows®
Imaging Component (WIC) é uma estrutura extensível para codificar,
decodificar e manipular imagens. Originalmente projetado para Windows
Vista® e Windows Presentation
Foundation (WPF), hoje o WIC não apenas acompanha o Windows Vista e o
Microsoft .NET Framework 3.0 e versões posteriores, mas também está
disponível como um download para o Windows XP e Windows Server® 2003 para uso em aplicativos nativos.
Uma
das várias estruturas nativas eficientes que capacitam o WPF, o WIC,
neste contexto, é a estrutura usada na implementação do namespace
System.Windows.Media.Imaging. Ele é, no entanto, também perfeitamente
adequado para aplicativos nativos escritos em C++ porque fornece uma API
simples, mas eficiente, exposta por meio de um conjunto de interfaces
COM.
O
WIC suporta diferentes formatos de imagem usando um conjunto extensível
de codecs de imagens. Cada codec suporta um formato de imagem diferente
e normalmente fornece um codificador e um decodificador. O WIC inclui
um conjunto de codecs internos para praticamente todos os principais
formatos de imagens, incluindo PNG, JPEG, GIF, TIFF, HD Photo (HDP), ICO
e, é claro, Windows BMP.
O
HDP é o único formato de que você pode nunca ter ouvido falar. Ele era
originalmente chamado de Windows Media Photo e foi desenvolvido em
conjunto com o Windows Vista para superar algumas das limitações com
formatos existentes e fornecer um melhor desempenho e qualidade de
imagem superior. Para obter mais informações sobre HDP, consulte a
especificação em microsoft.com/whdc/xps/wmphoto.mspx.
Felizmente, o WIC fornece um excelente suporte para esse novo formato
de imagem, de forma que os aplicativos não precisam saber as
especificações dos formatos para usá-los.
Este
mês, mostrarei a você como usar o WIC para codificar e decodificar
diferentes formatos de imagens e algumas coisas intermediárias. Na
próxima vez, explorarei alguns dos recursos mais avançados e mostrarei
como estender o WIC com seus próprios codecs de imagens.
Introdução
A
API do WIC consiste de interfaces COM, funções, estruturas e códigos de
erros, bem como GUIDs que identificam vários codecs, contêineres e
formatos. Todas as declarações necessárias estão incluídas nos arquivos
de cabeçalho wincodec.h e wincodecsdk.h fornecidos como parte do Windows
SDK (e incluídas com o Visual Studio®
2008). Também será preciso que você vincule à biblioteca
WindowsCodecs.lib, que fornece as várias definições necessárias. Você
pode adicionar o seguinte ao cabeçalho pré-compilado do seu projeto para
torná-lo totalmente disponível:
#include <wincodec.h> #include <wincodecsdk.h> #pragma comment(lib, "WindowsCodecs.lib")
Como
a API do WIC consiste principalmente de interfaces COM, eu uso a classe
CComPtr do ATL (Active Template Library) para tratar da criação e
gerenciamento dos ponteiros de interface. Se você desejar fazer o mesmo,
precisará incluir também o arquivo do cabeçalho atlbase.h que define a
classe de modelo CComPtr:
#include <atlbase.h>
A
API do WIC também usa a biblioteca COM, portanto a função
CoInitializeEx deve ser chamada em qualquer segmento que usará a API.
Finalmente,
a API do WIC usa HRESULTs para descrever erros. As amostras neste
artigo usam a macro HR para identificar claramente onde os métodos
retornam um HRESULT que precisa ser verificado. Você pode substituir
isso por sua própria estratégia de tratamento de erros — seja lançando
uma exceção ou retornando você mesmo o HRESULT.
Decodificando imagens
Os
decodificadores são representados pela interface IWICBitmapDecoder. O
WIC fornece algumas maneiras de criar um objeto decodificador, mas, no
mínimo, você pode simplesmente usar um CLSID particular do decodificador
para criar uma instância. O exemplo a seguir cria um decodificador para
uma imagem TIFF:
CComPtr<IWICBitmapDecoder> decoder; HR(decoder.CoCreateInstance(CLSID_WICTiffDecoder));
A Figura 1 lista os codecs incluídos com o
WIC e os CLSIDs que você pode usar para criar os diferentes
decodificadores. Depois que o decodificador é criado, ele precisa ser
inicializado com um fluxo contendo os pixels e metadados opcionais em um
formato entendido pelo decodificador:
| Formato | Decodificador | Codificador |
|---|---|---|
| BMP | CLSID_WICBmpDecoder | CLSID_WICBmpEncoder |
| PNG | CLSID_WICPngDecoder | CLSID_WICPngEncoder |
| ICO | CLSID_WICIcoDecoder | Não disponível |
| 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));
Discutirei
os fluxos mais tarde neste artigo, mas IStream é apenas a interface de
fluxo COM tradicional usada por várias APIs, incluindo o analisador
XmlLite de que falei na edição de abril de 2007 da MSDN® Magazine (msdn.microsoft.com/msdnmag/issues/07/04/Xml).
O
segundo parâmetro para o método Initialize descreve como você gostaria
que o decodificador lesse as informações da imagem a partir do fluxo.
WICDecodeMetadataCacheOnDemand indica que o decodificador deve apenas
ler as informações da imagem a partir do fluxo, conforme necessário.
Isso pode ser particularmente útil se o formato da imagem contiver ou
suportar vários quadros. A alternativa é WICDecodeMetadataCacheOnLoad,
que indica que o decodificador deve armazenar em cache todas as
informações da imagem imediatamente. Todas as solicitações subseqüentes
para o decodificador seriam então preenchidas diretamente a partir da
memória. Isso possui algumas implicações adicionais para códigos
gerenciados sobre o que falarei posteriormente.
Com
o decodificador inicializado, você pode livremente consultar o
decodificador em busca de informações. A coisa mais comum que
provavelmente você irá perguntar é o conjunto de quadros que formam uma
imagem. Os quadros são os bitmaps reais que contêm os pixels. Você pode
pensar no formato da imagem como um contêiner para quadros. Como
mencionei, alguns formatos de imagens suportam vários quadros.
A função GetFrameCount é usada para determinar o número de quadros na imagem:
UINT frameCount = 0; HR(decoder->GetFrameCount(&frameCount));
Dado o número de quadros, quadros individuais podem ser recuperados usando o método GetFrame:
for (UINT index = 0; index < frameCount; ++index)
{
CComPtr<IWICBitmapFrameDecode> frame;
HR(decoder->GetFrame(index, &frame));
}
A
interface IWICBitmapFrameDecode retornado por GetFrame deriva da
interface IWICBitmapSource que representa um bitmap somente leitura.
IWICBitmapFrameDecode fornece informações associadas com o quadro, como
metadados e perfis de cor. IWICBitmapSource fornece o tamanho e a
resolução do bitmap, o formato do pixel e outras características
opcionais, como sua tabela de cores. IWICBitmapSource também fornece o
método CopyPixels que pode ser usado para realmente ler pixels do
bitmap.
Você pode obter as dimensões do quadro expressas em pixels usando o método GetSize:
UINT width = 0; UINT height = 0; HR(frame->GetSize(&width, &height));
E pode obter a resolução do quadro expressa em pontos por polegada (dpi) usando o método GetResolution:
double dpiX = 0; double dpiY = 0; HR(frame->GetResolution(&dpiX, &dpiY));
Embora
a resolução não exerça impacto nos pixels em si, ela afeta como a
imagem pode ser exibida ao usar um sistema de coordenadas lógico, como o
usado pelo WPF.
O
último atributo crítico do quadro é o formato do pixel. Ele descreve o
layout dos pixels na memória e também implica o intervalo de cores ou
espaço de cor que ele suporta. O método GetPixelFormat retorna o formato
do pixel:
GUID pixelFormat = { 0 };
HR(frame->GetPixelFormat(&pixelFormat));
Os
formatos do pixel são definidos como GUIDs cujos nomes descrevem bem
claramente o layout da memória. Por exemplo, o formato
GUID_WICPixelFormat24bppRGB indica que cada pixel usa 24 bits (3 bytes)
de armazenamento com canal de 1 byte por cor. Além disso, a ordem das
letras R (vermelho), G (verde) e B (azul) indica a ordem dos bytes do
menos significativo para o mais significativo. Como um exemplo, o
formato GUID_WICPixelFormat32bppBGRA indica que cada pixel usa 32 bits
(4 bytes) de armazenamento com canal de 1 byte por cor, bem como 1 byte
para o canal alfa. Neste caso, os canais são ordenados com o canal azul
(B) sendo menos significativo e o canal alfa (A) sendo o mais
significativo.
Os pixels reais podem ser recuperados usando o método CopyPixels.
HRESULT CopyPixels( const WICRect* rect, UINT stride, UINT bufferSize, BYTE* buffer);
O
parâmetro rect especifica um retângulo dentro do bitmap a copiar. Você
pode definir esse parâmetro como zero e ele copiará o bitmap inteiro.
Falarei do stride em breve. Os parâmetros buffer e bufferSize indicam
onde os pixels serão escritos e quanto espaço há disponível.
O
stride pode ser um dos aspectos mais confusos de bitmaps. Stride é a
contagem de bytes entre verificações de linha. De modo geral, os bits
que formam os pixels de um bitmap são incluídos em linhas. Uma única
linha deve ser longa o suficiente para armazenar uma linha dos pixels do
bitmap. O stride é o comprimento de uma linha medido em bytes,
arredondado para o DWORD mais próximo (4 bytes). Isso permite que
bitmaps com menos de 32 bits por pixel (bpp) consumam menos memória
enquanto ainda fornecem um bom desempenho. É possível usar a seguinte
função para calcular o stride para um determinado bitmap:
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;
}
Com isso concluído, você pode chamar o método CopyPixels como a seguir, supondo que o quadro representa um bitmap de 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]));
Meu
exemplo usa a classe de coleção CAtlArray do ATL para alocar o buffer,
mas você pode obviamente usar o armazenamento que desejar. Para tratar
de maiores bitmaps com mais eficiência, você pode chamar CopyPixels
várias vezes para ler diferentes partes do bitmap.
Codificando imagens
Os
codificadores são representados pela interface IWICBitmapEncoder. Como
ocorre com os decodificadores, o WIC fornece algumas maneiras de criar
codificadores, mas, no mínimo, você pode simplesmente usar um CLSID
particular do codificador para criá-lo. Esse código, por exemplo, cria
um codificador para uma imagem PNG:
CComPtr<IWICBitmapEncoder> encoder; HR(encoder.CoCreateInstance(CLSID_WICPngEncoder));
A Figura 1 lista os CLSIDs que você pode usar
para criar os diferentes codificadores incluídos com o WIC. Depois que o
codificador é criado, ele precisa ser inicializado com um fluxo que
finalmente receberá os pixels codificados e metadados opcionais:
CComPtr<IStream> stream; // Create stream object here... HR(encoder->Initialize( stream, WICBitmapEncoderNoCache));
O
segundo parâmetro para o método Initialize é menos interessante, uma
vez que WICBitmapEncoderNoCache é o único sinalizador suportado agora.
Com
o codificador inicializado, você pode agora começar a adicionar
quadros. O método CreateNewFrame cria um novo quadro que você pode
configurar e no qual é possível escrever pixels:
CComPtr<IWICBitmapFrameEncode> frame; CComPtr<IPropertyBag2> properties; HR(encoder->CreateNewFrame( &frame, &properties));
CreateNewFrame
retorna uma interface IWICBitmapFrameEncode representando o novo quadro
e uma interface IPropertyBag2. A última é opcional e pode ser usada
para determinar qualquer propriedade específica do codificador, como a
qualidade da imagem para JPEG ou o algoritmo de compactação para TIFF.
Por exemplo, veja como você pode definir a qualidade da imagem para uma
imagem 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));
O
valor para a qualidade da imagem deve estar entre 0,0 para a qualidade
mais baixa possível e 1,0 para a qualidade mais alta possível.
Com as propriedades do codificador definidas, você precisa chamar o método Initialize antes de configurar e escrever no quadro:
HR(frame->Initialize(properties));
A próxima etapa é definir as dimensões e o formato do pixel do novo quadro antes que você possa escrever pixels nele:
HR(frame->SetSize(width, height)); GUID pixelFormat = GUID_WICPixelFormat24bppBGR; HR(frame->SetPixelFormat(&pixelFormat)); ASSERT(GUID_WICPixelFormat24bppBGR == pixelFormat);
O
parâmetro SetPixelFormat é um parâmetro de entrada/saída [in, out]. Na
entrada, ele especifica o formato de pixel desejado. Na saída, ele
contém o formato de pixel suportado mais próximo. Isso normalmente não é
um problema a menos que o formato seja definido em runtime, talvez com
base no formato de pixel de outro bitmap.
Escrever pixels no quadro é realizado usando-se o método WritePixels, como exibido aqui:
HRESULT WritePixels( UINT lineCount, UINT stride, UINT bufferSize, BYTE* buffer);
O
parâmetro lineCount especifica quantas linhas de pixels devem ser
escritas. Isso implica que você pode chamar WritePixels várias vezes
para escrever o quadro completo. O parâmetro stride indica como os
pixels do buffer são incluídos em linhas. Eu descrevi como você pode
calcular o stride na seção anterior.
Após
ter chamado WritePixels uma ou mais vezes para escrever o quadro
completo, você precisa informar ao codificador que o quadro está pronto
chamando o método Commit do quadro. E depois de ter confirmado todos os
quadros que formam a imagem, você precisa dizer ao codificador que a
imagem está pronta para ser salva chamando o método Commit do
codificador.
Até
aqui, eu tratei das noções básicas da codificação e decodificação de
imagens. Antes de seguir adiante, quero concluir com um exemplo simples.
A Figura 2 mostra uma função CopyIconToTiff que
demonstra como ler os bitmaps individuais que formam um ícone e como
copiá-los para uma imagem TIFF com vários quadros.
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;
}
Neste
exemplo, eu simplifiquei as coisas ainda mais me aproveitando de uma
alternativa ao método WritePixels. Em vez de primeiro copiar os pixels
do quadro de origem e, em seguida, escrevê-los no quadro de destino,
estou usando o método WriteSource que lê os pixels diretamente a partir
de uma interface IWICBitmapSource fornecida. Como a interface
IWICBitmapFrameDecode deriva de IWICBitmapSource, isso fornece uma
solução distinta.
O Imaging Factory do WIC
O
WIC fornece um recurso chamado Imaging Factory para criar vários
objetos relacionados a ele. Ele é exposto por meio de uma interface
IWICImagingFactory e pode ser criado da seguinte maneira:
CComPtr<IWICImagingFactory> factory; HR(factory.CoCreateInstance(CLSID_WICImagingFactory));
Já
mostrei como você pode criar um decodificador para um formato de imagem
específico, dado um CLSID que identifica uma implementação. É claro
que, como se pode deduzir, seria muito mais útil se não fosse preciso
especificar uma implementação particular ou, até mesmo, codificar o
formato da imagem.
Felizmente,
o WIC fornece a solução. Antes de criar um decodificador, o WIC pode
examinar um determinado fluxo em busca de padrões que possam identificar
o formato da imagem. Quando uma melhor correspondência for encontrada,
ele irá criar o decodificador apropriado e inicializá-lo com o mesmo
fluxo. Essa funcionalidade é fornecida pelo método
CreateDecoderFromStream:
CComPtr<IWICBitmapDecoder> decoder; HR(factory->CreateDecoderFromStream( stream, 0, // vendor WICDecodeMetadataCacheOnDemand, &decoder));
O
segundo parâmetro identifica o fornecedor do decodificador. Ele é
opcional, mas pode ser útil se você preferir um codec particular do
fornecedor. Tenha em mente que isso é apenas uma dica e, se um
fornecedor particular não possuir um decodificador adequado instalado,
um decodificador ainda será escolhido independentemente do fornecedor.
IWICImagingFactory
também fornece os métodos CreateDecoderFromFilename e
CreateDecoderFromFileHandle que fornecem a mesma funcionalidade, dado um
caminho para um arquivo e um manipulador de arquivos, respectivamente.
Como alternativa, você pode criar um decodificador sem especificar um
CLSID ou um fluxo, mas em vez disso indicando o formato da imagem. O
método CreateDecoder faz exatamente isso:
CComPtr<IWICBitmapDecoder> decoder; HR(factory->CreateDecoder( GUID_ContainerFormatIco, 0, // vendor &decoder)); HR(decoder->Initialize( stream, WICDecodeMetadataCacheOnDemand));
Da
mesma forma, o método CreateEncoder permite que você crie um
codificador para um formato de imagem particular sem considerar sua
implementação, assim:
CComPtr<IWICBitmapEncoder> encoder; HR(factory->CreateEncoder( GUID_ContainerFormatBmp, 0, // vendor &encoder)); HR(encoder->Initialize( stream, WICBitmapEncoderNoCache));
A Figura 3 lista os GUIDs identificando os
formatos de imagem independentes de implementação, também conhecidos
como formatos de contêiner.
| Formato | GUID |
|---|---|
| BMP | GUID_ContainerFormatBmp |
| PNG | GUID_ContainerFormatPng |
| ICO | GUID_ContainerFormatIco |
| JPEG | GUID_ContainerFormatJpeg |
| GIF | GUID_ContainerFormatGif |
| TIFF | GUID_ContainerFormatTiff |
| HDP | GUID_ContainerFormatWmp |
Trabalhando com fluxos
Você
é livre para fornecer qualquer implementação IStream válida para usar
com o WIC. Você pode, por exemplo, usar as funções CreateStreamOnHGlobal
ou SHCreateStreamOnFile que descrevi no meu artigo sobre XmlLite ou até
mesmo escrever sua própria implementação. O WIC também fornece uma
implementação IStream flexível que pode ser útil.
Usando
o Imaging Factory que apresentei na seção anterior, você pode criar um
objeto de fluxo não-inicializado da seguinte forma:
CComPtr<IWICStream> stream; HR(factory->CreateStream(&stream));
A
interface IWICStream herda de IStream e fornece alguns métodos para
associar o fluxo com diferente armazenamento de backup. Por exemplo,
você pode usar InitializeFromFilename para criar um fluxo apoiado por um
determinado arquivo:
HR(stream->InitializeFromFilename( L"file path", GENERIC_READ));
Você
pode usar também InitializeFromIStreamRegion para criar um fluxo como
um subconjunto de outro fluxo ou usar InitializeFromMemory para criar um
fluxo sobre um bloco de memória.
WIC via WPF
Como
já mencionei, o WIC fornece a estrutura na qual a funcionalidade de
imagem do WPF é baseada. As várias classes de imagens são definidas no
namespace System.Windows.Media.Imaging. Para mostrar como é fácil usar a
partir de código gerenciado, a Figura 4 mostra a função CopyIconToTiff da Figura 2 reescrita em C# usando as classes wrapper do WPF.
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);
}
O
valor de BitmapCacheOption.OnDemand corresponde à opção do
decodificador WICDecodeMetadataCacheOnDemand usada em código nativo. E,
de forma semelhante, o valor de BitmapCacheOption.OnLoad corresponde à
opção do decodificador WICDecodeMetadataCacheOnLoad.
Já
descrevi como essas opções influenciam quando o decodificador lê as
informações da imagem na memória. Existe, no entanto, um efeito
colateral adicional do qual você deve estar ciente ao tratar dessas
opções em código gerenciado. Considere o que acontece quando você
especifica BitmapCacheOption.OnDemand. O decodificador manterá uma
referência ao fluxo subjacente e poderá ler a partir dele em algum ponto
após o objeto de decodificador do bitmap tiver sido criado. Isso supõe
que o fluxo ainda estará disponível. É preciso ter cuidado para que seu
aplicativo não feche o fluxo prematuramente. É uma questão de gerenciar a
vida útil do fluxo de forma que ele não seja fechado antes que o
decodificador tenha terminado com ele.
Isso
não afeta o código nativo porque a interface IStream é uma interface
COM padrão cuja vida útil é controlada pela contagem de referência. Seu
aplicativo pode ter lançado todas as suas referências a ele, mas o
decodificador manterá uma enquanto ela for necessária. Além disso, o
fluxo é fechado apenas após todos os ponteiros de interface terem sido
lançados.
O que vem a seguir?
O
WIC fornece uma estrutura incrivelmente eficiente e flexível na qual
baseará suas necessidades de imagens. Com um conjunto generoso de codecs
e uma API simples, você pode começar a aproveitar vários de seus
recursos rapidamente.
Na
minha próxima coluna, explorarei alguns dos recursos mais avançados
oferecidos pelo WIC. Mostrarei como você pode desenvolver seus próprios
codecs e ilustrarei o processo de registro e descoberta, incluindo os
recursos de correspondência de padrões, em detalhes. Ainda, corrigirei
uma limitação em um dos codecs internos.
Nenhum comentário:
Postar um comentário