Technical Article

O modelo de objetos lógicos do PDF: tipos, referências e estrutura

No seu âmago, um ficheiro PDF é uma coleção de objetos que apontam uns para os outros. Se retirarmos a compressão, o registo estrutural de referências cruzadas e os offsets de bytes, o que resta é um grafo: um pequeno conjunto de valores tipados, interligados por referências, com raiz num único objeto que o leitor sabe como encontrar. Tudo o que um PDF pode expressar, desde um parágrafo de texto a um tipo de letra incorporado ou a uma assinatura digital, é construído a partir de apenas oito tipos primitivos de objetos e da regra que permite a um objeto referenciar outro. Compreendidos estes conceitos, o resto do formato revela-se como uma composição e não um mistério.

Esta é a camada lógica do PDF, definida na cláusula 7.3 da norma ISO 32000-1, e situa-se um nível acima do layout físico do ficheiro (o cabeçalho, corpo, tabela de referências cruzadas e trailer, tema abordado na visão geral técnica da estrutura de ficheiros PDF). O modelo lógico é a interpretação que se dá a esses bytes após a análise (parsing). Um visualizador lê o ficheiro de trás para a frente para localizar o trailer, segue-o até à raiz e, a partir daí, o documento desenvolve-se como uma teia de objetos que se referenciam mutuamente. Esta é a secção em que deve pensar ao depurar uma página malformada, ao escrever um analisador ou ao confiar numa biblioteca para montar um documento.

Oito tipos de objetos, e nada mais

O PDF define exatamente oito tipos básicos de objetos. Qualquer valor num documento pertence a um deles, o que torna o formato gerível apesar do seu alcance.

Consistem em:

Booleanos correspondem às palavras-chave true e false. Ativam ou desativam sinalizadores, como o que indica se uma anotação deve ser impressa.

Números apresentam-se sob duas formas que a especificação trata como um único tipo: inteiros como 42 e reais como 3.14 ou -0.002. O PDF não possui notação exponencial, pelo que nunca encontrará 1e6 num ficheiro conforme. As coordenadas, tamanhos de letra e ângulos de rotação são todos números.

Cadeias de caracteres contêm sequências de bytes, escritas entre parênteses, (Hello), ou entre parênteses angulares sob a forma hexadecimal, <48656C6C6F>. Ambas as notações codificam o mesmo conteúdo; o formato hexadecimal funciona como alternativa para bytes problemáticos dentro de parênteses. As strings transportam texto, mas constituem bytes em primeiro lugar, o que se torna relevante ao processar caracteres além da tabela ASCII.

Nomes são tokens atómicos iniciados por uma barra: /Type, /Pages, /MediaBox. Um nome não é uma string; trata-se de um identificador utilizado como chave de dicionário ou valor enumerado, e dois nomes só são idênticos se coincidirem byte a byte. A barra faz parte da sintaxe e não do nome. Isto confunde os principiantes, que assumem /Times-Roman e a string (Times-Roman) as equivalentes; o formato distingue-os.

Matrizes constituem listas ordenadas e heterogéneas delimitadas por parênteses retos: [0 0 612 792] descreve um retângulo de página, e uma matriz pode misturar tipos livremente, incluindo referências a outros objetos. Os Dicionários constituem o motor de trabalho do formato. Escrito entre << e >>, um dicionário mapeia chaves (que são nomes) para valores de qualquer tipo, e quase todas as estruturas relevantes no PDF, página, catálogo, tipo de letra, anotação, consistem num dicionário com a chave /Type a declarar de que estrutura se trata.

Fluxos são dicionários acompanhados de um bloco de bytes brutos entre as palavras-chave stream e endstream. O dicionário descreve os bytes (o seu comprimento e eventuais filtros como FlateDecode para compressão) e os bytes contêm o conteúdo pesado: instruções de desenho da página, programas de tipos de letra incorporados ou imagens. Um fluxo é o local onde o PDF armazena tudo o que seja demasiado extenso ou binário para constar diretamente na linha de texto.

O oitavo tipo é o objeto nulo, representado pela palavra-chave null. Trata-se de um valor real, distinto da ausência de uma chave. Uma entrada de dicionário definida como null é tratada como se estivesse ausente, e uma referência que aponte para um objeto inexistente também resulta em null em vez de gerar um erro. Este comportamento tolerante é intencional: permite que um ficheiro danificado sofra uma degradação parcial em vez de recusar a abertura. Não existe um nono tipo de objeto; tudo o que o PDF expressa provém da combinação destes oito.

