Un fichier PDF est, au fond (at heart), une collection d'objets qui pointent les uns vers les autres. Enlevez la compression, la comptabilité des références croisées (cross-reference bookkeeping) et les décalages d'octets (byte offsets), et ce qui reste est un graphe : un petit ensemble de valeurs typées, câblées ensemble par des références, enracinées dans un seul objet que le lecteur sait comment trouver. Tout ce qu'un PDF peut exprimer, d'un paragraphe de texte à une police intégrée ou à une signature numérique, est construit à partir de huit types d'objets primitifs et de la règle qui permet à un objet de faire référence à un autre. Apprenez-les, et le reste du format se lit comme une composition plutôt que comme un mystère
C'est la couche logique (logical layer) du PDF, définie dans la clause 7.3 de l'ISO 32000-1, et elle se situe un niveau au-dessus de la disposition du fichier physique (l'en-tête, le corps, la table des références croisées et le trailer, qui constituent leur propre sujet dans l'aperçu technique de la structure des fichiers PDF). Le modèle logique est ce que ces octets signifient une fois analysés (parsed). Une visionneuse lit le fichier à l'envers pour trouver le trailer, le suit jusqu'à la racine (root), et à partir de là, le document se déroule sous forme d'objets faisant référence à des objets. C'est la partie sur laquelle vous raisonnez (reason about) lorsque vous déboguez une page mal formée (malformed page), écrivez un analyseur (parser) ou faites confiance à une bibliothèque pour assembler un document
Huit types d'objets, et rien d'autre
Le format PDF définit exactement huit types d'objets de base. Chaque valeur d'un document est l'une d'entre elles, ce qui permet au format de rester traitable (tractable) malgré sa portée
Les Booléens (Booleans) sont les mots-clés true et false. Ils activent et désactivent des indicateurs (flags), par exemple si une annotation s'imprime
Les Nombres (Numbers) se déclinent en deux saveurs que la spécification traite comme un seul type : les entiers (integers) comme 42 et les réels (reals) comme 3.14 ou -0.002. Le format PDF n'a pas de notation d'exposant (exponent notation), vous ne verrez donc 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 (Strings) contiennent des séquences d'octets, écrites soit entre parenthèses, (Hello), soit entre crochets obliques (angle brackets) en hexadécimal, <48656C6C6F>. Les deux notations codent un contenu identique ; l'hexa est la porte de sortie (escape hatch) pour les octets encombrants à l'intérieur des parenthèses. Les chaînes transportent du texte, mais ce sont d'abord des octets, ce qui compte dès que vous manipulez autre chose que de l'ASCII
Les Noms (Names) sont des jetons (tokens) atomiques introduits par une barre oblique (slash) : /Type, /Pages, /MediaBox. Un nom n'est pas une chaîne ; c'est un identifiant, utilisé comme clé de dictionnaire (dictionary key) ou comme valeur énumérée (enumerated value), 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 fait trébucher les nouveaux venus qui traitent /Times-Roman et la chaîne (Times-Roman) comme interchangeables ; le format ne le fait pas
Les Tableaux (Arrays) sont des listes hétérogènes ordonnées entre crochets : [0 0 612 792] est un rectangle de page, et un tableau peut mélanger les types librement, y compris les références à d'autres objets. Les Dictionnaires (Dictionaries) sont la bête de somme (workhorse). Écrit entre << et >>, un dictionnaire associe (maps) des clés de nom (name keys) à des valeurs de tout type, et presque toutes les structures significatives en PDF, page, catalogue, police, annotation, sont un dictionnaire avec une clé /Type déclarant ce qu'il est
Les Flux (Streams) sont des dictionnaires avec une queue d'octets bruts (raw bytes) entre les mots-clés stream et endstream. Le dictionnaire décrit les octets (leur longueur et tout filtre tel que FlateDecode qui les compresse), et les octets transportent la charge utile encombrante (bulky payload) : instructions de contenu de page, programmes de polices intégrés, images. Un flux est l'endroit où le PDF place tout ce qui est trop grand ou trop binaire pour se trouver en ligne (sit inline)
Le huitième type est l'objet nul (null object), le mot-clé null. Il s'agit d'une valeur réelle, distincte de l'absence d'une clé. Une entrée de dictionnaire définie sur null est traitée comme si elle n'était pas présente, et une référence qui se résout en un objet inexistant (non-existent object) donne également null plutôt qu'une erreur. Ce comportement indulgent (forgiving behavior) est délibéré : il permet à un fichier endommagé de se dégrader au lieu de refuser de s'ouvrir. Il n'y a pas de neuvième type ; tout ce que le format PDF exprime provient de la façon dont ces huit se combinent
Valeurs directes, objets indirects et références
Chacun de ces huit types peut apparaître de deux manières. Un objet direct est écrit sur place, comme le 612 dans un tableau MediaBox. On donne à un objet indirect une identité afin que d'autres objets puissent le pointer : deux entiers, un numéro d'objet (object number) et un numéro de génération (generation number), enveloppant la définition dans 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 polices. N'importe où ailleurs dans le fichier, un autre objet y fait référence avec une référence indirecte (indirect reference) : les deux mêmes nombres suivis du mot-clé R, 12 0 R. La référence est un pointeur. Lorsque 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 la réutilisation. Lorsqu'un objet est libéré (freed) et son emplacement réutilisé, la génération s'incrémente afin qu'un 12 0 R obsolète (stale) ne puisse pas se résoudre au nouveau locataire de l'emplacement 12. Les fichiers fraîchement écrits sont presque tous de génération 0, mais un fichier fortement édité peut porter des numéros plus élevés, et un analyseur (parser) qui ignore la génération finira par lire le mauvais objet
L'indirection est ce qui rend le format PDF efficace et modifiable. Une police, une image ou un espace colorimétrique (color space) peut être défini une fois et référencé à partir d'une centaine de pages. Une petite modification peut être ajoutée en tant que 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 d'octets (byte offset), de sorte que le lecteur saute directement à 12 0 obj sans analyser, mais c'est une optimisation physique. Logiquement, tout ce que vous devez savoir est que 12 0 R signifie "l'objet identifié comme 12 0."
Le catalogue : où chaque 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 catalogue de documents (document catalog) : la racine du graphe d'objets, un dictionnaire avec /Type /Catalog. Le lecteur l'atteint en premier car le trailer est trouvé en premier, et à partir de là, toutes les autres parties du document sont accessibles en suivant les références
Le catalogue ne contient que deux entrées strictement requises : son /Type, et /Pages, une référence indirecte à la racine de l'arborescence des pages. Les autres sont facultatives et décrivent le comportement à l'échelle du document (document-wide behavior) plutôt que le contenu : /Outlines pointe vers l'arborescence des signets (bookmark tree), /Names contient des arborescences de noms (name trees) classées par chaîne, /Metadata fait référence à un flux de métadonnées XMP, et /PageMode et /PageLayout suggèrent la manière dont une visionneuse doit ouvrir le document. Aucune de ces informations n'est nécessaire pour générer une page ; elles configurent l'expérience autour des pages. Les structures de signets, de métadonnées et d'annotations accrochées au catalogue sont abordées dans l'article sur les métadonnées, signets et annotations PDF
Le diagramme ci-dessous montre où se trouve le corps de l'objet (object body) dans le fichier environnant. Le catalogue et l'arborescence des pages vivent à l'intérieur de ce corps sous forme d'objets indirects ordinaires ; l'en-tête, la table des références croisées et le trailer qui les entourent constituent l'échafaudage physique (physical scaffolding) qui permet au lecteur de les localiser

