Technical Article

Carregar PDFs de Referência Híbrida do Word e Excel em Delphi

Abra um PDF produzido pelo Microsoft Word ou Excel, folheie-o e nada parecerá invulgar. Carregue-o num programa Delphi, leia o número de páginas e a contagem estará correta. Depois, guarde-o novamente com a encriptação ativada e o processo falhará com um erro EListError, ou o resultado abrirá com um aviso de tabela de referências cruzadas danificada. O ficheiro nunca esteve corrompido. Trata-se de um ficheiro de referência híbrida, e a própria estrutura que permite a um visualizador com quinze anos de idade abri-lo é a estrutura que derrota um leitor que para de ler demasiado cedo.

Esta é uma das formas mais comuns pelas quais um pipeline de PDF que passou em todos os testes internos se depara com um ficheiro que não consegue processar de forma completa. As entradas foram todas geradas internamente, pelo que nunca foram híbridas. O primeiro ficheiro híbrido chega no dia em que um cliente encaminha uma fatura exportada de uma folha de cálculo.

O que o Word e o Excel realmente escrevem

A norma ISO 32000-1 descreve o esquema de referência híbrida no §7.5.8.4. Uma aplicação que pretenda funcionalidades do PDF 1.5, tais como fluxos de objetos (object streams), mantendo a compatibilidade com leitores de PDF 1.4, escreve a informação de referência cruzada duas vezes. Existe uma tabela clássica de referências cruzadas, com as linhas ASCII de largura fixa que terminavam todos os PDFs até à versão 1.4, e existe um fluxo de referências cruzadas que indexa o resto. O trailer da secção clássica contém uma entrada /XRefStm cujo valor é o deslocamento (offset) de bytes desse fluxo.

A divisão de trabalho é deliberada. Os objetos que um leitor antigo precisa de 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 diretamente por eles e nunca tropece numa estrutura que não consiga analisar. Las suas localizações reais residem apenas no fluxo de referências cruzadas. A assinatura de um ficheiro deste tipo é a sua cauda: uma secção clássica curta, frequentemente nada mais do que xref seguido de um cabeçalho de subsecção 0 0, cujo trailer aponta para o /XRefStm onde residem os dados reais de recuperação.

Porque é que uma contagem correta de páginas não prova nada

Como o catálogo e a árvore de páginas são intencionalmente alcançáveis a partir da tabela clássica, um carregador que leia apenas essa tabela encontra o /Root, percorre a árvore de páginas e reporta o número correto de páginas. Tudo o que um leitor antigo precisa está presente, pelo que o ficheiro parece saudável. Os objetos que ficaram em falta são aqueles que foram 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 de ser visíveis para um visualizador legado.

Não se nota esta lacuna até que algo toque nesses objetos, e um processo completo de gravação (resave) afeta todos eles. Percorrer o documento para re-encriptar ou reescrevê-lo é precisamente a operação que solicita cada número de objeto por ordem, razão pela qual o sintoma surge no momento de guardar e não de carregar, longe da sua causa raiz.

A armadilha é um detetor que vê xref e para

A forma mais simples de determinar como um ficheiro é indexado é seguir startxref e inspecionar os primeiros bytes para os quais ele aponta. A palavra-chave xref indica uma tabela clássica; um objeto de fluxo (stream object) indica um fluxo de referências cruzadas. Esse teste está correto para qualquer ficheiro que adote apenas um esquema. Está incorreto para um ficheiro híbrido, cujo startxref aponta para uma secção clássica com o único propósito de satisfazer leitores antigos, enquanto o /XRefStm no trailer dessa secção é onde a maior parte do documento está realmente indexada. Um detetor que retorne "clássico" no primeiro xref que encontra nunca lê o /XRefStm, tornando invisíveis todos os objetos que residem exclusivamente no fluxo.

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 detetor de saída antecipada ativado, o carregamento parece correto e a nova gravação é o momento em que os objetos ausentes se anunciam. A correção não passa por ler mais bytes no início; consiste em reconhecer o trailer híbrido e seguir o /XRefStm antes de decidir que o ficheiro está processado.

