Artigo Técnico

Decoder PDF em Pascal puro com tabelas Huffman JBIG2

Faça um dump byte a byte de um segmento JBIG2 Tables (tipo 53) e a primeira surpresa é o que ele deixa de fora: os próprios códigos Huffman. Você recebe um byte de flags, dois limites e uma sequência de números pequenos, e esses números são comprimentos de código. Encoder e decoder reconstruem os bit patterns reais a partir dos comprimentos com o procedimento do ITU-T T.88 Annex B.3. Reconstrua numa ordem um pouco diferente e você ainda obtém um prefix code válido, todo pattern continua decodificando, e todo valor sai anexado ao pattern errado

Numa página codificada com symbol IDs Huffman e tables fornecidas pelo encoder, essa divergência parece um scan ruim: glyphs alguns pixels fora de lugar, caracteres montados dos symbols errados, ou números plausíveis até uma largura sair da faixa vários segmentos depois. A maior parte do JBIG2 em arquivos PDF é arithmetic-coded, e a maior parte do JBIG2 Huffman-coded fica nas standard tables B.1 a B.15. Arquivos que embarcam as próprias tables são raros, e costumam exercitar o parser de tables, refinement Huffman-coded e collective bitmaps não comprimidos todos de uma vez

O que um segmento Tables realmente carrega

Um segmento Tables descreve exatamente uma tabela Huffman. O T.88 §7.4.13 e o Annex B.2 o definem como um byte de flags, um HTLOW signed de 32 bits, um HTHIGH signed de 32 bits, e depois pares (prefix length, range length) que ladrilham o intervalo de HTLOW até HTHIGH. O bit 0 das flags é o HTOOB, que diz se a tabela tem um código out-of-band. Os bits 1 a 3 mais um dão o HTPS, a largura de cada campo de prefix length. Os bits 4 a 6 mais um dão o HTRS, a largura de cada campo de range length. O bit 7 é reservado

Os limites são onde um parser escrito à mão costuma tropeçar primeiro. Leia o HTLOW como unsigned e uma tabela que começa em -2, o que é comum para larguras e alturas de symbols delta-coded, de repente alega começar um pouco abaixo de 4,3 bilhões. O PDF Library for Delphi lê os dois limites como valores signed, recusa uma tabela cujo HTLOW não esteja abaixo do HTHIGH ou cujo range length exceda 32, e confere toda leitura de campo contra o bit em que os dados declarados do segmento acabam. Essa última checagem importa mais do que parece: uma tabela que lê além do segmento dela consome o header do próximo segmento como prefix lengths e parseia um nonsense completo. Bit 7 setado falha com reserved custom Huffman table flag. Preferimos essa recusa a um chute, porque uma tabela chutada produz pixels errados que ninguém consegue rastrear até a causa

Depois das linhas regulares vêm as escape lines do Annex B.2. A lower range line começa em HTLOW menos um e conta para baixo por um offset de 32 bits, a upper range line começa em HTHIGH com um range de 32 bits, e a out-of-band line existe só quando o HTOOB está setado. Essas linhas carregam os mesmos marcadores que as standard tables embutidas usam, então uma custom table e a Table B.6 passam por um único loop de decodificação. Uma custom table pode ter até 65.536 linhas

Layout do segmento Tables por trás da decodificação Huffman customizada do JBIG2 no PDFlibPas: um byte de flags carregando HTOOB, HTPS e HTRS mais um bit reservado que é recusado, limites signed HTLOW e HTHIGH, uma sequência de pares prefix e range length, e as escape lines jbig2HuffmanLOW, uma linha alta fixa de 32 bits e jbig2HuffmanOOB opcional
Todo campo do segmento é lido através de um helper com bounds check, porque uma tabela lendo além do fim declarado dela consumiria o header do próximo segmento como prefix lengths, e as sentinel escape lines casam com as standard tables embutidas

Por que os prefix codes têm de ser atribuídos na ordem das linhas da tabela?

Porque a ordem de declaração é a única coisa que liga um bit pattern a uma faixa de valores. O Annex B.3 conta quantas linhas usam cada prefix length, distribui códigos de comprimento 1 primeiro, desloca à esquerda para mudar para o comprimento 2, e assim por diante; dentro de um comprimento, os códigos vão para as linhas na ordem em que aparecem no segmento. Uma tabela pequena torna isso concreto. Seis linhas, rotuladas A a F em ordem de arquivo, com estes prefix lengths:

LinhaPrefix lengthCódigo (ordem de arquivo mantida)Código se B e D trocam
A20000
B3100101
C20101
D3101100
E3110110
F0não usadonão usado

As duas colunas são prefix codes válidos. Só a da esquerda é o que o encoder queria dizer: com B e D trocadas, o pattern 100 rende a faixa de valores de D e o decoder continua com um número do intervalo errado. A linha F tem comprimento 0, que o B.3 define como não usada; dê a ela um código de um bit e toda atribuição posterior se desloca

