Artigo Técnico

Porque É Que Algumas Streams de Objetos PDF Descodificam Para Dados Corrompidos em Delphi

Uma stream de objetos PDF que se descomprime sem erro mas que ainda assim se lê como ruído está normalmente a faltar um passo: inverter o Predictor da ISO 32000-1. Quando o dicionário /DecodeParms de uma stream transporta um /Predictor 2 ou superior, os bytes que o FlateDecode devolve não são os dados originais — são valores diferenciados por linha ao estilo PNG ou diferenciados horizontalmente ao estilo TIFF, que precisam de uma segunda passagem de reconstrução antes de qualquer pesquisa em dicionário fazer sentido. O PDFiumPas, a biblioteca de componente PDF VCL nativa para Delphi e C++Builder, acrescentou essa passagem de reconstrução na v2.16.0, especificamente porque as streams de objetos do PDF 1.5+ se expandiam para bytes diferenciados que nenhum interpretador de dicionário conseguia ler

Porque é que o FlateDecode sozinho não chega

O próprio FlateDecode é apenas descompressão DEFLATE (ISO 32000-1 §7.4.4.1): reproduz os bytes que o codificador entregou ao compressor, nada mais. O Predictor vive uma camada acima, no dicionário /DecodeParms da stream, e descreve uma transformação que o codificador aplicou antes da compressão — a diferenciação transforma longas sequências de valores estruturados semelhantes, como os inteiros compactados de uma stream de referência cruzada ou de uma stream de objetos, 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 ao afirmar que desfazer esta transformação faz parte da descodificação de uma stream filtrada, não uma limpeza opcional, e ainda assim é fácil escrever um auxiliar de FlateDecode que se limita a chamar inflate e para por aí

O sintoma é distintivo assim que se sabe o que procurar. Os bytes diferenciados pelo Predictor não são ruído aleatório — continuam a transportar a forma de uma stream comprimida, pelo que um interpretador ingénuo muitas vezes percorre alguns tokens que parecem válidos antes de encontrar uma sequência de bytes que não pode de forma alguma ser um nome, um número, ou um delimitador de PDF, e linhas diferentes falham em offsets diferentes consoante o quanto os valores subjacentes calharam diferir dos seus vizinhos. Essa inconsistência é o que torna o erro difícil de fixar a partir de um único ficheiro que falhe: dois PDFs do mesmo produtor podem diferir apenas em quais valores calham repetir-se, pelo que um é interpretado quase por acidente enquanto o outro falha por completo

O que faz efetivamente o parâmetro Predictor do PDF?

A entrada /Predictor em /DecodeParms diz a um leitor conforme que inversão executar, e a Tabela 8 da ISO 32000-1 define os valores que importam na prática: 1 significa que não foi aplicada qualquer predição, 2 seleciona o TIFF Predictor 2 (diferenciação horizontal), e qualquer valor de 10 a 15 seleciona a predição ao estilo PNG. Mais três chaves viajam a par dele — /Colors, /BitsPerComponent, e /Columns — e, juntas, descrevem a geometria de linha em função da qual a diferenciação foi calculada, mesmo quando a stream não contém qualquer dado de imagem: uma stream de objetos não é uma imagem, mas os escritores de PDF reutilizam a mesma maquinaria de predição baseada em linhas para ela, porque a diferenciação seguida de deflate comprime muito melhor os inteiros e offsets de objeto compactados do que comprimi-los em bruto

O TIFF Predictor 2 é o mais simples dos dois esquemas: cada componente é guardado como a diferença em relação ao mesmo componente no pixel anterior na mesma linha, e cada linha reinicia na sua margem esquerda, em vez de transportar 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 etiqueta — 0 para None, 1 para Sub, 2 para Up, 3 para Average, 4 para Paeth — e é essa etiqueta, não o valor /Predictor declarado, que decide como essa linha específica é reconstruída. Um /Predictor de 12 é, na realidade, apenas a sugestão do codificador de que favoreceu o filtro Up, em que cada byte é restaurado somando o byte diretamente acima dele na linha anterior, mas um descodificador correto ainda tem de ler a etiqueta em cada linha, em vez de assumir Up ao longo de toda a stream

Porque é que as streams de objetos tornam invisível um Predictor em falta?

As streams de objetos agravam o problema em vez de o repetirem apenas. A ISO 32000-1 §7.5.7 permite que um escritor de PDF 1.5+ empacote vários objetos indiretos num único contentor comprimido, um /ObjStm, e é comum que precisamente os objetos de que um validador mais precisa — o catálogo, os /OutputIntents, ou uma stream de metadados XMP /Metadata — viajem através desse contentor com /Predictor 12 associado, porque esses objetos são suficientemente curtos e repetitivos para beneficiar da diferenciação por linha. Quando o passo de predição está em falta, expandir a stream de objetos não gera um erro: produz uma sequência de bytes que parece superficialmente plausível mas que não se tokeniza nos objetos esperados, pelo que o que estava empacotado lá dentro simplesmente não aparece. A renderização raramente repara nisto, porque um motor de renderização conforme já reconstrói os dados diferenciados por predictor antes de sequer chegarem à composição da página; o código que repara é exatamente o tipo em que este erro se escondia — um validador, assinador, ou verificador de versão que percorre os próprios bytes em bruto do PDF para responder a uma questão estrutural, sem qualquer alternativa assim que a sua própria visão da stream de objetos volta errada