Valores diretos, objetos indiretos e referências

Qualquer um destes oito tipos pode manifestar-se de duas formas. Um objeto direto é escrito no local da sua utilização, como o valor 612 dentro de uma matriz MediaBox. A um objeto indireto é atribuída uma identidade para que outros objetos possam apontar para ele: dois inteiros (um número de objeto e um número de geração) delimitando a definição com os marcadores obj e endobj:

12 0 obj
<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>
endobj

Este é o objeto 12, geração 0, um dicionário de tipo de letra. Em qualquer outro local do ficheiro, outro objeto referencia-o com uma referência indireta: os mesmos dois números seguidos da palavra-chave R, ou seja, 12 0 R. A referência funciona como um ponteiro. Quando o dicionário de recursos de uma página indica /Font << /F1 12 0 R >>, associa o objeto 12 ao recurso de nome /F1, sem necessidade de duplicar a definição do tipo de letra na página.

O número de geração serve para gerir eliminações e reutilizações. Quando um objeto é libertado e a sua posição é reutilizada, a geração incrementa para evitar que uma referência antiga 12 0 R aponte erradamente para o novo inquilino da posição 12. Os ficheiros gravados de raiz contêm quase sempre a geração 0, mas um ficheiro sujeito a várias edições pode apresentar números mais elevados, e um analisador que ignore a geração acabará por ler o objeto incorreto.

A indireção é o que torna o PDF eficiente e editável. Um único tipo de letra, imagem ou espaço de cores pode ser definido uma vez e referenciado a partir de uma centena de páginas. Uma pequena alteração pode ser anexada como uma nova revisão que substitui um único objeto, sem necessidade de reescrever todo o ficheiro. A tabela de referências cruzadas funciona como o índice que converte um número de objeto num offset de bytes, permitindo ao leitor saltar diretamente para 12 0 obj sem varrimento prévio. Contudo, essa constitui uma otimização física. Logicamente, apenas necessita de saber que 12 0 R significa 'o objeto identificado como 12 0'.

O catálogo: onde começa qualquer documento

A resolução de referências tem de começar em algum ponto, e esse ponto é a entrada /Root do trailer, que aponta para o catálogo do documento: a raiz do grafo de objetos, um dicionário com /Type /Catalog. O leitor alcança-o em primeiro lugar porque o trailer é localizado primeiro e, a partir dele, qualquer outra secção do documento torna-se acessível seguindo as referências.

O catálogo contém apenas duas entradas estritamente obrigatórias: o seu /Type e /Pages, que constitui uma referência indireta para a raiz da árvore de páginas. As restantes chaves são opcionais e descrevem comportamentos globais do documento e não o seu conteúdo: /Outlines aponta para a árvore de marcadores (bookmarks), /Names armazena árvores de nomes indexadas por strings, /Metadata referencia um fluxo de metadados XMP, e /PageMode e /PageLayout sugerem como o visualizador deve apresentar o documento. Nenhuma destas chaves é necessária para renderizar uma página; destinam-se a configurar a experiência em redor das páginas. As estruturas de marcadores, metadados e anotações que dependem do catálogo são abordadas no artigo sobre metadados, marcadores e anotações em PDF.

O diagrama abaixo apresenta a localização do corpo dos objetos no ficheiro. O catálogo e a árvore de páginas residem dentro desse corpo como objetos indiretos comuns; o cabeçalho, a tabela de referências cruzadas e o trailer em redor constituem o suporte físico que permite ao leitor localizá-los.

Diagrama das quatro secções físicas de um ficheiro PDF: um cabeçalho de versão, um corpo que contém os objetos do documento incluindo o catálogo e a árvore de páginas, uma tabela de referências cruzadas de offsets de objetos e um trailer que aponta para a raiz

A árvore de páginas: uma hierarquia equilibrada de páginas

A partir de /Pages, o documento ramifica-se na árvore de páginas, onde a opção do PDF por um grafo em vez de uma lista plana revela a sua utilidade. As páginas não são armazenadas numa sequência simples; dependem de uma árvore cujos nós internos constituem nós da árvore de páginas (/Type /Pages) e cujas folhas constituem objetos de página (/Type /Page). Um nó interno lista os seus descendentes numa matriz /Kids e regista, em /Count, quantas páginas folha residem abaixo dele. Cada nó, exceto a raiz, contém uma referência /Parent para o nível superior, permitindo percorrer a árvore em ambas as direções.