O caminho fácil para obter a coluna da direita é um sort genérico por prefix length. O TArray.Sort e o TList<T>.Sort do Delphi não são estáveis, então comprimentos iguais podem voltar em qualquer ordem. Para canonical codes nas suas próprias ferramentas, caminhe pelas linhas em ordem de arquivo, uma vez por comprimento:

uses
  System.SysUtils;

type
  TCodeLine = record
    PrefixLen: Integer;   // como lido do segmento Tables
    Code: Cardinal;       // atribuído abaixo
  end;

// Atribuição canônica do Annex B.3, escrita para as suas próprias ferramentas.
// Comprimentos iguais mantêm a ordem de arquivo; comprimento 0 não recebe código.
procedure AssignCanonicalCodes(var Lines: array of TCodeLine);
var
  LenCount: array[0..32] of UInt64;
  FirstCode: array[0..32] of UInt64;
  NextCode: UInt64;
  CurLen, I: Integer;
begin
  FillChar(LenCount, SizeOf(LenCount), 0);
  for I := 0 to High(Lines) do
  begin
    if (Lines[I].PrefixLen < 0) or (Lines[I].PrefixLen > 32) then
      raise Exception.Create('prefix length out of range');
    Inc(LenCount[Lines[I].PrefixLen]);
  end;
  LenCount[0] := 0;
  FirstCode[0] := 0;
  for CurLen := 1 to 32 do
  begin
    FirstCode[CurLen] := (FirstCode[CurLen - 1] + LenCount[CurLen - 1]) shl 1;
    // Kraft check: mais códigos do que este comprimento comporta
    if FirstCode[CurLen] + LenCount[CurLen] > (UInt64(1) shl CurLen) then
      raise Exception.Create('oversubscribed prefix lengths');
    NextCode := FirstCode[CurLen];
    for I := 0 to High(Lines) do       // a ordem de arquivo é o desempate
      if Lines[I].PrefixLen = CurLen then
      begin
        Lines[I].Code := Cardinal(NextCode);
        Inc(NextCode);
      end;
  end;
end;

Alimente com as seis linhas acima e A recebe 00, C recebe 01, B recebe 100, D recebe 101 e E recebe 110; F fica sem código. O PDF Library for Delphi faz o mesmo trabalho com uma passada de contagem, que é estável por construção e evita reescanear a tabela 32 vezes, e aplica os mesmos dois guards. Comprimentos sobrecarregados falham com oversubscribed Huffman prefix codes; deixe passar e um pattern pode casar com duas linhas. Prefixos maiores que 32 bits também falham pelo nome, embora o T.88 os permita no papel. Achamos que esse teto é o trade certo: cada código cabe numa word de máquina, e um arquivo que precise de mais tem muito mais chance de estar danificado do que de ser esperto

Os valores pedem o mesmo cuidado. A lower range line decodifica como HTLOW menos um menos um offset unsigned de 32 bits, e com um intermediário Integer de 32 bits essa subtração pode dar wrap numa largura positiva de aparência comum. O decoder faz a conta em 64 bits e levanta Huffman value exceeds signed integer range quando o resultado verdadeiro não cabe

Atribuição de prefix code canônico no decoder JBIG2 do PDFlibPas: só os prefix lengths chegam no segmento, um counting sort estável sobre Counts, Starts e Positions mantém a ordem de declaração dentro de cada comprimento, códigos de comprimento um são distribuídos primeiro e o código desloca à esquerda por comprimento, com oversubscription recusada pelo Kraft check
O encoder nunca escreve os bit patterns, então qualquer desvio da ordem das linhas da tabela constrói silenciosamente um prefix code diferente mas válido e o output parece plausível; linhas de comprimento zero saem como não usadas e prefixos maiores que 32 bits são recusados

Ordem dos seletores, e duas armadilhas que se cobrem mutuamente

Uma text region ou um symbol dictionary não nomeia as tables dele. Ele carrega seletores de dois bits, e cada seletor setado para custom toma o próximo segmento Tables da lista referred-to, na ordem de referral. O T.88 §7.4.3.1.2 ordena os seletores de text region como FS, DS, DT, RDW, RDH, RDX, RDY e RSIZE; o §7.4.2.1.1 ordena os seletores de symbol dictionary como DH, DW, BMSIZE e AGGINST. Valores 0 e 1 escolhem standard tables, 3 escolhe uma custom, e 2 é reservado em campos com só duas standard tables. O PDF Library for Delphi rejeita valores de seletor reservados, e falha com missing custom Huffman table reference quando os seletores querem mais custom tables do que a região refere

