Abra um PDF produzido pelo Microsoft Word ou Excel, percorra as páginas e nada parece incomum. Carregue-o em um programa Delphi, leia de volta a contagem de páginas e o número está correto. Em seguida, salve-o novamente com a criptografia ativada e o trabalho falha com um EListError, ou a saída é aberta com um aviso de referência cruzada danificada. O ficheiro nunca esteve corrompido. É um ficheiro de referência híbrida, e a mesma estrutura que permite que um visualizador de quinze anos o abra é a estrutura que derrota um carregador que para de ler muito cedo
Esta é uma das maneiras mais comuns de um pipeline de PDF que passou em todos os testes internos encontrar um ficheiro que não consegue guardar de volta (round-trip). As entradas foram todas geradas internamente, então elas 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
O ISO 32000-1 descreve o layout de referência híbrida na §7.5.8.4. Um aplicação que deseja os recursos do PDF 1.5, como fluxos de objetos, e ao mesmo tempo permitir que um leitor de PDF 1.4 abra o ficheiro, grava as informações de referência cruzada duas vezes. Existe uma tabela de referência cruzada clássica, as linhas ASCII de largura fixa que terminavam todos os PDFs até a versão 1.4, e existe 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 em bytes daquele fluxo
A divisão de trabalho é deliberada. Os objetos que um leitor antigo tem que alcançar, o catálogo e a árvore de páginas entre eles, são endereçáveis a partir da tabela clássica. Os objetos que foram dobrados 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 residem apenas no fluxo de referência cruzada. A assinatura de um ficheiro desse tipo é sua cauda: 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 estão os dados reais de recuperação
Por que uma contagem de páginas correta não prova nada
Como o catálogo e a árvore de páginas podem ser acessados 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 que um leitor antigo precisa está presente, então o ficheiro parece saudável. Os objetos que desapareceram são os agrupados em fluxos de objetos: dicionários de campos AcroForm, elementos de estrutura de PDF com tags, a longa cauda de pequenos dicionários que nunca precisaram ficar visíveis para um visualizador legado
Você não percebe a lacuna até que algo toque nesses objetos, e um salvamento completo toca em todos eles. Percorrer o documento para criptografá-lo ou reescrevê-lo novamente é precisamente a operação que pede cada número de objeto, um por um, e é por isso que o sintoma surge no momento de guardar e não no momento do carregamento, longe da sua causa
A armadilha é um detector que vê o xref e para
A maneira mais barata de decidir como um ficheiro é indexado é seguir o startxref e inspecionar os primeiros bytes para os quais ele aponta. A palavra-chave xref significa uma tabela clássica; um objeto de fluxo significa um fluxo de referência cruzada. Esse teste é correto para qualquer ficheiro que se comprometa com um único esquema. É errado para um ficheiro 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 é realmente indexada. Um detector que retorna "clássico" no primeiro xref que encontra nunca lê /XRefStm, e cada objeto que reside apenas no fluxo torna-se invisível
Com o detector de saída antecipada em vigor, o carregamento parece normal e o salvamento é onde os objetos ausentes se anunciam. A solução não é ler mais bytes no início; é reconhecer o trailer híbrido e seguir /XRefStm antes de decidir que o ficheiro está concluído
A ordem de mesclagem não é negociável
Uma vez que ambos os índices tenham sido lidos, eles só podem ser combinados em uma direção. O fluxo de referência cruzada tem que ser mesclado primeiro, com as entradas clássicas preenchidas em torno dele. O motivo é a pequena decepção no coração do formato. Um ficheiro híbrido marca seus objetos compactados como livres na tabela clássica para que leitores antigos os ignorem. Um carregador que honra uma política de 'o primeiro visto vence' e lê a tabela clássica primeiro registrará esses números de objeto como livres e 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 sendo um número de fluxo de objeto mais um índice, ganham os slots que deveriam possuir, e as entradas clássicas se acomodam ao redor delas
A mesma disciplina protege contra a ressurreição de um objeto excluído por uma revisão mais antiga. Atualizações incrementais encadeiam-se de volta através de /Prev, e uma entrada livre do tipo 0 é uma sentinela de que uma seção mais recente aposentou um número de objeto. Não se deve permitir que uma seção posterior e mais antiga da cadeia sobrescreva aquela sentinela com um local desatualizado. Trate o 'primeiro a ser visto' como definitivo para os marcadores livres e o objeto excluído continuará excluído; trate-o sem cuidado e a própria história de um ficheiro reanimará o conteúdo que a última revisão removeu
O que isso significa no HotPDF
O mecanismo resolve ficheiros de referência híbrida para você, e faz isso em cada caminho que precisa 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 de uso único, como EncryptFile, que lê uma entrada e escreve uma saída. De qualquer forma, a recuperação lê o /XRefStm, mescla a seção de fluxo à frente das entradas clássicas e resolve os objetos que vivem em fluxos antes que a gravação os enumere. O caminho de criptografia AES-256 é onde o problema se manifestou pela primeira vez, porque criptografar um documento reescreve todos os objetos e, portanto, exige que cada objeto já tenha sido localizado
O detalhe que vale a pena levar reside a montante da API. Arquivos que chegam do Word, Excel, PowerPoint e de uma longa lista de pipelines do tipo "Salvar como PDF" são rotineiramente híbridos, de modo que um carregador que você exercita apenas com a saída do seu próprio gerador pode nunca encontrar um durante os testes. Preencha seus ambientes de teste com documentos exportados de aplicações Office reais, não apenas com ficheiros que seu próprio código produziu
Verificando um ficheiro suspeito
Duas inspeções resolvem a questão rapidamente. Abra o ficheiro numa visualização hexadecimal e leia os bytes após o startxref final; um ficheiro híbrido mostra uma pequena seção clássica cujo dicionário do 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 lacuna significa que os objetos estão escondidos em fluxos que o carregador não abriu, o que é a mesma falha que se transforma num erro de salvamento mais tarde
A cauda de uma exportação típica do Excel torna a primeira verificação concreta. Tudo após a palavra-chave final xref é puro ASCII, portanto, a assinatura é legível diretamente de uma visualização hexadecimal (deslocamentos ilustrativos, anotações adicionadas)
A subseção 0 0 é o indício: uma tabela clássica com zero entradas existe apenas para carregar o trailer, e o trailer existe principalmente para dizer /XRefStm 87325. Um detector que para na palavra-chave xref viu, neste ponto, um índice de nada. Quando você prefere fazer um script para a verificação em vez de analisar a olho nu, o marcador sempre fica nos últimos kilobytes do ficheiro, portanto, uma leitura retroativa limitada é suficiente
Trate a sonda como uma triagem, não como um analisador: ela diz quais ficheiros num lote merecem atenção antes que um trabalho de salvamento seja executado, e nada mais. O que um carregador deve então fazer com o deslocamento que encontrar, seguindo a cadeia da seção, mesclando as entradas de fluxo à frente das clássicas, respeitando as sentinelas de entrada livre, é explicado passo a passo em nosso artigo complementar sobre a manipulação de PDFs de referência híbrida de aplicações do Office
O lado de gravação desta história, ou seja, como fluxos de objetos e referências cruzadas compactadas são produzidos em primeiro lugar, é abordado em nosso artigo sobre fluxos de objetos e atualizações incrementais. Quando o ficheiro híbrido em questão também é muito grande, as técnicas de carregamento explicadas no passo a passo da Direct File API para fluxos de trabalho de PDFs grandes permitem que você o inspecione sem ter que ler o ficheiro inteiro na memória. Ambos combinam naturalmente com a recuperação descrita aqui, que é fornecida como parte do Componente HotPDF para Delphi e C++Builder junto com as APIs de carregamento, edição, criptografia e assinatura abordadas em outras partes deste blog