Artigo Técnico

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

O objeto número 1 não é a página 1. Esse único fato confunde mais os códigos de processamento de PDF do que qualquer outro aspecto do formato, e entender o porquê requer ir além do que o visualizador mostra e observar o grafo de objetos que o visualizador realmente lê

Um arquivo PDF é uma coleção de objetos indiretos numerados. Cada objeto carrega um número de objeto e um número de geração, e outros objetos apontam para ele com uma referência escrita como N G R: 3 0 R significa a versão atual do objeto 3. As páginas estão entre esses objetos, mas sua sequência de exibição não tem nada a ver com o local onde estão no arquivo ou quais números elas carregam. A ordem de exibição é determinada inteiramente pela árvore /Pages, uma estrutura vinculada enraizada no catálogo do documento. Se você ignorar a árvore e verificar os objetos numericamente, montará as páginas na ordem errada para uma fração significativa de arquivos do mundo real

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

Todo 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, um array /Kids de referências indiretas e um /Count informando a contagem total de páginas-folha abaixo dele. A ordem de exibição é a travessia dessa árvore em profundidade, da esquerda para a direita, e ponto final

Um arquivo mínimo de três páginas torna isso 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

O array /Kids lê-se [20 0 R 4 0 R 9 0 R], então 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 objetos em ordem numérica e colete aqueles com /Type /Page produzirá a sequência errada neste arquivo

Por que os geradores produzem layouts não sequenciais? Várias razões. Uma biblioteca que pré-aloca números de objetos para todas as páginas antes de escrever seu conteúdo as numerará na ordem de criação e, em seguida, gravará os bytes reais na ordem que convier ao serializador. Uma ferramenta de mesclagem que une 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 o novo array /Kids raiz mantém a sequência de exibição correta. Atualizações incrementais anexam novos objetos ao final do arquivo com novos números, de modo que uma página adicionada como revisão fica perto do final 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. Geradores simples produzem uma estrutura plana: um nó /Pages raiz cujo array /Kids contém apenas objetos folha /Page. Isso é fácil de percorrer: um nível de profundidade, uma passagem

Documentos grandes, rotineiramente, usam uma árvore balanceada em vez disso. O array /Kids do nó /Pages raiz contém nós /Pages intermediários, cada um dos quais, por sua vez, contém seu próprio array /Kids. O /Count em cada nó intermediário relata o número total de páginas-folha em sua subárvore, de modo que um visualizador pode ignorar subárvores inteiras ao pular para uma página por índice sem analisar todos os objetos. Um documento de 1.000 páginas estruturado como uma árvore balanceada com 10 páginas por nó folha pode localizar a página 750 por meio de pesquisa binária em três ou quatro buscas em dicionário, em vez de escanear 750 entradas /Kids

A consequência para o código de processamento: você não pode presumir que o primeiro nível de /Kids contém objetos /Page. Cada filho deve ser verificado. Se o seu /Type for /Pages, entre nele recursivamente. Se o seu /Type for /Page, ele é uma folha. Parar no primeiro nível descarta silenciosamente subárvores inteiras em qualquer documento em que o gerador escolheu aninhar. O motivo pelo qual os criadores escolhem árvores profundas em primeiro lugar, do que as ferramentas de achatamento abrem mão e como a corrupção do /Count ocorre na prática são abordados em nosso artigo complementar sobre forma da árvore de páginas, dispersão e integridade do /Count

Atributos de página herdados

A árvore de páginas também carrega um mecanismo de compartilhamento de recursos. Certos atributos da página: /MediaBox, /CropBox, /Resources e /Rotate são herdáveis (ISO 32000-2 §7.7.3.4). Se um dicionário /Page omite um deles, um leitor sobe pela cadeia /Parent até encontrar o atributo ou alcançar a raiz. Colocar um dicionário de fonte compartilhado no nó raiz /Pages em vez de copiá-lo para cada página-folha pode reduzir visivelmente o tamanho do arquivo para documentos que usam os mesmos tipos de letra em toda a sua extensão

