Artigo Técnico

Validando PDFs compactados: fluxos de objetos e XRef

Você escreve um pequeno validador. Ele abre um PDF, busca até o fim, encontra startxref, lê o deslocamento e espera cair na palavra-chave xref com uma tabela de referência cruzada de largura fixa abaixo dela. A partir dessa tabela, ele coleta deslocamentos de objetos e, em seguida, faz uma varredura para trás em busca da palavra-chave trailer para descobrir o /Root e o /Size. Funciona perfeitamente em cada arquivo que você gerou para testá-lo. Então, um arquivo produzido por uma versão atual do Word, ou por uma biblioteca voltada para PDF 1.5, chega, e o validador o declara como quebrado. Não há palavra-chave xref onde os pontos de deslocamento, nenhum dicionário trailer em qualquer lugar e a tabela de objetos que o validador construiu está quase vazia. O arquivo é válido. O validador o está lendo por meio de uma lente de quinze anos de idade

Esta é a razão mais comum pela qual uma verificação de PDF no nível de bytes escrita em relação ao layout clássico falha em documentos modernos. A estrutura da qual depende, a tabela de referência cruzada em texto simples e a palavra-chave trailer, tornou-se opcional no PDF 1.5 e está frequentemente ausente. Dois recursos o substituíram: o fluxo de referência cruzada e o fluxo de objetos compactados. Ambos são descritos na ISO 32000-1 e um validador que não os conhece vê um arquivo íntegro como um monte de objetos ausentes

O que o PDF 1.5 mudou sobre a cauda do arquivo

A ISO 32000-1 §7.5.8 define o fluxo de referência cruzada e §7.5.7 define o fluxo de objetos do tipo /ObjStm. Juntos, eles permitem que um gravador elimine as duas estruturas nas quais um analisador clássico se baseia. Um arquivo PDF 1.5 pode terminar sem nenhuma tabela xref. Em seu lugar, o objeto para o qual startxref aponta é um objeto de fluxo comum cujo dicionário carrega /Type /XRef e esse fluxo contém os dados de referência cruzada em uma forma binária compacta. Também não há palavra-chave trailer, porque o trailer agora é o próprio dicionário do fluxo. As chaves que um analisador clássico procurava, /Root, /Size e /ID, vivem dentro desse dicionário

A segunda alteração move os próprios objetos. Em vez de escrever cada objeto indireto em seu próprio deslocamento de byte, um gravador pode empacotar muitos objetos pequenos, os dicionários de página, os dicionários de anotação, a árvore de estrutura, em um único fluxo de objetos e compactar o contêiner inteiro com Flate. Os objetos individuais não têm mais um deslocamento de byte no arquivo. Eles têm uma posição dentro de um blob compactado. Um validador que verifica os bytes brutos para 1 0 obj nunca os encontra, porque esse texto só existe após a inflação. Para um analisador clássico, metade do documento simplesmente desapareceu

As chaves do trailer são texto sem formatação, mesmo em um arquivo compactado

A parte tranquilizadora é que ler o trailer de um fluxo de referência cruzada não exige a inflação de nada. Um objeto de fluxo é escrito como um dicionário seguido pela palavra-chave stream e, em seguida, os bytes compactados. O dicionário é um texto sem formatação. Portanto, quando startxref aponta para um fluxo de referência cruzada, os bytes imediatamente após o número do objeto parecem um dicionário comum, e /Root, /Size e /ID ficam lá em claro, antes da palavra-chave stream e do início dos dados Flate

Isso significa que um validador pode aprender os três fatos de que mais precisa, onde está o catálogo, quantos objetos o arquivo reivindica e o identificador do arquivo, analisando apenas o dicionário de fluxo. Ele não precisa descompactar os dados de referência cruzada e não precisa interpretar as entradas binárias dentro dele. O trabalho que derrota um analisador ingênuo não é ler o trailer; é encontrar os objetos. Esses são dois problemas separáveis ​​e a solução para o primeiro é barata

Fluxos de objetos: um cabeçalho, depois um blob Flate