O PDFiumPas incorria exatamente nesta falha antes da v2.16.0. As streams de objetos construídas com /Predictor 12, o caso comum para escritores de PDF 1.5+, expandiam-se através de PdfExpandObjectStreams para bytes diferenciados que o scanner estrutural não conseguia interpretar, pelo que o catálogo, os /OutputIntents, e os objetos /Metadata empacotados lá dentro ficavam efetivamente invisíveis às verificações de conformidade — sem exceção, sem aviso, apenas uma verificação que silenciosamente se comportava como se esses objetos estivessem ausentes. A mecânica mais profunda de como o PDFiumPas resolve uma stream de objetos em função da tabela de referência cruzada ativa, incluindo os casos híbridos e de stream xref pura, é abordada separadamente em o artigo sobre validar streams de objetos e xref com o PDFiumPas; o passo de predictor aqui descrito corre depois dessa resolução, sobre os bytes que cada objeto comprimido efetivamente contém

Inverter linhas Predictor de PNG e TIFF em Pascal

O PDFiumPas inverte a diferenciação numa única rotina, PdfApplyPredictor, e a sua matemática de geometria vale a pena conhecer, quer se chame a rotina, quer se reimplemente a ideia em código Delphi próprio. A largura de linha em bytes é ceil(Columns × Colors × BitsPerComponent ÷ 8) e a largura por pixel em bytes que ambos os algoritmos usam é ceil(Colors × BitsPerComponent ÷ 8) — errar qualquer um dos arredondamentos faz a reconstrução ler para além de uma fronteira de linha em vez de dentro dela. Um /Predictor abaixo de 2 é deixado intocado, já que 1 significa que o codificador não aplicou qualquer transformação; 2 seleciona o ramo TIFF mostrado abaixo, e qualquer valor a partir de 10 cai na reconstrução de filtros de linha PNG, onde o byte de etiqueta no início de cada linha — não o valor /Predictor declarado — decide como essa 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 alterou na v2.16.0

A correção lançada na v2.16.0 do PDFiumPas está dentro de PdfReadAndDecodeStream, a rotina que lê os bytes em bruto de uma stream e os descodifica para qualquer chamador que precise de inspecionar a estrutura do PDF ao nível dos bytes, incluindo a expansão de streams de objetos; só tenta a reconstrução depois de confirmar que /Filter é um FlateDecode simples, nunca uma cascata, porque um filtro encadeado não pode ser corrigido em segurança pelo Predictor a este nível. Ler de volta /Predictor, /Colors, /BitsPerComponent, e /Columns do dicionário da stream também não precisa de um interpretador de dicionário genérico: o PdfDictRefNum encontra cada chave por pesquisa direta de token de nome dentro do intervalo de bytes desse único dicionário, o que é seguro aqui precisamente porque estas quatro chaves não se podem repetir nem aninhar dentro de um único dicionário de stream. Essa mesma pesquisa por token de nome é muito mais arriscada assim que é apontada para uma região maior ou menos delimitada de um ficheiro PDF, tema do artigo relacionado sobre interpretar dicionários de PDF em 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, uma stream de objetos construída com /Predictor 12 expandia-se para bytes diferenciados sem qualquer erro gerado, pelo que qualquer objeto de catálogo, /OutputIntents, ou /Metadata empacotado lá dentro desaparecia das verificações estruturais do PDFiumPas sem qualquer aviso. Depois da correção, a mesma stream de objetos descomprime e depois reconstrói corretamente, e os objetos empacotados lá dentro voltam a ficar visíveis para essas verificações. Limites defensivos viajaram a par da correção: o PdfApplyPredictor agora rejeita por completo /Colors acima de 64, /BitsPerComponent acima de 32, e /Columns acima de 2^24, porque essas combinações descrevem geometrias de linha de que nenhum produtor real de PDF precisa e existem sobretudo para levar um descodificador a 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 por predictor do PDFiumPas tem dois limites que vale a pena conhecer antes de nela confiar. A reconstrução do TIFF Predictor 2 só cobre o caso de 8 bits por componente; o PDF permite empacotamentos mais estreitos, mas dados diferenciados ao estilo TIFF com sub-byte passam sem reconstrução em vez de serem adivinhados, pelo que uma stream que declare /Predictor 2 com /BitsPerComponent 1, 2, ou 4 não é descodificada corretamente por este caminho, para já. A predição PNG não tem essa restrição — cada linha fornece a sua própria etiqueta 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 à forma como a filtragem ao estilo PNG realmente funciona: o valor declarado é mais uma sugestão sobre o que o codificador usou predominantemente do que uma promessa sobre cada linha

O motor de renderização nativo do PDFium já reconstrói corretamente os dados de imagem e de fluxo de conteúdo diferenciados por predictor, o que é exatamente a razão pela qual um ficheiro pode renderizar na perfeição em qualquer visualizador comum enquanto um validador, assinador, ou verificador de versão ao nível dos bytes construído por cima lê os mesmos bytes de forma errada. A descodificação sensível ao predictor aqui descrita sustenta as funcionalidades de validação PDF/A, verificação estrutural, e assinatura do PDFiumPas, o componente PDFium VCL nativo para Delphi e C++Builder