Um leitor de PDF não começa no início do arquivo. Ele começa no final. Os últimos bytes contêm o endereço de todo o resto, e um analisador (parser) que não entende essa ordem interpretará erroneamente o formato desde a primeira linha. Portanto, a maneira mais útil de aprender sobre PDF no disco é aprendê-lo da forma como um leitor faz: a cauda primeiro, depois pular de volta para o mapa e, em seguida, resolver os objetos para os quais o mapa aponta
Os próprios bytes são simples o suficiente para serem lidos em um editor de texto quando nada está compactado. Um documento mínimo de uma página que desenha "Hello, World!" cabe em menos de quinhentos bytes, e cada elemento estrutural do formato é visível nele. Aqui está o arquivo inteiro, 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 descendo no arquivo: um cabeçalho, um corpo de objetos, uma tabela de referência cruzada (cross-reference table) e um trailer. O problema é que você os lê quase na ordem inversa. O ISO 32000-2 §7.5.1 expõe a mesma anatomia de quatro partes, e o motivo do acesso de trás para frente é puramente prático: um leitor que pula direto para o objeto de que precisa é muito mais rápido do que um que varre (scans) cada byte a partir do topo, e esse acesso aleatório é exatamente o que o trailer e a tabela de referência cruzada existem para fornecer
O cabeçalho tem duas linhas, e a segunda importa
A primeira linha é %PDF-1.0. O sinal de porcentagem a torna um comentário no que diz respeito à sintaxe, mas os leitores a tratam como a assinatura do arquivo e extraem o número da versão dela. O tratamento de versão é flexível na prática. Um leitor construído para PDF 2.0 abrirá felizmente um arquivo que afirma ser 1.0, e a maioria dos leitores tentará abrir um arquivo cuja versão declarada está incorreta ou cuja linha de versão está enterrada um pouco dentro do arquivo em vez de no byte zero. O número é uma dica sobre quais recursos esperar, não um portão (gate)
A segunda linha é aquela que as pessoas excluem por acidente e depois passam a tarde depurando. Também é um comentário, mas sua carga útil (payload) consiste em quatro bytes acima do ASCII 127. Eles existem para que qualquer coisa que mova o arquivo no "modo de texto" o reconheça como binário e pare de reescrever finais de linha. Um PDF carrega fluxos compactados cujos bytes podem corresponder a um retorno de carro (carriage return) ou avanço de linha (line feed) por coincidência; se uma ferramenta de transferência os reescreve, o comprimento do fluxo registrado no dicionário não corresponde mais aos bytes no disco e o arquivo é corrompido. O comentário de byte alto (high-byte) é uma defesa de quarenta anos contra FTP em modo ASCII, e ainda está em todos os arquivos que uma ferramenta séria grava porque a falha que ela evita é silenciosa e total
O corpo guarda os objetos, cada um numerado
Tudo o que compõe o documento reside no corpo como uma sequência plana de objetos indiretos. Cada um abre com dois inteiros e a palavra-chave obj, guarda seu conteúdo e fecha com endobj. O Objeto 1 na amostra acima é o nó da árvore de páginas: 1 0 obj, depois um dicionário, então endobj. O primeiro inteiro é o número do objeto, o segundo é o número da geração. A geração quase sempre é zero em um arquivo recém-gravado; ela sobe apenas quando um número de objeto é reutilizado entre as edições, o que é raro o suficiente para que você possa tratar uma geração diferente de zero como um sinal de que o arquivo passou por atualizações incrementais. O conteúdo entre as palavras-chave é um dicionário aqui, escrito entre << e >>, mas poderia muito bem ser um número, uma string, um array ou um fluxo
O que torna isso um grafo em vez de uma lista é o token de referência 2 0 R. Isso significa "objeto 2, geração 0, onde quer que ele esteja no arquivo". O nó da árvore de páginas acima não contém a sua página; ele aponta para o objeto 2, que aponta para seus recursos e fluxo de conteúdo pelo mesmo mecanismo. O corpo é disposto na ordem que o criador considerou conveniente e as referências o costuram numa árvore enraizada no catálogo. A posição no arquivo não tem significado. A identidade vem do número do objeto, e a localização vem da tabela de referência cruzada
A tabela de referência cruzada é um índice de deslocamentos (offsets) de bytes
A tabela xref é o que transforma números de objeto em posições de arquivo. Ela é a razão pela qual um leitor pode abrir um documento de mil páginas e renderizar a página 850 sem analisar (parsing) as 849 páginas antes dela. Cada entrada registra exatamente onde seu objeto começa, contados em bytes desde o início do arquivo:
xref
0 6 % 6 entries, starting at object 0
0000000000 65535 f % entry 0: head of the free list
0000000015 00000 n % object 1 begins at byte 15
0000000074 00000 n % object 2 begins at byte 74
0000000192 00000 n % object 3 begins at byte 192
0000000291 00000 n % object 4 begins at byte 291
0000000409 00000 n % object 5 begins at byte 409
A largura fixa é intencional. Cada entrada tem exatamente vinte bytes: um deslocamento (offset) de dez dígitos, um espaço, uma geração de cinco dígitos, um espaço, um tipo de um caractere, e um fim de linha de dois bytes. Como as linhas são uniformes, um leitor pode indexar direto para a entrada do objeto n por meio de aritmética, em vez de fazer uma varredura (scanning), de modo que a tabela que dá acesso aleatório ao corpo é, ela própria, acessível aleatoriamente. A linha 0 6 é um cabeçalho de subseção: ela diz que as próximas entradas descrevem seis objetos começando pelo número 0
O Objeto 0 é especial e sempre está presente. Seu tipo é f de 'free' (livre), sua geração é 65535, e ele encabeça a lista ligada (linked list) de números de objetos livres. Em um arquivo que nunca foi editado a lista livre é apenas esta única entrada, uma formalidade. Ela compensa (earns its keep) durante atualizações incrementais, quando deletar um objeto adiciona o seu número àquela lista para que uma edição posterior possa recuperá-lo. As outras entradas são do tipo n para 'in-use' (em uso), e seu número de dez dígitos é o deslocamento que você procuraria (seek to) a fim de ler a definição daquele objeto
O trailer é o ponto de entrada e fica no final
O trailer é a primeira coisa que um leitor realmente consome, mesmo que seja escrito por último. Um analisador (parser) abre o arquivo, busca até o final (seeks to the end), e caminha para trás procurando por %%EOF. Logo acima dele fica startxref seguido de um único número, e esse número é o deslocamento de bytes da palavra-chave xref. Com ele, o leitor pula diretamente para a tabela de referência cruzada sem ter varrido um único objeto:
trailer
<<
/Root 5 0 R % the document catalog
/Size 6 % one more than the highest object number
>>
startxref
459 % byte offset of the xref table
%%EOF
O dicionário de trailer carrega os dois valores de que um leitor precisa 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ência cruzada deve conter, que é uma a mais que o maior número de objeto por causa da entrada livre no slot zero. Do %%EOF toda a sequência de leitura se desenrola: encontre o marcador, leia startxref para localizar a tabela, carregue a tabela para saber onde cada objeto vive, leia /Root para encontrar o catálogo, e resolva os objetos sob demanda a partir daí. O cabeçalho, posicionado no topo, é pouco consultado até o final. O mapa na parte inferior é o que o leitor precisa primeiro
A atualização incremental anexa um segundo mapa em vez de reescrever
Esse design de "cauda primeiro" compensa quando um arquivo muda. Um PDF pode ser editado sem reescrever nenhum dos bytes já no disco. Objetos novos e modificados são anexados ao final, seguidos de uma nova seção de referência cruzada e de um novo trailer, e o arquivo original subjacente é deixado intocado. O único trabalho de manutenção (bookkeeping) adicional é uma entrada /Prev no novo trailer, mantendo o deslocamento de byte da tabela de referência cruzada 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
Um leitor ainda começa no %%EOF final, ainda segue o startxref até a tabela mais recente, mas agora segue a cadeia /Prev para trás até tabelas mais antigas, fundindo-as para que a entrada mais nova de qualquer número de objeto ganhe. As seções de referência cruzada formam uma lista ligada (linked list) descendo através do arquivo, cada uma substituindo a anterior para os objetos em que toca. Um objeto que uma edição substituiu ainda existe fisicamente em seu deslocamento antigo; ele simplesmente não é mais alcançável, porque uma entrada xref posterior aponta para um lugar mais novo
Esse é o mecanismo que torna os PDFs assinados verificáveis. Uma assinatura digital cobre um intervalo de bytes do arquivo, e, como uma atualização incremental apenas anexa as coisas, os bytes assinados nunca se movem. A assinatura ainda se valida contra o intervalo original enquanto as revisões posteriores ficam além dele, cada uma com o seu próprio xref e trailer. É também o por que um PDF pode carregar um histórico recuperável: todo objeto substituído ainda está no disco, em uma seção de referência cruzada anterior, o que é um recurso para o rastreamento de versão e um risco para qualquer um que achasse que "excluir" significava que os bytes teriam desaparecido
O custo é o crescimento. Cada edição é anexada; nada é recuperado no lugar (reclaimed in place), de modo que um arquivo revisado várias vezes acumula objetos inativos (dead objects) e uma longa cadeia de seções xref. O remédio é uma reescrita completa: carregue o documento e o salve de novo (save it fresh), o que renumera os objetos sobreviventes, descarta os inalcançáveis e emite uma única e limpa tabela de referência cruzada. As duas estratégias se contrapõem diretamente. Anexar é rápido e preserva assinaturas e histórico; reescrever é mais lento e descarta os dois, em troca de um arquivo compacto
Lendo as quatro partes na prática
Conhecer o layout é suficiente para depurar (debug) à mão a maioria dos problemas do tipo "este arquivo não abre". Se um leitor rejeita um PDF, os culpados habituais estão nas duas extremidades, não no meio. Um download truncado perde o trailer, então o startxref ou o %%EOF está faltando e o leitor não tem um ponto de entrada; leitores tolerantes recaem no rastreamento de todo o arquivo (scanning) para reconstruir o xref, que é exatamente o caminho lento que a tabela deveria evitar. Uma transferência em modo texto malfeita (botched) corrompe os bytes do fluxo ou os deslocamentos (offsets) deixam de corresponder à realidade, e os objetos são carregados da posição errada. Quando os deslocamentos na tabela não apontam mais para palavras-chave obj reais, o arquivo fica estruturalmente quebrado (structurally broken), mesmo se cada objeto estiver bem individualmente
Para novos códigos, a lição sobre layout é deixar que uma biblioteca possua o controle de bytes. Os deslocamentos na tabela de referência cruzada têm de concordar com as posições reais de cada objeto até o byte, o trailer tem de apontar para a tabela certa, e as atualizações incrementais têm de se encadear corretamente através do /Prev. Um componente nativo como o Componente HotPDF para Delphi e C++Builder cuida de tudo isso quando grava um arquivo, incluindo a escolha entre anexar uma revisão incremental e reescrever uma compacta. Se você deseja ver a mesma estrutura sendo construída a partir do nada, em vez de dissecada, a peça que a acompanha sobre construir um documento PDF do zero orienta a emissão do cabeçalho, objetos, xref e trailer em ordem