Article technique

Le modèle logique d'objets PDF : types, références et structure

Un fichier PDF est, dans son essence, une collection d'objets qui se pointent mutuellement. Faites abstraction de la compression, de la gestion des références croisées et des décalages en octets, et ce qui reste est un graphe : un petit ensemble de valeurs typées, reliées entre elles par des références, enracinées dans un unique objet que le lecteur sait localiser. Tout ce qu'un PDF peut exprimer, d'un paragraphe de texte à une police incorporée en passant par une signature numérique, se construit à partir de huit types d'objets primitifs et de la règle qui permet à un objet de faire référence à un autre. Maîtrisez ceux-ci et le reste du format se lit comme une composition plutôt que comme un mystère.

C'est la couche logique du PDF, définie dans la clause 7.3 de la norme ISO 32000-1, et elle se situe un niveau au-dessus de la disposition physique du fichier (l'en-tête, le corps, la table de références croisées et le trailer, qui font l'objet d'un article distinct dans la vue d'ensemble technique de la structure des fichiers PDF). Le modèle logique est ce que ces octets signifient une fois analysés. Un lecteur parcourt le fichier à rebours pour trouver le trailer, le suit jusqu'à la racine, et à partir de là le document se déroule comme des objets référençant des objets. C'est la partie sur laquelle on raisonne quand on débogue une page malformée, qu'on écrit un analyseur, ou qu'on fait confiance à une bibliothèque pour assembler un document.

Huit types d'objets, et rien d'autre

PDF définit exactement huit types d'objets de base. Toute valeur dans un document est l'un d'eux, ce qui rend le format gérable malgré sa portée.

Les booléens sont les mots-clés true et false. Ils activent ou désactivent des indicateurs, par exemple si une annotation est imprimable.

Les nombres se présentent en deux variantes que la spécification traite comme un seul type : les entiers comme 42 et les réels comme 3.14 ou -0.002. PDF n'a pas de notation exposant, donc on ne verra jamais 1e6 dans un fichier conforme. Les coordonnées, les tailles de police et les angles de rotation sont tous des nombres.

Les chaînes contiennent des séquences d'octets, écrites soit entre parenthèses, (Hello), soit entre chevrons comme hexadécimal, <48656C6C6F>. Les deux notations encodent un contenu identique ; l'hexadécimal est l'échappatoire pour les octets gênants dans des parenthèses. Les chaînes transportent du texte, mais ce sont d'abord des octets, ce qui importe dès qu'on manipule autre chose que de l'ASCII.

Les noms sont des jetons atomiques introduits par une barre oblique : /Type, /Pages, /MediaBox. Un nom n'est pas une chaîne ; c'est un identifiant, utilisé comme clé de dictionnaire ou valeur énumérée, et deux noms ne sont égaux que s'ils correspondent octet par octet. La barre oblique est de la syntaxe, pas une partie du nom. Cela déroute les débutants qui traitent /Times-Roman et la chaîne (Times-Roman) comme interchangeables ; le format ne le fait pas.

Les tableaux sont des listes ordonnées et hétérogènes entre crochets : [0 0 612 792] est un rectangle de page, et un tableau peut mélanger librement les types, y compris des références vers d'autres objets. Les dictionnaires sont le cheval de bataille. Écrits entre << et >>, un dictionnaire associe des clés-noms à des valeurs de n'importe quel type, et presque toute structure significative dans PDF, page, catalog, police, annotation, est un dictionnaire avec une clé /Type déclarant ce qu'il est.

Les flux sont des dictionnaires avec une queue d'octets bruts entre les mots-clés stream et endstream. Le dictionnaire décrit les octets (leur longueur, et tous filtres comme FlateDecode qui les compressent), et les octets portent la charge utile volumineuse : les instructions du contenu de page, les programmes de police incorporés, les images. Un flux est là où PDF place tout ce qui est trop grand ou trop binaire pour rester en ligne.

Le huitième type est l'objet null, le mot-clé null. C'est une vraie valeur, distincte de l'absence d'une clé. Une entrée de dictionnaire définie à null est traitée comme si elle était absente, et une référence qui se résout vers un objet inexistant donne également null plutôt qu'une erreur. Ce comportement tolérant est délibéré : il permet à un fichier endommagé de se dégrader plutôt que de refuser de s'ouvrir. Il n'y a pas de neuvième type ; tout ce que PDF exprime provient de la façon dont ces huit se combinent.

