Abra um PDF gerado pelo Microsoft Word ou Excel, folheie suas páginas e nada parecerá incomum. Carregue-o em um programa Delphi, leia a contagem de páginas e o número estará correto. Em seguida, salve-o novamente com a criptografia ativada e a tarefa falhará com um EListError, ou a saída será aberta com um aviso de referência cruzada danificada. O arquivo nunca esteve corrompido. Trata-se de um arquivo de referência híbrida, e a mesma estrutura que permite a um visualizador de quinze anos de idade abri-lo é a estrutura que derrota um carregador que interrompe a leitura cedo demais.
Esta é uma das maneiras mais comuns de um fluxo de PDF que passou em todos os testes internos encontrar um arquivo que não consegue processar por completo. As entradas eram todas geradas internamente, portanto nunca eram híbridas. O primeiro arquivo híbrido chega no dia em que um cliente encaminha uma fatura exportada de uma planilha.
O que o Word e o Excel realmente gravam
A ISO 32000-1 descreve o layout de referência híbrida no §7.5.8.4. Um aplicativo que deseja recursos do PDF 1.5, como fluxos de objetos, mas ainda permitindo que um leitor de PDF 1.4 abra o arquivo, grava as informações de referência cruzada duas vezes. Há uma tabela de referência cruzada clássica, com as linhas ASCII de largura fixa que encerravam todos os PDFs até a versão 1.4, e há um fluxo de referência cruzada que indexa o restante. O trailer da seção clássica contém uma entrada /XRefStm cujo valor é o deslocamento de bytes desse fluxo.
A divisão de trabalho é deliberada. Os objetos que um leitor antigo precisa alcançar, incluindo o catálogo e a árvore de páginas, são endereçáveis a partir da tabela clássica. Os objetos que foram agrupados em fluxos de objetos compactados são marcados como livres na tabela clássica, com uma entrada do tipo f, para que um leitor 1.4 passe direto por eles e nunca tropece em uma estrutura que não consiga analisar. Suas localizações reais vivem apenas no fluxo de referência cruzada. A assinatura de um arquivo desse tipo é o seu final: uma seção clássica curta, frequentemente nada mais do que xref seguido por um cabeçalho de subseção 0 0, cujo trailer aponta para o /XRefStm onde os dados reais de recuperação estão localizados.
Por que uma contagem de páginas correta não prova nada
Como o catálogo e a árvore de páginas são alcançáveis a partir da tabela clássica de propósito, um carregador que lê apenas essa tabela encontra o /Root, percorre a árvore de páginas e relata o número correto de páginas. Tudo o que um leitor antigo precisa está presente, logo o arquivo parece saudável. Os objetos que ficaram ausentes são aqueles compactados em fluxos de objetos: dicionários de campos AcroForm, elementos de estrutura de PDF estruturado (tagged-PDF) e a longa cauda de pequenos dicionários que nunca precisaram ser visíveis para um visualizador antigo.
Você não percebe essa lacuna até que algo toque nesses objetos, e um salvamento completo afeta todos eles. Percorrer o documento para criptografá-lo ou gravá-lo novamente é exatamente a operação que solicita cada número de objeto por vez, e é por isso que o sintoma surge no momento de salvar, e não no carregamento, longe de sua causa original.
A armadilha é um detector que vê xref e para
A maneira mais simples de decidir como um arquivo é indexado é seguir o startxref e inspecionar os primeiros bytes para os quais ele aponta. A palavra-chave xref indica uma tabela clássica; um objeto de fluxo indica um fluxo de referência cruzada. Esse teste é correto para qualquer arquivo que adote apenas um esquema. Porém, está errado para um arquivo híbrido, cujo startxref aponta para uma seção clássica com o único propósito de satisfazer leitores antigos, enquanto o /XRefStm no trailer dessa seção é onde a maior parte do documento está realmente indexada. Um detector que retorna "classic" no primeiro xref que encontra nunca lê o /XRefStm, e cada objeto que reside apenas no fluxo torna-se invisível.
var
Pdf: THotPDF;
PageCount: Integer;
begin
Pdf := THotPDF.Create(nil);
try
PageCount := Pdf.LoadFromFile('Invoice_XLS.pdf'); // count is correct
// inspect or edit the loaded document here
Pdf.SaveLoadedDocument('Invoice_secured.pdf'); // walks every object
finally
Pdf.Free;
end;
end;
Com o detector de saída antecipada em ação, o carregamento parece bom e o salvamento posterior é onde os objetos ausentes se anunciam. A correção não consiste em ler mais bytes no início; trata-se de reconhecer o trailer híbrido e seguir o /XRefStm antes de decidir que a leitura do arquivo terminou.
A ordem de mesclagem não é negociável
Uma vez lidos ambos os índices, eles podem ser combinados em apenas uma direção. O fluxo de referência cruzada deve ser mesclado primeiro, com as entradas clássicas preenchidas ao redor dele. A razão é a pequena artimanha no coração do formato. Um arquivo híbrido marca seus objetos compactados como livres na tabela clássica para que os leitores antigos os ignorem. Um carregador que adota uma política de prioridade para o que lê primeiro e processa a tabela clássica antes registrará esses números de objetos como livres e, em seguida, descartará as entradas de fluxo que realmente os localizam, porque os slots já estão ocupados. Inverta a ordem e as entradas do tipo 2 do fluxo, cada uma com um número de fluxo de objeto mais um índice, conquistam os slots que devem possuir, e as entradas clássicas se acomodam ao redor delas.
A mesma disciplina impede que uma revisão mais antiga ressuscite um objeto excluído. As atualizações incrementais encadeiam-se para trás por meio de /Prev, e uma entrada livre do tipo 0 é uma sentinela de que uma seção mais recente aposentou um número de objeto. Uma seção posterior e mais antiga na cadeia não deve ter permissão para sobrescrever essa sentinela com uma localização desatualizada. Trate o primeiro visto como autoridade para marcadores livres e o objeto excluído permanecerá excluído; trate isso sem cuidado e o próprio histórico do arquivo reanimará o conteúdo que a revisão mais recente removeu.
O que isso significa no HotPDF
O mecanismo resolve os arquivos de referência híbrida para você, e faz isso em todos os caminhos que precisam analisar os dados de referência cruzada. Carregue um documento com LoadFromFile ou LoadFromStream, faça suas alterações e chame SaveLoadedDocument; ou execute uma operação direta como EncryptFile que lê uma entrada e grava uma saída. De qualquer forma, a recuperação lê o /XRefStm, mescla a seção do fluxo antes das entradas clássicas e resolve os objetos localizados nos fluxos antes que a gravação os enumere. O caminho de criptografia AES-256 foi onde o problema se manifestou pela primeira vez, porque criptografar um documento grava novamente cada objeto e, portanto, exige que todos os objetos já tenham sido localizados.
// One-shot: read the hybrid input, write an AES-256 encrypted copy
Pdf.EncryptFile('Letter_DOC.pdf', 'Letter_secured.pdf',
'owner-secret', '', aes256, [prPrint, prFillAnnotations]);
O detalhe importante a ser lembrado está acima da API. Arquivos provenientes de Word, Excel, PowerPoint e de uma longa lista de pipelines de "Salvar como PDF" são rotineiramente híbridos, portanto um carregador que você executa apenas contra a saída do seu próprio gerador pode nunca encontrar um em testes. Alimente seus testes com documentos exportados de aplicativos reais do Office, e não apenas com arquivos gerados pelo seu próprio código.
Verificando um arquivo suspeito
Duas inspeções resolvem a questão rapidamente. Abra o arquivo em uma visualização hexadecimal e leia os bytes após o último startxref; um arquivo híbrido exibe uma seção clássica curta cujo dicionário de trailer contém /XRefStm. Ou compare a contagem de objetos que uma análise completa relata com o maior número de objeto que o /Size declara no trailer. Uma grande diferença significa que os objetos estão escondidos em fluxos que o carregador não abriu, a mesma lacuna que resultará em uma falha ao salvar mais tarde.
O lado da gravação desta história, sobre como os fluxos de objetos e referências cruzadas compactadas são produzidos originalmente, é coberto em nosso artigo sobre fluxos de objetos e atualizações incrementais. Quando o arquivo híbrido em questão também for muito grande, as técnicas de carregamento do tutorial da API Direct File para fluxos de PDF grandes permitem que você o inspecione sem carregar tudo na memória. Ambos se alinham de forma natural com a recuperação descrita aqui, que é fornecida como parte do Componente HotPDF para Delphi e C++Builder, juntamente com as APIs de carregamento, edição, criptografia e assinatura descritas em outras seções deste blog.