Artigo Técnico

PDF Sem um Dicionário de Páginas: Implicações na Análise

O dicionário Catalog do PDF tem exatamente uma chave de navegação obrigatória: /Pages. Essa chave deve apontar para um objeto indireto do tipo /Pages, que por sua vez contém o array /Kids e o /Count (contagem) total de páginas. Remova esse ponteiro e nenhum leitor em conformidade poderá localizar uma única página no arquivo. A ISO 32000-1 §7.7.2 é inequívoca sobre este ponto: o Catalog deve ter uma entrada /Pages, e o objeto referenciado deve ser do tipo /Pages. Arquivos que violam esse requisito não são apenas não-conformes; eles são estruturalmente corrompidos de uma forma que a maioria dos analisadores lida mal

O que a especificação realmente diz

Um PDF em conformidade mínimo tem pelo menos três objetos. O objeto 1 é o Catalog, o objeto 2 é a raiz de Pages, e do objeto 3 em diante são os dicionários individuais de Page (Página). O Catalog aponta para a raiz de Pages; a raiz de Pages lista seus filhos em /Kids; cada Page carrega uma referência reversa /Parent (pai). Toda a cadeia é bidirecional por design, de modo que um analisador pode começar de qualquer extremidade e atravessar para qualquer página em tempo O(log n) para árvores balanceadas

% Minimal conforming structure (ISO 32000-1 §7.7.2)
1 0 obj
<< /Type /Catalog /Pages 2 0 R >>
endobj

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

3 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Contents 5 0 R /Resources << >> >>
endobj

4 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Contents 6 0 R /Resources << >> >>
endobj

A árvore de Páginas pode ser aninhada. Um documento com milhares de páginas geralmente agrupa páginas em objetos de nó intermediários que também carregam o tipo /Pages, cada um com seus próprios /Kids e um /Count refletindo a subárvore abaixo dele. O /Count do nó raiz sempre é igual à contagem total de páginas. Essa contagem é o que os visualizadores exibem no campo de número de página antes de terem analisado uma única página, porque ler um número inteiro do objeto 2 é muito mais barato do que percorrer a árvore inteira

Como é a aparência de um arquivo sem Pages

Arquivos sem o dicionário Pages normalmente se originam de geradores de PDF que gravam objetos de página diretamente, sem agrupá-los em uma árvore, ou de corrupções que removem o nó raiz, mas deixam os objetos Page folha intactos. O Catalog em um arquivo assim carece inteiramente da chave /Pages ou contém uma referência a um objeto que não existe mais na tabela de referência cruzada

% Non-conforming: Catalog with no /Pages reference
1 0 obj
<< /Type /Catalog >>
endobj

% Page objects exist but are unreachable from the Catalog
5 0 obj
<< /Type /Page /MediaBox [0 0 612 792] /Contents 6 0 R /Resources << >> >>
endobj

15 0 obj
<< /Type /Page /MediaBox [0 0 612 792] /Contents 16 0 R /Resources << >> >>
endobj

25 0 obj
<< /Type /Page /MediaBox [0 0 612 792] /Contents 26 0 R /Resources << >> >>
endobj

Um analisador (parser) que segue a especificação lerá o Catalog, tentará resolver /Pages, não encontrará nada (ou uma referência inoperante) e levantará um erro ou relatará zero páginas. O que ele não deve fazer é continuar como se o arquivo tivesse zero páginas e obter sucesso silenciosamente; isso produz uma saída em branco que parece correta para ferramentas automatizadas e incorreta para todo ser humano que o abre

Por que os analisadores travam (crash)

A maioria dos analisadores de PDF aloca sua tabela de páginas interna no momento do carregamento, com base no valor /Count da raiz de Pages. Quando essa raiz está ausente, o analisador lê zero, não aloca nada e, em seguida, desreferencia um ponteiro nulo (null pointer) na primeira vez que qualquer código solicitar a página 1, ou lê lixo e aloca um buffer incrivelmente incorreto. Nenhum dos dois resultados é amigável. A violação de acesso em 0x008E5D78 que aparece nos logs de travamento (crash logs) ao processar tal arquivo é exatamente isso: uma desreferenciação de ponteiro nulo dentro do caminho de acesso à página, desencadeada pela ausência da estrutura que o analisador presumiu que estaria sempre ali

A suposição de design subjacente é razoável. A grande maioria dos PDFs existentes possui um dicionário Pages. Analisadores que ignoram a verificação de existência para salvar algumas instruções não estão sendo imprudentes; eles estão otimizando para o caso comum. Os arquivos que punem essa otimização são raros o suficiente para que o código de produção talvez nunca encontre um até que o faça, momento em que o travamento é tanto reproduzível quanto desconcertante, caso o engenheiro não tenha lido o §7.7.2

Recuperação sem uma árvore de Pages

Se um analisador precisa lidar com esses arquivos em vez de rejeitá-los, a recuperação segue um caminho previsível: varrer todo objeto indireto na tabela de referência cruzada, coletar aqueles com /Type /Page e classificá-los por número de objeto. A ordem do número do objeto não tem garantia de corresponder à ordem de leitura na especificação, mas na prática, os geradores que omitem a árvore de Pages tendem a emitir as páginas sequencialmente, de modo que a ordem do número do objeto é correta na maioria das vezes

A verificação em si é barata. Antes de percorrer o ponteiro /Pages do Catalog, confirme que o ponteiro existe, que ele resolve para um objeto real e que o /Type do objeto resolvido é igual a /Pages. Se qualquer uma dessas três condições falhar, recorra à varredura linear. A varredura é mais lenta do que a travessia de árvore para documentos grandes, porque lê cada cabeçalho de objeto em vez de seguir um caminho balanceado, mas funciona, e para um arquivo que já está malformado, a exatidão supera a velocidade

Um caso extremo que a varredura linear não resolve automaticamente: a ordem das páginas. Sem um array /Kids para definir a sequência, a ordem "correta" é indefinida pela especificação. A ordem dos números dos objetos é o padrão pragmático; se o arquivo for importante o suficiente para ser processado com cuidado, verificar se os objetos Page contêm um /StructParents explícito ou referências de anotação que implicam uma sequência de leitura vale o trabalho extra

Implicações para geradores de PDF

Para quem está escrevendo um gerador de PDF em vez de um analisador, a lição é restrita: sempre emita a raiz de Pages antes de fechar o arquivo. O Catalog sem uma entrada /Pages não é um PDF válido sob nenhuma revisão da especificação. Geradores que constroem objetos de página em tempo real e montam a árvore na finalização (a abordagem que a maioria dos escritores de fluxo - streaming writers - usa) estão bem, contanto que a finalização realmente aconteça. O modo de falha comum é uma exceção ou retorno antecipado que aborta a gravação antes que o trailer esteja completo, deixando para trás um arquivo que abre em alguns visualizadores (que possuem heurísticas de recuperação) e falha em outros (que não as possuem)

PDF/A e PDF/UA impõem restrições adicionais na árvore de páginas além do que a especificação básica exige, mas nenhum deles relaxa a exigência do /Pages. Um validador que checa a conformidade com a ISO 19005 ou ISO 14289 pegará um dicionário Pages ausente como uma violação da especificação básica antes mesmo de chegar às regras específicas do perfil