A versão 3.539.22 do PDFlibPas decodifica tabelas Huffman customizadas de JBIG2 nativamente: o decoder em Pascal puro em PDFlibJBIG2.pas faz parse do segmento Tables (tipo 53), atribui códigos de prefixo canônicos na ordem das linhas da tabela, como exige a Annex B.3 da ITU-T T.88, consome referências a tabelas customizadas na ordem dos seletores para dicionários de símbolos e regiões de texto, e limita toda leitura pelo comprimento de segmento declarado em vez de pelos bytes que por acaso vierem depois
O arquivo que forçou esse trabalho não tinha nada de especial na superfície. Um contrato digitalizado, comprimido em JBIG2 com codificação Huffman de símbolos em vez da codificação aritmética muito mais comum, e com o encoder trazendo as próprias tabelas de código no lugar das tabelas padrão B.1 a B.15. Dois decoders independentes discordavam nos pixels de refinement, e o decoder do PDFlibPas da época produzia texto que parecia ter passado por um triturador: fragmentos de glyph deslocados alguns pixels, uma coluna faltando em todo caractere. Nada levantava erro. Essa é a forma de bug que sobrevive por anos, porque um decoder que rejeita um arquivo ganha um ticket de suporte, enquanto um decoder que renderiza um pouco errado ganha um cliente que assume que a digitalização estava ruim
O que um segmento Tables de JBIG2 contém de fato?
Um segmento Tables é uma descrição compacta de uma tabela Huffman: um byte de flags, dois limites de 32 bits com sinal e então uma sequência de pares (comprimento de prefixo, comprimento de faixa) que particionam o intervalo entre os limites, conforme o layout da T.88 §7.4.13 e da Annex B.2. O bit 0 do byte de flags é o HTOOB e diz se a tabela tem código out-of-band. Os bits 1 a 3 mais um dão o HTPS, o número de bits usados para escrever cada comprimento de prefixo; os bits 4 a 6 mais um dão o HTRS, a largura de cada campo de comprimento de faixa. O bit 7 é reservado, e o PDFlibPas recusa o segmento se ele estiver setado, em vez de adivinhar o que uma revisão futura quis dizer com aquilo. HTLOW e HTHIGH vêm em seguida como inteiros de 32 bits com sinal, e é aí que um decoder pode errar pela primeira vez: lê-los como unsigned faz uma tabela cujo limite inferior é negativo — perfeitamente normal para larguras de símbolo codificadas por delta — parecer começar em quatro bilhões. Todo campo passa por um helper local ReadField que confere o pedido contra a posição de bit onde os dados do segmento terminam antes de tocar no reader, porque uma tabela que lê além do próprio segmento estaria consumindo o header do próximo segmento como comprimentos de prefixo
// TCodeTableSegment.readSegment, PDFlibJBIG2.pas
EndBit := (Int64(decoder.reader.bytePointer) +
segmentHeader.getSegmentDataLength) * 8;
Flags := ReadField(8);
if (Flags and $80) <> 0 then
raise EJBIG2DecodeError.Create('reserved custom Huffman table flag');
PrefixBits := ((Flags shr 1) and 7) + 1; // HTPS
RangeBits := ((Flags shr 4) and 7) + 1; // HTRS
LowValue := Integer(ReadField(32)); // HTLOW com sinal
HighValue := Integer(ReadField(32)); // HTHIGH com sinal
if LowValue >= HighValue then
raise EJBIG2DecodeError.Create('invalid custom Huffman range bounds');
CurrentValue := LowValue;
while CurrentValue < HighValue do
begin
PrefixLength := ReadField(PrefixBits);
RangeLength := ReadField(RangeBits);
if RangeLength > 32 then
raise EJBIG2DecodeError.Create('invalid custom Huffman range length');
AddLine(CurrentValue, PrefixLength, RangeLength);
Inc(CurrentValue, Int64(1) shl RangeLength);
end;
AddLine(LowValue - 1, ReadField(PrefixBits), jbig2HuffmanLOW);
AddLine(HighValue, ReadField(PrefixBits), 32);
if (Flags and 1) <> 0 then
AddLine(0, ReadField(PrefixBits), jbig2HuffmanOOB);
As duas linhas anexadas depois do loop são as linhas de escape da Annex B.2: a linha de faixa inferior começa em HTLOW menos um e conta para baixo, a linha de faixa superior começa em HTHIGH com uma faixa de 32 bits fixos, e a linha OOB opcional não tem valor nenhum. O PDFlibPas as marca com os comprimentos de faixa sentinela jbig2HuffmanLOW ($FFFFFFFD) e jbig2HuffmanOOB ($FFFFFFFE), a mesma convenção que suas quinze tabelas padrão embutidas usam, então o loop de decodificação não se importa se a tabela veio da especificação ou do arquivo
Por que os códigos de prefixo precisam ser atribuídos na ordem das linhas da tabela?
Porque o encoder nunca escreve os códigos. Um segmento Tables de JBIG2 carrega apenas comprimentos de prefixo, e os dois lados reconstroem os padrões de bits de fato com o procedimento canônico da Annex B.3: conte quantas linhas têm cada comprimento, atribua primeiro os códigos de comprimento um, depois desloque à esquerda e continue, e dentro de um mesmo comprimento distribua os códigos na ordem em que as linhas aparecem. Qualquer desvio dessa ordem produz silenciosamente uma tabela diferente. O decoder não vai notar, porque todo padrão de bits que ele gera continua sendo um código de prefixo válido, só que não o que o encoder usou, e a saída é um bitmap de aparência plausível montado com os símbolos errados
// THuffmanDecoder.buildTable, PDFlibJBIG2.pas
FillChar(Counts, SizeOf(Counts), 0);
for I := 0 to length - 1 do
begin
if table[I].prefixLen > 32 then
raise EJBIG2DecodeError.Create(
'Huffman prefixes longer than 32 bits are not supported');
Inc(Counts[table[I].prefixLen]);
end;
Active := 0;
Code := 0;
for Bits := 1 to 32 do
begin
Starts[Bits] := Active;
Positions[Bits] := Active;
Inc(Active, Counts[Bits]);
if Code + UInt64(Counts[Bits]) > (UInt64(1) shl Bits) then
raise EJBIG2DecodeError.Create('oversubscribed Huffman prefix codes');
Code := (Code + UInt64(Counts[Bits])) shl 1;
end;
SetLength(Result, Active + 1);
for I := 0 to length - 1 do // estável: a ordem original é mantida
if table[I].prefixLen > 0 then // dentro de cada comprimento de prefixo
begin
Result[Positions[table[I].prefixLen]] := table[I];
Inc(Positions[table[I].prefixLen]);
end;
Code := 0;
for Bits := 1 to 32 do
begin
for I := Starts[Bits] to Positions[Bits] - 1 do
begin
Result[I].prefix := Cardinal(Code);
Inc(Code);
end;
Code := Code shl 1;
end;
Result[Active].rangeLen := jbig2HuffmanEOT;
O THuffmanDecoder.buildTable é um counting sort e não um sort por comparação por um motivo: uma passada de contagem sobre Counts, Starts e Positions é estável por construção, então linhas de mesmo comprimento de prefixo chegam ao resultado na ordem em que foram declaradas — exatamente a ordem pela qual a Annex B.3 atribui códigos. Linhas com comprimento de prefixo zero são descartadas antes da atribuição de códigos, porque a B.3 as define como não usadas, e não como códigos de um bit. Duas guardas ficam no mesmo loop. A checagem de oversubscription pega uma tabela cujos comprimentos reivindicam mais códigos do que um código de prefixo daquela profundidade comporta, que é a desigualdade de Kraft expressa como comparação de inteiros; sem ela, uma tabela hostil produz um código que casa com duas linhas e o decoder escolhe a que varrer primeiro. O teto de 32 bits existe porque prefix é um Cardinal e o matcher em decodeInt acumula bits num só. A T.88 permite prefixos mais longos no papel, o PDFlibPas os recusa por nome, e nenhum encoder real foi visto emitindo um. A aritmética de valor precisa do mesmo cuidado que a aritmética de código: THuffmanTable.val é um Int64, e a linha de faixa inferior é decodificada como val - readBits(32), um offset unsigned de 32 bits subtraído de HTLOW menos um. Com intermediários Integer essa subtração dá wrap, e o valor com wrap é aceito como largura de símbolo. O caminho de 64 bits calcula o valor verdadeiro, confere contra a faixa de 32 bits com sinal e levanta erro se não couber, o que transforma uma corrupção silenciosa numa recusa explícita
Por que as tabelas customizadas nunca disparavam antes da 3.539.22?
Dois defeitos escondiam um ao outro. O primeiro era um bug de setter de uma linha: TTextRegionHuffmanFlags.setFlags recebia o argumento com o mesmo nome do campo em que armazenava, então Self.flagsAsInt := flagsAsInt atribuía o campo não inicializado a ele mesmo e todo seletor voltava como zero, o que mandava as regiões de texto que pediam tabelas customizadas pelas tabelas padrão F, H e K. O segundo defeito fazia com que corrigir só o primeiro ainda produzisse símbolos corrompidos. Quando um dicionário de símbolos Huffman armazena seus símbolos como collective bitmap não comprimido, o último byte de cada linha é parcial, e o loop de cópia antigo tratava padding, que guarda o número de bits válidos, como a posição do bit válido mais baixo; uma linha de 63 pixels de largura copiava um bit do byte final em vez de sete. O loop corrigido roda for bitPointer := 7 downto ((8 - padding) and 7), e fixtures sintéticos com larguras de 7 e 9 bits fixam os dois lados da fronteira de byte. Com os seletores lendo corretamente, as tabelas são distribuídas na ordem em que a especificação as lista, que a T.88 §7.4.3.1.2 fixa para regiões de texto como FS, DS, DT, RDW, RDH, RDX, RDY e RSIZE e a §7.4.2.1.1 fixa para dicionários de símbolos como DH, DW, BMSIZE e AGGINST. Cada seletor de dois bits significa tabela padrão 0 ou 1, reservado para 2 nos campos com apenas duas tabelas padrão, e customizada para 3, e toda seleção customizada consome o próximo segmento Tables entre os segmentos referenciados, na ordem de referência. O NextCustomHuffmanTable faz exatamente essa caminhada e levanta missing custom Huffman table reference quando uma região refere menos tabelas do que seus seletores exigem. Mais uma linha pertence à mesma correção: um dicionário de símbolos Huffman cujos símbolos de entrada e novos somam um calcula um comprimento de código de símbolo zero pela fórmula de log2, enquanto a variante Huffman do formato escreve todo symbol ID com pelo menos um bit, então if sdHuffman and (symbolCodeLength = 0) then symbolCodeLength := 1 em TSymbolDictionarySegment impede que o caminho de refinement e aggregate leia zero bits por symbol ID
O que a fronteira de segmento garante?
O PDFlibPas trata o comprimento de dados do segmento em todo header como um contrato que as duas direções precisam honrar: um segmento não pode ler além do fim declarado, e não pode terminar antes e deixar o próximo header num offset imprevisível. As regras que saem desse contrato são pequenas individualmente. Um comprimento de dados com o bit 31 setado é o marcador de comprimento desconhecido da T.88 §7.2.7, e handleSegmentDataLength o mapeia para um valor negativo que readSegments recusa de imediato, em vez de varrer à frente procurando um terminador. Todo número de segmento referenciado precisa ser menor que o número do segmento atual e já existir, então uma referência adiante ou pendurada falha antes que qualquer região tente resolvê-la. END_OF_PAGE e END_OF_FILE precisam declarar zero bytes de dados. Um segmento Profiles (tipo 52) carrega uma contagem de 32 bits seguida por essa quantidade de identificadores de 32 bits e nenhum pixel, então é checado como 4 mais 4 vezes a contagem contra o comprimento declarado, é pulado, e fica na lista de segmentos só para que segmentos posteriores ainda possam se referir a ele por número. Um identificador de profile desconhecido não é uma codificação desconhecida, e tratá-lo como se fosse recusaria arquivos que decodificam perfeitamente bem
// TJBIG2StreamDecoder.readSegments, PDFlibJBIG2.pas
DataLength := segmentHeader.getSegmentDataLength;
if DataLength < 0 then
raise EJBIG2DecodeError.Create(Context +
'unknown or oversized segment length is not supported');
if DataLength > Length(reader.Data) - reader.bytePointer then
raise EJBIG2DecodeError.Create(Context + 'truncated segment data');
DataEnd := reader.bytePointer + DataLength;
for I := 0 to noOfReferredToSegments - 1 do
if (referredToSegments[I] >= segmentHeader.getSegmentNumber) or
(findSegment(referredToSegments[I]) = nil) then
raise EJBIG2DecodeError.Create(Context + 'invalid segment reference');
// ... cria o objeto de segmento para este tipo ...
reader.SegmentEnd := DataEnd;
segment.readSegment;
if reader.bytePointer > DataEnd then
raise EJBIG2DecodeError.Create(Context +
'decoded data exceeds declared segment length');
if reader.bytePointer < DataEnd then
begin
reader.bytePointer := DataEnd; // o MMR pode deixar o EOFB não lido
reader.bitPointer := 7;
end;
A cauda desse loop é onde uma versão anterior do decoder errava em regiões codificadas em MMR. Um decoder MMR sabe que terminou quando o último pixel da última linha é produzido, o que pode acontecer antes de ele ter consumido o terminador EOFB que a T.88 §6.2.5.7 coloca no fim dos dados. O código antigo supunha que o reader estava posicionado no próximo header, então os bytes do terminador sobrando eram lidos como número de segmento e o stream falhava alguns bytes depois com um erro enganoso. Agora o fim declarado é que manda: ler além dele é erro, parar antes é normal, e o reader é movido para DataEnd com o bit pointer zerado, para que o próximo header seja lido de onde o arquivo disse que estaria. A mesma disciplina aparece onde quer que o PDFlibPas faça parse de estruturas PDF não confiáveis: o comprimento declarado é a fronteira, e o decoder não sai procurando uma mais amigável
Onde o refinement Huffman lê o tamanho do bitmap?
Antes de o decoder aritmético começar, e a partir de um campo que só existe no modo Huffman. Quando uma instância de região de texto carrega refinement (RI é diferente de zero) e o SBHUFF está setado, a T.88 §6.4.11 manda o decoder ler RDW, RDH, RDX e RDY com as tabelas selecionadas, depois BMSIZE com a tabela RSIZE, depois alinhar a uma fronteira de byte, e só então rodar a decodificação genérica de refinement sobre exatamente BMSIZE bytes. Regiões de texto em modo aritmético não têm esse campo, e um decoder que compartilha um único caminho de código para os dois modos vai pulá-lo, começar o decoder aritmético dois ou mais bytes antes e refinar todo símbolo contra lixo. O caminho de dicionário de símbolos com REFAGG e uma única instância de refinement, descrito na §6.5.8.2.2, tem o mesmo campo BMSIZE com as mesmas consequências. No PDFlibPas o limite superior desse tamanho é TStreamReader.SegmentEnd, o fim do segmento atual conforme definido por readSegments, e não o fim do stream inteiro, porque um BMSIZE que só se satisfaz tomando bytes emprestados do segmento seguinte está malformado e validá-lo contra o comprimento do stream deixaria o decoder aritmético ler dentro do próximo header. O limite inferior de dois bytes reflete o par de bytes inicial que o decoder aritmético sempre consome, e depois do refinement o reader salta para RefinementEnd independentemente de quanto o decoder aritmético leu adiantado, já que a posição final dele não é a posição do próximo campo codificado em Huffman
// Decodificação de região de texto em TJBIG2Bitmap, caminho de refinement Huffman
RefinementSize := huffmanDecoder.decodeInt(huffmanRSizeTable).intResult;
huffmanDecoder.consumeRemainingBits;
if (RefinementSize < 2) or
(RefinementSize > huffmanDecoder.reader.SegmentEnd -
huffmanDecoder.reader.bytePointer) then
raise EJBIG2DecodeError.Create('invalid refinement bitmap size');
RefinementEnd := huffmanDecoder.reader.bytePointer + RefinementSize;
arithmeticDecoder.start;
// ... readGenericRefinementRegion ...
if huffmanDecoder.reader.bytePointer > RefinementEnd then
raise EJBIG2DecodeError.Create('refinement data exceeds declared size');
huffmanDecoder.reader.bytePointer := RefinementEnd;
huffmanDecoder.reader.bitPointer := 7;
O que foi verificado, e o que continua sendo recusado
A amostra que deu origem a tudo isso, uma imagem JBIG2 de 500 por 473 pixels com tabelas customizadas e refinement Huffman, agora decodifica para um bitmap com zero pixels divergentes em relação a um decoder independente, e os fixtures sintéticos de collective bitmap de 7 e 9 bits produzem as linhas esperadas nos dois. Os dois decoders independentes que discordavam na amostra original continuam discordando entre si; o PDFlibPas casa com um deles, e a afirmação honesta é que a saída nativa concorda com uma implementação independente e com a especificação conforme lida, não que todo decoder do mundo concorde. O lado malformado da suíte cobre:
- um bit de flag reservado ou um valor de seletor reservado
- uma tabela truncada no meio de uma linha
- comprimentos de prefixo oversubscribed e prefixos maiores que 32 bits
- uma região cujos seletores pedem mais tabelas customizadas do que ela refere
- confirmação de que a saída obsoleta é limpa depois de uma decodificação falha, em vez de ficar no lugar para o chamador confundir com um resultado
Três limites continuam deliberados. A organização de stream em random access, em que todos os headers de segmento precedem todos os dados de segmento, levanta JBIG2 random-access organisation is not supported assim que as flags do header do arquivo são lidas, porque não existe amostra representativa para validá-la e um caminho meio implementado é pior que uma recusa com nome. Tabelas customizadas têm teto de 65.536 linhas e prefixos de 32 bits. E a entrada pública de decodificação, TPLJBIG2Decoder.LoadFromByteArray, devolve o bitmap da primeira página na ordem do stream via getPageAsJBIG2Bitmap(0), o primeiro segmento de page information encontrado, em vez de buscar a associação de página zero; streams PDF embutidos rotineiramente numeram sua única página como 1, e pedir a página 0 por associação não encontraria nada. O texto da falha cai em TPLJBIG2Decoder.LastError, o diagnóstico interno do decoder que carrega o número do segmento, o tipo e o byte offset da falha, e não é a mesma coisa que o TPDFlib.LastErrorCode no nível da biblioteca. Nada disso toca o lado de codificação, que está coberto nas notas sobre backends de encoder JBIG2 e como eles são linkados; o caminho de leitura tem de aceitar o que quer que o encoder de outra pessoa tenha decidido emitir, e compartilha suas regras com o resto da pilha de imagem, incluindo o decoder TIFF embutido e suas recusas de BigTIFF e layout tiled: recuse com nome, nunca tome bytes emprestados através de uma fronteira declarada, e mantenha a aritmética larga o bastante para que um intermediário com wrap não passe por resposta válida. Se você está avaliando um caminho nativo de leitura JBIG2 para Delphi ou C++Builder, o decoder e o resto do tratamento de imagem estão documentados na página da PDF Library for Delphi