Article technique

PDF sans dictionnaire Pages : implications pour l'analyse

Le dictionnaire Catalog d'un PDF n'a exactement qu'une seule clé de navigation obligatoire : /Pages. Cette clé doit pointer vers un objet indirect de type /Pages, qui à son tour contient le tableau /Kids et le /Count total de pages. Retirez ce pointeur et aucun lecteur conforme ne peut localiser la moindre page dans le fichier. L'ISO 32000-1 §7.7.2 est formelle sur ce point : le Catalog doit avoir une entrée /Pages, et l'objet référencé doit être de type /Pages. Les fichiers qui violent cette exigence ne se contentent pas d'être non conformes ; ils sont structurellement cassés d'une manière que la plupart des analyseurs gèrent mal

Ce que stipule réellement la spécification

Un PDF conforme minimal compte au moins trois objets. L'objet 1 est le Catalog, l'objet 2 est la racine Pages, et à partir de l'objet 3 viennent les dictionnaires Page individuels. Le Catalog pointe vers la racine Pages ; la racine Pages liste ses enfants dans /Kids ; chaque Page porte une référence inverse /Parent. Toute la chaîne est bidirectionnelle par conception, si bien qu'un analyseur peut partir de l'un ou l'autre bout et atteindre n'importe quelle page en O(log n) pour les arbres équilibrés

