Technical Article

Criar um PDF mínimo manualmente: os cinco objetos de que necessita

No seu âmago, um PDF é um contentor de texto simples. Abra a maioria dos ficheiros num editor hexadecimal e o topo é legível: um comentário da versão, depois uma série de objetos numerados, seguidos de um pequeno índice e de um ponteiro na parte inferior que diz ao leitor por onde começar. Se retirarmos a compressão, o formato é suficientemente simples para que possa digitar um documento funcional num editor de texto e fazer com que um visualizador o abra. Fazer isto uma única vez ensina-lhe mais sobre a estrutura do PDF do que qualquer leitura da especificação, porque tem de interligar os objetos manualmente e o ficheiro recusa-se a abrir enquanto essa ligação não estiver correta.

Este tutorial cria o menor PDF que realmente renderiza alguma coisa: uma única página, as palavras 'Hello, World!' num tipo de letra integrado e em papel formato Letter. O ficheiro final necessita exatamente de cinco objetos e de algumas linhas de registo estrutural em redor deles. Escreveremos primeiro os objetos e depois montaremos o cabeçalho, a tabela de referências cruzadas (xref) e o trailer que os unem num ficheiro que um leitor aceitará.

Os cinco objetos que um visualizador exige

Um leitor de PDF não examina o documento de cima a baixo à procura de conteúdo. Começa no trailer, segue uma referência para o catálogo do documento e, a partir daí, percorre uma cadeia de objetos. Cada objeto nessa cadeia tem de existir, caso contrário a abertura falha. Para um documento de uma página, a cadeia é curta e cada elemento tem uma função única:

  • O Catálogo (Catalog) é a raiz. É o objeto para o qual o trailer aponta, e a sua única entrada obrigatória aqui é uma referência para a árvore de páginas.
  • A classe Páginas (Pages) é o nó da árvore de páginas. Lista as páginas no documento e indica quantas existem.
  • A Página (Page) descreve uma página física: o seu tamanho, os recursos que utiliza para desenhar e qual o fluxo de conteúdo que a pinta.
  • O Fluxo de conteúdo (Content stream) contém os operadores de desenho, que são os comandos pós-fixados que colocam texto e gráficos na página.
  • O Tipo de letra (Font) declara a tipografia a que o fluxo de conteúdo se refere. Utilize um dos 14 tipo de letra padrão e não precisará de 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 0 é o seu número de geração (sendo sempre 0 num ficheiro novo). Em qualquer outro local do ficheiro, aponta para esse objeto com uma referência: 5 0 R significa 'objeto 5'. Estas referências constituem a cablagem estrutural. O catálogo contém 2 0 R na nossa numeração para aceder à árvore de páginas, a árvore de páginas contém uma referência que desce para a página, e assim por diante. Se errar num número, o leitorará um ponteiro perdido para o nada.

Nomes, dicionários e fluxos

Três elementos de sintaxe suportam quase tudo. Um nome começa com uma barra: /Type, /Page, /F0. Os nomes são identificadores sensíveis a maiúsculas e minúsculas, não cadeias de caracteres, e o PDF utiliza-os para chaves de dicionário e para etiquetar o tipo de um objeto. Um dicionário é um conjunto de pares chave-valor delimitados por parênteses angulares duplos, onde cada chave é um nome: << /Type /Page /MediaBox [0 0 612 792] >>. Os valores podem ser números, nomes, matrizes (arrays) em parênteses retos, referências ou dicionários aninhados. A maioria dos objetos PDF são dicionários.

Um fluxo (stream) é um dicionário seguido de um bloco de bytes entre as palavras-chave stream e endstream. É aí que residem os operadores de desenho de página, e, nos ficheiros reais, onde as imagens comprimidas e os tipos de letra incorporados também se encontram. O dicionário do fluxo descreve os bytes; num ficheiro de produção, deve conter uma entrada /Length indicando a contagem exata de bytes, e frequentemente um /Filter como /FlateDecode quando os dados estão comprimidos. Vamos recorrer a uma ferramenta para preencher a /Length, uma vez que contar bytes manualmente é a parte deste exercício sem qualquer ganho educativo e com grande probabilidade de um erro de desvio por um que corrompa o ficheiro.

