Technical Article

Estrutura do Ficheiro PDF: Cabeçalho, Corpo, Xref e Trailer

Um leitor de PDF não começa no início do ficheiro. Começa no fim. Os últimos bytes contêm o endereço de tudo o resto, e um analisador (parser) que não compreenda essa ordem lerá incorretamente o formato a partir da primeira linha. Assim, a forma mais útil de aprender sobre o PDF no disco é aprendê-lo da mesma forma que um leitor o faz: primeiro o fim, depois saltar para trás para o mapa e, em seguida, resolver os objetos para os quais o mapa aponta.

Os próprios bytes são fáceis de ler num editor de texto quando nada está comprimido. Um documento minimal de uma página que desenha "Hello, World!" ocupa menos de quinhentos bytes, e todos os elementos estruturais do formato são visíveis nele. Aqui está o ficheiro completo, com as quatro partes marcadas:

%PDF-1.0                          % Header
%âãÏÓ

1 0 obj                           % Body: the object sequence
<<
/Kids [2 0 R]
/Count 1
/Type /Pages
>>
endobj

2 0 obj
<<
/Rotate 0
/Parent 1 0 R
/Resources 3 0 R
/MediaBox [0 0 612 792]
/Contents [4 0 R]
/Type /Page
>>
endobj

3 0 obj
<< /Font << /F0 << /BaseFont /Times-Italic /Subtype /Type1 /Type /Font >> >> >>
endobj

4 0 obj
<< /Length 65 >>
stream
1. 0. 0. 1. 50. 700. cm BT
  /F0 36. Tf
  (Hello, World!) Tj
ET
endstream
endobj

5 0 obj
<< /Pages 1 0 R /Type /Catalog >>
endobj

xref                              % Cross-reference table
0 6
0000000000 65535 f
0000000015 00000 n
0000000074 00000 n
0000000192 00000 n
0000000291 00000 n
0000000409 00000 n

trailer                           % Trailer
<<
/Root 5 0 R
/Size 6
>>
startxref
459
%%EOF

Quatro partes, sempre nesta ordem ao longo do ficheiro: um cabeçalho, um corpo de objetos, uma tabela de referências cruzadas (cross-reference table) e um trailer. A questão é que os lê quase na ordem inversa. A norma ISO 32000-2 §7.5.1 define a mesma anatomia de quatro partes, e a razão para o acesso de trás para a frente é puramente prática: um leitor que salte diretamente para o objeto de que necessita é muito mais rápido do que um que verifique todos os bytes desde o início, e esse acesso aleatório é exatamente o que o trailer e a tabela de referências cruzadas servem para fornecer.

O cabeçalho tem duas linhas, e a segunda é a que importa

A primeira linha é %PDF-1.0. O sinal de percentagem torna-a num comentário no que diz respeito à sintaxe, mas os leitores tratam-na como a assinatura do ficheiro e extraem dela o número da versão. A gestão da versão é flexível na prática. Um leitor concebido para PDF 2.0 abrirá sem problemas um ficheiro que afirme ser 1.0, e a maioria dos leitores tentará abrir um ficheiro cuja versão declarada esteja incorreta ou cuja linha de versão esteja localizada um pouco mais abaixo no ficheiro, em vez de no byte zero. O número é uma indicação sobre as funcionalidades a esperar, não uma barreira.

A segunda linha é aquela que as pessoas eliminam por acidente e depois passam a tarde a depurar. Também é um comentário, mas o seu conteúdo são quatro bytes acima de ASCII 127. Existem para que qualquer ferramenta que mova o ficheiro em "modo de texto" o reconheça como binário e pare de reescrever as quebras de linha. Um PDF contém fluxos comprimidos cujos bytes podem coincidir com um retorno de carro (carriage return) ou avanço de linha (line feed) por coincidência; se uma ferramenta de transferência reescrever estes bytes, o comprimento do fluxo registado no dicionário deixará de corresponder aos bytes no disco e o ficheiro ficará corrompido. O comentário com bytes elevados é uma defesa com quarenta anos contra o FTP em modo ASCII, e ainda se encontra em todos os ficheiros que uma ferramenta profissional escreve porque a falha que previne é silenciosa e total.

O corpo contém os objetos, cada um numerado

