Você escreve um pequeno validador. Ele abre um PDF, procura o final, 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 os deslocamentos dos objetos e, em seguida, varre para trás procurando a palavra-chave trailer para obter o /Root e /Size. Isso funciona perfeitamente em todos os arquivos gerados para teste. 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 corrompido. Não há a palavra-chave xref onde o deslocamento aponta, nenhum dicionário trailer em lugar nenhum e a tabela de objetos que o validador construiu está quase vazia. O arquivo é válido. O validador o está lendo sob uma perspectiva de quinze anos atrás.
Este é o motivo mais comum pelo qual uma verificação de PDF em nível de byte escrita contra o layout clássico falha em documentos modernos. A estrutura da qual ela depende, a tabela de referência cruzada em texto simples e a palavra-chave trailer, tornou-se opcional no PDF 1.5 e frequentemente está ausente. Dois recursos a substituíram: o stream de referência cruzada (cross-reference stream) e o stream de objetos compactados (compressed object stream). Ambos são descritos na ISO 32000-1, e um validador que não os conhece enxerga um arquivo íntegro como um amontoado de objetos ausentes.
O que o PDF 1.5 mudou no final do arquivo
A ISO 32000-1 §7.5.8 define o stream de referência cruzada, e a seção §7.5.7 define o stream de objetos do tipo /ObjStm. Juntos, eles permitem que o gravador descarte as duas estruturas em que um analisador clássico se baseia. Um arquivo PDF 1.5 pode terminar sem tabela xref nenhuma. Em seu lugar, o objeto para o qual startxref aponta é um objeto de stream comum cujo dicionário carrega /Type /XRef, e esse stream mantém os dados de referência cruzada em um formato binário compacto. Também não há a palavra-chave trailer, porque o trailer agora é o próprio dicionário do stream. As chaves que um analisador clássico procurava, como /Root, /Size e /ID, residem dentro desse dicionário.
A segunda alteração move os próprios objetos. Em vez de gravar cada objeto indireto em seu próprio deslocamento de byte, o gravador pode agrupar muitos objetos pequenos (os dicionários de páginas, os dicionários de anotações, a árvore de estrutura) em um único stream de objetos e compactar todo o contêiner com Flate. Os objetos individuais não possuem mais um deslocamento de byte no arquivo. Eles têm uma posição dentro de um bloco compactado. Um validador que varre os bytes brutos procurando por 1 0 obj nunca os encontra, porque esse texto só existe após a descompactação. Para um analisador clássico, metade do documento simplesmente desapareceu.
As chaves do trailer são texto simples, mesmo em um arquivo compactado
A parte tranquilizadora é que ler o trailer de um stream de referência cruzada não exige descompactar nada. Um objeto de stream é gravado como um dicionário seguido pela palavra-chave stream e depois pelos bytes compactados. O dicionário é texto simples. Portanto, quando startxref aponta para um stream de referência cruzada, os bytes imediatamente após o número do objeto parecem um dicionário comum, e /Root, /Size e /ID estão lá claramente, antes do início da palavra-chave stream e dos dados Flate.
Isso significa que um validador pode obter os três fatos que mais precisa (onde o catálogo está, quantos objetos o arquivo afirma ter e o identificador do arquivo) analisando apenas o dicionário do stream. 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 dificulta um analisador ingênuo não é ler o trailer; é encontrar os objetos. Esses são dois problemas distintos, e resolver o primeiro é simples.
Streams de objetos: um cabeçalho e depois um bloco Flate
Um stream de objetos é um contêiner. Seu dicionário carrega /Type /ObjStm, uma entrada /N indicando o número de objetos empacotados e uma entrada /First fornecendo o deslocamento de byte, dentro dos dados descompactados, onde o corpo do primeiro objeto começa. A carga compactada, uma vez descompactada, começa com um pequeno cabeçalho de /N pares de inteiros. 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 corpos dos objetos concatenados.
Expandir um deles é mecânico assim que os bytes são descompactados. Você lê o dicionário para obter /N e /First, descompacta o stream com um decodificador Flate, percorre os pares /N iniciais para saber qual número de objeto reside em qual deslocamento e, em seguida, extrai cada corpo como se fosse um objeto indireto comum. A única dependência real é o decodificador Flate, e você já possui um: o Delphi traz o System.ZLib e o Free Pascal traz a unidade zstream, ambos encapsulando o zlib e descompactando um stream Flate bruto sem código de terceiros. Uma rotina que anexa cada objeto extraído à tabela de objetos do validador faz com que o resto do validador, a parte que percorre /Root e verifica a árvore de páginas, se comporte exatamente como faria em um arquivo clássico.
O que você não precisa implementar
É fácil superestimar o trabalho. Ler as chaves do trailer a partir de um arquivo compactado não requer decodificar as entradas binárias do stream de referência cruzada. O stream de referência cruzada da seção §7.5.8 usa três tipos de entradas, e a entrada do tipo 2, aquela que diz este objeto vive dentro do stream de objetos N no índice i
, é o que você decodificaria para construir um mapa completo de deslocamento. 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 em texto simples, e não precisa dele para expandir streams 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 de previsão (predictors) de PNG e TIFF que um stream de referência cruzada pode aplicar por meio de seu /DecodeParms apenas para obter as chaves do trailer. Os preditores filtram as linhas binárias de referência cruzada para que se compactem melhor; eles não têm nada a ver com o dicionário que precede o stream. A atualização mínima que torna um validador clássico compatível com PDFs modernos é simples: quando startxref cair em um stream em vez da palavra-chave xref, analise o dicionário do stream em busca das chaves do trailer e expanda quaisquer objetos /ObjStm encontrados para que seus conteúdos entrem na tabela de objetos. Decodificar entradas do tipo 2 e preditores é 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 os streams primeiro
Isso deixa de ser acadêmico no momento em que você executa uma verificação de perfil. Um validador de PDF/A ou PDF/X inspeciona objetos específicos: o catálogo do documento para um array /OutputIntents, o stream /Metadata para um pacote XMP com o identificador correto, cada descritor de fonte para um arquivo de fonte incorporado, o trailer para um /ID. Em um arquivo compactado, a maioria desses objetos está dentro de streams de objetos. Um validador que não expandiu os streams de objetos não consegue ver las chaves do catálogo, não consegue encontrar os metadados e não consegue enumerar as fontes. Ele relará um documento perfeitamente em conformidade como carecendo de sua intenção de saída, de seu XMP e de metade de sua estrutura, porque as informações necessárias ainda estão em um bloco Flate que ele nunca descompactou.
A ordem importa. A expansão deve acontecer antes que as verificações sejam executadas, e não paralelamente a elas, porque cada verificação assume que pode alcançar um objeto por número. Se você conectar uma verificação de perfil diretamente a uma varredura de bytes brutos, ela herdará a cegueira do analisador clássico e produzirá violações falsas exatamente nos arquivos modernos que têm mais probabilidade de estarem bem formados, pois vieram de ferramentas novas o suficiente para gravar streams de referência cruzada.
Deixando o PDFium fazer a análise por você
O PDFium Component analisa streams de referência cruzada e streams de objetos como parte do carregamento de um documento, que é a maneira prática de evitar criar manualmente a etapa de descompactação e expansão. Quando você carrega um arquivo com o componente TPdf, os objetos empacotados nos contêineres /ObjStm já estão resolvidos, e os pontos de entrada de validação enxergam 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 no carregamento, um array /OutputIntents ou uma fonte incorporada que residia dentro de um stream de objetos é localizado, e não relatado 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; // parses xref/object streams on load
Result := Pdf.ValidatePdfA; // sees the expanded object table
finally
Pdf.Free;
end;
end;
O mesmo se aplica ao ValidatePdfX, que retorna um TPdfXValidationResult com a mesma estrutura. O objetivo de direcionar pelo PDFium é que a descompactação estrutural descrita acima ocorre uma única vez, de forma correta, dentro do carregador, para 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.
var
Pdf: TPdf;
R : TPdfXValidationResult;
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: ', Ord(R.Conformance))
else
Writeln('Not conformant; issue count = ', SizeOf(R.Issues));
finally
Pdf.Free;
end;
end;
Se os bytes já estiverem em memória e não em disco, a mesma sequência de carregar e validar funciona por meio da sobrecarga de LoadDocument(const Data: TBytes), que recebe o conteúdo bruto do arquivo e analisa seus streams de referência cruzada e de objetos da mesma forma que o caminho do arquivo faz. O aprendizado para um validador manual é a regra estrutural, não a API: leia as chaves do trailer no dicionário de stream em texto simples, expanda cada /ObjStm com um decodificador Flate antes de percorrer o documento e trate a decodificação de entradas binárias de referência cruzada como a tarefa opcional e maior que ela é.
Assim que a estrutura for expandida, um validador pode executar o restante do fluxo de trabalho sobre ela. Para um utilitário de 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 CLI de relatório de preflight em lote. Quando a validação é uma etapa prévia à divisão de um documento grande, as técnicas apresentadas em nosso guia sobre como dividir documentos PDF em vários arquivos combinam-se naturalmente com o padrão de carregar e verificar mostrado aqui. Ambos utilizam a superfície de carregamento e validação do PDFium Component para Delphi e C++Builder.