L'arborescence des pages (page tree) : une hiérarchie équilibrée de pages
A partir de /Pages, le document se ramifie (branches) dans l'arborescence des pages, où le choix de PDF d'un graphe plutôt qu'une liste plate est payant. Les pages ne sont pas stockées sous la forme d'une simple séquence ; elles sont suspendues à un arbre dont les nœuds intérieurs sont des nœuds d'arborescence de pages (page tree nodes) (/Type /Pages) et dont les feuilles sont des objets de page (page objects) (/Type /Page). Un nœud intérieur répertorie ses enfants dans un tableau /Kids et enregistre, dans /Count, combien de pages feuilles vivent en dessous. Chaque nœud à l'exception de la racine porte une référence /Parent vers le haut, de sorte que l'arbre marche dans l'une ou l'autre direction
2 0 obj % racine de l'arborescence des 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 pages supplémentaires
<< /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, et un décompte (count) qui n'est pas en accord avec la structure réelle est une façon courante de voir un fichier édité à la main tourner mal. L'intérêt de l'arborescence 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 une poignée de nœuds, car un arbre bien formé reste peu profond et équilibré. Construire un tel arbre à la main est suffisamment délicat (fiddly) pour valoir la peine d'être vu de bout en bout, ce que fait la présentation sur la création d'un document PDF à partir de zéro
L'arbre gagne sa deuxième utilité grâce à l'héritage (inheritance). Une poignée d'attributs de page, /Resources, /MediaBox, /CropBox et /Rotate, peuvent être définis sur un nœud intérieur et laissés de côté sur les pages individuelles, qui héritent alors de la valeur de l'ancêtre le plus proche. Définissez /MediaBox une fois sur la racine et chaque feuille obtient 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 du modèle objet où la signification d'une valeur dépend de la position d'un objet dans l'arborescence, et non seulement de son propre contenu
Ce qu'une page feuille contient réellement
Un objet page est le point de jonction (join point) entre le modèle structurel et le contenu visible. Son entrée /Contents référence un ou plusieurs flux de contenu (content streams), les opérateurs de dessin qui peignent du texte et des graphiques sur la page. Son dictionnaire /Resources nomme les polices, les images et les espaces colorimétriques (color spaces) sur lesquels ces opérateurs s'appuient, chaque entrée étant une référence indirecte à un objet partagé entre les pages. La /MediaBox donne le rectangle de page en points (1/72 pouce), et des entrées telles que /Rotate et /CropBox ajustent la façon dont il est présenté
Cette division du travail est l'ensemble du modèle en miniature. Le dictionnaire de pages est une structure : des entrées typées et des références qui indiquent ce qu'est la page et avec quoi elle dessine. Le flux de contenu est des instructions : un blob séparé et compressible qui indique 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 rendre (render) une page, et les mêmes motifs s'adaptent (scale) à l'ensemble du document. Les opérateurs de flux de contenu à l'intérieur de ce blob sont couverts séparément pour le texte et les polices et pour les graphiques et les éléments visuels
Pourquoi ce modèle vaut la peine d'être connu
La plupart des développeurs ne rencontrent le modèle objet que lorsque quelque chose se casse : une page est rendue vide car sa référence /Contents pend (dangles), le texte apparaît sous forme de boîtes car une ressource de police n'a jamais été intégrée (embedded), un outil signale un /Count qui ne correspond pas aux pages qu'il peut trouver. Chacun de ces éléments est une déclaration sur le graphe, et la lecture directe du graphe vaut mieux que de deviner. Les huit types et la règle de référence constituent un vocabulaire suffisamment restreint pour être gardé en tête, et une fois que vous voyez un PDF comme des objets pointant vers des objets, les fichiers mal formés cessent d'être opaques
Cela dit, écrire le modèle à la main est rarement le bon choix au-delà de l'apprentissage. Maintenir la cohérence des décalages (offsets) des références croisées, des numéros de génération, des comptes d'arborescence de pages et des longueurs de flux à travers les modifications est le type de comptabilité (bookkeeping) qu'une bibliothèque gère. En production, une bibliothèque de développement PDF mature gère le graphe d'objets tout en vous laissant penser en pages et en contenu. Connaître le modèle est toujours payant : vous comprenez ce que la bibliothèque construit en dessous, et pourquoi