Um PDF é, em sua essência, um contêiner de texto simples. Abra a maioria dos arquivos em um editor hexadecimal e a parte superior será legível: um comentário de versão, seguido por uma sequência de objetos numerados, depois um pequeno índice e um ponteiro bem na parte inferior que diz ao leitor onde começar. Remova a compactação e o formato será acessível o suficiente para que você possa digitar um documento funcional em um editor de texto e fazer um visualizador abri-lo. Fazer isso uma vez ensina mais sobre como um PDF se estrutura do que qualquer quantidade de leitura da especificação, porque você precisa conectar os objetos uns aos outros manualmente e o arquivo se recusa a abrir até que você faça a conexão correta
Este passo a passo cria o menor PDF que realmente renderiza algo: uma página, as palavras "Hello, World!" em uma fonte embutida, em papel Letter (Carta) dos EUA. O arquivo final precisa de exatamente cinco objetos e algumas linhas de controle ao redor deles. Escreveremos os objetos primeiro, depois montaremos o cabeçalho, a tabela de referência cruzada (xref) e o trailer que os unem em um arquivo que um leitor aceitará
Os cinco objetos que um visualizador exige
Um leitor não examina um PDF de cima para baixo procurando por conteúdo. Ele começa no trailer, segue uma referência para o catálogo (Catalog) do documento e percorre uma cadeia de objetos a partir daí. Todos os objetos nessa cadeia devem existir, caso contrário a abertura falha. Para um documento de uma página, a cadeia é curta e cada elo tem um único trabalho:
- Catalog é a raiz. É o objeto para o qual o trailer aponta, e sua única entrada obrigatória aqui é uma referência à árvore de páginas
- Pages é o nó da árvore de páginas. Ele lista as páginas no documento e relata quantas existem
- Page descreve uma página física: seu tamanho, os recursos com os quais desenha e qual fluxo de conteúdo (content stream) a pinta
- Content stream guarda os operadores de desenho, os comandos postfix que colocam texto e gráficos naquela página
- Font declara o tipo de fonte a que o fluxo de conteúdo se refere. Use uma das 14 fontes padrão e você não precisará incorporar nada
Cada objeto é numerado e endereçável. Um objeto indireto é escrito como N 0 obj ... endobj, onde N é o número do objeto e o 0 é o seu número de geração (sempre 0 em um arquivo que você cria do zero). Em qualquer outro lugar do arquivo, você aponta para aquele objeto com uma referência: 5 0 R significa "objeto 5". Essas referências são a fiação. O catálogo guarda 2 0 R na nossa numeração para alcançar a árvore de páginas, a árvore de páginas guarda uma referência de volta para a página, e assim por diante. Erre um número e o leitor seguirá um ponteiro solto para o nada
Nomes, dicionários e fluxos (streams)
Três partes da sintaxe carregam quase tudo. Um nome começa com uma barra: /Type, /Page, /F0. Nomes são identificadores sensíveis a maiúsculas e minúsculas, não strings, e o PDF os usa para chaves de dicionário e para marcar o que é um objeto. Um dicionário é um conjunto de pares chave-valor envoltos por colchetes angulares duplos, onde cada chave é um nome: << /Type /Page /MediaBox [0 0 612 792] >>. Os valores podem ser números, nomes, matrizes (arrays) entre colchetes, referências ou dicionários aninhados. A maioria dos objetos PDF são dicionários
Um fluxo (stream) é um dicionário seguido por um bloco de bytes entre as palavras-chave stream e endstream. É aí que residem os operadores de desenho de página e, em arquivos reais, é onde imagens compactadas e fontes incorporadas também residem. O dicionário de fluxo descreve os bytes; em um arquivo de produção, ele deve carregar uma entrada /Length informando a contagem exata de bytes, e frequentemente um /Filter, como /FlateDecode, quando os dados estão compactados. Vamos nos apoiar em uma ferramenta para preencher o /Length, porque contar bytes manualmente é a parte deste exercício sem nenhum retorno educacional e com alta chance de um erro de "off-by-one" que quebra o arquivo
Escrevendo os objetos
Aqui estão os cinco objetos em ordem. O detalhe de coordenadas a ter em mente antes de ler o fluxo de conteúdo: o PDF mede a partir do canto inferior esquerdo da página em pontos, onde um ponto é 1/72 de polegada, e o Y cresce para cima. Uma página US Letter tem 612 por 792 pontos, então 50 700 fica perto do canto superior esquerdo, não do inferior
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]
/Resources << /Font << /F0 4 0 R >> >>
/Contents 5 0 R
>>
endobj
4 0 obj
<< /Type /Font
/Subtype /Type1
/BaseFont /Helvetica
>>
endobj
5 0 obj
<< /Length 44 >>
stream
BT
/F0 36 Tf
50 700 Td
(Hello, World!) Tj
ET
endstream
endobj
Leia as referências e a estrutura se revela. O objeto 1, o catálogo, aponta sua entrada /Pages para o objeto 2. O objeto 2, a árvore de páginas, lista o objeto 3 em /Kids e declara /Count 1. O objeto 3, a página, aponta o /Parent de volta para o objeto 2 (a árvore e a página referenciam uma à outra, o que é obrigatório), define seu próprio tamanho com /MediaBox, expõe a fonte sob o nome local /F0 em seus /Resources, e nomeia o objeto 5 como seu conteúdo. O objeto 4 é a fonte: /BaseFont /Helvetica escolhe uma das 14 fontes padrão que todo leitor em conformidade já possui, portanto não há nada para incorporar. O objeto 5 é o fluxo de conteúdo
O que o fluxo de conteúdo realmente diz
O corpo do fluxo é um pequeno programa na linguagem de descrição de página do PDF, que é postfix: os operandos vêm primeiro, depois o operador que os consome. Cinco linhas fazem o trabalho. BT e ET abrem e fecham um objeto de texto; tudo o que posiciona ou mostra texto tem que ficar entre eles. /F0 36 Tf define a fonte atual para o recurso nomeado /F0 em 36 pontos (Tf significa "definir a fonte de texto e o tamanho"). 50 700 Td move a posição do texto para (50, 700) nas coordenadas da página. (Hello, World!) Tj mostra a string, que o PDF escreve como texto literal entre parênteses, usando Tj para pintá-la na posição atual. Deixe de fora BT/ET e um leitor rigoroso rejeitará os operadores de texto; esqueça de definir uma fonte antes de Tj e não haverá fonte atual para desenhar
O /Length 44 no dicionário do fluxo é a contagem de bytes entre stream e endstream, e precisa ser exata. Este é o valor que vale a pena delegar a uma ferramenta em vez de contar novas linhas manualmente, especialmente porque o fato de o seu editor escrever as quebras de linha como LF ou CRLF muda o total
Cabeçalho, xref e trailer
Os objetos são o conteúdo. Três peças estruturais os transformam em um arquivo. A primeira é o cabeçalho, a primeiríssima linha, nomeando o formato e a versão:
%PDF-1.7
O % inicia um comentário na sintaxe PDF, mas um leitor trata esse comentário específico como a assinatura do formato e lê a versão a partir dele. Um escritor real o segue imediatamente com uma segunda linha de comentário de bytes de bit alto, uma dica para ferramentas de transferência de arquivos de que o arquivo é binário e não deve ser corrompido como texto
No final do arquivo vem a tabela de referência cruzada (cross-reference table), o índice que torna o acesso aleatório possível. Ela registra o deslocamento (offset) de bytes de cada objeto desde o início do arquivo, para que um leitor possa buscar diretamente o objeto 3 sem analisar primeiro os objetos 1 e 2. A tabela é rígida: as entradas têm largura fixa, 20 bytes cada, incluindo a quebra de linha, formatadas como um deslocamento de 10 dígitos, uma geração de 5 dígitos, uma palavra-chave (n para em uso, f para livre) e um terminador de dois bytes. Uma tabela correta para nossas seis entradas (o objeto 0 é sempre a cabeça da lista livre) fica assim:
xref
0 6
0000000000 65535 f
0000000009 00000 n
0000000058 00000 n
0000000115 00000 n
0000000235 00000 n
0000000308 00000 n
trailer
<< /Size 6
/Root 1 0 R
>>
startxref
408
%%EOF
Esses deslocamentos são a parte frágil de se escrever PDF manualmente. Cada um deles é a posição exata em bytes onde o N 0 obj correspondente começa, e cada deslocamento muda no momento em que você adiciona um caractere em qualquer lugar acima dele. O trailer é o ponto de entrada que um leitor usa por último e por primeiro: /Root 1 0 R nomeia o catálogo, /Size 6 informa a contagem de objetos, e startxref 408 fornece o deslocamento de bytes da própria palavra xref. Um leitor abre o arquivo, pula para o final, lê startxref, busca a tabela de referência cruzada e, a partir daí, alcança o catálogo e tudo o que está abaixo dele. %%EOF marca o último byte
Deixe uma ferramenta consertar a contagem de bytes
Os deslocamentos acima são ilustrativos; na prática eles estarão incorretos assim que você terminar de digitar, porque dependem do layout exato de bytes do seu arquivo. Em vez de recalculá-los, escreva a estrutura com valores temporários e deixe um utilitário reconstruir a tabela de referência cruzada e os comprimentos dos fluxos. A ferramenta gratuita e multiplataforma pdftk faz isso em uma única passagem:
pdftk hello-draft.pdf output hello.pdf
Ela analisa seus objetos, recalcula cada deslocamento de bytes, preenche os valores /Length corretos, escreve uma tabela xref e um trailer válidos e emite hello.pdf. Abra-o em qualquer visualizador e você obterá uma página com "Hello, World!" em Helvetica de 36 pontos perto do topo. O Qpdf faz o mesmo trabalho, e muitos visualizadores também reparam um arquivo ligeiramente malformado instantaneamente. A questão de se apoiar em uma ferramenta aqui não é preguiça; é que a aritmética de deslocamento é a única parte do formato com zero conteúdo conceitual e a maior taxa de erros, então automatizá-la permite que a estrutura permaneça sendo aquilo que você está aprendendo
Por que isso escala para documentos reais
Nada a respeito de um relatório de cem páginas muda a forma que você acabou de construir. O catálogo ainda fica na raiz, a árvore de páginas ainda reúne as páginas, e cada página ainda aponta para seus recursos e um fluxo de conteúdo. O que cresce é a largura, não a espinha dorsal: a árvore de páginas se ramifica para que um leitor possa pular subárvores inteiras, os fluxos de conteúdo carregam centenas de operadores em vez de cinco, as fontes são incorporadas como seus próprios objetos de fluxo com tabelas de largura e codificações, e as imagens chegam como fluxos com filtros específicos de imagem. Os arquivos modernos também tendem a empacotar muitos objetos em fluxos de objetos compactados e substituir a tabela xref simples por um fluxo de referência cruzada, e é por isso que abrir um PDF real em um editor de texto geralmente mostra uma parede de dados binários. O modelo por baixo é idêntico ao do seu arquivo feito à mão. Para o gráfico de objetos mais amplo e como o catálogo, a árvore de páginas e os dicionários de recursos se relacionam em um documento maior, o tour aprofundado da estrutura do documento PDF continua de onde paramos aqui, e a visão geral da estrutura de arquivo cobre atualizações incrementais e como o trailer se encadeia através das revisões
Da escrita manual para uma biblioteca
Digitar objetos manualmente é um exercício de aprendizado, não uma técnica de produção. No instante em que você precisa de fontes reais, texto com quebra automática, imagens ou mais do que uma página trivial, o controle de bytes que o pdftk consertou para você se torna todo o trabalho, e você vai querer uma biblioteca que cuide disso. Os mesmos cinco objetos continuam sendo gravados, mas uma biblioteca computa cada deslocamento, gerencia os dicionários de fontes e recursos, e compacta os fluxos de conteúdo sem que você rastreie um único byte. No Delphi e no C++Builder, o Componente HotPDF reduz esse arquivo inteiro a um punhado de chamadas: configure o documento, chame BeginDoc, SetFont e TextOut para colocar a mesma saudação, e então EndDoc para gravar um catálogo correto, árvore de páginas, xref e trailer. Entender os objetos por baixo é o que permite que você raciocine sobre a saída quando um documento não é renderizado da maneira que você esperava