Um fluxo de objetos é um contêiner. Seu dicionário carrega /Type /ObjStm, uma entrada /N que fornece o número de objetos agrupados e uma entrada /First que fornece o deslocamento de byte, nos dados inflados, onde o corpo do primeiro objeto começa. A carga útil compactada, depois de inflada, começa com um pequeno cabeçalho de pares de inteiros /N. Cada par é um número de objeto e o deslocamento do corpo desse objeto em relação a /First. Após o cabeçalho, vêm os próprios corpos dos objetos, concatenados

A expansão de um deles é mecânica depois que os bytes são inflados. Você lê o dicionário para obter /N e /First, infla o fluxo com um decodificador Flate, percorre os pares iniciais /N para descobrir qual número de objeto reside em qual deslocamento e, em seguida, retira cada corpo como se fosse um objeto indireto comum. A única dependência real é o decodificador Flate e você já tem um: o Delphi fornece System.ZLib e o Free Pascal fornece a unidade zstream, ambos envolvem o zlib e inflam um fluxo bruto Flate sem nenhum código de terceiros. Uma rotina que anexa cada objeto extraído à tabela de objetos do validador faz com que o restante do validador, a parte que percorre /Root e verifica a árvore de páginas, se comporte exatamente como se estivesse em um arquivo clássico

O que você não precisa implementar

É fácil superestimar o trabalho. A leitura das chaves de trailer de um arquivo compactado não exige a decodificação das entradas binárias do fluxo de referência cruzada. O fluxo de referência cruzada de §7.5.8 usa três tipos de entrada e a entrada do tipo 2, aquela que diz este objeto reside dentro do fluxo de objeto N no índice i, é o que você decodificaria para construir um mapa de deslocamento completo. Você precisa desse mapa para resolver objetos arbitrários por número. Você não precisa dele para ler /Root, /Size e /ID, que estão no dicionário de texto simples, e não precisa dele para expandir fluxos de objetos, porque cada /ObjStm anuncia seu próprio conteúdo por meio de /N e /First

Você também não precisa lidar com as funções preditoras PNG e TIFF que um fluxo de referência cruzada pode aplicar por meio de seu /DecodeParms apenas para obter as chaves de trailer. Os preditores filtram as linhas de referência cruzada binárias para fazê-las compactar melhor; eles não têm nada a ver com o dicionário que antecede o fluxo. A atualização mínima que torna um validador clássico compatível com PDFs modernos é, portanto, pequena: quando startxref cai em um fluxo em vez da palavra-chave xref, analise o dicionário de fluxo para as chaves de trailer e expanda todos os objetos /ObjStm que encontrar para que seus conteúdos entrem na tabela de objetos. A decodificação de entradas e preditores do tipo 2 é uma tarefa separada e maior, que você pode adiar até que realmente precise de resolução de objetos aleatórios

Por que uma verificação de conformidade deve expandir fluxos primeiro

Isso deixa de ser acadêmico no momento em que você executa uma verificação de perfil. Um validador PDF/A ou PDF/X inspeciona objetos específicos: o catálogo de documentos para uma matriz /OutputIntents, o fluxo /Metadata para um pacote XMP com o identificador correto, cada descritor de fonte para um arquivo de fonte incorporado, o trailer de um /ID. Em um arquivo compactado, a maioria desses objetos está dentro de fluxos de objetos. Um validador que não expandiu os fluxos de objetos não consegue ver as chaves do catálogo, não consegue encontrar os metadados e não consegue enumerar as fontes. Ele relatará um documento perfeitamente compatível como ausente sua intenção de saída, ausente seu XMP e ausente metade de sua estrutura, porque as evidências de que precisa ainda estão em um blob Flate que nunca foi inflado

A ordem é importante. A expansão precisa acontecer antes que as verificações sejam executadas, e não junto com elas, porque toda verificação presume que pode acessar um objeto pelo número. Se você conectar uma verificação de perfil diretamente em uma verificação de byte bruto, ela herdará a cegueira do analisador clássico e produzirá violações falsas exatamente nos arquivos modernos com maior probabilidade de serem bem formados, pois vieram de conjuntos de ferramentas novas o suficiente para gravar fluxos de referência cruzada em primeiro lugar

