Pular para o conteúdo
getnextpdf.com

A anatomia de um arquivo PDF

Abra qualquer PDF em um editor de texto simples e a primeira coisa que você vê é reconfortante: um cabeçalho %PDF-1.x ou %PDF-2.0. A última coisa que você vê é %%EOF. Tudo entre essas duas linhas é uma pequena e arrumada máquina para encontrar objetos por número. Esta página é uma dissecação. Abrimos o arquivo, nomeamos cada órgão e mostramos como eles se conectam.

É a companheira estrutural de dois vizinhos. O que um PDF realmente é trata o arquivo como um grafo de objetos; atualizações incrementais cobrem como ele cresce ao longo do tempo. Esta página fica perto dos bytes — as regiões físicas que um parser percorre, na ordem em que elas se assentam no disco.

Você quase nunca precisa disto para usar um PDF. Você precisa no dia em que um deles dá errado. Um arquivo abre em um visualizador e não em outro; um validador reporta uma “tabela de referências cruzadas danificada”; um documento assinado de repente falha ao verificar. Nenhum desses casos é mistério quando você consegue ler a anatomia. Eles são um número que já não corresponde a uma posição, uma região no lugar errado, ou uma cauda que aponta para o nada.

Conhecer o layout transforma “o PDF está corrompido” em um diagnóstico sobre o qual você pode agir. É a diferença entre dar de ombros diante de uma caixa-preta e apontar para o byte exato que mente.

Um PDF em conformidade tem quatro partes físicas, nesta ordem de arquivo (Spec: ISO 32000-2, §7.5.1):

  1. Um cabeçalho — uma linha, %PDF-2.0, nomeando a versão.
  2. Um corpo — o grosso do arquivo: uma sequência de objetos indiretos numerados.
  3. Uma seção de referências cruzadas — um índice do número de objeto até o offset de byte onde esse objeto vive. PDFs clássicos usam uma tabela de texto; o PDF 2.0 usa um xref stream comprimido.
  4. Um trailer — um pequeno dicionário nomeando o ponto de entrada, seguido de startxref, um offset e %%EOF.

A reviravolta: um leitor não começa pelo topo. Ele começa pelo fim, lê startxref para achar o índice, e usa esse índice para alcançar qualquer objeto diretamente. O arquivo é escrito de frente para trás, mas lido de trás para a frente.

Vamos percorrer as quatro regiões em ordem, com os bytes à nossa frente.

O cabeçalho é uma linha. O NextPDF escreve %PDF-2.0 e, por convenção, uma segunda linha de comentário com bytes de bit alto, para que ferramentas de transferência ingênuas tratem o arquivo como binário, não como texto. Essa segunda linha é o motivo de um PDF aberto como texto simples mostrar um pequeno embaralhado logo depois da versão.

O corpo é onde o documento vive. Cada objeto indireto é um número, uma geração, a palavra-chave obj, um valor, e endobj (Spec: ISO 32000-2, §7.3.10). O valor é uma de poucas formas — mas duas carregam quase todo o peso:

  • Um dicionário, << /Key value … >>, é um mapa de nomes para valores. A árvore de páginas, o catálogo, os descritores de fonte: todos dicionários.
  • Um stream é um dicionário seguido de stream, um bloco de bytes arbitrários, e endstream. O conteúdo da página, fontes embutidas e imagens são streams, quase sempre comprimidos. (Seus filtros são uma história à parte, contada em streams e filtros.)

Os objetos apontam uns para os outros por referência indireta2 0 R significa “objeto 2, geração 0”. É essa a fiação que transforma uma lista plana de objetos em um grafo.

A seção de referências cruzadas é a parte que a maioria das pessoas nunca imagina corretamente. Na forma clássica ela é texto simples: a palavra-chave xref, depois subseções de linhas de largura fixa de 20 bytes (Spec: ISO 32000-2, §7.5.4). Cada linha é um offset de byte de dez dígitos, uma geração de cinco dígitos, e um único flag — n para em uso, f para livre — preenchida até exatamente vinte bytes, de modo que um leitor pode saltar para qualquer entrada apenas por aritmética. O PDF 2.0 substitui isso por um xref stream: o mesmo índice, mas binário e comprimido dentro de um objeto stream marcado como /Type /XRef (Spec: ISO 32000-2, §7.5.8). Menor, e capaz de descrever objetos empacotados dentro de object streams.

O trailer é o sumário do arquivo (Spec: ISO 32000-2, §7.5.5). Ele nomeia /Root — o catálogo do documento, o único objeto do qual tudo o mais pende — e /Size, a contagem de objetos. Depois vem o aperto de mão que torna possível a leitura de trás para a frente: startxref, um offset de byte em sua própria linha, e %%EOF. Um leitor salta para o fim, lê esse offset, pula direto para a seção de referências cruzadas, e parte dali.

  1. CabeçalhoUma linha, %PDF-2.0, nomeando a versão. Um comentário marcador de binário geralmente vem em seguida.
  2. CorpoObjetos indiretos numerados — dicionários e streams — referenciados por N G R.
  3. Seção de referências cruzadasUma tabela de texto de entradas de 20 bytes, ou um stream comprimido /Type /XRef no PDF 2.0.
  4. TrailerNomeia /Root e /Size, depois startxref + offset + %%EOF.
  5. Ordem de leituraUm leitor começa em %%EOF, segue startxref até o índice, e então alcança cada objeto diretamente.
The four physical regions of a PDF in file order, and the path a reader actually takes through them — starting at the trailer and working inward via the cross-reference section.

