Technical Article

Ordenação de Páginas em PDF: Como a Árvore de Páginas Controla a Sequência

O objeto número 1 não é a página 1. Esse simples facto perturba mais código de processamento de PDF do que qualquer outro aspeto do formato, e compreender a razão exige olhar além do que o visualizador lhe mostra e examinar o grafo de objetos que o visualizador realmente lê.

Um ficheiro PDF é uma coleção de objetos indiretos numerados. As páginas estão entre esses objetos, mas a sua sequência de exibição não tem nada a ver com a sua posição no ficheiro ou com os números que possuem. A ordem de exibição é determinada inteiramente pela árvore /Pages, uma estrutura ligada enraizada no catálogo do documento. Se ignorar a árvore e analisar os objetos numericamente, organizará as páginas na ordem errada numa fração significativa de ficheiros do mundo real.

A árvore de páginas: o que realmente define a ordem

Cada PDF começa com um catálogo de documento (ISO 32000-2 §7.7.2). O catálogo contém uma entrada /Pages que aponta para o nó raiz da árvore de páginas. Esse nó raiz é um dicionário com /Type /Pages, uma matriz (array) /Kids de referências indiretas e um /Count que fornece o número total de páginas folha (leaf pages) sob ele. A ordem de exibição é a travessia dessa árvore de forma profunda e da esquerda para a direita, sem mais.

Um ficheiro minimal de três páginas torna isto concreto:

%PDF-1.7

1 0 obj
<< /Type /Catalog /Pages 2 0 R >>
endobj

2 0 obj
<< /Type /Pages /Kids [20 0 R  4 0 R  9 0 R] /Count 3 >>
endobj

% Object 4 is stored third in the file but is page 2 in display order
4 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792]
   /Contents 5 0 R /Resources << /Font << /F1 6 0 R >> >> >>
endobj

% Object 9 is stored fourth but is page 3
9 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792]
   /Contents 10 0 R /Resources << /Font << /F1 6 0 R >> >> >>
endobj

% Object 20 is stored last but is page 1; Kids[0] decides, not object number
20 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792]
   /Contents 21 0 R /Resources << /Font << /F1 6 0 R >> >> >>
endobj

A matriz /Kids contém [20 0 R 4 0 R 9 0 R], pelo que o objeto 20 é a página 1, o objeto 4 é a página 2 e o objeto 9 é a página 3. A numeração dos objetos é irrelevante. Qualquer código que itere os objetos em ordem numérica e recolha aqueles com /Type /Page produzirá a sequência incorreta neste ficheiro.

Porque é que os geradores produzem layouts não sequenciais? Por várias razões. Uma biblioteca que pré-aloca números de objetos para todas as páginas antes de escrever o seu conteúdo irá numerá-las na ordem de criação, escrevendo depois os bytes reais na ordem que for conveniente para o serializador. Uma ferramenta de fusão (merge) que junta documentos renumera os objetos de cada documento de origem para evitar colisões; os objetos de página renumerados acabam espalhados pela tabela de objetos combinada, enquanto a nova matriz /Kids raiz contém a sequência de exibição correta. As atualizações incrementais anexam novos objetos no fim do ficheiro com novos números, pelo que uma página adicionada numa revisão vive perto do fim do fluxo de bytes, mesmo que pertença à posição 1 da ordem de exibição.

Árvores planas e subárvores aninhadas

A especificação permite duas formas para a árvore de páginas. Os geradores simples produzem uma estrutura plana: um nó /Pages raiz cuja matriz /Kids não contém nada além de objetos folha /Page. Isso é fácil de percorrer: um nível de profundidade, uma única passagem.

Documentos grandes utilizam rotineiramente uma árvore equilibrada. A matriz /Kids do nó /Pages raiz contém nós /Pages intermédios, cada um dos quais contém a sua própria matriz /Kids. O /Count em cada nó intermédio indica o número total de páginas folha na sua subárvore, permitindo que o visualizador ignore subárvores inteiras ao saltar para uma página por índice, sem necessidade de analisar todos os objetos. Um documento de 1000 páginas estruturado como uma árvore equilibrada com 10 páginas por nó folha pode localizar a página 750 através de uma pesquisa binária em três ou quatro pesquisas de dicionário, em vez de verificar 750 entradas /Kids.

A consequência para o código de processamento: não pode assumir que o primeiro nível de /Kids contém objetos /Page. Cada elemento filho deve ser verificado. Se o seu /Type for /Pages, faça uma chamada recursiva. Se o seu /Type for /Page, trata-se de uma folha. Parar no primeiro nível irá descartar silenciosamente subárvores inteiras em qualquer documento onde o gerador tenha optado pelo aninhamento.