% Structure conforme minimale (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

L'arbre des pages peut être imbriqué. Un document contenant des milliers de pages regroupe généralement ses pages dans des objets nœuds intermédiaires, qui portent également le type /Pages, chacun possédant son propre tableau /Kids et un /Count reflétant son sous-arbre. Le /Count du nœud racine est toujours égal au nombre total de pages du document. Ce total est la valeur que le lecteur affiche dans la barre de numéro de page avant même d'analyser la moindre page, car lire un entier depuis l'objet 2 est bien plus rapide que de parcourir tout l'arbre

À quoi ressemble un fichier sans dictionnaire Pages

Les fichiers dépourvus du dictionnaire Pages proviennent typiquement de générateurs PDF qui écrivent les objets Page directement sans les assembler en arbre, ou d'une corruption qui retire le nœud racine en laissant intacts les objets Page feuilles. Dans un tel fichier, le Catalog n'a soit pas du tout de clé /Pages, soit une référence vers un objet qui n'existe plus dans la table des références croisées

Diagramme PDF contrastant une chaîne de structure PDF conforme du catalog à l'arbre de pages avec un fichier mal formé dont le catalog est dépourvu de l'entrée /Pages et dont les objets de pages sont orphelins et inaccessibles
Un fichier conforme enfile chaque page à travers la racine /Pages, tandis qu'un fichier sans Pages abandonne des objets de pages intacts sans entrée navigable depuis le Catalog
% Non conforme : Catalog sans référence /Pages
1 0 obj
<< /Type /Catalog >>
endobj

% Les objets Page existent mais sont inatteignables depuis le 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

Un analyseur conforme à la spécification lit le catalogue, tente de résoudre /Pages, constate qu'il pointe vers le vide (ou une référence morte), puis lève une erreur ou signale zéro page. Ce qu'il ne peut pas faire, c'est continuer à prétendre que le fichier contient des pages et "réussir" silencieusement — ce qui produirait un résultat vide, apparaissant normal pour les outils automatisés mais révélant un problème pour quiconque tente de l'ouvrir

Pourquoi l'analyseur plante

La plupart des analyseurs PDF allouent des tables de pages internes lors du chargement sur la base de la valeur /Count du nœud racine Pages. Lorsque le nœud racine est absent, l'analyseur lit soit zéro et n'alloue rien, puis déréférence un pointeur nul la première fois que le code demande la page 1, soit lit des données corrompues et alloue un tampon d'une taille complètement incorrecte. Aucun de ces résultats n'est un traitement élégant. La violation d'accès à l'adresse 0x008E5D78 apparaissant dans les journaux de crash lors du traitement de tels fichiers est précisément due à cela : un déréférencement de pointeur nul dans le chemin d'accès aux pages, déclenché par l'absence d'une structure que l'analyseur suppose toujours présente

Cette hypothèse de conception sous-jacente est en soi raisonnable. La grande majorité des PDF existants possèdent un dictionnaire Pages. Un analyseur qui omet la vérification d'existence pour économiser quelques instructions n'est pas imprudent, mais optimise pour le cas le plus courant. Les fichiers qui déclenchent cette anomalie sont si rares que le code de production peut ne jamais en rencontrer — jusqu'au jour où cela arrive, rendant le plantage à la fois reproductible et déroutant, à moins que l'ingénieur n'ait préalablement lu la section §7.7.2

Récupération sans arbre Pages

Si l'analyseur doit traiter ces fichiers plutôt que de les rejeter, le chemin de récupération est prévisible : parcourir chaque objet indirect dans la table des références croisées, collecter tous les objets avec /Type /Page, et les trier par numéro d'objet. Bien que la spécification ne garantisse pas que l'ordre des numéros d'objet soit identique à l'ordre de lecture, dans la pratique, les générateurs omettant l'arbre des Pages écrivent généralement les pages séquentiellement, de sorte que l'ordre des numéros d'objet est généralement correct

Cette vérification est peu coûteuse en soi. Avant de parcourir le pointeur /Pages du catalogue, confirmez que le pointeur existe, qu'il pointe vers un objet réel, et que le /Type de l'objet résolu est bien /Pages. Si l'une de ces trois conditions n'est pas remplie, basculez sur un balayage linéaire. Pour les grands documents, le balayage linéaire est plus lent que le parcours d'arbre car il doit lire chaque en-tête d'objet plutôt que de suivre un chemin équilibré ; mais il fonctionne, et pour un fichier corrompu, la correction prime sur la vitesse

Organigramme PDF de la récupération d'un analyseur PDF montrant trois verrous de validation sur l'entrée Pages et un scan linéaire de repli qui collecte les objets Type Page triés par numéro d'objet
Trois validations bon marché gardent le pointeur /Pages, et tout échec aiguille l'analyseur vers un balayage linéaire qui reconstruit la table des pages par numéro d'objet

Un cas limite que le balayage linéaire ne résout pas automatiquement : l'ordre des pages. Sans tableau /Kids pour définir la séquence, l'ordre « correct » n'est pas défini par la spécification. L'ordre des numéros d'objet est le choix par défaut pragmatique ; si le fichier est assez important pour mériter un traitement soigné, vérifier si les objets Page portent un /StructParents explicite ou des références d'annotations qui impliquent un ordre de lecture vaut le travail supplémentaire

Implications pour les générateurs PDF

Pour ceux qui écrivent des générateurs PDF plutôt que des analyseurs, la conclusion est simple : écrivez toujours le nœud racine Pages avant de fermer le fichier. Un catalogue sans entrée /Pages n'est un PDF valide sous aucune version de la spécification. Les générateurs qui construisent dynamiquement des objets de page pendant le traitement et assemblent l'arbre des pages lors de la phase finale (ce que font la plupart des moteurs d'écriture en flux) ne posent aucun problème tant que l'étape de finalisation est effectivement exécutée. Le mode d'échec classique est une exception ou un retour prématuré interrompant le processus d'écriture avant que la fin du fichier ne soit écrite, laissant un fichier qui s'ouvre dans certains lecteurs (avec des heuristiques de récupération) mais échoue dans d'autres (sans elles)

PDF/A et PDF/UA imposent des contraintes supplémentaires sur l'arbre des pages au-delà de ce qu'exige la spécification de base, mais aucun des deux n'assouplit l'exigence /Pages. Un validateur qui contrôle la conformité à l'ISO 19005 ou à l'ISO 14289 signale un dictionnaire Pages manquant comme violation de la spécification de base avant même d'atteindre les règles propres au profil