Technical Article

PDF sem um dicionário de Pages: implicações na análise

O dicionário de Catálogo (Catalog) do PDF possui 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 a matriz /Kids e a contagem total (/Count) de páginas. Ao remover esse ponteiro, nenhum leitor conforme conseguirá localizar uma única página no ficheiro. A norma ISO 32000-1 §7.7.2 é inequívoca quanto a isto: o Catálogo deve possuir uma entrada /Pages, e o objeto referenciado deve ser do tipo /Pages. Os ficheiros que violam este requisito não são apenas não conformes; estão estruturalmente corrompidos de uma forma que a maioria dos analisadores (parsers) processa com dificuldade.

O que a especificação realmente diz

Um PDF conforme mínimo possui pelo menos três objetos. O objeto 1 é o Catálogo, o objeto 2 é a raiz de Pages (árvore de páginas), e do objeto 3 em diante encontram-se os dicionários de Página (Page) individuais. O Catálogo aponta para la raiz de Pages; a raiz de Pages lista os seus descendentes em /Kids; cada Página inclui uma referência inversa /Parent. Toda a cadeia é bidirecional por conceção, permitindo que um analisador comece em qualquer extremidade e percorra até qualquer página em tempo O(log n) para árvores equilibradas.

% 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 Pages pode ser aninhada. Um documento com milhares de páginas geralmente agrupa-as em objetos de nós intermédios que também possuem o tipo /Pages, cada um com a sua própria matriz /Kids e um valor /Count que reflete a subárvore abaixo dele. O valor /Count do nó raiz corresponde sempre ao total de páginas do documento. Essa contagem é o que os visualizadores apresentam no campo do número de página antes de analisarem uma única página, porque a leitura de um inteiro do objeto 2 é muito mais económica do que percorrer a árvore completa.

Como se apresenta um ficheiro sem Pages

Os ficheiros que não possuem o dicionário Pages têm geralmente origem em geradores de PDF que escrevem objetos de página diretamente, sem os organizar numa árvore, ou em corrupções de ficheiro que removem o nó raiz, deixando os objetos de Página (folhas) intactos. O Catálogo de um ficheiro deste tipo não tem a chave /Pages ou contém uma referência para um objeto que já não existe na tabela de referências cruzadas.

% 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 que siga a especificação lerá o Catálogo, tentará resolver a referência /Pages, não encontrará nada (or uma referência morta) e gerará um erro ou reportará zero páginas. O que não deve fazer é continuar como se o ficheiro contivesse zero páginas e concluir a operação silenciosamente com sucesso; isso produz uma saída em branco que parece correta para ferramentas automatizadas, mas está incorreta para qualquer pessoa que abra o ficheiro.

Porque é que os analisadores falham

A maior parte dos analisadores de PDF aloca a 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, de seguida, desreferencia um ponteiro nulo (null pointer) na primeira vez que o código solicita a página 1, ou lê lixo e aloca um buffer incorreto. Nenhum dos resultados é aceitável. A violação de acesso em 0x008E5D78 que surge nos relatórios de erro ao processar um ficheiro deste tipo traduz-se exatamente nisto: uma desreferenciação de ponteiro nulo no caminho de acesso à página, desencadeada pela ausência da estrutura que o analisador assumiu que estaria sempre presente.

A premissa de conceção subjacente é razoável. A esmagadora maioria dos ficheiros PDF existentes possui um dicionário Pages. Os analisadores que ignoram a verificação de existência para poupar algumas instruções não estão a ser imprudentes; estão a otimizar para o caso mais comum. Os ficheiros que comprometem essa otimização são raros o suficiente para que o código em produção possa nunca encontrar um, até ao momento em que isso acontece, ponto em que a falha é reproduzível e incompreensível se o engenheiro não tiver lido a secção §7.7.2.

Recuperação sem uma árvore Pages

Se um analisador for obrigado a processar estes ficheiros em vez de os rejeitar, o processo de recuperação segue um caminho previsível: analisa todos os objetos indiretos na tabela de referências cruzadas, recolhe os que contêm /Type /Page e ordena-os pelo número de objeto. A ordem dos números de objeto não garante a correspondência com a ordem de leitura na especificação, mas, na prática, os geradores que omitem a árvore Pages tendem a emitir as páginas sequencialmente, pelo que a ordem pelo número de objeto está correta na maior parte das vezes.

A própria verificação é económica. Antes de percorrer o ponteiro /Pages do Catálogo, confirme se o ponteiro existe, se aponta para um objeto real e se o /Type do objeto resolvido é /Pages. Se qualquer uma destas três condições falhar, recorra à análise linear. A análise é mais lenta do que a travessia de árvore em documentos grandes, porque lê o cabeçalho de cada objeto em vez de seguir um caminho equilibrado, mas funciona, e para um ficheiro que já se encontra malformado, a correção tem prioridade sobre a velocidade.

Existe um caso limite que a análise linear não resolve automaticamente: a ordenação das páginas. Sem uma matriz /Kids para definir a sequência, a ordem 'correta' não está definida pela especificação. A ordem pelo número de objeto é o padrão pragmático; se o ficheiro for suficientemente importante para ser processado com rigor, verificar se os objetos de Página possuem um campo /StructParents explícito ou referências de anotação que sugiram uma sequência de leitura compensa o trabalho adicional.

Implicações para geradores de PDF

Para quem desenvolve um gerador de PDF em vez de um analisador, a lição é direta: crie sempre a raiz de Pages antes de fechar o ficheiro. O Catálogo sem uma entrada /Pages não constitui um PDF válido sob qualquer revisão da especificação. Os geradores que criam objetos de página dinamicamente e montam a árvore na finalização (a abordagem da maioria dos gravadores em fluxo) funcionam perfeitamente, desde que a finalização seja efetivamente executada. O modo de falha comum é uma exceção ou retorno prematuro que aborta a gravação antes de o trailer estar concluído, deixando um ficheiro que abre em alguns visualizadores (que possuem heurísticas de recuperação) e falha noutros (que não as têm).

As normas PDF/A e PDF/UA impõem restrições adicionais à árvore de páginas para lá das exigidas pela especificação base, mas nenhuma delas flexibiliza a obrigatoriedade da entrada /Pages. Um validador que verifique a conformidade com as normas ISO 19005 ou ISO 14289 detetará a ausência do dicionário Pages como uma violação da especificação base antes mesmo de avaliar as regras específicas do perfil.