O HotPDF, o componente PDF nativo para Delphi, decodifica imagens CCITTFaxDecode com suporte completo à extensão de modo uncompressed do T.4/T.6 e à recuperação de linhas danificadas guiada por DamagedRowsBeforeError. O modo uncompressed só é acessado pelo código de extensão terminado em 111, e linhas danificadas só são reparadas em dados Group 3 com EndOfLine definido. Todo o resto falha de forma limpa em vez de produzir uma página inclinada
Imagens codificadas em fax são a cauda longa da decodificação de imagens PDF. A maioria dos streams CCITT que você encontra saiu de um driver de scanner ou de um pipeline de captura de documentos que só emite Group 4 puro, e um decodificador que cuida do caso comum passa em todos os testes que você lançar contra ele. Aí chega um arquivo de um servidor de fax de quinze anos, uma página renderiza como ruído diagonal da linha 400 para baixo, e você descobre que as duas partes das recomendações ITU-T que ninguém implementa são exatamente as duas que esse arquivo usa. Este artigo percorre essas duas partes como o HotPDF as trata, e os lugares onde um decodificador pode errar continuando a parecer correto
Quais parâmetros do CCITTFaxDecode realmente mudam o bit stream?
Quatro entradas no dicionário DecodeParms mudam como o decodificador enquadra o bit stream: K, EndOfLine, EncodedByteAlign e DamagedRowsBeforeError. A ISO 32000-1 §7.4.6 (Tabela 11) as define, e errar qualquer uma desloca toda linha depois do primeiro descasamento. BlackIs1, Columns, Rows e EndOfBlock também importam, mas afetam polaridade, geometria e onde a decodificação para, não como um limite de linha é encontrado
Kescolhe o esquema de codificação: negativo significa Group 4 bidimensional puro (T.6), zero significa Modified Huffman unidimensional (Group 3 1D), e positivo significa Group 3 2D misto, em que um tag bit depois de cada EOL diz se a próxima linha é 1D ou 2DEndOfLinediz que toda linha Group 3 é precedida por um código EOL, onze bits zero seguidos de um bit umEncodedByteAligndiz que cada linha codificada começa em um limite de byte, então o decodificador precisa descartar os bits de preenchimento antes de lê-laDamagedRowsBeforeErroré quantas linhas corrompidas o decodificador pode tolerar antes de desistir, com padrão 0
O HotPDF lê todos esses valores do dicionário de imagem quando você chama THotPDF.ExtractLoadedImage ou THotPDF.DecodeLoadedImageStream. Um desvio deliberado vale conhecer: quando Columns está ausente, o HotPDF cai para o Width da imagem em vez do padrão 1728 da especificação, e quando Rows está ausente usa o Height. Um produtor que omite Columns em um scan A4 de 2480 pixels está errado, mas é comum o suficiente para que confiar na geometria da imagem entregue uma página utilizável onde o padrão literal entregaria lixo
Por que 000000 não pode ser tratado como o fim do bloco?
Em linhas bidimensionais, seis bits zero iniciais são ambíguos, e um decodificador que os lê como início de EOFB vai interpretar uma entrada válida de modo uncompressed como o fim da imagem. O Group 4 não tem EOLs entre linhas; os únicos códigos cheios de zeros são o EOFB, que são dois EOLs de costas, e os códigos de extensão do ITU-T T.4 e T.6. Ambos começam com 000000, e o sétimo bit é onde eles divergem
O decodificador CCITT do HotPDF resolve o prefixo bit a bit. Um sétimo bit 1 significa que os próximos três bits são um código de extensão, sendo o padrão completo 0000001xxx. Apenas xxx = 111 entra no modo uncompressed; qualquer outro valor de extensão é tratado como malformado, já que um filtro de fax PDF não tem uso para os outros. Um sétimo bit 0 só pode continuar um EOL, então o decodificador exige mais quatro bits zero e um 1 de fechamento, onze zeros e um um no total, antes de reportar EOFB. Qualquer coisa mais curta ou mais longa é um erro, não um fim de imagem complacente
A versão complacente é tentadora porque faz uma classe inteira de arquivos quebrados parecer ok: a decodificação simplesmente para cedo e você ganha uma imagem curta sem exceção. Também é exatamente o bug que trunca faxes legítimos no primeiro trecho de dados uncompressed, que em um formulário escaneado costuma ser uma assinatura densa ou um carimbo. O HotPDF escolhe a leitura estrita, então uma imagem curta significa que o encoder realmente terminou ali
Como o modo uncompressed codifica pixels?
O modo uncompressed substitui a codificação run-length por padrões literais de pixels, cada code word carregando no máximo cinco pixels, até um código de saída voltar atrás. Linhas unidimensionais entram nele com o código de 12 bits 000000001111; linhas bidimensionais usam a extensão de 10 bits 0000001111 descrita acima. Encoders trocam para ele em regiões meio-tonais, em que runs de um ou dois pixels custariam mais do que os próprios pixels
Dentro do modo, as code words são contadas pelos zeros iniciais. Zero a quatro zeros seguidos de um 1 significam aquela quantidade de pixels brancos e depois um pixel preto. Cinco zeros seguidos de um 1 significam cinco pixels brancos e nada mais. Seis a dez zeros seguidos de um 1 são códigos de saída: os zeros além de seis emitem até quatro pixels brancos a mais, e o bit único depois do código de saída, o tag bit, define a cor do próximo run quando a codificação por runs comum retoma
{ Palavras de código do modo não comprimido (ITU-T T.4 / T.6)
W = pixel branco, B = pixel preto, T = bit de etiqueta
1 B
01 W B
001 W W B
0001 W W W B
00001 W W W W B
000001 W W W W W
0000001T sair e retomar as sequências com a cor T
00000001T W, sair
000000001T W W, sair
0000000001T W W W, sair
00000000001T W W W W, sair
mais de dez zeros: malformado }
O tag bit é a parte sutil. A codificação por runs tem estado: uma linha 1D alterna runs brancos e pretos, e uma linha 2D rastreia a cor do elemento de mudança atual a0. Depois do código de saída, o decodificador precisa retomar com a cor que o tag indicar, que pode ou não casar com o último pixel literal. O decodificador do HotPDF registra uma mudança de cor na posição atual quando o tag difere da cor que ele estava rastreando, para que o próximo run comece da cor certa. Ignore o tag e o resto da linha sai com as cores invertidas, e no modo 2D toda linha seguinte herda o dano pela sua linha de referência
O que DamagedRowsBeforeError recupera, e o que não recupera?
O DamagedRowsBeforeError permite que o HotPDF substitua uma linha corrompida e continue, mas apenas quando K é 0 ou positivo e EndOfLine é true. Essas duas condições não são arbitrárias. A recuperação precisa de um jeito de achar onde a próxima linha começa depois que os bits deixam de fazer sentido, e o único marco confiável em um stream CCITT é o código EOL. Dados Group 4 não têm EOLs, e dados Group 3 sem EOLs obrigatórios não dão garantia de que um exista, então em ambos os casos uma linha danificada encerra a decodificação independente do valor do parâmetro
Quando uma linha falha, o decodificador do HotPDF varre para frente até o próximo EOL, uma sequência de pelo menos onze bits zero seguidos de um um, e substitui por uma linha de reposição. A primeira linha danificada depois de uma boa é substituída por uma cópia da linha anterior, o que em uma página de texto escaneado costuma ser próximo o suficiente para passar invisível. Uma linha danificada que vem direto depois de outra linha danificada é substituída por uma linha toda branca, porque copiar uma cópia esfumaria uma linha de texto em uma barra preta alta. A substituta então vira a linha de referência para a próxima linha 2D, que é o que mantém um stream em modo misto decodificável depois do reparo
Dois limites são fáceis de perder. O orçamento é cumulativo pela imagem inteira, não por sequência de linhas ruins, então DamagedRowsBeforeError 3 tolera três linhas danificadas no total e a quarta derruba a decodificação. E uma decodificação que falha no HotPDF é tudo ou nada: o THotPDF.ExtractLoadedImage retorna nil em vez de um bitmap com a borda de baixo rasgada. O THPDFLoadedImageInfo.Decodable reporta que o filtro é suportado, não que este stream em particular está intacto, então sempre verifique se o bitmap é nil
Por que a varredura precisa parar no início do EOL
A varredura de ressincronização deixa o leitor no primeiro bit zero do EOL, não depois dele. O decodificador tem um único loop de enquadramento que consome EOLs, verifica o EOL duplo que encerra o bloco e lê o tag bit do modo misto. Se a varredura de recuperação engolir o próprio EOL, esse loop vê uma linha sem EOL, também a trata como danificada e queima uma segunda unidade do orçamento enquanto descarta uma linha boa. Pior, um tag bit pulado significa que uma linha 2D é decodificada como 1D ou o contrário. A mesma regra vale quando um decodificador de runs 1D encontra um EOL inesperado no meio da linha: ele rebobina para onde o código de run começou, de modo que o marcador continua lá para a varredura de recuperação
Fazendo o inventário de imagens CCITT em um PDF carregado
Antes de confiar em um lote de faxes arquivados, liste quais imagens usam CCITTFaxDecode e quais falham ao decodificar. O THotPDF.GetLoadedImageInfo te dá geometria e o nome do filtro sem decodificar nada, e o THotPDF.ExtractLoadedImage diz se o stream sobreviveu. Para o quadro mais amplo de como o dispatcher de imagens encaminha cada filtro, veja o guia de extração de imagens PDF carregadas e seus decode filters
var
Pdf: THotPDF;
Info: THPDFLoadedImageInfo;
Bmp: TBitmap;
I: Integer;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('fax-archive.pdf', '') <= 0 then
Exit;
for I := 0 to Pdf.GetLoadedImageCount - 1 do
begin
if not Pdf.GetLoadedImageInfo(I, Info) then
Continue;
if Pos('CCITT', UpperCase(String(Info.Filter))) = 0 then
Continue;
Bmp := Pdf.ExtractLoadedImage(I); // nil quando o stream está danificado demais
try
if Bmp = nil then
Writeln(Format('obj %d: %dx%d CCITT failed to decode',
[Info.ObjectNumber, Info.Width, Info.Height]))
else
Bmp.SaveToFile(Format('fax_%d.bmp', [I]));
finally
Bmp.Free; // quem chama é dono do bitmap
end;
end;
finally
Pdf.Free;
end;
end;
Para as falhas, guarde a evidência. O THotPDF.ExtractLoadedImageToStream com o modo padrão lismEncoded escreve os bytes armazenados do stream intocados, que é o que você quer entregar a um segundo decodificador, embrulhar em um contêiner TIFF para inspeção ou anexar a um relatório de bug. Recodificar um fax danificado só esconde onde o dano estava
procedure SaveRawFax(Pdf: THotPDF; ImageIndex: Integer; const FileName: string);
var
Fs: TFileStream;
begin
Fs := TFileStream.Create(FileName, fmCreate);
try
if not Pdf.ExtractLoadedImageToStream(ImageIndex, Fs, lismEncoded) then
raise Exception.CreateFmt('Image %d has no stream data', [ImageIndex]);
finally
Fs.Free;
end;
end;
Se você percorre os streams de imagem por conta própria via THotPDF.CollectLoadedImageStreams, passe cada THPDFStreamObject para o THotPDF.DecodeLoadedImageStream e você recebe o mesmo caminho CCITT, os mesmos parâmetros e as mesmas regras de recuperação. Libere a lista retornada, mas não os objetos de stream dentro dela, que permanecem sob posse do documento
Vale elevar DamagedRowsBeforeError em arquivos que você não controla?
Não, não editando o PDF, e geralmente nem de longe. O parâmetro pertence ao produtor, que é a única parte que sabe se o canal de transmissão dele derruba bits. Um arquivo que declara DamagedRowsBeforeError 0 e depois contém uma linha danificada está dizendo que a cópia do arquivo está corrompida, e o resultado honesto é uma falha de decodificação que você pode registrar. Remendar linhas em silêncio em um contrato ou um prontuário médico transforma um defeito detectável em uma imagem plausível com uma linha faltando, o que é um desfecho pior para quem confiar nela depois
O padrão mais seguro para arquivos não confiáveis é manter a decodificação estrita, registrar quais números de objeto falharam e encaminhar esses para um humano ou para um decodificador separado. Se o arquivo é grande e vem de muitas fontes, rodar a decodificação de imagens fora do processo limita o que um stream hostil pode fazer com a aplicação hospedeira; o artigo sobre isolamento de codecs de imagem ao processar PDFs não confiáveis cobre esse arranjo, e o guia de compressão bilevel JBIG2 compara o outro filtro bilevel que você vai encontrar nos mesmos arquivos escaneados
FAQ
O HotPDF suporta o modo uncompressed CCITT em imagens Group 4?
Sim. O HotPDF reconhece a extensão de 10 bits 0000001111 em linhas bidimensionais e o código de entrada de 12 bits 000000001111 em linhas unidimensionais, decodifica as code words literais e honra o tag bit de saída quando a codificação por runs retoma
Por que DamagedRowsBeforeError não tem efeito no meu fax Group 4?
A recuperação precisa de marcadores EOL para ressincronizar. O HotPDF aplica DamagedRowsBeforeError apenas quando K é 0 ou positivo e EndOfLine é true; dados Group 4 (K negativo) não têm EOLs, então qualquer linha danificada encerra a decodificação
O que ExtractLoadedImage retorna para uma imagem CCITT danificada?
O THotPDF.ExtractLoadedImage retorna nil quando o dano excede o que o DamagedRowsBeforeError permite ou não pode ser recuperado de forma alguma. Ele nunca retorna um bitmap parcial, então teste para nil e use ExtractLoadedImageToStream para guardar os bytes brutos
O HotPDF trata imagens CCITT, JBIG2, JPEG, JPEG 2000 e Flate em PDFs carregados a partir de código Delphi e C++Builder puro, sem DLL externa para o caminho de fax. Detalhes e edições estão na página do componente HotPDF Delphi para PDF