Tudo o que constitui o documento vive no corpo como uma sequência plana de objetos indiretos. Cada um abre com dois números inteiros e a palavra-chave obj, contém o seu conteúdo e fecha com endobj. O objeto 1 no exemplo acima é o nó da árvore de páginas: 1 0 obj, depois um dicionário e endobj. O primeiro número inteiro é o número do objeto, o segundo é o número de geração. A geração é quase sempre zero num ficheiro recém-escrito; só aumenta quando um número de objeto é reutilizado em várias edições, o que é suficientemente raro para que possa tratar uma geração diferente de zero como um sinal de que o ficheiro passou por atualizações incrementais. O conteúdo entre as palavras-chave é um dicionário aqui, escrito entre << e >>, mas poderia ser um número, uma string, um array ou um fluxo (stream).

O que faz disto um grafo e não uma lista é o token de referência 2 0 R. Isto significa "objeto 2, geração 0, onde quer que ele se encontre no ficheiro". O nó da árvore de páginas acima não contém a sua página; aponta para o objeto 2, que por sua vez aponta para os seus recursos e fluxo de conteúdo através do mesmo mecanismo. O corpo é organizado na ordem que o escritor considerou conveniente, e as referências ligam-no numa árvore enraizada no catálogo. A posição no ficheiro não tem qualquer significado. A identidade vem do número do objeto, e a localização vem da tabela de referências cruzadas.

A tabela de referências cruzadas é um índice de desvios de bytes

A tabela xref é o que transforma os números de objetos em posições de ficheiro. É a razão pela qual um leitor pode abrir um documento de mil páginas e renderizar a página 850 sem analisar as 849 páginas anteriores. Cada entrada regista exatamente onde o seu objeto começa, contado em bytes a partir do início do ficheiro:

xref
0 6                  % 6 entries, starting at object 0
0000000000 65535 f
0000000015 00000 n
0000000074 00000 n
0000000192 00000 n
0000000291 00000 n
0000000409 00000 n

A largura fixa é deliberada. Cada entrada tem exatamente vinte bytes: um desvio (offset) de dez dígitos, um espaço, uma geração de cinco dígitos, um espaço, um caractere de tipo e um fim de linha de dois bytes. Como as linhas são uniformes, o leitor pode indexar diretamente para a entrada do objeto n por aritmética em vez de varrimento, de modo que a tabela que fornece acesso aleatório ao corpo é, ela própria, aleatoriamente acessível. A linha 0 6 é o cabeçalho de uma subsecção: indica que as entradas seguintes descrevem seis objetos a partir do número 0.

O objeto 0 é especial e está sempre presente. O seu tipo é f para livre (free), a sua geração é 65535, e lidera a lista ligada de números de objetos livres. Num ficheiro que nunca foi editado, a lista livre é apenas esta entrada, uma formalidade. Mostra a sua utilidade durante atualizações incrementais, quando a eliminação de um objeto adiciona o seu número a essa lista para que uma edição posterior o possa reutilizar. As restantes entradas são do tipo n para em uso, e o seu número de dez dígitos é o desvio para o qual deve navegar para ler a definição desse objeto.

O trailer é o ponto de entrada, e encontra-se no fim

O trailer é a primeira coisa que o leitor consome, embora seja escrito em último lugar. Um analisador abre o ficheiro, navega até ao fim e recua à procura de %%EOF. Logo acima dele encontra-se startxref seguido de um único número, e esse número é o desvio em bytes da palavra-chave xref. Com ele, o leitor salta diretamente para a tabela de referências cruzadas sem ter analisado um único objeto:

trailer
<<
/Root 5 0 R
/Size 6
>>
startxref
459
%%EOF

O dicionário do trailer contém os dois valores que o leitor necessita antes de poder fazer qualquer outra coisa. /Root aponta para o catálogo do documento, objeto 5 aqui, que é o topo do grafo de objetos e o caminho para a árvore de páginas. /Size é a contagem de entradas que a tabela de referências cruzadas deve conter, que é uma unidade superior ao maior número de objeto devido à entrada livre na posição zero. A partir de %%EOF, toda a sequência de leitura se desenrola: encontrar o marcador, ler startxref para localizar a tabela, carregar a tabela para saber onde vive cada objeto, ler /Root para encontrar o catálogo e resolver objetos a pedido a partir daí. O cabeçalho, no início, mal é consultado até mais tarde. O mapa no fim é o que o leitor necessita primeiro.

A atualização incremental anexa um segundo mapa em vez de reescrever

