Pular para o conteúdo

O que um PDF realmente é

Evidence: Standard-backed

Um PDF não é uma descrição de página que por acaso está em um arquivo. É um pequeno banco de dados em grafo com uma impressora acoplada. Esta página descreve as quatro partes presentes em todo PDF — header, body, tabela de referências cruzadas, trailer — e como o NextPDF as grava para que um leitor encontre cada objeto sem adivinhar.

A maioria dos bugs de PDF não é bug de renderização. São bugs de estrutura: um byte offset que aponta um caractere além do objeto correto, um trailer que nomeia o root errado, uma entrada de referência cruzada que discorda de onde o objeto de fato está. Nenhum deles muda a aparência de uma página até que um leitor siga um caminho diferente pelo arquivo e avance além do fim dele.

Se você tratar um PDF como opaco, essas falhas parecem aleatórias. Se você conhece o modelo de objetos, elas parecem exatamente o que são: um número que não corresponde a uma posição. Entender o formato é a diferença entre “o PDF está corrompido” e “o offset do objeto 14 está defasado porque o writer o mediu antes de finalizar o comprimento do stream.”

Um PDF tem quatro partes, na ordem do arquivo:

  1. Um header — uma linha que nomeia a versão (%PDF-2.0).
  2. Um body — uma sequência de objetos indiretos numerados: dicionários, streams, arrays, números, strings, nomes.
  3. Uma tabela de referências cruzadas (ou, no PDF 2.0, um stream de referências cruzadas) — um mapeamento do número do objeto para o byte offset, de modo que qualquer objeto possa ser alcançado sem varrer o arquivo.
  4. Um trailer — um pequeno dicionário que nomeia o objeto root do documento e aponta para onde a seção de referências cruzadas começa.

Um leitor não lê um PDF do início ao fim. Ele lê a última linha primeiro, encontra startxref, vai para a seção de referências cruzadas e a usa como índice no body. O formato foi construído para ser lido de trás para frente. Esse único fato explica a maior parte do seu design.

O NextPDF constrói um PDF do mesmo modo como o formato é lido: primeiro o objeto, depois o offset registrado, e por último a tabela gravada.

Todo objeto indireto recebe um número a partir de um único registro (src/Core/ObjectRegistry.php). O registro atribui números sequenciais por meio de allocate() e, depois que os bytes de um objeto são gravados no buffer de saída, registra o byte offset por meio de register(). Os offsets nunca são adivinhados de antemão. Eles são observados a partir de BinaryBuffer::getOffset() no momento em que o cabeçalho do objeto é emitido. É por isso que uma entrada de referência cruzada do NextPDF não pode divergir do objeto que descreve: o offset é exatamente a posição em que o buffer de fato estava.

Quando o body está completo, uma estratégia de serialização específica da versão (src/Writer/PdfSerializationStrategy.php) grava a seção de referências cruzadas e o trailer:

  • Pdf20StreamStrategy emite um stream comprimido de referências cruzadas (/Type /XRef) — o padrão do PDF 2.0.
  • Pdf17TableStrategy e Pdf14TableStrategy emitem uma tabela tradicional de referências cruzadas de 20 bytes, mais um dicionário de trailer separado — exigido pelos perfis PDF/A que requerem a estrutura de arquivo mais antiga.

A estratégia é escolhida pelo perfil de saída, não inferida. Independentemente dela, os bytes finais têm o mesmo formato: a seção de referências cruzadas, depois startxref, depois o byte offset, depois %%EOF. Essa cauda é o que um leitor encontra primeiro.

  1. Step 1 of 4: ISO 32000-2 §7.5.5 — %%EOF and startxref at the file end
  2. Step 2 of 4: ISO 32000-2 §7.5.4 / §7.5.8 — the cross-reference section maps object number to offset
  3. Step 3 of 4: ISO 32000-2 §7.5.5 — the trailer names /Root, the document catalog
  4. Step 4 of 4: ISO 32000-2 §7.3.10 — each indirect object is reached at its recorded offset
Como um leitor resolve um objeto em um arquivo do NextPDF, e a cláusula da ISO 32000-2 que define cada passo: ele começa no fim do arquivo e segue para dentro.

A estrutura de quatro partes não é uma convenção do NextPDF; é a cláusula de estrutura de arquivo da Spec: ISO 32000-2, §7.5 . O padrão define um PDF como um header, um body de objetos, uma tabela de referências cruzadas e um trailer, e estabelece que um leitor deve fazer parsing a partir do fim do arquivo. A última linha é %%EOF, e as duas linhas anteriores são a palavra-chave startxref e o byte offset para a seção de referências cruzadas.

Evidence: Standard-backed