Escrever os objetos

Eis os cinco objetos em ordem. O detalhe das coordenadas a reter antes de ler o fluxo de conteúdo: o PDF mede em pontos a partir do canto inferior esquerdo da página, onde um ponto equivale a 1/72 de polegada, e o eixo Y cresce para cima. Uma página no formato US Letter tem 612 por 792 pontos, pelo que a coordenada 50 700 situa-se perto do topo esquerdo, e não do fundo.

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 revela-se. O objeto 1, o catálogo, aponta a 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 /Parent de volta para o objeto 2 (a árvore e a página referenciam-se mutuamente, o que é obrigatório), define as suas dimensões com /MediaBox, expõe o tipo de letra sob o nome local /F0 nos seus /Resources e define o objeto 5 como o seu conteúdo. O objeto 4 é o tipo de letra: /BaseFont /Helvetica escolhe uma das 14 tipografias padrão que qualquer leitor conforme já possui, pelo que não há nada a 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 é pós-fixada: os operandos vêm primeiro, seguidos do operador que os consome. Cinco linhas realizam o trabalho. Os operadores BT e ET abrem e fecham um objeto de texto; tudo o que posiciona ou exibe texto deve situar-se entre eles. /F0 36 Tf define o tipo de letra atual para o recurso denominado /F0 a 36 pontos (Tf significa 'definir tipo de letra e tamanho do texto'). O comando 50 700 Td desloca a posição do texto para (50, 700) nas coordenadas da página. O operador (Hello, World!) Tj exibe a cadeia de caracteres, que o PDF escreve como texto literal entre parênteses, recorrendo a Tj para a desenhar na posição atual. Se omitir BT/ET, um leitor rigoroso rejeitará os operadores de texto; se esquecer de definir um tipo de letra antes de Tj, não existirá nenhum tipo de letra ativo com o qual desenhar.

A instrução /Length 44 no dicionário do fluxo indica a contagem exata de bytes entre stream e endstream. Este é o valor que vale a pena delegar a uma ferramenta em vez de contar quebras de linha manualmente, sobretudo porque a gravação de fins de linha pelo seu editor como LF ou CRLF altera o total.

Cabeçalho, xref e trailer

Os objetos constituem o conteúdo. Três elementos estruturais transformam-nos num ficheiro. O primeiro é o cabeçalho, na primeira linha, que indica o formato e a versão:

%PDF-1.7

O caráter % inicia um comentário na sintaxe PDF, mas o leitor trata este comentário específico como a assinatura do formato e obtém a versão a partir dele. Um gerador real coloca imediatamente a seguir uma segunda linha de comentário com bytes de bit alto, uma indicação para as ferramentas de transferência de ficheiros de que o ficheiro é binário e não deve ser corrompido como texto.

No final do ficheiro surge a tabela de referências cruzadas (xref), o índice que viabiliza o acesso aleatório. Esta tabela regista a posição em bytes (offset) de cada objeto a partir do início do ficheiro, de modo a que o leitor possa saltar diretamente para o objeto 3 sem analisar primeiro os objetos 1 e 2. A tabela é rígida: as entradas possuem largura fixa de 20 bytes cada, incluindo o fim de linha, formatadas como um offset 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 as nossas seis entradas (o objeto 0 é sempre o início da lista livre) apresenta a seguinte forma:

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