Esse design com prioridade ao fim compensa quando um ficheiro é alterado. Um PDF pode ser editado sem reescrever nenhum dos bytes que já se encontram no disco. Os objetos novos e modificados são anexados ao fim, seguidos por uma nova secção de referências cruzadas e um novo trailer, deixando o ficheiro original inalterado por baixo. A única nova tarefa de contabilidade é uma entrada /Prev no novo trailer, que guarda o desvio em bytes da tabela de referências cruzadas anterior:

% ... original file, unchanged, ends here ...

6 0 obj                          % an object added by this edit
<< /Type /Annot /Subtype /Text /Rect [100 700 120 720] >>
endobj

xref                             % a second xref section, for the new object only
6 1
0000000612 00000 n

trailer
<<
/Root 5 0 R
/Size 7
/Prev 459                        % byte offset of the earlier xref table
>>
startxref
680                              % offset of this new xref section
%%EOF

O leitor continua a começar no %%EOF final, continua a seguir o startxref para a tabela mais recente, mas agora segue a cadeia /Prev de volta para as tabelas mais antigas, fundindo-as para que a entrada mais recente para qualquer número de objeto prevaleça. As secções de referências cruzadas formam uma lista ligada ao longo do ficheiro, com cada uma a substituir a anterior para os objetos que altera. Um objeto que uma edição substituiu continua a existir fisicamente no seu antigo desvio; simplesmente já não está acessível, porque uma entrada xref posterior aponta para um local mais recente.

Este é o mecanismo que torna os PDFs assinados digitalmente verificáveis. Uma assinatura digital cobre um intervalo de bytes do ficheiro e, como uma atualização incremental apenas anexa, os bytes assinados nunca se movem. A assinatura continua a ser validada em relação ao intervalo original, enquanto as revisões posteriores se situam além dele, cada uma com o seu próprio xref e trailer. É também a razão pela qual um PDF pode conter um histórico recuperável: cada objeto substituído continua no disco sob uma secção de referências cruzadas anterior, o que é uma funcionalidade útil para o acompanhamento de versões e uma responsabilidade para quem pensava que "eliminar" significava que os bytes tinham desaparecido.

O custo é o crescimento. Cada edição anexa dados; nada é recuperado no local, pelo que um ficheiro revisto muitas vezes acumula objetos obsoletos e uma longa cadeia de secções xref. O remédio é uma reescrita completa: carregar o documento e guardá-lo como novo, o que renumera os objetos sobreviventes, descarta os inacessíveis e emite uma única tabela de referências cruzadas limpa. As duas estratégias compensam-se diretamente: anexar é rápido e preserva assinaturas e histórico; reescrever é mais lento e descarta ambos, em troca de um ficheiro compacto.

Ler as quatro partes na prática

Conhecer o esquema é suficiente para depurar manualmente a maioria dos problemas de "este ficheiro não abre". Se um leitor rejeitar um PDF, os culpados habituais estão nas duas extremidades, não no meio. Um descarregamento truncado perde o trailer, pelo que startxref ou %%EOF fica em falta e o leitor não tem ponto de entrada; os leitores tolerantes recorrem à verificação de todo o ficheiro para reconstruir o xref, que é exatamente o caminho lento que a tabela devia evitar. Uma transferência incorreta em modo de texto corrompe os bytes do fluxo ou faz com que os desvios deixem de corresponder à realidade, e os objetos são carregados a partir da posição errada. Quando os desvios na tabela já não apontam para palavras-chave obj reais, o ficheiro está estruturalmente quebrado, mesmo que cada objeto individual esteja correto.

Para novo código, a lição do esquema é deixar que uma biblioteca trate da contabilidade dos bytes. Os desvios na tabela de referências cruzadas têm de corresponder à posição real de cada objeto ao byte, o trailer tem de apontar para a tabela correta e as atualizações incrementais têm de encadear-se corretamente através de /Prev. Um componente nativo como o Componente HotPDF para Delphi e C++Builder trata de toda essa gestão quando escreve um ficheiro, incluindo a escolha entre anexar uma revisão incremental e reescrever um ficheiro compacto. Se quiser ver esta mesma estrutura construída a partir do nada em vez de dissecada, o artigo complementar sobre a construção de um documento PDF a partir do zero detalha a emissão do cabeçalho, objetos, xref e trailer em ordem.