2 0 obj                                  % root of the page tree
<< /Type /Pages /Kids [3 0 R 4 0 R] /Count 3 >>
endobj

3 0 obj                                  % a leaf page
<< /Type /Page /Parent 2 0 R
   /MediaBox [0 0 612 792]
   /Resources << /Font << /F1 12 0 R >> >>
   /Contents 5 0 R >>
endobj

4 0 obj                                  % an interior node grouping two more pages
<< /Type /Pages /Parent 2 0 R /Kids [6 0 R 7 0 R] /Count 2 >>
endobj

Neste exemplo, o objeto 2 é a raiz, com três páginas abaixo dele: a página folha 3 e mais duas acessíveis através do nó interno 4. O valor /Count de 3 na raiz tem de corresponder ao total de folhas abaixo dela, e a discrepância deste valor com a estrutura real é um erro frequente em ficheiros editados manualmente. O objetivo da árvore é a localização do acesso. Um leitor que abra a página 900 de um documento de mil páginas não percorre 900 objetos; desce apenas alguns nós, porque uma árvore bem estruturada mantém-se pouco profunda e equilibrada. Construir uma árvore destas manualmente é suficientemente complexo para merecer ser visto em pormenor, o que é feito no guia sobre criar um documento PDF do zero.

A árvore revela outra utilidade através da herança. Alguns atributos de página, como /Resources, /MediaBox, /CropBox e /Rotate, podem ser definidos num nó interno e omitidos nas páginas individuais, que herdam o valor do antepassado mais próximo. Definir /MediaBox uma única vez na raiz faz com que todas as folhas herdem o mesmo tamanho de página sem repetição; uma página que necessite de dimensões diferentes declara o seu próprio valor. Este é o único local no modelo de objetos onde o significado de um valor depende da posição do objeto na árvore, e não apenas do seu próprio conteúdo.

O que uma página folha realmente contém

Um objeto de página constitui o ponto de ligação entre o modelo estrutural e o conteúdo visível. A sua entrada /Contents referencia um ou mais fluxos de conteúdo, que são os operadores de desenho que pintam texto e gráficos na página. A sua entrada /Resources identifica os tipos de letra, imagens e espaços de cores de que esses operadores dependem, consistindo cada entrada numa referência indireta a um objeto partilhado pelas páginas. A propriedade /MediaBox indica o retângulo da página em pontos (1/72 de polegada), e campos como /Rotate e /CropBox ajustam a forma como a página é apresentada.

Esta divisão de tarefas resume todo o modelo em miniatura. O dicionário da página define a estrutura: entradas tipadas e referências que descrevem o que a página é e que recursos utiliza. O fluxo de conteúdo contém as instruções: um bloco de dados independente e comprimível que indica como desenhar. O tipo de letra associado a /F1 é um recurso partilhado, definido uma única vez e referenciado sempre que necessário. O dicionário, o fluxo de dados e a referência cooperam para renderizar uma página, e os mesmos padrões aplicam-se a todo o documento. Os operadores do fluxo de conteúdo no interior desse bloco são abordados separadamente nos artigos sobre texto e tipos de letra e sobre gráficos e elementos visuais.

Porque é que vale a pena conhecer este modelo

A maioria dos programadores depara-se com o modelo de objetos apenas quando algo falha: uma página surge em branco porque a referência /Contents está corrompida, o texto é apresentado como caixas vazias devido à falta de incorporação de um tipo de letra, ou uma ferramenta reporta um valor /Count que não coincide com as páginas encontradas. Cada uma destas falhas constitui um problema no grafo, e analisar o grafo diretamente é preferível a tentar adivinhar a causa. Os oito tipos e a regra das referências formam um vocabulário simples de reter, e assim que passa a visualizar o PDF como um conjunto de objetos que apontam para outros objetos, os ficheiros danificados deixam de ser indecifráveis.

Ainda assim, escrever o modelo manualmente raramente é a decisão correta fora de um contexto de aprendizagem. Manter a consistência de offsets de referências cruzadas, números de geração, contagens da árvore de páginas e comprimentos de fluxos em cada edição é precisamente o tipo de trabalho burocrático que justifica a existência de uma biblioteca. Em produção, uma biblioteca de desenvolvimento de PDF madura faz a gestão do grafo de objetos, permitindo-lhe focar a atenção no conteúdo e nas páginas. Conhecer o modelo continua a compensar: passa a compreender o que a biblioteca constrói nos bastidores, e porquê.