Um object stream de PDF que infla sem erro, mas ainda lê como ruído, geralmente está sem uma etapa: reverter o Predictor da ISO 32000-1. Quando o dicionário /DecodeParms de um stream carrega /Predictor 2 ou superior, os bytes que o FlateDecode devolve não são os dados originais — são valores diferenciados por linha no estilo PNG ou diferenciados horizontalmente no estilo TIFF que precisam de uma segunda passada de reconstrução antes de qualquer busca de dicionário fazer sentido. O PDFiumPas, a biblioteca de componente PDF VCL nativa para Delphi e C++Builder, adicionou essa passada de reconstrução na v2.16.0, especificamente porque object streams de PDF 1.5+ estavam se expandindo em bytes diferenciados que nenhum analisador de dicionário conseguia ler
Por que o FlateDecode sozinho não é suficiente
O FlateDecode em si é apenas descompressão DEFLATE (ISO 32000-1 §7.4.4.1): ele reproduz quaisquer bytes que o codificador entregou ao compressor, nada mais. O Predictor vive uma camada acima, no dicionário /DecodeParms do stream, e descreve uma transformação que o codificador aplicou antes da compressão — a diferenciação transforma longas sequências de valores estruturados similares, como os inteiros compactados dentro de um stream de referência cruzada ou um object stream, em longas sequências de números pequenos que o DEFLATE comprime muito melhor. A ISO 32000-1 §7.4.4.3 (Tabela 8) é explícita de que desfazer essa transformação é parte de decodificar um stream filtrado, não uma passada de limpeza opcional, mas ainda assim é fácil escrever um auxiliar de FlateDecode que só chama inflate e para por ali
O sintoma é distintivo assim que você sabe o que procurar. Bytes diferenciados por Predictor não são ruído aleatório — ainda carregam a forma de um stream comprimido, de modo que um analisador ingênuo frequentemente percorre alguns tokens de aparência válida antes de encontrar uma sequência de bytes que não pode de forma alguma ser um nome, número ou delimitador de PDF, e linhas diferentes falham em deslocamentos diferentes dependendo de quanto os valores subjacentes por acaso diferiam de seus vizinhos. Essa inconsistência é o que torna o bug difícil de identificar a partir de um único arquivo com falha: dois PDFs do mesmo produtor podem diferir apenas em quais valores por acaso se repetem, de modo que um analisa quase por acidente enquanto o outro falha completamente
O que o parâmetro Predictor do PDF de fato faz?
A entrada /Predictor em /DecodeParms diz a um leitor em conformidade qual reversão executar, e a Tabela 8 da ISO 32000-1 define os valores que importam na prática: 1 significa que nenhuma predição foi aplicada, 2 seleciona o TIFF Predictor 2 (diferenciação horizontal), e qualquer valor de 10 a 15 seleciona predição estilo PNG. Mais três chaves viajam junto com ela — /Colors, /BitsPerComponent, e /Columns — e juntas descrevem a geometria de linha contra a qual a diferenciação foi calculada, mesmo quando o stream não contém nenhum dado de imagem: um object stream não é uma imagem, mas escritores de PDF reutilizam a mesma maquinaria de predictor baseada em linha para ele, porque delta-e-depois-deflate comprime inteiros e deslocamentos de objeto compactados melhor do que fazer deflate neles brutos
O TIFF Predictor 2 é o mais simples dos dois esquemas: cada componente é armazenado como a diferença do mesmo componente no pixel anterior na mesma linha, e cada linha reinicia em sua borda esquerda, em vez de carregar uma diferença vinda da linha acima. A predição PNG é mais particular, porque o filtro real pode mudar de linha para linha: cada linha começa com um único byte de tag — 0 para None, 1 para Sub, 2 para Up, 3 para Average, 4 para Paeth — e essa tag, não o valor /Predictor declarado, decide como aquela linha específica é reconstruída. Um /Predictor de 12 é realmente apenas a dica do codificador de que favoreceu o filtro Up, onde cada byte é restaurado adicionando o byte diretamente acima dele na linha anterior, mas um decodificador correto ainda precisa ler a tag em cada linha, em vez de assumir Up ao longo de tudo
Por que object streams tornam um Predictor perdido invisível?
Object streams agravam o problema, em vez de apenas repeti-lo. A ISO 32000-1 §7.5.7 permite que um escritor de PDF 1.5+ empacote múltiplos objetos indiretos em um único contêiner comprimido, um /ObjStm, e é comum que exatamente os objetos que um validador mais precisa — o catálogo, /OutputIntents, ou um stream de metadados XMP /Metadata — viajem por esse contêiner com /Predictor 12 anexado, porque esses objetos são curtos e repetitivos o bastante para se beneficiar de diferenciação por linha. Quando a etapa de predictor está faltando, expandir o object stream não levanta um erro: produz uma sequência de bytes que parece superficialmente plausível, mas não se tokeniza nos objetos esperados, de modo que o que foi empacotado dentro simplesmente não aparece. A renderização raramente percebe, porque um motor de renderização em conformidade já reconstrói dados diferenciados por predictor antes mesmo de chegar ao layout; o código que percebe é exatamente o tipo em que esse bug se escondia — um validador, assinador, ou verificador de versão que percorre os próprios bytes brutos do PDF para responder a uma pergunta estrutural, sem nenhum fallback assim que sua própria visão do object stream volta errada
O PDFiumPas caiu exatamente nessa falha antes da v2.16.0. Object streams construídos com /Predictor 12, o caso comum para escritores de PDF 1.5+, se expandiam por meio de PdfExpandObjectStreams em bytes diferenciados que o scanner estrutural não conseguia analisar, de modo que qualquer objeto de catálogo, /OutputIntents, ou /Metadata empacotado dentro ficava efetivamente invisível para varreduras de conformidade — sem exceção, sem aviso, apenas uma varredura que silenciosamente se comportava como se esses objetos estivessem ausentes. A mecânica mais profunda de como o PDFiumPas resolve um object stream contra a tabela de referência cruzada ativa, incluindo os casos híbridos e de xref stream puro, é coberta separadamente em o artigo sobre validação de streams de objeto e xref com o PDFiumPas; a etapa de predictor descrita aqui roda depois dessa resolução, sobre os bytes que cada objeto comprimido de fato contém
Revertendo linhas de Predictor PNG e TIFF em Pascal
O PDFiumPas reverte a diferenciação em uma única rotina, PdfApplyPredictor, e sua matemática de geometria vale a pena conhecer, seja você chamando-a ou reimplementando a ideia em seu próprio código Delphi. A largura de linha em bytes é ceil(Columns × Colors × BitsPerComponent ÷ 8) e a largura de byte por pixel que ambos os algoritmos usam é ceil(Colors × BitsPerComponent ÷ 8) — erre qualquer um dos arredondamentos e a reconstrução lê através de um limite de linha em vez de dentro de uma. Um /Predictor abaixo de 2 é deixado intocado, já que 1 significa que o codificador não aplicou nenhuma transformação; 2 seleciona o ramo TIFF mostrado abaixo, e qualquer coisa de 10 para cima cai para a reconstrução de filtro de linha PNG, onde o byte de tag no início de cada linha — não o valor /Predictor declarado — decide como aquela linha específica é desfeita
function PdfApplyPredictor(const Src: TBytes;
Predictor, Colors, Bpc, Columns: Integer): TBytes;
var
RowLen, Bpp, R, I: Integer;
begin
Result:= Src;
if Predictor< 2 then
Exit; // 1 = no prediction, nothing to undo
if Colors<= 0 then Colors:= 1;
if Bpc<= 0 then Bpc:= 8;
if Columns<= 0 then Columns:= 1;
if (Colors> 64)or (Bpc> 32)or (Columns> 1 shl 24) then
Exit; // reject hostile row geometries
RowLen:= (Columns* Colors* Bpc+ 7) div 8; // ceil(), per ISO 32000-1 Table 8
Bpp:= (Colors* Bpc+ 7) div 8;
if Predictor= 2 then
begin
if Bpc<> 8 then
Exit; // only the 8-bit layout is reconstructed
Result:= Copy(Src, 0, Length(Src));
R:= 0;
while R+ RowLen<= Length(Result) do
begin
for I:= R+ Bpp to R+ RowLen- 1 do
Result[I]:= Byte(Result[I]+ Result[I- Bpp]);
Inc(R, RowLen);
end;
Exit;
end;
// Predictor >= 10 falls through to PNG row-filter reconstruction below
end;
// Continues inside PdfApplyPredictor once Predictor>= 10 (PNG row filters).
// Rows:= Length(Src) div (RowLen+ 1); each row is a 1-byte filter tag
// followed by RowLen data bytes, decoded left to right.
for R:= 0 to Rows- 1 do
begin
SrcOfs:= R* (RowLen+ 1);
DstOfs:= R* RowLen;
Tag:= Src[SrcOfs];
Inc(SrcOfs);
for I:= 0 to RowLen- 1 do
begin
if I>= Bpp then A:= Result[DstOfs+ I- Bpp] else A:= 0; // byte to the left
if R> 0 then B:= Result[DstOfs+ I- RowLen] else B:= 0; // byte above
case Tag of
1: Result[DstOfs+ I]:= Byte(Src[SrcOfs+ I]+ A); // Sub
2: Result[DstOfs+ I]:= Byte(Src[SrcOfs+ I]+ B); // Up
3: Result[DstOfs+ I]:= Byte(Src[SrcOfs+ I]+ (A+ B) div 2); // Average
// Paeth (tag 4) adds whichever of A, B or the byte above-left sits
// closest to the linear predictor A+ B- C; tag 0 (None) copies the
// filtered byte through unchanged
else Result[DstOfs+ I]:= Src[SrcOfs+ I];
end;
end;
end;
O que o PDFiumPas mudou na v2.16.0
A correção lançada no PDFiumPas v2.16.0 fica dentro de PdfReadAndDecodeStream, a rotina que lê os bytes brutos de um stream e os decodifica para todo chamador que precisa inspecionar a estrutura do PDF em nível de byte, incluindo expansão de object stream; ela só tenta a reconstrução depois de confirmar que /Filter é um FlateDecode puro, nunca uma cascata, porque um filtro encadeado não pode ser corrigido por predictor com segurança nessa camada. Ler /Predictor, /Colors, /BitsPerComponent, e /Columns de volta do dicionário de stream também não precisa de um analisador de dicionário geral: PdfDictRefNum encontra cada chave por busca direta de token de nome dentro do intervalo de bytes daquele único dicionário, o que é seguro aqui precisamente porque essas quatro chaves não podem se repetir nem aninhar dentro de um único dicionário de stream. Essa mesma busca de token de nome é muito mais arriscada assim que é apontada para uma região maior ou menos limitada de um arquivo PDF, que é o assunto de o artigo complementar sobre analisar dicionários de PDF com segurança
// Inside PdfReadAndDecodeStream, right after PdfInflate() has already run:
if PdfFilterIsPureFlate(DictTxt) then
begin
Inflated:= PdfInflate(Raw);
Predictor:= PdfDictRefNum(Data, DS, DE, 'Predictor');
if Predictor>= 2 then
begin
PColors:= PdfDictRefNum(Data, DS, DE, 'Colors');
PBpc:= PdfDictRefNum(Data, DS, DE, 'BitsPerComponent');
PColumns:= PdfDictRefNum(Data, DS, DE, 'Columns');
Result:= PdfApplyPredictor(Inflated, Predictor, PColors, PBpc, PColumns);
end
else
Result:= Inflated;
end;
Antes da v2.16.0, um object stream construído com /Predictor 12 se expandia para bytes diferenciados sem nenhum erro levantado, de modo que qualquer objeto de catálogo, /OutputIntents, ou /Metadata empacotado dentro dele desaparecia das varreduras estruturais do PDFiumPas sem nenhum aviso. Depois da correção, o mesmo object stream infla e depois reconstrói corretamente, e os objetos empacotados dentro dele se tornam visíveis para essas varreduras novamente. Limites defensivos vieram junto com a correção: PdfApplyPredictor agora rejeita /Colors acima de 64, /BitsPerComponent acima de 32, e /Columns acima de 2^24 completamente, porque essas combinações descrevem geometrias de linha que nenhum produtor de PDF real precisa e existem principalmente para fazer um decodificador alocar muito mais memória do que os bytes de entrada justificam
var
Pdf: TPdf;
Report: TPdfAValidationResult;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'incoming.pdf';
Pdf.Active := True;
Report := Pdf.ValidatePdfA;
if not Report.IsCompliant then
LogNonCompliance(Report); // caller-supplied handler
finally
Pdf.Free;
end;
end;
Limites que vale a pena conhecer
A reconstrução de predictor do PDFiumPas tem duas bordas que vale a pena conhecer antes de confiar nela. A reconstrução de TIFF Predictor 2 só cobre o caso de 8 bits por componente; o PDF permite compactações mais estreitas, mas dados diferenciados por TIFF sub-byte passam sem reconstrução, em vez de serem adivinhados, de modo que um stream declarando /Predictor 2 com /BitsPerComponent 1, 2, ou 4 não vai decodificar corretamente por esse caminho hoje. A predição PNG não tem tal restrição — cada linha fornece sua própria tag de filtro, e todos os cinco tipos definidos são reconstruídos independentemente de qual seja o valor /Predictor declarado entre 10 e 15, o que corresponde a como a filtragem estilo PNG de fato funciona: o valor declarado está mais para uma dica sobre o que o codificador majoritariamente usou do que uma promessa sobre cada linha
O motor de renderização nativo do PDFium já reconstrói dados de imagem e content-stream diferenciados por predictor corretamente, que é exatamente o motivo pelo qual um arquivo pode renderizar perfeitamente em qualquer visualizador comum enquanto um validador, assinador, ou verificador de versão em nível de byte construído sobre ele lê os mesmos bytes errado. A decodificação ciente de predictor descrita aqui apoia os recursos de validação PDF/A, varredura estrutural e assinatura do PDFiumPas, o componente PDFium VCL nativo para Delphi e C++Builder