Um objeto indireto é definido como um número de objeto e um número de geração, separados por espaço em branco, seguidos pelo valor do objeto delimitado pelas palavras-chave obj e endobj. A combinação de número de objeto e número de geração identifica o objeto de forma única; uma referência indireta a ele é escrita como o número do objeto, o número de geração e a palavra-chave R. O ObjectRegistry do NextPDF espelha isso exatamente: um número sequencial, geração 0 para objetos recém-gravados e um offset registrado.

A partir do PDF 1.5, objetos também podem residir dentro de um object stream, onde são armazenados sem as palavras-chave obj/endobj e devem ter geração zero. O stream de referências cruzadas (/Type /XRef, Spec: ISO 32000-2, §7.5.8 ) é o mecanismo do PDF 2.0 que indexa tanto os objetos comuns quanto os objetos comprimidos. O CrossReferenceStream do NextPDF o constrói com um array de larguras de campo /W e compressão FlateDecode.

Este é o formato do body de um PDF mínimo e do seu trailer. Os números na seção de referências cruzadas são byte offsets. Eles precisam estar exatamente corretos, e é por isso que o NextPDF os registra a partir do buffer em vez de calculá-los.

%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
0000000122 00000 n
trailer
<< /Size 4 /Root 1 0 R >>
startxref
196
%%EOF

Um leitor abre isso de baixo para cima: %%EOF, depois startxref 196; então vai até o byte 196, onde xref começa, lê que o objeto 1 reside no byte 9, segue /Root 1 0 R até o catálogo e percorre a árvore de páginas a partir daí. O objeto 0 é sempre a cabeça da free-list com geração 65535 — uma peculiaridade herdada do design mais antigo do formato, reproduzida fielmente porque os leitores a esperam.

A armadilha é acreditar que um PDF é lido de cima para baixo como código-fonte. Não é. Os objetos no body podem estar em qualquer ordem. Os números de objeto não precisam ser sequenciais no arquivo, e um leitor nunca conta com isso. O único índice autoritativo é a seção de referências cruzadas, e a única forma de encontrá-la é o trailer no fim. Um PDF com um body perfeitamente válido e um único número errado em startxref é ilegível. Um PDF com objetos gravados em uma ordem embaralhada, mas com uma tabela de referências cruzadas correta, está correto. A posição em si não importa; a posição registrada é tudo.

Esta página descreve a estrutura de arquivo, não o conteúdo de página. Como as marcações chegam a uma página — content streams, operadores gráficos, exibição de texto — é um tópico separado. Ela também não cobre o que acontece quando um arquivo é alterado depois de ser gravado. Esse é o papel dos incremental updates, em que o writer anexa uma segunda seção de referências cruzadas e o trailer encadeia para trás.

O NextPDF é um writer. O comportamento descrito aqui é como ele serializa um documento que ele próprio construiu. Ele não é um parser de PDF de uso geral nem uma ferramenta de reparo. Ele não promete ler, reconstruir ou recuperar um arquivo arbitrário de terceiros com uma tabela de referências cruzadas danificada. A garantia é estreita e deliberada. Os arquivos que o NextPDF grava têm offsets correspondentes, porque são medidos, não previstos.

Por que existem números de geração se arquivos novos sempre usam 0? Os números de geração existem para o reuso de objetos entre atualizações. Um arquivo recém-gravado tem todos os objetos na geração 0. Gerações diferentes de zero aparecem apenas quando um arquivo foi atualizado incrementalmente e um número de objeto é reaproveitado.

Dois objetos podem ter o mesmo número? Em uma única seção de referências cruzadas, não. Ao longo de incremental updates, um arquivo pode conter fisicamente várias cópias do mesmo número de objeto. A entrada de referência cruzada mais recente prevalece. Esse é o tema da próxima página.

A ordem dos objetos no arquivo importa para a saída? Não. O NextPDF grava os objetos em uma ordem determinística para builds reproduzíveis, mas um leitor resolve tudo por meio da seção de referências cruzadas, então a ordem física não tem significado semântico.

  • Objeto indireto — um objeto numerado no body, escrito como N G obj … endobj, onde N é o número do objeto e G o número de geração.
  • Referência indireta — um ponteiro para um objeto indireto, escrito N G R.
  • Tabela de referências cruzadas (xref) — o índice do número do objeto para o byte offset. No PDF 2.0 isso costuma ser um stream de referências cruzadas (/Type /XRef) em vez da clássica tabela de texto de 20 bytes por entrada.
  • Trailer — o dicionário no fim de uma seção de referências cruzadas que nomeia /Root (o catálogo do documento) e /Size, e é encontrado por meio do offset de startxref.
  • Object stream — um objeto de stream que ele próprio contém outros objetos indiretos (comprimidos juntos); os membros não têm obj/endobj e têm geração zero.
  • Catálogo do documento — o objeto nomeado por /Root; o ponto de entrada para a árvore de páginas e todo o restante do documento.