Se você escrever o seu próprio decoder, dois erros são fáceis de cometer aqui, e cada um esconde o outro. O primeiro morde no Delphi, ou no FPC em modo Delphi (o modo objfpc rejeita o código como identificador duplicado): um setter de flags cujo parâmetro tem o mesmo nome do campo. O parâmetro sombreia o campo, Flags := Flags atribui a ele mesmo, e o campo fica zero. Todo seletor então lê zero, uma text region usa as standard tables B.6, B.8 e B.11 para FS, DS e DT não importando o que o arquivo pediu, e o caminho custom nunca roda

O segundo fica no symbol dictionary. Um Huffman dictionary pode armazenar os symbols dele como um collective bitmap não comprimido (BMSIZE de zero), em que cada linha termina num byte parcial. O valor de padding é o número de bits válidos naquele byte, e é tentador lê-lo como a posição do bit válido mais baixo:

// Último byte de uma linha de collective-bitmap, bits lidos MSB primeiro.
// Padding := TotalWidth mod 8  -> número de bits VÁLIDOS neste byte

// Errado: trata Padding como o índice do bit mais baixo a copiar
for BitPos := 7 downto Padding do
  CopyPixel((LastByte shr BitPos) and 1);

// Certo: o índice do bit válido mais baixo é 8 - Padding (0 quando Padding = 0)
for BitPos := 7 downto ((8 - Padding) and 7) do
  CopyPixel((LastByte shr BitPos) and 1);

// CopyPixel representa o que quer que escreva o pixel no seu próprio decoder

Confira as larguras de fronteira. Uma linha de 63 pixels tem padding 7: o loop errado copia um bit do último byte, o certo copia sete. Uma linha de 9 pixels tem padding 1: o loop errado copia sete, o certo copia um. A 64 pixels ambos copiam os oito, então múltiplos de 8 nunca o expõem. Collective bitmaps sintéticos de 7 e 9 pixels de largura cravam os dois lados da fronteira de byte. E o dano viaja, porque o refinement usa esses symbols como referências

Se o seu próprio decoder tiver as duas armadilhas, debugar fica miserável. Corrija só o setter e o caminho custom finalmente roda, direto para collective bitmaps quebrados, então a página continua parecendo errada. Corrija só o padding e nada visível muda, porque o caminho custom continua inalcançável. Teste os dois com inputs sintéticos antes de confiar em qualquer uma das mudanças. Um detalhe relacionado: um Huffman symbol dictionary cujo input e new symbols somam um recebe um symbol code length de 0 pela fórmula log2, mas a variante Huffman escreve todo symbol ID com pelo menos um bit. O PDF Library for Delphi sobe esse comprimento para 1 em modo Huffman

Onde o refinement Huffman lê o tamanho do bitmap dele?

De um campo BMSIZE que só existe em modo Huffman, decodificado à frente dos dados de refinement aritmético. Quando uma instância de text region tem refinement (RI não-zero) e o SBHUFF está setado, o T.88 §6.4.11 manda o decoder ler RDW, RDH, RDX e RDY com as tables selecionadas, depois BMSIZE com a table RSIZE, depois pular para uma fronteira de byte, e só então rodar decodificação de refinement genérico sobre exatamente BMSIZE bytes. Text regions em modo aritmético não têm esse campo, então um decoder que compartilha um caminho de código entre os modos o pula, inicia o arithmetic decoder cedo, e refina todo symbol contra ruído. O caminho de symbol dictionary com REFAGG e uma única instância de refinement (§6.5.8.2.2) tem o mesmo campo

O PDF Library for Delphi limita o BMSIZE dos dois lados. O mínimo é 2, o par inicial de bytes que o arithmetic decoder sempre consome. O máximo é o que resta do segmento corrente, deliberadamente não do stream inteiro: um BMSIZE que só cabe tomando bytes emprestados do próximo segmento está malformado, e uma checagem de comprimento de stream deixaria o arithmetic decoder ler para dentro do header seguinte. Tamanhos fora da faixa falham com invalid refinement bitmap size. Depois o leitor Huffman retoma exatamente BMSIZE bytes além do campo, não importando o que o arithmetic decoder leu à frente

Limites de refinement em modo Huffman no decoder JBIG2 do PDFlibPas: RDW, RDH, RDX e RDY decodificam das tables deles, BMSIZE decodifica da table RSIZE e é alinhado a byte, então o arithmetic decoder refina exatamente BMSIZE bytes mantidos entre RefinementEnd e SegmentEnd, recusando tamanhos abaixo de dois ou além da fronteira do segmento
O limite inferior de dois bytes reflete o par inicial que o arithmetic decoder sempre consome, o limite superior é o segmento corrente em vez do stream inteiro, e após o refinement o leitor pula para RefinementEnd independentemente do read-ahead

O comprimento declarado do segmento é a fronteira