Valeurs directes, objets indirects et références

N'importe lequel de ces huit types peut apparaître de deux façons. Un objet direct est écrit en place, comme le 612 à l'intérieur d'un tableau MediaBox. Un objet indirect reçoit une identité pour que d'autres objets puissent pointer vers lui : deux entiers, un numéro d'objet et un numéro de génération, encadrant la définition entre obj et endobj :

12 0 obj
<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>
endobj

C'est l'objet 12, génération 0, un dictionnaire de police. Ailleurs dans le fichier, un autre objet y fait référence avec une référence indirecte : les deux mêmes nombres suivis du mot-clé R, 12 0 R. La référence est un pointeur. Quand le dictionnaire de ressources d'une page indique /Font << /F1 12 0 R >>, il nomme l'objet 12 comme la police derrière le nom de ressource /F1, sans copier la définition de la police dans la page.

Le numéro de génération existe pour les suppressions et les réutilisations. Quand un objet est libéré et que son emplacement est réutilisé, la génération s'incrémente de sorte qu'un ancien 12 0 R ne puisse pas se résoudre vers le nouveau locataire de l'emplacement 12. Les fichiers fraîchement écrits sont presque tous de génération 0, mais un fichier très édité peut avoir des numéros plus élevés, et un analyseur qui ignore la génération finira par lire le mauvais objet.

L'indirection est ce qui rend PDF efficace et modifiable. Une police, une image ou un espace colorimétrique peut être défini une seule fois et référencé depuis cent pages. Une petite modification peut être ajoutée comme une nouvelle révision qui remplace un seul objet plutôt que de réécrire le fichier. La table de références croisées est l'index qui transforme un numéro d'objet en décalage en octets, de sorte que le lecteur accède directement à 12 0 obj sans analyse séquentielle, mais c'est une optimisation physique. Logiquement, tout ce qu'il faut savoir c'est que 12 0 R signifie « l'objet identifié comme 12 0 ».

Le catalog : où tout document commence

La résolution des références doit commencer quelque part, et ce quelque part est l'entrée /Root du trailer, qui pointe vers le catalog du document : la racine du graphe d'objets, un dictionnaire avec /Type /Catalog. Le lecteur l'atteint en premier parce que le trailer est trouvé en premier, et à partir de là toute autre partie du document est accessible en suivant des références.

Le catalog ne comporte que deux entrées strictement obligatoires : son /Type, et /Pages, une référence indirecte vers la racine de l'arbre de pages. Le reste est optionnel et décrit le comportement à l'échelle du document plutôt que le contenu : /Outlines pointe vers l'arbre de signets, /Names contient des arbres de noms indexés par chaîne, /Metadata référence un flux de métadonnées XMP, et /PageMode et /PageLayout suggèrent comment une visionneuse doit ouvrir le document. Aucune de ces entrées n'est nécessaire pour afficher une page ; elles configurent l'expérience autour des pages. Les structures de signets, de métadonnées et d'annotations accrochées au catalog sont traitées dans l'article sur les métadonnées, signets et annotations PDF.

Le diagramme ci-dessous montre où le corps d'objets se situe dans le fichier environnant. Le catalog et l'arbre de pages vivent dans ce corps comme des objets indirects ordinaires ; l'en-tête, la table de références croisées et le trailer autour d'eux sont l'échafaudage physique qui permet au lecteur de les localiser.

Diagramme des quatre sections physiques d'un fichier PDF : un en-tête de version, un corps contenant les objets du document dont le catalog et l'arbre de pages, une table de références croisées des décalages d'objets, et un trailer pointant vers la racine

L'arbre de pages : une hiérarchie équilibrée de pages

À partir de /Pages, le document se ramifie dans l'arbre de pages, où le choix de PDF d'un graphe plutôt que d'une liste plate porte ses fruits. Les pages ne sont pas stockées comme une simple séquence ; elles sont suspendues à un arbre dont les nœuds intérieurs sont des nœuds d'arbre de pages (/Type /Pages) et dont les feuilles sont des objets page (/Type /Page). Un nœud intérieur liste ses enfants dans un tableau /Kids et enregistre, dans /Count, le nombre de pages feuilles en dessous de lui. Chaque nœud sauf la racine comporte une référence /Parent vers le haut, de sorte que l'arbre peut être parcouru dans les deux sens.

