Article technique

Ordre des pages PDF : comment l'arborescence des pages contrôle la séquence des pages

Le numéro d'objet 1 n'est pas la page 1. Ce simple fait fait trébucher (trips up) plus de code de traitement PDF que tout autre aspect du format, et comprendre pourquoi nécessite de regarder au-delà de ce qu'une visionneuse vous montre pour examiner le graphe d'objets que la visionneuse lit réellement

Un fichier PDF est une collection d'objets indirects numérotés. Chaque objet porte un numéro d'objet et un numéro de génération, et d'autres objets le pointent avec une référence écrite sous la forme N G R : 3 0 R signifie la version actuelle de l'objet 3. Les pages font partie de ces objets, mais leur séquence d'affichage n'a rien à voir avec leur emplacement dans le fichier ou les numéros qu'elles portent. L'ordre d'affichage est entièrement déterminé par l'arborescence /Pages, une structure liée enracinée dans le catalogue du document. Si vous ignorez l'arborescence et analysez (scan) les objets numériquement, vous assemblerez les pages dans le mauvais ordre pour une fraction importante des fichiers du monde réel

L'arborescence des pages : ce qui définit réellement l'ordre

Chaque PDF commence par un catalogue de documents (ISO 32000-2 §7.7.2). Le catalogue contient une entrée /Pages qui pointe vers le nœud racine (root node) de l'arborescence des pages. Ce nœud racine est un dictionnaire avec /Type /Pages, un tableau /Kids de références indirectes et un /Count donnant le nombre total de pages feuilles (leaf-page count) en dessous. L'ordre d'affichage est la traversée de gauche à droite en profondeur (depth-first left-to-right traversal) de cet arbre, point final (full stop)

Un fichier minimal de trois pages rend cela concret :

%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

% L'objet 4 est stocké en troisième position dans le fichier mais constitue la page 2 dans l'ordre d'affichage
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

% L'objet 9 est stocké en quatrième position mais constitue la 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

% L'objet 20 est stocké en dernier mais constitue la page 1 ; c'est Kids[0] qui décide, pas le numéro d'objet
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

Le tableau /Kids indique [20 0 R 4 0 R 9 0 R], donc l'objet 20 est la page 1, l'objet 4 est la page 2, et l'objet 9 est la page 3. La numérotation des objets n'a pas d'importance. Tout code qui itère les objets dans l'ordre numérique et collecte ceux dont le /Type /Page est défini produira la mauvaise séquence sur ce fichier

Pourquoi les générateurs produisent-ils des dispositions (layouts) non séquentielles ? Pour plusieurs raisons. Une bibliothèque qui pré-alloue des numéros d'objet pour toutes les pages avant d'écrire leur contenu les numérotera dans l'ordre de création, puis écrira les octets réels dans l'ordre qui convient au sérialiseur. Un outil de fusion qui assemble des documents renumérote les objets de chaque document source pour éviter les collisions ; les objets de page renumérotés finissent dispersés dans la table d'objets combinée tandis que le nouveau tableau racine /Kids contient la séquence d'affichage correcte. Les mises à jour incrémentielles (Incremental updates) ajoutent de nouveaux objets à la fin du fichier avec de nouveaux numéros, de sorte qu'une page ajoutée en tant que révision se trouve près de la fin du flux d'octets même si elle appartient à la position 1 de l'ordre d'affichage

Arbres plats et sous-arbres imbriqués (nested subtrees)

La spécification autorise deux formes pour l'arborescence des pages. Les générateurs simples produisent une structure plate : un nœud racine /Pages dont le tableau /Kids ne contient rien d'autre que des objets feuilles /Page. C'est facile à traverser : un niveau de profondeur, un seul passage

Les documents volumineux utilisent couramment un arbre équilibré (balanced tree) à la place. Le tableau /Kids du nœud racine /Pages contient des nœuds /Pages intermédiaires, chacun d'entre eux contenant à son tour son propre tableau /Kids. Le /Count sur chaque nœud intermédiaire signale le nombre total de pages feuilles dans son sous-arbre, de sorte qu'une visionneuse peut sauter des sous-arbres entiers lorsqu'elle saute à une page par index sans analyser chaque objet. Un document de 1 000 pages structuré sous forme d'arbre équilibré avec 10 pages par nœud feuille peut localiser la page 750 par recherche binaire (binary search) via trois ou quatre recherches de dictionnaire (dictionary lookups) plutôt que d'analyser 750 entrées /Kids

La conséquence pour le code de traitement : vous ne pouvez pas supposer que le premier niveau de /Kids contient des objets /Page. Chaque enfant doit être vérifié. Si son /Type est /Pages, effectuez une récursion (recurse into it). Si son /Type est /Page, c'est une feuille. S'arrêter au premier niveau abandonne (drops) silencieusement des sous-arbres entiers sur tout document où le générateur a choisi d'imbriquer (to nest). Pourquoi les écrivains (writers) choisissent des arbres profonds en premier lieu, ce que les outils d'aplatissement (flattening tools) abandonnent, et comment la corruption de /Count se déroule dans la pratique sont abordés dans notre article d'accompagnement sur la forme de l'arborescence des pages, le déploiement (fan-out) et l'intégrité de /Count

Attributs de page hérités