Estes offsets constituem a parte mais frágil da escrita manual de um PDF. Cada um representa a posição exata em bytes onde o respetivo N 0 obj começa, e cada offset altera-se no momento em que adiciona um caráter em qualquer ponto acima dele. O trailer é o ponto de entrada que o leitor utiliza em último e em primeiro lugar: /Root 1 0 R identifica o catálogo, /Size 6 indica a contagem de objetos e startxref 408 fornece o offset em bytes da própria palavra xref. O leitor abre o ficheiro, salta para o fim, lê startxref, procura a tabela de referências cruzadas e, a partir dela, acede ao catálogo e a tudo o que se encontra abaixo. O marcador %%EOF assinala o último byte.

Deixe que uma ferramenta corrija os offsets e tamanhos

Os offsets acima são ilustrativos; na prática, estarão incorretos quando terminar a digitação, porque dependem da disposição exata de bytes do seu ficheiro. Em vez de os recalcular, escreva a estrutura com valores fictícios e permita que um utilitário reconstrua a tabela de referências cruzadas e os comprimentos dos fluxos. A ferramenta gratuita e multiplataforma pdftk faz isto numa única passagem:

pdftk hello-draft.pdf output hello.pdf

A ferramenta analisa os seus objetos, recalcula cada offset de bytes, preenche os valores corretos de /Length, escreve uma tabela xref e um trailer válidos e gera o ficheiro hello.pdf. Abra-o em qualquer visualizador e obterá uma página com 'Hello, World!' em Helvetica de 36 pontos perto do topo. O utilitário qpdf realiza a mesma tarefa, e muitos visualizadores também corrigem ficheiros ligeiramente malformados em tempo de execução. O objetivo de recorrer a uma ferramenta aqui não é preguiça; deve-se ao facto de a aritmética dos offsets constituir a única parte do formato com zero conteúdo conceptual e com a maior taxa de erros, pelo que automatizá-la permite focar a atenção na aprendizagem da estrutura.

Porque é que isto se aplica a documentos reais

Nada num relatório de cem páginas altera a estrutura que acabou de construir. O catálogo continua na raiz, a árvore de páginas agrupa as páginas e cada página aponta para os seus recursos e para um fluxo de conteúdo. O que aumenta é a amplitude, não a espinha dorsal do documento: a árvore de páginas ramifica-se para que o leitor possa ignorar subárvores inteiras, os fluxos de conteúdo contêm centenas de operadores em vez de cinco, os tipos de letra são incorporados como objetos de fluxo com as suas tabelas de larguras e codificações, e as imagens surgem como fluxos com filtros específicos. Os ficheiros modernos também tendem a agrupar muitos objetos em fluxos de objetos comprimidos e a substituir a tabela xref simples por um fluxo de referências cruzadas, motivo pelo qual abrir um PDF real num editor de texto revela frequentemente uma barreira de caracteres binários. O modelo subjacente é idêntico ao do seu ficheiro manual. Para compreender o grafo de objetos mais amplo e como o catálogo, a árvore de páginas e os dicionários de recursos se relacionam num documento maior, a visita detalhada à estrutura do documento PDF continua a partir do ponto onde este tutorial termina, e a visão geral da estrutura de ficheiros cobre atualizações incrementais e a forma como o trailer se liga entre revisões.

Da escrita manual para uma biblioteca

Digitar objetos manualmente é um exercício de aprendizagem, não uma técnica de produção. No momento em que necessita de tipos de letra reais, texto moldado, imagens ou mais do que uma página simples, a gestão de bytes que o pdftk corrigiu passa a ser o trabalho completo, exigindo uma biblioteca dedicada. Os mesmos cinco objetos continuam a ser escritos, mas a biblioteca calcula cada offset, gere os dicionários de recursos e tipos de letra, e comprime os fluxos de conteúdo sem que precise de controlar um único byte. Em Delphi e C++Builder, o Componente HotPDF reduz todo este ficheiro a um punhado de chamadas: configure o documento, chame BeginDoc, SetFont e TextOut para posicionar a mesma saudação, e depois EndDoc para escrever um catálogo, árvore de páginas, xref e trailer corretos. Compreender os objetos subjacentes é o que lhe permite analisar a saída quando um documento não é renderizado como seria de esperar.