Você tem um PDF no disco, um cliente o digitalizou a partir de uma pilha de notas fiscais, e sua tarefa é extrair as imagens das páginas como bitmaps para uma etapa de OCR. Você carrega o arquivo, encontra os XObjects de imagem e então descobre a parte que ninguém avisa: os bytes desses streams não são pixels. Eles podem ser um codestream JPEG, ou um blob JPEG 2000 comprimido por wavelets, ou uma execução fax Group 4, ou um raster indexado atrás de uma paleta por trás de um filtro Flate. O objeto de imagem conhece a largura e a altura, mas as amostras reais ficam seladas dentro do filtro escolhido pelo produtor. Conseguir um TBitmap utilizável significa desfazer esse filtro, e o PDF oferece cerca de oito maneiras diferentes de selar esses bytes
Essa é a lacuna que ExtractLoadedImage preenche no HotPDF, o componente PDF nativo VCL para Delphi e C++Builder. Ele enumera os XObjects de imagem em um documento carregado, informa o que cada um é e decodifica os que conseguir de volta para um bitmap de 24 bits. O interessante não é a superfície da API, que são três métodos. O interessante é por que um caminho separado de decodificação precisa existir e o que ele consegue, ou não, transformar de volta em pixels
Por que uma imagem carregada ainda precisa ser decodificada
O carregador do HotPDF foi construído em torno de fidelidade de passagem direta. Quando você chama LoadFromFile, os streams de imagem são mantidos exatamente como aparecem no arquivo de origem: o filtro original, os bytes comprimidos originais, o dicionário original. Isso é deliberado. O objetivo de carregar um documento normalmente é copiar páginas, mesclar arquivos, carimbá-los, alterar permissões e gravá-los de volta, e para tudo isso o caminho mais barato e mais seguro é deixar cada stream de imagem intocado. Decodificar todas as imagens para um raster na carga gastaria memória e CPU em um trabalho que a maioria dos chamadores nunca precisa, e recodificar na gravação degradaria imagens que deveriam ter sido copiadas literalmente
A consequência é que o grafo de objetos carregado não carrega pixels. Um XObject de imagem cujo /Filter é /DCTDecode contém bytes JPEG; o HotPDF nunca executou um decodificador JPEG sobre ele, porque nada no caminho de copiar e regravar precisava disso. Então, quando você realmente quer pixels, a API de extração precisa fazer a decodificação por conta própria, do zero, para o filtro que aquela imagem específica estiver usando. Esse é o mesmo motivo pelo qual os codecs do lado de codificação são independentes do carregador: o artigo sobre adicionar imagens JPEG 2000 a PDFs em Delphi descreve como o motor JPX entra no lado de criação, e esse motor simplesmente não estava ligado ao caminho de leitura até a API de extração precisar dele
A API enxuta de três métodos
A superfície é pequena. GetLoadedImageCount retorna quantos XObjects de imagem o documento carregado contém. GetLoadedImageInfo preenche um registro descritivo para um deles pelo índice. ExtractLoadedImage retorna o bitmap decodificado, ou nil quando não consegue decodificar aquela imagem. A enumeração é baseada em índice e é estável para um carregamento dado: internamente ela percorre a tabela de objetos indiretos e coleta todo stream cujo /Subtype resolve para /Image, então o índice passado a GetLoadedImageInfo é o mesmo índice passado a ExtractLoadedImage
var
Pdf: THotPDF;
Info: THPDFLoadedImageInfo;
Bmp: TBitmap;
I, Count: Integer;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('scanned-invoices.pdf', '') <= 0 then
Exit;
Count := Pdf.GetLoadedImageCount;
for I := 0 to Count - 1 do
begin
if not Pdf.GetLoadedImageInfo(I, Info) then
Continue;
if not Info.Decodable then
Continue; // filter or colour space not supported
Bmp := Pdf.ExtractLoadedImage(I);
if Bmp <> nil then
try
Bmp.SaveToFile(Format('img_%d.bmp', [I]));
finally
Bmp.Free; // caller owns the bitmap
end;
end;
finally
Pdf.Free;
end;
end;
Dois detalhes contratuais importam aqui. Primeiro, o TBitmap retornado é seu para liberar; o documento não o armazena nem o possui. Segundo, verifique Decodable antes de chamar e verifique o resultado contra nil depois. O método não lança exceção para um filtro sem suporte, ele retorna nil, e um nil silencioso em um loop em lote é exatamente o tipo de coisa que engole uma página inteira de um trabalho com mil páginas sem ninguém perceber
Lendo o descritor antes de decodificar
THPDFLoadedImageInfo informa o que uma imagem é sem obrigar você a fazer a decodificação completa. Os campos vêm direto do dicionário da imagem: Width e Height em amostras, BitsPerComponent, ColorComponents e ColorSpace descrevendo a interpretação depois da decodificação (1 para cinza, 3 para RGB, 4 para CMYK), Filter como a compressão nomeada, IsImageMask para máscaras de estêncil, ObjectNumber para o objeto indireto subjacente e Decodable
Essa última flag é a honesta. Decodable só é True quando a build em execução realmente consegue transformar essa combinação específica de filtro e espaço de cor em um bitmap. Ela codifica a matriz real de suporte, não uma vontade: uma imagem cujo Filter a build atual não entende retorna Decodable = False, e você pode ramificar em cima disso para registrar, pular ou cair de volta para extrair o stream bruto por conta própria. Trate isso como pré-condição, não como sugestão
// Triage every image before committing to a decode.
var
Pdf: THotPDF;
Info: THPDFLoadedImageInfo;
I: Integer;
begin
// ... Pdf loaded ...
for I := 0 to Pdf.GetLoadedImageCount - 1 do
begin
if not Pdf.GetLoadedImageInfo(I, Info) then
Continue;
if Info.Decodable then
// ExtractLoadedImage(I) will return a TBitmap
else
// unsupported filter/colour space: log the object and skip
Writeln(Format('Image %d obj %d: %dx%d %s/%s not decodable',
[I, Info.ObjectNumber, Info.Width, Info.Height,
String(Info.Filter), String(Info.ColorSpace)]));
end;
end;
Um detalhe de implementação pega quem monta registros descritivos na mão. THPDFLoadedImageInfo contém dois campos AnsiString, Filter e ColorSpace. Esses são tipos gerenciados com contagem de referências, então o reflexo de zerar um registro com FillChar(Info, SizeOf(Info), 0) está errado aqui: isso sobrescreve a referência da string sem decrementá-la, o que vaza memória ou corrompe. O HotPDF inicializa o registro campo por campo exatamente por esse motivo, e, se você copiar esse padrão no seu próprio código, faça o mesmo
Um despachante para oito caminhos de filtro
O motivo de esse recurso ter sido entregue em uma série de versões, e não em uma só, é que PDF não tem um formato único de imagem. Ele tem filtros, e a §8.9.5 da ISO 32000-1 permite que um XObject de imagem nomeie qualquer um deles em /Filter, enquanto a interpretação das amostras é governada separadamente por /ColorSpace, /BitsPerComponent e um array opcional /Decode. ExtractLoadedImage lê o nome do filtro e encaminha para um decodificador dedicado em cada caso. O conjunto suportado, construído ao longo das versões v2.229 a v2.231, agora cobre oito caminhos distintos
- Raw rasters (FlateDecode, LZWDecode, or no filter) in 8-bit DeviceRGB or DeviceGray. The bytes inflate to a packed raster, and the only transform is a channel swap, covered below
- DCTDecode (JPEG). The codestream is handed to the VCL's
TJPEGImage, which resolves geometry and colour, and the result is assigned into a 24-bit bitmap - JPXDecode (JPEG 2000). Decoded through the OpenJPEG backend, the same engine described in the JPEG 2000 article, with high-bit-depth components resampled down to 8 bits
- Indexed colour. The palette is read from the
[/Indexed base hival lookup]array and each sample is expanded through the lookup table to true colour - DeviceCMYK. Four-channel samples are converted to RGB with the standard ink-on-white formula
- Sub-8-bit DeviceGray and Indexed at 1, 2, or 4 bits per component, unpacked sample by sample and scaled to the 0–255 range
- CCITTFaxDecode, the Group 3 and Group 4 fax filters, decoded by a dedicated T.4/T.6 backend
- JBIG2Decode, the high-ratio bilevel filter, decoded through the registered JBIG2 backend that the native JBIG2 compression article covers from the encode side
Em todos os casos o resultado vai para o mesmo lugar: um bitmap BGR de 24 bits, porque é isso que um TBitmap VCL armazena nativamente e o que todo consumidor downstream espera
As transformações que alteram pixels silenciosamente
Desses caminhos, dois envolvem transformações que são fáceis de errar sutilmente e que valem ser entendidas mesmo que você nunca toque no decodificador. A primeira é a troca da ordem das cores. Um raster PDF DeviceRGB armazena amostras na ordem red-green-blue, com a linha superior primeiro. Um scanline VCL de 24 bits armazena blue-green-red. Então decodificar uma imagem RGB simples não é um memcpy; o primeiro e o terceiro byte de cada pixel são trocados ao entrar no scanline. Se você inverter isso, o vermelho e o azul trocam de lugar, o que parece certo em uma imagem de teste em tons de cinza e fica desastroso em uma imagem colorida. A ordem das linhas, por sua vez, passa direto: os rasters top-down do PDF se alinham com ScanLine[0] do VCL como a linha visual superior, então nenhuma inversão vertical é necessária
A segunda é CMYK. Imagens PDF DeviceCMYK carregam quatro tintas, e a conversão para RGB é um cálculo por canal, não uma tabela: cada canal de saída é (255 - ink) * (255 - K) / 255. Isso é uma aproximação de dispositivo, não uma conversão com gerenciamento de cor via perfil ICC, então o resultado é bom o suficiente para exibição e rerasterização, mas não é o caminho certo se você precisa de cor fiel para impressão. Se o seu fluxo exigir fidelidade, trate o bitmap extraído como uma prévia e mantenha o stream CMYK original para o pipeline com gerenciamento de cor
O caminho Indexed esconde sua própria armadilha de parse. A paleta em um espaço de cor /Indexed pode ser armazenada como uma string literal ou como uma string hexadecimal, e o HotPDF guarda o valor de uma string hex como o texto hexadecimal, não como os bytes decodificados. Então, quando a paleta é uma string hex, a tabela de lookup precisa primeiro passar por uma decodificação de hex para bytes; uma string literal já é byte bruto. Se você errar esse ramo, uma imagem indexada de quatro cores sai como lixo, porque cada entrada de paleta está sendo lida na fronteira de byte errada
Cadeias de filtros: o último filtro é o próprio formato da imagem
Um único nome /Filter é o caso mais simples. O PDF também permite uma cadeia de filtros, em que o stream foi passado por vários em sequência, listados na ordem em um array /Filter como [/ASCII85Decode /FlateDecode] ou [/ASCIIHexDecode /DCTDecode] (ISO 32000-1 §7.4). A semântica é precisa: os filtros se aplicam da esquerda para a direita na codificação, então, na decodificação, você desfaz da direita para a esquerda, e o último filtro do array é o que realmente define o formato da imagem. Os filtros anteriores são apenas codificações de transporte enroladas em volta dele
O extrator lida com isso em camadas. Antes de qualquer decodificador de imagem entrar em ação, todos os filtros da cadeia, exceto o último, são aplicados para produzir a entrada que o filtro final espera, e só então ocorre o despacho para esse último filtro. Então [/ASCII85Decode /DCTDecode] primeiro desfaz o ASCII85 do stream e depois encaminha o resultado para o caminho JPEG; [/FlateDecode] enrolado em um raster bruto descomprime e então executa o caminho raster. É isso que permite que os oito decodificadores continuem simples. Nenhum deles precisa saber sobre wrappers de transporte ASCII85 ou hex, porque, quando o decodificador vê os bytes, os wrappers já desapareceram. Isso também significa que uma cadeia cujo filtro final não é suportado falha de forma limpa no passo de despacho, e não no meio do processo
Onde a extração termina, e o que fazer depois
Seja honesto consigo mesmo sobre os limites. Uma imagem cujo filtro final está fora do conjunto suportado retorna nil, e o mesmo acontece com uma imagem cujo espaço de cor a build não consegue interpretar. Máscaras suaves e alpha não são reconstruídos no bitmap; você recebe a imagem base, não um resultado composto. Profundidades de bits acima de 8 vindas de JPEG 2000 são reamostradas para baixo, o que é propositalmente com perda e a escolha errada se você estiver rearquivando em vez de apenas exibindo. E uma image mask, um estêncil de um bit sem cor própria, é descrita pelo descritor mas é algo diferente de uma imagem pictórica; decodificá-la esperando uma foto vai trazer surpresa
Quando extrair não basta, o stream bruto continua ali no grafo de objetos carregado, filtro e tudo, e você pode puxá-lo byte por byte e entregá-lo a um codec especializado seu. Esse é o fallback que o design de passagem direta preserva de propósito: os bytes originais nunca são descartados, então o pior caso é você mesmo decodificá-los em vez de a informação desaparecer. Para a maioria dos trabalhos reais, porém, os oito filtros suportados cobrem o que scanners, suítes de escritório e mecanismos de relatório realmente geram, e um loop sobre GetLoadedImageCount com uma guarda Decodable converte um PDF carregado de volta para uma pasta de bitmaps em poucas linhas
A API de extração de imagens carregadas, junto com o conjunto completo de filtros de decodificação descritos aqui, acompanha o HotPDF Component para Delphi e C++Builder