Artigo Técnico

CCITTFaxDecode em Delphi: T.4/T.6 e recuperação de linhas

O HotPDF, o componente PDF nativo para Delphi, descodifica imagens CCITTFaxDecode com suporte completo à extensão de modo uncompressed de T.4/T.6 e à recuperação de linhas danificadas conduzida pelo DamagedRowsBeforeError. O modo uncompressed só é atingido pelo código de extensão terminado em 111, e as linhas danificadas só são reparadas em dados Group 3 com EndOfLine ligado. Todo o resto falha de forma limpa em vez de produzir uma página enviesada

Imagens codificadas em fax são a cauda longa da descodificação de imagens PDF. A maioria dos streams CCITT que encontra saiu de um driver de scanner ou de um pipeline de captura de documentos que só emite Group 4 simples, e um descodificador que trata o caso comum passa em todos os testes que lhe atirar. Depois chega um arquivo de um servidor de fax com quinze anos, uma página renderiza como ruído diagonal da linha 400 para baixo, e descobre que as duas partes das recomendações ITU-T que ninguém implementa são exatamente as duas partes que aquele ficheiro usa. Este artigo percorre essas duas partes como o HotPDF as trata, e os sítios onde um descodificador pode falhar continuando a parecer correto

Que parâmetros CCITTFaxDecode alteram afinal o bit stream?

Quatro entradas no dicionário DecodeParms mudam a forma como o descodificador enquadra o bit stream: K, EndOfLine, EncodedByteAlign e DamagedRowsBeforeError. A ISO 32000-1 §7.4.6 (Tabela 11) define-as, e errar qualquer uma desalinha todas as linhas depois do primeiro desfasamento. BlackIs1, Columns, Rows e EndOfBlock também importam, mas afetam a polaridade, a geometria e onde a descodificação para, e não a forma como um limite de linha é encontrado

  • O K escolhe o esquema de codificação: negativo significa Group 4 puramente bidimensional (T.6), zero significa Modified Huffman unidimensional (Group 3 1D), e positivo significa Group 3 2D misto, em que um bit de tag depois de cada EOL diz se a próxima linha é 1D ou 2D
  • O EndOfLine diz que cada linha Group 3 é precedida por um código EOL, onze bits zero seguidos de um um
  • O EncodedByteAlign diz que cada linha codificada começa num limite de byte, por isso o descodificador tem de descartar os bits de enchimento antes de a ler
  • O DamagedRowsBeforeError é quantas linhas corrompidas o descodificador pode tolerar antes de desistir, com predefinição 0

O HotPDF lê tudo isto do dicionário da imagem quando chama o THotPDF.ExtractLoadedImage ou o THotPDF.DecodeLoadedImageStream. Um desvio deliberado vale a pena conhecer: quando Columns falta, o HotPDF recorre à Width da imagem em vez da predefinição da especificação, 1728, e quando Rows falta usa a Height. Um produtor que omita Columns numa digitalização A4 de 2480 pixéis está errado, mas é comum o suficiente para que confiar na geometria da imagem dê uma página utilizável onde a predefinição literal daria lixo

Porque é 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 descodificador que os leia como o início do EOFB vai interpretar mal uma entrada válida do modo uncompressed como o fim da imagem. O Group 4 não tem EOLs entre linhas; os únicos códigos carregados de zeros são o EOFB, que são dois EOLs seguidos, e os códigos de extensão de ITU-T T.4 e T.6. Ambos começam com 000000, e o sétimo bit é onde divergem

O descodificador CCITT do HotPDF resolve o prefixo bit a bit. Um sétimo bit 1 significa que os três bits seguintes são um código de extensão, sendo o padrão completo 0000001xxx. Só 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, por isso o descodificador exige então mais quatro bits zero e um 1 de fecho, 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 indulgente

A versão indulgente é tentadora porque faz toda uma classe de ficheiros partidos parecer boa: a descodificação simplesmente para cedo e fica com uma imagem curta sem exceção. É também exatamente o bug que trunca faxes legítimos no primeiro trecho de dados não comprimidos, que num formulário digitalizado costuma ser uma assinatura densa ou um carimbo. O HotPDF escolhe a leitura estrita, por isso uma imagem curta significa que o codificador realmente acabou ali

Como é que o modo uncompressed codifica pixéis?

O modo uncompressed substitui a codificação run-length por padrões literais de pixéis, cada code word transportando no máximo cinco pixéis, 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. Os codificadores mudam para ele em regiões de meio-tom onde comprimentos de corrida de um ou dois pixéis custariam mais do que os próprios pixéis

Dentro do modo, os code words contam-se pelos zeros iniciais. Zero a quatro zeros seguidos de um 1 significam tantos pixéis brancos e depois um pixel preto. Cinco zeros seguidos de um 1 significam cinco pixéis 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é mais quatro pixéis brancos, e o bit único a seguir ao código de saída, o tag bit, define a cor da próxima corrida quando a codificação run normal retoma

{ Code words do modo uncompressed (ITU-T T.4 / T.6)
  W = pixel branco, B = pixel preto, T = tag bit

  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     exit, then resume runs with color T
  00000001T    W, exit
  000000001T   W W, exit
  0000000001T  W W W, exit
  00000000001T W W W W, exit
  more than ten zeros: malformed }

O tag bit é a parte subtil. A codificação run tem estado: uma linha 1D alterna corridas brancas e pretas, e uma linha 2D segue a cor do elemento de mudança atual a0. Depois do código de saída, o descodificador tem de retomar com a cor que o tag indicar, que pode ou não coincidir com o último pixel literal. O descodificador do HotPDF regista uma mudança de cor na posição atual quando o tag difere da cor que estava a seguir, para a próxima corrida começar da cor certa. Ignore o tag e o resto da linha sai com as cores invertidas, e em modo 2D todas as linhas seguintes herdam o dano pela sua linha de referência

