Technical Article

Árvores de Páginas em PDF: Porque é que a Ordem das Páginas Não é a Ordem dos Objetos

A página 1 de um PDF não é o objeto 1. Essa distinção é a causa mais comum de erros de extração de páginas incorretas em analisadores de PDF, e a solução passa por ler a especificação em vez dos bytes do ficheiro.

Objetos, referências e o catálogo

Um ficheiro PDF é uma coleção de objetos numerados. Cada um possui um número de objeto único e um número de geração, escrito como N G obj, onde G é quase sempre 0 em ficheiros que não sofreram atualizações incrementais. Os objetos referenciam-se mutuamente com a notação N G R, pelo que 3 0 R significa "a versão atual do objeto 3". O trailer aponta para um objeto de catálogo raiz, cuja entrada /Pages conduz à árvore de páginas. Tudo o que é navegável num PDF começa a partir dessa raiz, e não a partir do primeiro byte do corpo do ficheiro.

A tabela de referências cruzadas (ou fluxo de referências cruzadas no PDF 1.5+) mapeia os números dos objetos para desvios no ficheiro. O seu papel é o acesso aleatório, não a ordenação. Um escritor que crie um documento de forma incremental pode anexar novos objetos no fim com números mais elevados, embora esses objetos precedam logicamente os existentes na sequência de páginas. Isso não é um defeito; é por design.

A árvore de páginas (ISO 32000-1 §7.7.3)

A sequência de páginas reside na árvore de páginas. O catálogo raiz contém uma referência /Pages que aponta para um nó do tipo /Pages. A matriz /Kids desse nó lista os seus elementos filhos por ordem de leitura. Cada filho é um nó folha do tipo /Page ou outro nó intermédio /Pages que contém a sua própria matriz /Kids. A página 1 é a primeira folha alcançada por uma travessia profunda, da esquerda para a direita, das matrizes Kids. A entrada /Count em cada nó intermédio armazena em cache o número total de páginas folha descendentes, permitindo ao visualizador saltar para a página 500 sem percorrer toda a árvore.

Aqui está o aspeto de uma árvore minimal de três páginas na sintaxe nativa do PDF:

16 0 obj
<<
  /Type /Pages
  /Count 3
  /Kids [20 0 R  1 0 R  4 0 R]
  /MediaBox [0 0 612 792]
>>
endobj

20 0 obj
<< /Type /Page  /Parent 16 0 R  /Contents 21 0 R  /Resources 22 0 R >>
endobj

1 0 obj
<< /Type /Page  /Parent 16 0 R  /Contents 2 0 R   /Resources 3 0 R >>
endobj

4 0 obj
<< /Type /Page  /Parent 16 0 R  /Contents 5 0 R   /Resources 6 0 R >>
endobj

A matriz Kids contém [20 0 R, 1 0 R, 4 0 R]. A página lógica 1 é o objeto 20, a página lógica 2 é o objeto 1, a página lógica 3 é o objeto 4. Qualquer código que itere os números de objetos a partir de 1 em diante irá encontrá-los na ordem 1, 4, 20, produzindo a sequência página-2, página-3, página-1. O documento resultante é renderizado numa ordem baralhada que pode parecer perfeitamente normal num visualizador que siga a árvore, e catastroficamente errada num que não o faça.

Herança

Os nós intermédios podem conter propriedades que os seus descendentes herdam. As entradas herdadas mais comuns são /MediaBox (dimensões da página), /CropBox, /Resources (tipos de letra e imagens) e /Rotate. Uma página folha que omita /MediaBox não está corrompida; herda o valor do nó ancestral mais próximo que o defina. Uma página que defina /MediaBox substitui o valor definido pelo pai, apenas para essa página.

Isto é importante para a análise. Ler um objeto /Page isoladamente e assumir que as suas propriedades estão completas reportará dimensões incorretas para qualquer página que dependa de herança. Um leitor correto percorre a cadeia /Parent, recolhendo as propriedades que ainda não viu, parando na raiz.

Árvores aninhadas

Nada na especificação limita a árvore a um único nível. Um documento grande pode agrupar páginas sob nós intermédios que correspondem vagamente a capítulos:

2 0 obj   % root Pages node, Count = 8
<< /Type /Pages  /Count 8  /Kids [3 0 R  4 0 R] >>
endobj

3 0 obj   % first chapter, 5 pages
<< /Type /Pages  /Parent 2 0 R  /Count 5
   /Kids [10 0 R  11 0 R  12 0 R  13 0 R  14 0 R]
   /MediaBox [0 0 612 792] >>
endobj

4 0 obj   % second chapter, 3 pages
<< /Type /Pages  /Parent 2 0 R  /Count 3
   /Kids [20 0 R  21 0 R  22 0 R]
   /MediaBox [0 0 612 792] >>
endobj

O algoritmo de travessia é o mesmo: visitar os Kids em ordem, fazer a chamada recursiva em qualquer nó /Pages, recolher os nós folha /Page. Os valores /Count permitem que o visualizador ignore uma subárvore inteira ao saltar para uma página que se situe além dela, razão pela qual estas contagens devem ser precisas. Alguns editores de PDF do final da década de 1990 e início de 2000 não os recalculavam após edições no local, pelo que um analisador defensivo verifica o /Count em relação à contagem real de folhas, em vez de confiar nele para a alocação da matriz.

Onde isto surge na prática

O erro de ordenação de páginas surge mais frequentemente em dois cenários. O primeiro é um analisador personalizado que procura por objetos do tipo /Page em vez de seguir a árvore. Encontra todas as páginas, mas na ordem do número do objeto, não na ordem de leitura. A solução é sempre a mesma: começar no trailer, resolver o catálogo raiz, seguir /Pages e percorrer as matrizes Kids.

O segundo cenário é um ficheiro de atualização incremental. Quando um editor de PDF anexa alterações sem reescrever todo o ficheiro, os novos objetos de página recebem números de objeto elevados, enquanto a matriz Kids na árvore original continua a controlar a sua posição lógica. Uma página que originalmente era o objeto 5 é substituída por um novo objeto 143, mas a matriz Kids agora referencia o 143 onde antes referenciava o 5, pelo que a ordem lógica é preservada. Percorrer por número de objeto colocaria a página de substituição na posição incorreta da sequência.

Os PDFs linearizados (otimizados para a Web) adicionam uma terceira variação: o ficheiro é fisicamente reorganizado para que o conteúdo da primeira página apareça perto do início do ficheiro para uma exibição rápida em ligações lentas. A estrutura da árvore de páginas continua a ser a autoridade para a ordem, mas a tabela de referências cruzadas aponta para os desvios reorganizados. Um analisador que dependa da posição do ficheiro em vez da tabela xref lerá incorretamente até mesmo a primeira página de um ficheiro linearizado.

O Componente HotPDF lida internamente com a travessia da árvore de páginas, resolução de herança e fusão de xrefs de atualizações incrementais. Trabalhar diretamente com os seus objetos de página significa que a ordenação da matriz Kids já está aplicada; os índices das páginas mapeiam para páginas lógicas, não para números de objetos.