Deixando o PDFium fazer a análise por você

O PDFium Component analisa fluxos de referência cruzada e fluxos de objetos como parte do carregamento de um documento, que é a maneira prática de evitar a etapa manual de inflar e expandir. Ao carregar um arquivo com o componente TPdf, os objetos agrupados nos contêineres /ObjStm já estão resolvidos e os pontos de entrada de validação veem o documento totalmente expandido. ValidatePdfA retorna um registro TPdfAValidationResult cujo campo Conformance é um valor TPdfAConformance como pac1b ou pacNone, cujo campo Issues é um conjunto de problemas específicos encontrados e cujo método IsCompliant é verdadeiro somente quando um nível de conformidade foi detectado e o conjunto de problemas está vazio. Como os objetos foram expandidos durante o carregamento, uma matriz /OutputIntents ou uma fonte incorporada que ficava dentro de um fluxo de objetos é encontrada, e não reportada como ausente

uses
  PDFium, FPdfPdfa;

function CheckPdfA(const FileName: string): TPdfAValidationResult;
var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := FileName;
    Pdf.Active := True;            // analisa xref/fluxos de objetos no carregamento
    Result := Pdf.ValidatePdfA;    // vê a tabela de objetos expandida
  finally
    Pdf.Free;
  end;
end;

O mesmo se aplica a ValidatePdfX, que retorna um TPdfXValidationResult com a mesma forma. O objetivo de rotear pelo PDFium é que a descompactação estrutural descrita acima acontece uma vez, de forma correta, dentro do carregador, de modo que seu código de validação nunca veja a diferença entre um arquivo clássico e um totalmente compactado. Ambos chegam ao validador como um conjunto resolvido de objetos

function PdfXConformanceName(C: TPdfXConformance): string;
begin
  case C of
    pxc1a: Result := 'PDF/X-1a';
    pxc3 : Result := 'PDF/X-3';
    pxc4 : Result := 'PDF/X-4';
  else
    Result := 'none';
  end;
end;

var
  Pdf: TPdf;
  R  : TPdfXValidationResult;
  Issue: TPdfXValidationIssue;
  IssueCount: Integer;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'Press_Ready.pdf';
    Pdf.Active := True;
    R := Pdf.ValidatePdfX;
    if R.IsCompliant then
      Writeln('PDF/X conformance: ', PdfXConformanceName(R.Conformance))
    else
    begin
      IssueCount := 0;
      for Issue in R.Issues do   // Issues é um conjunto: conte seus membros
        Inc(IssueCount);
      Writeln('Not conformant; issue count = ', IssueCount);
    end;
  finally
    Pdf.Free;
  end;
end;

Se os bytes já estiverem na memória em vez de no disco, a mesma sequência carregar-depois-validar funciona pela sobrecarga LoadDocument(const Data: TBytes), que usa o conteúdo bruto do arquivo e analisa sua referência cruzada e fluxos de objetos da mesma maneira que o caminho do arquivo faz. O aprendizado para um validador escrito à mão é a regra estrutural, não a API: leia as chaves do trailer do dicionário de fluxo em texto sem formatação, expanda cada /ObjStm com um decodificador Flate antes de percorrer o documento e trate a decodificação de entradas de referência cruzada binária como a tarefa opcional e maior que ela é

Uma vez expandida a estrutura, um validador pode orientar o restante de um fluxo de trabalho em torno dela. Para um preflight de linha de comando que relata a conformidade em uma pasta de entradas, consulte nosso passo a passo sobre a construção de um preflight em lote (CLI). Quando a validação é um portal à frente da divisão de um grande documento, as técnicas em nosso guia para dividir documentos PDF em vários arquivos combinam perfeitamente com o padrão carregar-e-verificar mostrado aqui. Ambos têm base na superfície de carregamento e validação do PDFium Component para Delphi e C++Builder