Atributos de página herdados

A árvore de páginas também possui um mecanismo de partilha de recursos. Certos atributos de página: /MediaBox, /CropBox, /Resources e /Rotate são herdáveis (ISO 32000-2 §7.7.3.4). Se um dicionário /Page omitir um deles, o leitor percorre a cadeia de /Parent até encontrar o atributo ou alcançar a raiz. Colocar um dicionário de tipos de letra partilhado no nó /Pages raiz, em vez de o copiar para cada página folha, pode reduzir visivelmente o tamanho do ficheiro em documentos que utilizam os mesmos tipos de letra em toda a sua extensão.

A regra de herança cria uma subtileza para o código que lê as propriedades da página. Ler /MediaBox diretamente de um objeto /Page e tratar uma chave ausente como um erro está incorreto; a chave pode ser simplesmente herdada. O código que resolve corretamente a geometria da página deve seguir a cadeia de pais. Também precisa de uma proteção contra ciclos: um ficheiro corrompido pode ter uma referência /Parent que aponta para um nó já visitado, o que entraria num ciclo infinito sem uma verificação de objetos visitados.

A tabela xref e os fluxos de referências cruzadas

A procura de objetos indiretos é feita através da tabela de referências cruzadas (ou do seu sucessor, o fluxo de referências cruzadas introduzido no PDF 1.5). O xref mapeia cada número de objeto para um desvio em bytes no ficheiro. Um leitor em conformidade utiliza o xref para saltar diretamente para qualquer objeto; não varre o ficheiro sequencialmente. Esse design de acesso aleatório é o que torna possível a navegação rápida pelas páginas: o visualizador lê o catálogo, resolve a referência /Pages através do xref, lê o nó /Pages raiz, resolve uma entrada /Kids e assim sucessivamente, acedendo apenas aos objetos de que necessita.

As atualizações incrementais adicionam uma nova secção xref no fim do ficheiro com um trailer que se liga ao anterior. Um objeto updated numa revisão recebe uma nova entrada na secção xref anexada; os bytes originais permanecem no local mas são substituídos. É assim que os PDFs assinados digitalmente permanecem verificáveis mesmo após a adição de anotações ou revisões de preenchimento de formulários: o intervalo de bytes assinado nunca é alterado e o novo conteúdo reside na secção anexada. A árvore de páginas também pode ser atualizada, pelo que adições ou exclusões de páginas numa revisão produzem uma nova raiz /Pages com uma matriz /Kids revista, enquanto o objeto raiz antigo ainda ocupa a sua posição original no ficheiro.

O que corre mal sem a travessia da árvore

O modo de falha para abordagens de varrimento de objetos é silencioso. O documento de saída parece plausível: tem o número correto de páginas e cada página contém conteúdo reconhecível. A ordem é simplesmente incorreta, e de uma forma que depende do gerador, do número de revisões e de se alguma página foi fundida a partir de fontes externas. Um conjunto de ficheiros de teste produzidos por uma única ferramenta pode passar completamente; ficheiros de outra ferramenta ou de um fluxo de fusão falharão. Essa inconsistência é a razão pela qual as soluções heurísticas nunca funcionam de forma duradoura.

Os ficheiros de atualização incremental são especialmente propensos a isto, porque as páginas adicionadas ou reorganizadas em revisões posteriores contêm números de objetos elevados, enquanto a ordem de exibição é controlada pela matriz /Kids atualizada. Um varrimento que processe os objetos em ordem numérica colocará essas páginas numeradas tardiamente no fim, independentemente do local onde a árvore indica que pertencem.

A solução não é complicada. Comece no catálogo, resolve a referência /Pages, percorra a matriz /Kids de forma recursiva e emita as folhas na ordem em que as encontra. Essa é a ordem de exibição por definição, independentemente dos números dos objetos, desvios de bytes ou estrutura do ficheiro. A maioria das bibliotecas de PDF maduras expõe uma contagem de páginas e um acessor de páginas indexado que já fazem isto corretamente; o risco reside no código que ignora o modelo de página da biblioteca e acede diretamente à camada de objetos.

Uma anomalia estrutural que vale a pena tratar explicitamente: o valor /Count num nó /Pages intermédio pode estar incorreto em ficheiros malformados. Confiar no /Count para verificação de limites e depois parar antes de uma travessia completa omitirá silenciosamente páginas quando a contagem for inferior à real. Utilizar o /Count apenas como uma sugestão de desempenho para pré-alocação de capacidade ou pesquisa binária, e obter a contagem real a partir da travessia é o padrão mais seguro.

Artigo Seguinte