A ordem de junção não é negociável

Após a leitura de ambos os índices, estes só podem ser combinados numa direção. O fluxo de referências cruzadas tem de ser fundido primeiro, com as entradas clássicas preenchidas à sua volta. A razão reside na pequena ilusão no centro do formato. Um ficheiro híbrido marca os seus objetos compactados como livres na tabela clássica para que os leitores antigos os ignorem. O carregador que honre uma política de "o primeiro a aparecer ganha" e leia a tabela clássica primeiro registará esses números de objeto como livres e, em seguida, descartará as entradas de fluxo que realmente os localizam, porque as posições já estão ocupadas. Inverta a ordem e as entradas do tipo 2 do fluxo (cada uma contendo um número de fluxo de objeto e um índice) conquistam as posições a que têm direito, e as entradas clássicas organizam-se à sua volta.

A mesma disciplina evita que uma revisão mais antiga ressuscite um objeto eliminado. As atualizações incrementais encadeiam-se para trás através de /Prev, e uma entrada livre do tipo 0 é uma sentinela que indica que uma secção mais recente retirou um número de objeto. Uma secção posterior e mais antiga na cadeia não deve poder sobrescrever essa sentinela com uma localização desatualizada. Trate a primeira ocorrência como autoritária para marcadores livres e o objeto eliminado permanece eliminado; trate-o sem cuidado e a própria história do ficheiro reanimará conteúdo que a revisão mais recente removeu.

O que isto significa no HotPDF

O motor resolve ficheiros de referência híbrida por si, e fá-lo em todos os caminhos que necessitem de analisar dados de referências cruzadas. Carregue um documento com LoadFromFile ou LoadFromStream, faça as suas alterações e chame SaveLoadedDocument; ou execute uma operação direta como EncryptFile, que lê uma entrada e escreve uma saída. Em qualquer dos casos, a recuperação lê o /XRefStm, funde a secção de fluxo antes das entradas clássicas e resolve os objetos que residem em fluxos antes que a escrita os enumere. O caminho de encriptação AES-256 foi onde o problema se manifestou pela primeira vez, porque encriptar um documento reescreve cada objeto e exige, por isso, que todos os objetos tenham sido localizados previamente.

// 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 reter situa-se antes da API. Os ficheiros gerados pelo Word, Excel, PowerPoint e uma longa lista de pipelines do tipo "Guardar como PDF" são rotineiramente híbridos, pelo que um leitor que teste apenas contra a saída do seu próprio gerador poderá nunca encontrar um em testes. Prepare os seus ambientes de teste com documentos exportados de aplicações Office reais, e não apenas com ficheiros gerados pelo seu próprio código.

Verificar um ficheiro suspeito

Duas inspeções esclarecem o problema rapidamente. Abra o ficheiro numa vista hexadecimal e leia os bytes após o último startxref; um ficheiro híbrido apresenta uma secção clássica curta cujo dicionário trailer contém /XRefStm. Em alternativa, compare o número de objetos reportado por uma análise completa com o maior número de objeto declarado por /Size no trailer. Uma grande diferença significa que os objetos estão escondidos em fluxos que o carregador não abriu, a mesma falha que se transforma numa falha ao guardar mais tarde.

O lado da escrita deste cenário, e a forma como os fluxos de objetos e as referências cruzadas compactadas são produzidos em primeiro lugar, é abordado no nosso artigo sobre fluxos de objetos e atualizações incrementais. Quando o ficheiro híbrido em questão também for muito grande, as técnicas de carregamento indicadas no tutorial da API de Ficheiro Direto para fluxos de trabalho de PDFs de grande dimensão permitem inspecioná-lo sem ler a totalidade do conteúdo para a memória. Ambos combinam perfeitamente com a recuperação descrita aqui, disponibilizada como parte do HotPDF Component para Delphi e C++Builder alongside the loading, editing, encryption, and signing APIs covered elsewhere on this blog.