O que recupera o DamagedRowsBeforeError, e o que não recupera?

O DamagedRowsBeforeError permite ao HotPDF substituir uma linha corrompida e continuar, mas só quando K é 0 ou positivo e EndOfLine é verdadeiro. Essas duas condições não são arbitrárias. A recuperação precisa de uma forma de encontrar onde a próxima linha começa depois de os bits deixarem de fazer sentido, e o único marco fiável num 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 exista um, por isso em ambos os casos uma linha danificada termina a descodificação independentemente do valor do parâmetro

Quando uma linha falha, o descodificador do HotPDF varre para a frente até ao próximo EOL, uma corrida 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 numa página de texto digitalizado costuma ficar suficientemente próximo para ser invisível. Uma linha danificada que se segue diretamente a outra linha danificada é substituída por uma linha toda branca, porque copiar uma cópia espalharia uma linha de texto numa barra preta alta. O substituto torna-se então a linha de referência para a próxima linha 2D, que é o que mantém um stream em modo misto descodificável depois da reparação

Dois limites são fáceis de perder. O orçamento é cumulativo em toda a imagem, não por corrida de linhas más, por isso DamagedRowsBeforeError 3 tolera três linhas danificadas no total e a quarta faz a descodificação falhar. E uma descodificação falhada no HotPDF é tudo ou nada: o THotPDF.ExtractLoadedImage devolve nil em vez de um bitmap com a borda inferior irregular. O THPDFLoadedImageInfo.Decodable reporta que o filtro é suportado, não que este stream em particular está intacto, por isso verifique sempre se o bitmap é nil

Porque é que a varredura tem de 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 descodificador tem um único loop de enquadramento que consome EOLs, verifica o EOL duplo que termina 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, trata-a também como danificada e queima uma segunda unidade do orçamento enquanto deita fora uma linha boa. Pior, um tag bit saltado significa que uma linha 2D é descodificada como 1D ou o contrário. A mesma regra aplica-se quando um descodificador de corridas 1D encontra um EOL inesperado a meio da linha: recua para onde o código de corrida começou, para o marcador continuar lá para a varredura de recuperação

Fazer o inventário de imagens CCITT num PDF carregado

Antes de confiar num lote de faxes arquivados, liste que imagens usam CCITTFaxDecode e quais falham ao descodificar. O THotPDF.GetLoadedImageInfo dá-lhe a geometria e o nome do filtro sem descodificar nada, e o THotPDF.ExtractLoadedImage diz-lhe se o stream sobreviveu. Para o quadro mais largo de como o despachante de imagens encaminha cada filtro, veja o guia de extração de imagens PDF carregadas e dos seus filtros de descodificação

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á demasiado danificado
      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;                         // o bitmap pertence a quem chama
      end;
    end;
  finally
    Pdf.Free;
  end;
end;

Para as falhas, guarde a evidência. O THotPDF.ExtractLoadedImageToStream com o modo predefinido lismEncoded escreve os bytes do stream armazenado intocados, que é o que quer entregar a um segundo descodificador, embrulhar num contentor 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 percorre os streams de imagens você mesmo através do THotPDF.CollectLoadedImageStreams, passe cada THPDFStreamObject ao THotPDF.DecodeLoadedImageStream e obtém o mesmo caminho CCITT, os mesmos parâmetros e as mesmas regras de recuperação. Liberte a lista devolvida mas não os objetos de stream dentro dela, que continuam a pertencer ao documento

Vale a pena ativar o DamagedRowsBeforeError em ficheiros que não controla?

Não, não editando o PDF, e normalmente de todo. O parâmetro pertence ao produtor, que é a única parte que sabe se o seu canal de transmissão deixa cair bits. Um ficheiro que declara DamagedRowsBeforeError 0 e depois contém uma linha danificada está a dizer-lhe que a cópia de arquivo está corrompida, e o resultado honesto é uma falha de descodificação que se pode registar no log. Remendar linhas em silêncio num contrato ou num registo médico transforma um defeito detetável numa imagem com aspeto plausível com uma linha a faltar, o que é um desfecho pior para quem depois depender dela

O padrão mais seguro para arquivos não confiáveis é manter a descodificação estrita, registar que números de objeto falharam e encaminhar esses para um humano ou para um descodificador separado. Se o arquivo é grande e vem de muitas fontes, correr a descodificação de imagens fora do processo limita o que um stream hostil pode fazer à aplicação anfitriã; o artigo sobre isolar codecs de imagem ao processar PDFs não confiáveis cobre essa configuração, e o guia de compressão bilevel JBIG2 compara o outro filtro bilevel que vai encontrar nos mesmos arquivos digitalizados

FAQ

O HotPDF suporta modo CCITT uncompressed 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, descodifica os code words literais e honra o tag bit de saída quando a codificação run retoma

Porque é que o DamagedRowsBeforeError não faz efeito no meu fax Group 4?

A recuperação precisa de marcadores EOL para ressincronizar. O HotPDF aplica o DamagedRowsBeforeError só quando K é 0 ou positivo e EndOfLine é verdadeiro; dados Group 4 (K negativo) não têm EOLs, por isso qualquer linha danificada termina a descodificação

O que devolve o ExtractLoadedImage para uma imagem CCITT danificada?

O THotPDF.ExtractLoadedImage devolve nil quando o dano excede o que o DamagedRowsBeforeError permite ou não pode ser recuperado de todo. Nunca devolve um bitmap parcial, por isso teste para nil e use o 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 simples, sem DLL externa para o caminho de fax. Detalhes e edições na página do componente PDF Delphi HotPDF