Todo header de segmento declara quantos bytes de dados vêm depois, e o decoder segura o segmento nesse número nas duas direções: ler além do fim é erro, terminar cedo é normal, e o próximo header é sempre lido de onde o arquivo disse que ele começa. Regras menores se seguem. O unknown data length do T.88 §7.2.7 é recusado de cara em vez de escanear por um terminador. Segmentos referred-to precisam carregar números menores que o corrente e já existir. END_OF_PAGE e END_OF_FILE precisam declarar zero bytes de dados. Um segmento Profiles (tipo 52) precisa ter exatamente 4 mais 4 vezes a contagem de identificadores dele; ele é então pulado mas mantido, para que segmentos posteriores ainda possam referenciá-lo, e um identificador de profile desconhecido não para o decode

O caso clássico de terminar cedo é MMR. Um decoder MMR está pronto quando produz o último pixel da última linha, o que pode acontecer antes do terminador EOFB que o T.88 §6.2.5.7 põe no fim dos dados. Um leitor que assume que agora está no próximo header parseia os bytes EOFB restantes como um número de segmento e falha alguns bytes depois, longe da causa real. Pular para o fim declarado evita a questão. O mesmo hábito percorre como o PDF Library for Delphi parseia estruturas PDF não confiáveis em geral

Decodificando um image stream JBIG2 do Delphi

O decoder vive na unit PDFlibJBIG2 como TPLJBIG2Decoder. O LoadFromByteArray recebe os dados JBIG2 embutidos de uma imagem /JBIG2Decode e, opcionalmente, os dados do stream /JBIG2Globals dela, que é onde custom tables e symbol dictionaries compartilhados costumam viver. Retorna False em qualquer recusa e deixa o motivo no LastError; output de uma chamada anterior é limpo no início de cada chamada, então um decode falhado nunca deixa pixels velhos para o caller confundir com um resultado

uses
  System.SysUtils, PDFlibJBIG2;

// ImageBytes: os dados do image stream com quaisquer filtros à frente de
//   JBIG2Decode já removidos
// GlobalBytes: os dados do stream /JBIG2Globals, ou um array vazio
function DecodeAndCountBlack(const ImageBytes, GlobalBytes: TJBIG2ByteArray;
  out BlackPixels: Int64): Boolean;
var
  Decoder: TPLJBIG2Decoder;
  Line: TJBIG2ByteArray;
  Row, Col: Integer;
begin
  BlackPixels := 0;
  Decoder := TPLJBIG2Decoder.Create;
  try
    Result := Decoder.LoadFromByteArray(ImageBytes, GlobalBytes);
    if not Result then
    begin
      // Form: 'JBIG2 segment <n> (type <t>) at byte <offset>: <reason>'
      Writeln('JBIG2 refused: ', Decoder.LastError);
      Exit;
    end;
    for Row := 0 to Decoder.Height - 1 do
    begin
      Decoder.GetScanline(Row, Line);   // um byte por pixel, 0 ou 1
      for Col := 0 to Decoder.Width - 1 do
        Inc(BlackPixels, Line[Col]);    // 1 = preto no JBIG2
    end;
  finally
    Decoder.Free;
  end;
end;

A contagem de pixels pretos é uma checagem sua, um fingerprint barato para comparar contra um segundo renderer. Falhas de segmento chegam prefixadas com número de segmento, tipo e offset de byte, então um (type 53) na frente de oversubscribed Huffman prefix codes aponta direto para um segmento Tables. Mantenha os dois canais de erro separados: o TPLJBIG2Decoder.LastError é o texto de diagnóstico deste decoder, enquanto o TPDFlib.LastErrorCode é o código numérico da API de nível de library. A seleção de página segue o stream: o decoder retorna o bitmap do primeiro segmento page-information que encontra e não procura associação de página 0, porque JBIG2 embutido em PDF carrega uma página e encoders rotineiramente a numeram 1

Comparar renderers nesses arquivos pede alguma humildade, porque decoders JBIG2 independentes nem sempre concordam em arquivos de custom tables com refinement Huffman. Dois decoders em desacordo não decide nada. Os campos do formato decidem: quais tables os seletores escolheram, o que o BMSIZE declarou, onde cada segmento termina

A codificação é coberta no artigo sobre backends de encoder JBIG2 e como são linkados. O caminho de leitura precisa aceitar o que o encoder de outra pessoa emitiu, e compartilha as regras dele com o decoder TIFF embutido e as checagens de BigTIFF e layout tiled dele: recuse pelo nome, nunca tome bytes emprestados através de uma fronteira declarada, mantenha a aritmética intermediária larga. Se uma pasta de PDFs escaneados continua tropeçando outros toolkits, verifique se as imagens são JBIG2 Huffman-coded com custom tables, então teste-as contra o caminho de leitura nativo Delphi e C++Builder descrito na página do produto PDF Library for Delphi