A regra de herança cria uma sutileza para o código que lê propriedades da página. Ler /MediaBox diretamente de um objeto /Page e tratar uma chave ausente como um erro está errado; a chave pode simplesmente ser herdada. O código que resolve corretamente a geometria da página deve seguir a cadeia pai. Ele também precisa de uma proteção contra ciclos: um arquivo corrompido pode ter uma referência /Parent que aponta de volta para um nó já visitado, o que geraria um loop infinito sem uma verificação de objeto visitado

A tabela xref e os fluxos de referência cruzada

A busca de objetos indiretos passa pela tabela de referências cruzadas (ou por sua sucessora, o fluxo de referência cruzada introduzido no PDF 1.5). O xref mapeia cada número de objeto para um deslocamento de bytes dentro do arquivo. Um leitor em conformidade usa o xref para pular diretamente para qualquer objeto; ele não varre o arquivo sequencialmente. Esse design de acesso aleatório é o que torna possível o salto rápido de páginas: o visualizador lê o catálogo, resolve a referência /Pages via o xref, lê o nó /Pages raiz, resolve uma entrada /Kids e assim por diante, tocando apenas nos objetos que precisa

Atualizações incrementais adicionam uma nova seção xref no final do arquivo com um trailer que se encadeia à anterior. Um objeto atualizado em uma revisão obtém uma nova entrada na seção xref anexada; os bytes originais permanecem no lugar, mas são substituídos. É assim que os PDFs assinados digitalmente permanecem verificáveis, mesmo após a adição de revisões de anotação ou preenchimento de formulário: o intervalo de bytes assinado nunca é tocado e o novo conteúdo fica na seção anexada. A árvore de páginas também pode ser atualizada, portanto as adições ou exclusões de páginas em uma revisão produzem uma nova raiz /Pages com um array /Kids revisado, enquanto o antigo objeto raiz ainda ocupa sua posição original no arquivo. Arquivos linearizados (otimizados para web) adicionam uma variação de layout de bytes: os objetos da página 1 são movidos fisicamente para o início do arquivo para que o visualizador possa exibir a primeira página enquanto o restante ainda está baixando, no entanto, a árvore de páginas continua sendo a única autoridade na ordem — apenas os deslocamentos registrados no xref mudam

O que dá errado sem a travessia da árvore

O modo de falha para abordagens de varredura de objetos é silencioso. O documento de saída parece plausível: tem o número certo de páginas e cada página contém conteúdo reconhecível. A ordem é simplesmente errada, e errada de uma maneira que depende do gerador, do número de revisões e de se alguma página foi mesclada a partir de fontes externas. Um corpus de teste de arquivos produzidos por uma única ferramenta pode passar completamente; arquivos de uma ferramenta diferente ou de um fluxo de trabalho de mesclagem falharão. Essa inconsistência é o motivo pelo qual as correções heurísticas nunca se sustentam. Para uma análise exata dessa falha em um documento real de cliente — sintoma, diagnóstico incorreto e a correção por travessia — veja nosso estudo de caso sobre depuração de ordem de páginas

Arquivos de atualização incremental são especialmente propensos a isso porque as páginas adicionadas ou reorganizadas em revisões posteriores carregam altos números de objetos, enquanto a ordem de exibição é controlada pelo array /Kids atualizado. Uma varredura que processa objetos em ordem numérica colocará essas páginas com números finais no final, independentemente de onde a árvore diz que elas pertencem

A correção não é complicada. Comece no catálogo, resolva a referência /Pages, caminhe pelo array /Kids recursivamente e emita as folhas na ordem em que as encontrar. Essa é a ordem de exibição por definição, independentemente dos números dos objetos, deslocamentos de bytes ou estrutura do arquivo. A maioria das bibliotecas de PDF maduras expõe uma contagem de páginas e um acessador de páginas indexado que já faz isso corretamente; o risco está no código que ignora o modelo de páginas da biblioteca e acessa a camada de objetos diretamente

Uma anomalia estrutural que vale a pena lidar explicitamente: o valor /Count em um nó /Pages intermediário pode estar errado em arquivos malformados. Confiar no /Count para verificação de limites e, em seguida, interromper a travessia antes de concluí-la irá omitir silenciosamente as páginas quando a contagem estiver subestimada. Usar o /Count apenas como uma dica de desempenho para pré-alocação de capacidade ou pesquisa binária e derivar a contagem real a partir da travessia é o padrão mais seguro para documentos importantes

 Próximo Artigo