Imagine que escreve um validador simples. Este abre um PDF, procura o fim, localiza startxref, lê o desvio e espera encontrar a palavra-chave xref com uma tabela de referência cruzada de largura fixa por baixo. A partir dessa tabela, recolhe os desvios dos objetos e, em seguida, analisa o documento no sentido inverso para localizar a palavra-chave trailer com o intuito de obter as chaves /Root e /Size. Funciona perfeitamente em todos os ficheiros gerados nos seus testes. Contudo, quando surge um ficheiro produzido por uma versão recente do Word, ou por uma biblioteca que tenha como objetivo o PDF 1.5, o validador declara-o corrompido. Não existe a palavra-chave xref no ponto indicado pelo desvio, não há nenhum dicionário trailer e a tabela de objetos criada pelo validador está praticamente vazia. O ficheiro é perfeitamente válido. O validador é que o está a analisar sob uma perspetiva com quinze anos.
Esta é a razão mais comum para que verificações de PDF ao nível dos bytes, desenvolvidas a pensar no layout clássico, falhem em documentos modernos. A estrutura da qual dependem - a tabela de referência cruzada em texto limpo e a palavra-chave trailer - passou a ser opcional a partir do PDF 1.5, estando frequentemente ausente. Duas funcionalidades substituíram-na: o fluxo de referência cruzada (cross-reference stream) e o fluxo de objetos comprimidos. Ambos estão descritos na norma ISO 32000-1, e um validador que não os conheça interpretará um ficheiro em bom estado como contendo uma falha de objetos.
O que o PDF 1.5 mudou no fim do ficheiro
A norma ISO 32000-1, secção 7.5.8, define o fluxo de referência cruzada, e a secção 7.5.7 define o fluxo de objetos do tipo /ObjStm. Em conjunto, permitem que o escritor prescinda das duas estruturas em que um analisador clássico se baseava. Um ficheiro PDF 1.5 pode terminar sem qualquer tabela xref. No seu lugar, o objeto para o qual o startxref aponta é um objeto de fluxo comum cujo dicionário contém a entrada /Type /XRef, e esse fluxo guarda os dados de referência cruzada num formato binário compacto. Também não existe a palavra-chave trailer, uma vez que o trailer corresponde agora ao próprio dicionário do fluxo. As chaves que um analisador clássico procuraria, como /Root, /Size e /ID, residem dentro desse dicionário.
A segunda alteração afeta os próprios objetos. Em vez de escrever cada objeto indireto no seu próprio desvio de bytes no ficheiro, o escritor pode agrupar vários objetos pequenos (os dicionários de páginas, de anotações, a árvore de estrutura) num único fluxo de objetos e comprimir o contentor completo com Flate. Os objetos individuais deixam de ter um desvio de bytes próprio no ficheiro. Passam sim a ter uma posição dentro de um bloco comprimido. Um validador que examine os bytes brutos à procura de 1 0 obj nunca os encontrará, dado que esse texto só existe após a descompressão (inflation). Para um analisador clássico, metade do documento simplesmente desapareceu.
As chaves do trailer estão em texto limpo, mesmo num ficheiro comprimido
A parte tranquilizadora é que ler o trailer de um fluxo de referência cruzada não exige a descompressão de nada. Um objeto de fluxo é escrito sob a forma de dicionário seguido da palavra-chave stream e, depois, dos bytes comprimidos. O dicionário é escrito em texto limpo. Por isso, quando o startxref aponta para um fluxo de referência cruzada, os bytes imediatamente após o número do objeto assemelham-se a um dicionário comum, e as chaves /Root, /Size e /ID encontram-se legíveis, antes de a palavra-chave stream e os dados Flate começarem.
Isto significa que um validador pode obter os três dados de que mais necessita - onde se encontra o catálogo, quantos objetos o ficheiro declara e o identificador do ficheiro - analisando unicamente o dicionário do fluxo. Não necessita de descomprimir os dados de referência cruzada e não tem de interpretar as entradas binárias contidas nos mesmos. O processamento que inviabiliza um analisador simples não é a leitura do trailer; é sim a localização dos objetos. Tratam-se de dois problemas distintos, e resolver o primeiro exige pouco esforço.
Fluxos de objetos: um cabeçalho e um bloco Flate
Um fluxo de objetos é um contentor. O seu dicionário inclui a entrada /Type /ObjStm, uma entrada /N que indica o número de objetos nele contidos e uma entrada /First que indica o desvio em bytes, dentro dos dados descomprimidos, onde tem início o corpo do primeiro objeto. O conteúdo comprimido, após descompressão, começa com um cabeçalho reduzido de /N pares de inteiros. Cada par é um número de objeto e o desvio do corpo desse objeto em relação à entrada /First. A seguir ao cabeçalho surgem os corpos dos objetos propriamente ditos, concatenados.
Expandir um destes fluxos é um processo mecânico assim que os bytes são descomprimidos. Lê o dicionário para obter /N e /First, descompressora o fluxo com um descodificador Flate, percorre os /N pares iniciais para saber que número de objeto reside em cada desvio, e depois extrai cada corpo como se tratasse de um objeto indireto normal. A única dependência real é o descodificador Flate, que já possui integrado: o Delphi disponibiliza System.ZLib e o Free Pascal disponibiliza a unidade zstream, que envolvem o zlib e descomprimem um fluxo Flate bruto sem necessidade de código de terceiros. Uma rotina que anexe cada objeto extraído à tabela de objetos do validador faz com que o resto do validador (a parte que percorre a /Root e verifica a árvore de páginas) se comporte exatamente como faria face a um ficheiro clássico.
O que não precisa de implementar
É fácil sobrestimar o trabalho necessário. Ler as chaves do trailer a partir de um ficheiro comprimido não exige a descodificação das entradas binárias do fluxo de referência cruzada. O fluxo de referência cruzada da secção 7.5.8 utiliza três tipos de entradas, e a entrada de tipo 2 - a que indica que o objeto reside dentro do fluxo de objetos N no índice i - é a que teria de descodificar para criar um mapa de desvios completo. Precisa desse mapa para resolver objetos arbitrários pelo seu número. Contudo, não precisa dele para ler as chaves /Root, /Size e /ID, que estão legíveis no dicionário, e não necessita dele para expandir os fluxos de objetos, visto que cada /ObjStm indica o seu próprio conteúdo através de /N e /First.
Também não tem de gerir as funções de predição PNG e TIFF que um fluxo de referência cruzada possa aplicar através de /DecodeParms apenas para obter as chaves do trailer. Os preditores filtram as linhas de referência cruzada binárias para melhorar a compressão, não tendo qualquer relação com o dicionário que antecede o fluxo. A atualização mínima necessária para dotar um validador clássico de suporte para PDFs modernos é, portanto, reduzida: quando o startxref incidir num fluxo em vez de na palavra-chave xref, analisa o dicionário do fluxo para extrair as chaves do trailer e expande os objetos /ObjStm que encontrar para que o seu conteúdo passe a integrar a tabela de objetos. Descodificar entradas binárias de tipo 2 e preditores é uma tarefa mais complexa que pode adiar até que necessite de facto de resolução aleatória de objetos.
Por que razão a verificação de conformidade deve expandir primeiro os fluxos
Este tema deixa de ser puramente académico no momento em que executa uma verificação de perfil. Um validador PDF/A ou PDF/X inspeciona objetos específicos: o catálogo do documento à procura de uma matriz /OutputIntents, o fluxo /Metadata para obter um pacote XMP com o identificador correto, os descritores de tipos de letra à procura de ficheiros incorporados, e o trailer para obter o /ID. Num ficheiro comprimido, a maioria desses objetos situa-se dentro de fluxos de objetos. Um validador que não tenha expandido os fluxos de objetos não conseguirá ver as chaves do catálogo, não localizará os metadados e não listará os tipos de letra. Acabará por reportar um documento perfeitamente em conformidade como carecendo de intenção de saída, sem XMP e sem metade da sua estrutura, porque as provas de que necessita continuam por expandir num bloco Flate.
A ordem de execução é crucial. A expansão tem de ocorrer antes de as validações correrem, e não em simultâneo, porque cada verificação assume que consegue aceder a um objeto pelo seu número. Se associar uma verificação de perfil diretamente a uma leitura de bytes brutos, herdará a cegueira do analisador clássico e gerará falsas violações precisamente nos ficheiros modernos com maior probabilidade de estarem corretos, uma vez que foram gerados por ferramentas recentes capazes de escrever fluxos de referência cruzada.
Deixar o PDFium tratar da análise por si
O PDFium Component analisa fluxos de referência cruzada e fluxos de objetos como parte do processo de carregamento de um documento, constituindo o método prático para evitar ter de programar manualmente a etapa de descompressão e expansão. Quando carrega um ficheiro com o componente TPdf, os objetos agrupados em contentores /ObjStm já estão resolvidos, e os pontos de entrada de validação veem o documento totalmente expandido. A função ValidatePdfA devolve um registo TPdfAValidationResult cujo campo Conformance é um valor TPdfAConformance (como pac1b ou pacNone), cujo campo Issues representa um conjunto de problemas específicos detetados, e cujo método IsCompliant é verdadeiro apenas quando é detetado um nível de conformidade e o conjunto de problemas está vazio. Como os objetos foram expandidos no carregamento, uma matriz /OutputIntents ou um tipo de letra incorporado que residisse num fluxo de objetos será localizado e não dado como em falta.
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 à função ValidatePdfX, que devolve um TPdfXValidationResult com a mesma estrutura. A vantagem de canalizar o processo através do PDFium é que a descompressão estrutural descrita acima ocorre uma única vez, de forma correta, dentro do carregador, pelo que o seu código de validação nunca nota a diferença entre um ficheiro clássico e um totalmente comprimido. 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á se encontrarem na memória e não no disco, a mesma sequência de carregamento e validação funciona através da sobrecarga LoadDocument(const Data: TBytes), que recebe o conteúdo do ficheiro bruto e analisa os seus fluxos de referência cruzada e de objetos da mesma forma que o caminho do ficheiro. A lição a reter para um validador programado manualmente prende-se com a regra estrutural e não com a API: leia as chaves do trailer a partir do dicionário do fluxo em texto limpo, expanda todos os objetos /ObjStm com um descodificador Flate antes de percorrer o documento e encare a descodificação de entradas de referência cruzada binárias como uma tarefa de maior dimensão e opcional.
Uma vez expandida a estrutura, o validador pode controlar o resto do fluxo de trabalho. Para um sistema de linha de comandos (CLI) que reporte a conformidade numa pasta de ficheiros de entrada, consulte o nosso guia sobre a criação de um CLI de relatório de pré-voo em lote. Quando a validação serve de filtro antes de dividir um documento de grande dimensão, as técnicas tratadas no nosso guia para dividir documentos PDF em múltiplos ficheiros articulam-se naturalmente com o padrão de carregamento e validação demonstrado aqui. Ambos se baseiam no ambiente de carregamento e validação do PDFium Component para Delphi e C++Builder.