Há uma quinta região que um arquivo de vida longa cria: uma atualização incremental. Uma mudança não é escrita no lugar. Os objetos alterados, uma nova seção de referências cruzadas e um novo trailer são anexados depois do primeiro %%EOF, e esse novo trailer carrega /Prev — o offset da seção de referências cruzadas anterior (Spec: ISO 32000-2, §7.5.6). As seções formam uma cadeia para trás; para qualquer número de objeto, a entrada mais nova vence. Como os bytes originais nunca se movem, a verificação criptográfica sobre a faixa de bytes que uma assinatura de fato cobre continua válida após uma atualização. Uma atualização incremental posterior ainda pode alterar o que um validador reporta sobre o documento como um todo — se as mudanças pós-assinatura são permitidas, e o que se entende que a assinatura certifica — mas ela não pode alterar os bytes assinados em si. Essa propriedade é o assunto inteiro de atualizações incrementais.

Aqui está a anatomia inteira em um arquivo mínimo. Os números sob xref são offsets de byte, e eles têm de ser exatos — aponte um caractere além de onde um objeto começa e um leitor estrito desiste.

%PDF-2.0
1 0 obj
<< /Type /Catalog /Pages 2 0 R >>
endobj
2 0 obj
<< /Type /Pages /Kids [3 0 R] /Count 1 >>
endobj
3 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>
endobj
xref
0 4
0000000000 65535 f
0000000009 00000 n
0000000058 00000 n
0000000115 00000 n
trailer
<< /Size 4 /Root 1 0 R >>
startxref
186
%%EOF

Leia-o do jeito que um parser faz. Última linha: %%EOF. Acima dela: startxref 186, então salte para o byte 186, onde xref começa. A tabela diz que o objeto 1 vive no byte 9. O /Root 1 0 R do trailer aponta para lá — o catálogo — e a partir do catálogo você percorre /Pages até a árvore de páginas e encontra a única página. O objeto 0 é sempre a cabeça da lista de livres com geração 65535, um fóssil do primeiro design do formato que todo leitor ainda espera ver.

A armadilha é ler um PDF como uma história — de cima para baixo, em ordem. Ele não é uma história; é um índice com um ponteiro para trás. Os números de objeto não precisam ser sequenciais no arquivo, os objetos podem aparecer em qualquer ordem física, e um leitor nunca confia na posição deles. O único mapa autoritativo é a seção de referências cruzadas, e a única forma de encontrar esse mapa é o offset startxref no fim de tudo.

A consequência surpreende as pessoas. Um PDF com um corpo impecável e um dígito errado em startxref é ilegível — o leitor não consegue achar o índice. Um PDF com seus objetos em ordem embaralhada, mas uma seção de referências cruzadas correta, está perfeitamente bem. A posição física não carrega significado algum. A posição registrada carrega todo ele.

Esta página descreve a estrutura física, não o conteúdo da página. Como as marcas chegam a uma página — operadores de stream de conteúdo, exibição de texto, estado gráfico — é um assunto à parte. Ela também descreve um arquivo bem-formado. PDFs do mundo real frequentemente estão um pouco quebrados e sobrevivem apenas porque visualizadores tolerantes reconstroem a tabela de referências cruzadas varrendo por palavras-chave obj. Esse resgate é um comportamento do visualizador, não algo que o formato garante.

Reading and repairing arbitrary third-party PDFs — edition availability
EditionAvailability
CoreO NextPDF é um escritor. Ele registra cada offset a partir do buffer de saída no momento em que cada objeto é emitido, de modo que os arquivos que produz têm uma seção de referências cruzadas que corresponde ao corpo por construção.
ProFazer parsing, reconstruir ou reparar uma tabela de referências cruzadas danificada em um arquivo que o NextPDF não escreveu está fora do escopo em todas as edições.
EnterprisePara inspeção da estrutura de um arquivo existente, use um parser ou validador dedicado; o NextPDF garante correção para o que escreve, não para o que lê.

Por que um PDF tem uma linha marcadora de binário depois do cabeçalho? Algumas ferramentas de transferência mais antigas estragavam um arquivo que julgavam ser texto simples. O comentário de bit alto faz o arquivo parecer inequivocamente binário, de modo que ele sobrevive à viagem sem alterações.

O xref stream é só uma tabela menor? Quase, com um poder extra. Além de ser comprimido, um xref stream pode descrever objetos armazenados dentro de object streams — entradas que a clássica tabela de texto de 20 bytes não tem como expressar.

Posso ter tanto uma tabela quanto um stream em um arquivo? Uma única revisão usa uma ou outra. Mas um arquivo híbrido pode parear uma tabela clássica para leitores antigos com um xref stream para os novos, de modo que cada tipo de leitor encontra um índice que entende.

  • Cabeçalho — a primeira linha, %PDF-2.0, nomeando a versão; geralmente seguida de um comentário marcador de binário.
  • Objeto indireto — um objeto numerado no corpo, escrito N G obj … endobj, onde N é o número do objeto e G a geração.
  • Dicionário — um mapa << /Key value … >> de nomes para valores; a forma de objeto mais comum.
  • Stream — um dicionário mais um bloco de bytes entre stream e endstream, usado para conteúdo, fontes e imagens.
  • Tabela de referências cruzadas (xref) — o índice do número de objeto até o offset de byte; classicamente uma tabela de texto de 20 bytes por entrada, um stream /Type /XRef no PDF 2.0.
  • Trailer — o dicionário que nomeia /Root e /Size, localizado via o offset startxref no fim do arquivo.
  • Atualização incremental — objetos alterados, uma nova seção de referências cruzadas e um novo trailer anexados depois de %%EOF, com /Prev encadeando de volta à seção anterior.