Notre explication complémentaire sur l'ordre des pages PDF couvre la règle de base : l'ordre d'affichage provient d'une marche de gauche à droite en profondeur (depth-first, left-to-right walk) des tableaux /Kids dans l'arborescence /Pages, jamais des numéros d'objet. Cet article examine l'arborescence sous un angle différent — sa forme. Pourquoi les écrivains PDF matures émettent-ils des hiérarchies de nœuds intermédiaires alors qu'un seul tableau plat serait parfaitement légal ? Qu'est-ce qui change réellement lorsqu'un outil aplatit ou reconstruit l'arbre ? Et que se passe-t-il lorsque la comptabilité (bookkeeping) /Count qui rend l'ensemble de la structure rapide cesse de dire la vérité
Le déploiement (fan-out) est une décision de performance
Rien n'oblige un écrivain (writer) à imbriquer (nest). Un document de 10 000 pages avec un nœud racine /Pages et 10 000 références de feuilles dans un seul tableau /Kids est conforme à la spécification. La référence PDF recommande néanmoins un arbre équilibré pour les documents volumineux, et les générateurs grand public (mainstream generators) suivent ce conseil avec un déploiement (fan-out) modeste, généralement quelques dizaines d'enfants (kids) par nœud intermédiaire
La raison est ce qu'une visionneuse doit lire avant de pouvoir montrer quoi que ce soit. Considérez un saut direct à la page 8 214 de ce fichier de 10 000 pages. Avec un arbre plat, le lecteur doit d'abord analyser le nœud racine, et ce nœud racine est un tableau énorme : à environ huit octets par référence indirecte, un objet de 80 Ko qui doit être tokenisé de bout en bout avant que l'entrée 8 213 puisse être résolue. Avec un arbre équilibré de déploiement 32, le même saut lit la racine, compare les totaux cumulés de /Count pour choisir le bon enfant, et descend — trois ou quatre petits dictionnaires au total, chacun de quelques centaines d'octets. C'est l'accès aléatoire (random access) O(log n) que l'arborescence a été conçue pour fournir, et c'est la raison entière pour laquelle /Count existe sur les nœuds intermédiaires : cela permet à un lecteur de sauter un sous-arbre entier sans ouvrir un seul objet à l'intérieur
La forme de l'arborescence fixe également le coût d'édition. Une mise à jour incrémentielle (incremental update) qui insère une page doit réécrire chaque nœud dont /Kids ou /Count a changé, ce qui signifie le chemin depuis le parent de la nouvelle feuille jusqu'à la racine. Dans un arbre équilibré, ce chemin correspond à une poignée de petits dictionnaires ajoutés (appended) au fichier. Dans un arbre plat, le "chemin" est l'unique tableau racine géant, dupliqué dans son intégralité à chaque révision. Un contrat qui passe par trente cycles de révision et d'annotation peut se retrouver à transporter trente copies remplacées (superseded) du même tableau de 80 Ko dans son flux d'octets
Les nœuds intérieurs portent des attributs hérités
Les nœuds intermédiaires ne sont pas que du routage. Les quatre attributs de page héritables — /Resources, /MediaBox, /CropBox et /Rotate — peuvent être hissés (hoisted) sur n'importe quel nœud /Pages, où ils s'appliquent à chaque feuille en dessous à moins qu'un descendant ne les remplace. Un écrivain produisant un rapport avec une annexe au format paysage peut exprimer cette disposition (layout) dans l'arborescence elle-même :
5 0 obj % racine du document
<< /Type /Pages /Count 6 /Kids [6 0 R 7 0 R] >>
endobj
6 0 obj % corps du rapport : portrait A4, police du corps
<< /Type /Pages /Parent 5 0 R /Count 3
/Kids [30 0 R 31 0 R 32 0 R]
/MediaBox [0 0 595 842]
/Resources << /Font << /F1 8 0 R >> >> >>
endobj
7 0 obj % annexe : paysage A4, pivoté, sa propre police
<< /Type /Pages /Parent 5 0 R /Count 3
/Kids [40 0 R 41 0 R 42 0 R]
/MediaBox [0 0 842 595] /Rotate 90
/Resources << /Font << /F2 9 0 R >> >> >>
endobj
40 0 obj % page d'annexe : hérite de la taille, de la rotation, des polices
<< /Type /Page /Parent 7 0 R /Contents 43 0 R >>
endobj
Les objets 40 à 42 sont presque vides. Leur taille de page, leur rotation et leurs ressources de polices arrivent toutes par héritage du nœud 7, ce qui maintient le fichier compact et auto-entretenu : ajoutez une quatrième page sous le nœud d'annexe et elle sortira automatiquement au format paysage
Le même mécanisme crée le risque classique de déplacement de page (page-move hazard). Supposons qu'un outil déplace l'objet 40 dans le corps du rapport en éditant les deux tableaux /Kids et en repointant /Parent vers le nœud 6. Le déplacement est structurellement valide, mais l'objet 40 hérite désormais de la /MediaBox portrait, d'aucune rotation et de la police /F1 — tandis que son flux de contenu sélectionne toujours /F2, qui ne se résout plus. La page rétrécit, se dé-pivote et perd son texte en une seule modification. Un code de réorganisation robuste matérialise donc les valeurs résolues des quatre attributs héritables sur le dictionnaire de page avant de le re-parenter (reparenting it). Si vous avez déjà fait glisser une page dans un éditeur et regardé comment elle change de taille ou d'orientation, c'est le mécanisme dont vous avez été témoin
Aplatissement (Flattening) : légal, courant, parfois coûteux
De nombreux outils font l'inverse. Les écrivains minimaux émettent un arbre à un seul niveau car c'est simple, et de nombreux utilitaires de fusion et de fractionnement (merge and split utilities) reconstruisent n'importe quel arbre qu'ils lisent en un seul tableau /Kids plat, car la génération d'une structure équilibrée représente un travail supplémentaire et une sortie plate est toujours conforme. Une reconstruction correcte doit résoudre l'héritage en même temps : chaque attribut dont héritait une feuille doit être copié sur la feuille, ou hissé vers la nouvelle racine s'il est uniforme dans tout le document — sinon la sortie change de géométrie exactement de la même manière que dans le cas du déplacement de page
Pour les documents typiques, l'aplatissement est inoffensif. Cela fait mal à l'échelle, des deux manières déjà décrites : le tableau racine devient un grand objet que chaque ouverture et chaque saut de page doivent analyser intégralement, et chaque édition structurelle le réécrit en entier. Ce que l'aplatissement ne détruit pas, c'est le partage par des références indirectes — un arbre plat dans lequel les 10 000 pages pointent vers le même objet dictionnaire /Resources est toujours dédupliqué. Ce qui est perdu, c'est seulement la possibilité de laisser l'entrée hors de la page et de laisser un ancêtre la fournir
Quand /Count ment
/Count est purement comptable : il doit être égal au nombre de pages feuilles dans le sous-arbre du nœud, et rien dans le format de fichier ne l'impose. Deux modèles de corruption (corruption patterns) expliquent la plupart des décomptes mensongers observés dans la nature
Le premier est le décompte obsolète (stale count) laissé par une mise à jour incrémentielle. Un éditeur insère une page, réécrit le parent immédiat avec un nouveau /Kids et un /Count mis à jour, ajoute les deux au fichier — et ne touche jamais aux ancêtres :
% Révision originale
12 0 obj
<< /Type /Pages /Count 9 /Kids [13 0 R 14 0 R 15 0 R] >>
endobj
14 0 obj
<< /Type /Pages /Parent 12 0 R /Count 3
/Kids [50 0 R 51 0 R 52 0 R] >>
endobj
% Révision ajoutée : une page insérée dans la branche du milieu.
% L'objet 14 est remplacé (superseded) ; l'objet 12 n'est jamais réécrit
14 0 obj
<< /Type /Pages /Parent 12 0 R /Count 4
/Kids [50 0 R 51 0 R 90 0 R 52 0 R] >>
endobj
L'arbre contient maintenant dix feuilles, mais la racine indique toujours neuf. Une visionneuse qui fait confiance à la racine signale neuf pages dans son compteur de pages. Celle qui utilise les décomptes intérieurs pour rechercher de manière binaire (binary-search) un saut de page calcule un index erroné pour chaque page après le point d'insertion. Une traversée complète en trouve dix. Trois réponses différentes, un seul fichier
Le second modèle est le décompte qui ne pourrait jamais être correct : négatif, zéro sur un nœud peuplé, ou absurdement énorme. Ceux-ci proviennent de tests par fuzzing (fuzzing), de dommages de transmission et parfois de bogues arithmétiques dans les éditeurs. Ils sont dangereux en particulier pour le code qui fait confiance à /Count pour l'allocation — dimensionner un tableau à partir d'un /Count de -3 déclenche une erreur de plage (range error) au mieux, et le faire à partir d'un /Count de deux milliards est une allocation de déni de service (denial-of-service allocation). La valeur est une entrée non fiable (untrusted input), comme tous les autres nombres du fichier
Les analyseurs (parsers) se divisent en deux camps à propos de tout cela. Les consommateurs stricts — outils de contrôle en amont (preflight tools), validateurs PDF/A, pipelines d'archivage — comparent /Count au résultat de la traversée et rejettent ou signalent le fichier. Les visionneuses interactives sont presque universellement indulgentes (lenient) : elles traversent, dérivent le décompte réel et ignorent silencieusement celui stocké, ce qui explique exactement pourquoi un fichier à décompte obsolète (stale-count file) peut circuler pendant des années sans plainte jusqu'à ce qu'il rencontre un analyseur plus strict dans un flux de travail automatisé. Le terrain d'entente défensif (defensive middle ground) pour le code de bibliothèque consiste à traiter /Count comme une indication (hint) — utile pour la pré-allocation et pour sauter des sous-arbres une fois vérifié — tout en laissant la traversée rester la source de vérité
Pour l'algorithme de traversée (traversal algorithm) lui-même, les règles de recherche d'héritage (inheritance lookup rules) et la marche du catalogue vers la feuille (catalog-to-leaf walk), commencez par l'explication de l'ordre des pages. Pour savoir à quoi ressemblent ces modes de défaillance lorsqu'un véritable document client atteint le code de production, lisez l'étude de cas de débogage d'ordre de page, qui suit un incident de pages mélangées (shuffled-pages incident) du symptôme à la cause fondamentale (root cause)
Le Composant HotPDF gère tout cela en interne : il traverse des arbres imbriqués de n'importe quelle profondeur, résout les attributs hérités lorsque des pages sont copiées ou déplacées, et vérifie /Count par rapport aux décomptes de feuilles réels au lieu de s'y fier, de sorte que les indices de page dans son API signifient toujours des pages logiques