L'arborescence des pages comporte également un mécanisme de partage de ressources. Certains attributs de page : /MediaBox, /CropBox, /Resources et /Rotate sont héritables (ISO 32000-2 §7.7.3.4). Si un dictionnaire /Page omet l'un d'entre eux, un lecteur remonte la chaîne /Parent jusqu'à ce qu'il trouve l'attribut ou atteigne la racine. Placer un dictionnaire de polices partagé dans le nœud racine /Pages plutôt que de le copier dans chaque page feuille peut réduire considérablement la taille du fichier pour les documents qui utilisent les mêmes polices de caractères (typefaces) tout au long

La règle d'héritage crée une subtilité pour le code qui lit les propriétés de la page. Lire /MediaBox directement à partir d'un objet /Page et traiter une clé manquante comme une erreur est faux ; la clé peut simplement être héritée. Le code qui résout correctement la géométrie de la page doit suivre la chaîne parente (parent chain). Il a également besoin d'une protection contre les cycles (cycle guard) : un fichier corrompu peut avoir une référence /Parent qui pointe vers un nœud déjà visité, ce qui bouclerait à l'infini sans une vérification des objets visités (visited-object check)

La table xref et les flux de références croisées (cross-reference streams)

La recherche d'objets indirects passe par la table de références croisées (ou son successeur, le flux de références croisées introduit dans le PDF 1.5). Le xref mappe (maps) chaque numéro d'objet à un décalage d'octets (byte offset) dans le fichier. Un lecteur conforme utilise le xref pour sauter directement à n'importe quel objet ; il n'analyse (scan) pas le fichier de manière séquentielle. Cette conception à accès aléatoire (random-access design) est ce qui rend le saut de page rapide possible : la visionneuse lit le catalogue, résout la référence /Pages via le xref, lit le nœud racine /Pages, résout une entrée /Kids, et ainsi de suite, en ne touchant que les objets dont elle a besoin

Les mises à jour incrémentielles ajoutent une nouvelle section xref à la fin du fichier avec un trailer qui s'enchaîne à la précédente. Un objet mis à jour dans une révision obtient une nouvelle entrée dans la section xref ajoutée (appended) ; les octets d'origine restent en place mais sont remplacés (superseded). C'est ainsi que les PDF signés numériquement restent vérifiables même après l'ajout d'annotations ou de révisions de remplissage de formulaires : la plage d'octets signée (signed byte range) n'est jamais touchée, et le nouveau contenu vit dans la section ajoutée. L'arborescence des pages peut également être mise à jour, de sorte que les ajouts ou suppressions de pages dans une révision produisent une nouvelle racine /Pages avec un tableau /Kids révisé, tandis que l'ancien objet racine occupe toujours sa position d'origine dans le fichier. Les fichiers linéarisés (optimisés pour le Web) ajoutent une torsion à la disposition des octets (byte-layout twist) : les objets de la page 1 sont physiquement déplacés vers le début du fichier afin qu'une visionneuse puisse afficher la première page pendant que le reste est encore en cours de téléchargement, mais l'arborescence des pages reste la seule autorité en matière d'ordre — seuls les décalages enregistrés dans le xref changent

Ce qui se passe mal sans la traversée de l'arbre (tree traversal)

Le mode de défaillance (failure mode) des approches d'analyse d'objets (object-scan) est silencieux. Le document de sortie semble plausible : il a le bon nombre de pages et chaque page contient un contenu reconnaissable. L'ordre est tout simplement faux, et faux d'une manière qui dépend du générateur, du nombre de révisions et de la fusion (merged) éventuelle de pages provenant de sources externes. Un corpus de test de fichiers produits par un seul outil peut réussir complètement ; les fichiers provenant d'un outil différent ou d'un flux de travail de fusion échoueront. Cette incohérence est la raison pour laquelle les corrections heuristiques ne tiennent jamais. Pour un examen (walkthrough) détaillé de cette défaillance exacte dans un vrai document client — symptôme, erreur de diagnostic et correction par traversée — consultez notre étude de cas de débogage d'ordre de page

Les fichiers de mise à jour incrémentielle y sont particulièrement sujets (prone) car les pages ajoutées ou réorganisées dans les révisions ultérieures portent des numéros d'objet élevés tandis que l'ordre d'affichage est contrôlé par le tableau /Kids mis à jour. Une analyse (scan) qui traite les objets dans l'ordre numérique placera ces pages à numéro tardif à la fin, quel que soit l'endroit où l'arborescence indique qu'elles appartiennent

La solution n'est pas compliquée. Commencez par le catalogue, résolvez la référence /Pages, parcourez le tableau /Kids de manière récursive et émettez les feuilles dans l'ordre où vous les rencontrez. C'est l'ordre d'affichage par définition, indépendamment des numéros d'objet, des décalages d'octets ou de la structure du fichier. La plupart des bibliothèques PDF matures exposent un nombre de pages et un accesseur de page indexé qui le font déjà correctement ; le risque réside dans le code qui contourne (bypasses) le modèle de page de la bibliothèque et touche directement la couche d'objets

Une anomalie structurelle mérite d'être traitée explicitement : la valeur /Count sur un nœud /Pages intermédiaire peut être fausse dans les fichiers mal formés. Faire confiance à /Count pour la vérification des limites (bounds checking) puis s'arrêter avant une traversée complète omettra silencieusement des pages lorsque le décompte est sous-estimé (understated). N'utiliser /Count que comme une indication de performance pour la pré-allocation de capacité ou la recherche binaire, et dériver le décompte réel de la traversée est le modèle (pattern) le plus sûr pour les documents importants

 Article suivant