Escreve um pequeno validador. Este abre um PDF, procura o fim, encontra o startxref, lê o deslocamento (offset) e espera chegar à palavra-chave xref com uma tabela de referências cruzadas de largura fixa por baixo. A partir dessa tabela recolhe os deslocamentos dos objetos, para depois procurar no sentido inverso a palavra-chave trailer para conhecer a /Root e o /Size. Funciona na perfeição em todos os ficheiros que gerou para o testar. Depois, chega um ficheiro produzido por uma versão atual do Word, ou por uma biblioteca que tenha como alvo o PDF 1.5, e o validador declara-o danificado. Não há qualquer palavra-chave xref para onde o deslocamento aponta, não há um dicionário trailer em lado nenhum, e a tabela de objetos construída pelo validador está quase vazia. O ficheiro é válido. O validador está a lê-lo através de uma lente com quinze anos
Este é o motivo mais comum para que uma verificação de PDF ao nível do byte escrita para o formato clássico falhe em documentos modernos. A estrutura de que depende, a tabela de referências cruzadas em texto simples e a palavra-chave trailer, tornou-se opcional no PDF 1.5 e está frequentemente ausente. Duas funcionalidades vieram substituí-la: o fluxo de referências cruzadas e o fluxo de objetos comprimidos. Ambos estão descritos na norma ISO 32000-1, e um validador que não as conheça encara um ficheiro saudável como um amontoado de objetos em falta
O que o PDF 1.5 mudou acerca da cauda do ficheiro
O §7.5.8 da norma ISO 32000-1 define o fluxo de referências cruzadas, e o §7.5.7 define o fluxo de objetos do tipo /ObjStm. Em conjunto, permitem a quem escreve abandonar as duas estruturas nas quais se baseia um analisador clássico. Um ficheiro PDF 1.5 pode acabar sem qualquer tabela xref. Em seu lugar, o objeto para o qual startxref aponta é um objeto de fluxo normal cujo dicionário contém /Type /XRef, e esse fluxo guarda os dados de referências cruzadas numa forma binária compacta. Também não existe palavra-chave trailer, porque o trailer é agora o próprio dicionário do fluxo. As chaves procuradas por um analisador clássico, /Root, /Size e /ID, vivem no interior desse dicionário
A segunda mudança diz respeito aos próprios objetos. Em vez de escrever cada objeto indireto no seu próprio deslocamento de byte, um escritor pode empacotar muitos objetos pequenos, os dicionários de página, os dicionários de anotação, a árvore de estrutura, num único fluxo de objetos e comprimir todo o contentor com Flate. Os objetos individuais já não têm um deslocamento de byte no ficheiro. Passam a ter uma posição no interior de um blob comprimido. Um validador que procure nos bytes em bruto por 1 0 obj nunca os irá encontrar, uma vez que esse texto só existe após a inflação (descompressão). Para um analisador clássico, metade do documento simplesmente desapareceu
As chaves do trailer estão em texto simples, mesmo num ficheiro comprimido
A parte tranquilizadora é que a leitura do trailer de um fluxo de referências cruzadas não exige a descompressão de nada. Um objeto de fluxo é escrito como um dicionário seguido da palavra-chave stream e, em seguida, os bytes comprimidos. O dicionário é um texto simples. Por isso, quando startxref aponta para um fluxo de referências cruzadas, os bytes imediatamente após o número do objeto parecem um dicionário comum, e o /Root, o /Size e o /ID estão ali de forma clara, antes de começarem a palavra-chave stream e os dados Flate
Isto significa que um validador pode aprender os três factos de que mais necessita, onde está o catálogo, quantos objetos o ficheiro reivindica e o identificador do ficheiro, analisando apenas o dicionário do fluxo. Não tem de descomprimir os dados de referência cruzada, nem de interpretar as entradas binárias dentro dele. O trabalho que derrota um analisador inexperiente não é a leitura do trailer; é encontrar os objetos. São dois problemas separáveis, e a solução do primeiro é barata
Fluxos de objetos: um cabeçalho e depois um blob Flate
Um fluxo de objetos é um contentor. O seu dicionário contém /Type /ObjStm, uma entrada /N que indica o número de objetos empacotados no interior e uma entrada /First que indica o deslocamento do byte, nos dados inflados, onde começa o corpo do primeiro objeto. A carga comprimida, depois de inflada, começa com um pequeno cabeçalho de /N pares de números inteiros. Cada par é um número de objeto e o deslocamento do corpo desse objeto relativamente a /First. Depois do cabeçalho vêm os próprios corpos dos objetos, concatenados
Expandir um deles é mecânico depois de os bytes serem inflados. Lemos o dicionário para obtermos o /N e o /First, inflamos o fluxo com um descodificador Flate, percorremos os pares iniciais /N para ficarmos a saber que número de objeto reside em cada deslocamento e, em seguida, retiramos cada corpo como se fosse um objeto indireto vulgar. A única verdadeira dependência é o descodificador Flate, e esse o utilizador já o tem: o Delphi inclui o System.ZLib, e o Free Pascal contém a unit zstream; ambos envolvem a zlib e inflam um fluxo Flate em bruto sem qualquer 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 num ficheiro clássico
O que não tem de implementar
É fácil sobrestimar o trabalho. A leitura das chaves do trailer a partir de um ficheiro comprimido não requer a descodificação das entradas binárias do fluxo de referências cruzadas. O fluxo de referências cruzadas do §7.5.8 utiliza três tipos de entrada, e a entrada de tipo 2, a que diz este objeto vive dentro do fluxo de objetos N no índice i
, é a que descodificaria para construir um mapa de deslocamentos completo. Precisa desse mapa para resolver objetos arbitrários por número. 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 os fluxos de objetos, porque cada /ObjStm anuncia os seus próprios conteúdos através de /N e /First
Também não tem de lidar com as funções de previsão PNG e TIFF que um fluxo de referências cruzadas pode aplicar através do seu /DecodeParms apenas para obter as chaves do trailer. Os preditores filtram as linhas de referências cruzadas binárias para que sejam comprimidas de forma mais eficaz; nada têm a ver com o dicionário que antecede o fluxo. O upgrade mínimo que torna um validador clássico ciente dos PDF modernos é portanto pequeno: quando o startxref cai num fluxo em vez de na palavra-chave xref, analisa o dicionário de fluxos em busca das chaves do trailer, e expande quaisquer objetos /ObjStm que encontre para que os seus conteúdos entrem na tabela de objetos. A descodificação de entradas de tipo 2 e de preditores é uma tarefa separada e de maior dimensão, que pode adiar até necessitar efetivamente da resolução aleatória de objetos
Por que uma verificação de conformidade deve expandir os fluxos em primeiro lugar
Isto deixa de ser académico no instante em que efetua uma verificação de perfil. Um validador de PDF/A ou de PDF/X inspeciona objetos específicos: o catálogo do documento em busca de um array de /OutputIntents, o fluxo de /Metadata para um pacote XMP com o identificador certo, cada descritor de tipo de letra para um ficheiro de tipo de letra incorporado, o trailer de um /ID. Num ficheiro comprimido, a maioria desses objetos encontra-se no interior de fluxos de objetos. Um validador que não tenha expandido os fluxos de objetos não consegue ver as chaves do catálogo, não consegue encontrar os metadados e não consegue enumerar os tipos de letra. Informará que a um documento em perfeita conformidade falta a intenção de saída, o XMP e metade da estrutura, porque as provas de que necessita continuam num blob Flate que nunca chegou a inflar
A ordem é importante. A expansão tem de ocorrer antes de as verificações serem executadas e não ao lado destas, uma vez que cada verificação assume que pode chegar a um objeto por número. Se associar uma verificação de perfil diretamente a uma verificação de bytes em bruto, ela herda a cegueira do analisador clássico e produz falsas violações exatamente nos ficheiros modernos com maior probabilidade de estarem bem formados, já que surgiram em cadeias de ferramentas suficientemente recentes para escrever fluxos de referências cruzadas desde logo
Deixar o PDFium fazer a análise por si
O Componente PDFium analisa fluxos de referências cruzadas e de objetos como parte do carregamento de um documento, que é a via mais prática de evitar que a fase de inflar-e-expandir seja feita à mão. Ao carregar um ficheiro com o componente TPdf, os objetos empacotados em contentores /ObjStm estão já resolvidos e os pontos de entrada de validação veem o documento totalmente expandido. ValidatePdfA devolve um registo TPdfAValidationResult cujo campo Conformance é um valor TPdfAConformance como pac1b ou pacNone, cujo campo Issues é um conjunto de problemas específicos detetados, e cujo método IsCompliant é verdadeiro apenas se foi detetado um nível de conformidade e o conjunto de questões for vazio. Como os objetos foram expandidos no momento do carregamento, é encontrado um array /OutputIntents ou um tipo de letra incorporado que se encontrava dentro de um fluxo de objetos, e não sinalizado 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 a ValidatePdfX, que devolve um TPdfXValidationResult com a mesma forma. A vantagem de o encaminhar pelo PDFium é que a descompressão estrutural acima descrita acontece de uma vez, corretamente, no interior do carregador, de modo a que o seu código de validação nunca veja a diferença entre um ficheiro clássico e um ficheiro totalmente comprimido. 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 is a set: count its members
Inc(IssueCount);
Writeln('Not conformant; issue count = ', IssueCount);
end;
finally
Pdf.Free;
end;
end;
Caso os bytes se encontrem já em memória e não no disco, a mesma sequência "carregar-e-depois-validar" funciona mediante o LoadDocument(const Data: TBytes), o qual capta o conteúdo do ficheiro em bruto e analisa os seus fluxos de objetos e de referências cruzadas da mesma forma que o percurso do ficheiro o faz. A lição a tirar para um validador manual é a regra estrutural e não a API: ler as chaves do trailer no dicionário de fluxos em texto simples, expandir todos os /ObjStm com um descodificador Flate antes de percorrer o documento e considerar a descodificação de entradas binárias de referências cruzadas como uma tarefa maior e opcional
Uma vez a estrutura expandida, um validador pode orientar o resto de um fluxo de trabalho em torno da mesma. Para um teste de preflight de linha de comandos que indique conformidade ao longo de uma pasta de dados introduzidos, consulte o nosso tutorial na construção de uma CLI de relatório de preflight em lote. Quando a validação funciona como barreira que impede de partir em bocados um documento extenso, as técnicas existentes no nosso guia de divisão de documentos PDF em ficheiros múltiplos combinam-se naturalmente com o padrão de carregar e verificar aqui exibido. Ambos assentam no plano de validação e de carregamento do Componente PDFium para Delphi e C++Builder