2 0 obj                                  % racine de l'arbre de pages
<< /Type /Pages /Kids [3 0 R 4 0 R] /Count 3 >>
endobj

3 0 obj                                  % une page feuille
<< /Type /Page /Parent 2 0 R
   /MediaBox [0 0 612 792]
   /Resources << /Font << /F1 12 0 R >> >>
   /Contents 5 0 R >>
endobj

4 0 obj                                  % un nœud intérieur regroupant deux autres pages
<< /Type /Pages /Parent 2 0 R /Kids [6 0 R 7 0 R] /Count 2 >>
endobj

Ici l'objet 2 est la racine, avec trois pages en dessous : la page feuille 3, plus deux autres accessibles via le nœud intérieur 4. Le /Count de 3 de la racine doit être égal au total des feuilles en dessous d'elle, et un comptage qui ne correspond pas à la structure réelle est une façon courante dont un fichier édité à la main se corrompt. L'intérêt de l'arbre est la localité d'accès. Un lecteur ouvrant la page 900 d'un document de mille pages ne parcourt pas 900 objets ; il descend quelques nœuds, car un arbre bien formé reste peu profond et équilibré. Construire un tel arbre à la main est assez délicat pour mériter d'être vu de bout en bout, ce que fait la présentation de la construction d'un document PDF à partir de zéro.

L'arbre tire un second avantage de l'héritage. Quelques attributs de page, /Resources, /MediaBox, /CropBox et /Rotate, peuvent être définis sur un nœud intérieur et omis des pages individuelles, qui héritent alors de la valeur de l'ancêtre le plus proche. Définissez /MediaBox une seule fois sur la racine et toutes les feuilles obtiennent la même taille de page sans la répéter ; une page qui doit différer déclare la sienne. C'est le seul endroit dans le modèle d'objets où la signification d'une valeur dépend de la position d'un objet dans l'arbre, et non seulement de son propre contenu.

Ce que contient réellement une page feuille

Un objet page est le point de jonction entre le modèle structurel et le contenu visible. Son entrée /Contents référence un ou plusieurs flux de contenu, les opérateurs de dessin qui peignent le texte et les graphiques sur la page. Son dictionnaire /Resources nomme les polices, les images et les espaces colorimétriques que ces opérateurs utilisent, chaque entrée étant une référence indirecte vers un objet partagé entre les pages. Le /MediaBox donne le rectangle de page en points (1/72 pouce), et des entrées comme /Rotate et /CropBox ajustent la façon dont il est présenté.

Cette division du travail est tout le modèle en miniature. Le dictionnaire de page est la structure : des entrées typées et des références qui disent ce qu'est la page et avec quoi elle dessine. Le flux de contenu est l'instruction : un blob séparé et compressible qui dit comment dessiner. La police derrière /F1 est une ressource partagée, définie une fois et pointée partout où elle est utilisée. Dictionnaire, flux et référence coopèrent pour afficher une page, et les mêmes modèles s'étendent à l'ensemble du document. Les opérateurs de flux de contenu à l'intérieur de ce blob sont traités séparément pour le texte et les polices et pour les graphiques et les éléments visuels.

Pourquoi ce modèle mérite d'être connu

La plupart des développeurs rencontrent le modèle d'objets seulement quand quelque chose se casse : une page s'affiche en blanc parce que sa référence /Contents est brisée, le texte sort en boîtes parce qu'une ressource de police n'a jamais été incorporée, un outil signale un /Count qui ne correspond pas aux pages qu'il trouve. Chacun de ces problèmes est un énoncé sur le graphe, et lire le graphe directement vaut mieux que de deviner. Les huit types et la règle de référence constituent un vocabulaire assez petit pour tenir en tête, et une fois qu'on voit un PDF comme des objets pointant vers des objets, les fichiers malformés cessent d'être opaques.

Cela dit, écrire le modèle à la main n'est rarement le bon choix au-delà de l'apprentissage. Maintenir cohérents les décalages des références croisées, les numéros de génération, les comptages de l'arbre de pages et les longueurs de flux à travers des modifications est le genre de gestion que l'existence d'une bibliothèque justifie. En production, une bibliothèque PDF mature gère le graphe d'objets tout en vous laissant penser en termes de pages et de contenu. Connaître le modèle reste payant : on comprend ce que la bibliothèque construit en dessous, et pourquoi.