Une page PDF ne stocke pas de pixels, et elle ne stocke pas non plus un arbre d'objets de forme comme SVG. Elle stocke un programme. Chaque ligne, courbe, remplissage et image placée sur la page est le résultat de l'exécution, de haut en bas, d'une séquence d'opérateurs dans le flux de contenu, sous l'état graphique en cours. Dès lors que l'on comprend ce fait, la majeure partie du comportement du format n'est plus une surprise : pourquoi l'opération de remplissage nécessite un opérateur de tracé distinct après la construction du chemin, pourquoi les couleurs et les épaisseurs de ligne débordent d'une forme à l'autre si vous ne les encapsulez pas, et pourquoi le même code de dessin atterrit à un endroit complètement différent après une transformation de coordonnées. C'est un voyage à travers le modèle d'exécution défini dans la norme ISO 32000 : les opérateurs que vous rencontrez en ouvrant un flux de contenu, et les règles qui déterminent ce qui s'affiche sur la page
Le flux de contenu est un bytecode suffixe
Le flux de contenu est une séquence d'octets plate composée d'opérandes suivis d'opérateurs. L'opérande vient en premier, suivi de l'opérateur qui le consomme, ce qui est l'inverse d'un appel de fonction et correspond parfaitement à une machine à pile : empiler les nombres, puis émettre le verbe. Il n'y a ni imbrication, ni syntaxe d'expression, ni variables ici. Un contour de triangle correspond à ces cinq lignes de code :
100 100 m % moveto: démarre un nouveau sous-tracé à (100, 100)
200 200 l % lineto: ajoute un segment vers (200, 200)
300 100 l % lineto: ajoute un segment vers (300, 100)
h % closepath: referme vers le point de départ
S % stroke: peint le contour du tracé
Les opérateurs sont délibérément conçus pour être concis. Une page réelle comporte des milliers de ces opérateurs, généralement compressés avec FlateDecode. Le prix de cette compacité est que le flux de contenu n'a pas de structure que vous puissiez interroger : le visualiseur ne peut pas demander "où se trouve le titre de cette page", il doit exécuter le programme et voir où l'encre atterrit. C'est la cause fondamentale pour laquelle il est difficile d'extraire du texte de n'importe quel PDF
L'origine est dans le coin inférieur gauche, l'axe Y augmentant vers le haut
Avant que les coordonnées n'aient un sens, vous devez savoir où se trouve (0, 0). Le PDF place l'origine dans le coin inférieur gauche de la page, l'axe X augmentant vers la droite et l'axe Y augmentant vers le haut, mesurés en points, à raison de 72 points par pouce (ISO 32000-2 §8.3.2). Sur une page US Letter, le bord supérieur se situe à y = 792, et non à y = 0. Quiconque est habitué aux graphismes sur écran (origine en haut à gauche, Y augmentant vers le bas) inversera les choses au premier essai, traçant la première ligne au-delà du bas de la page. Cette unité est indépendante du support : que la page soit rendue sur un écran de téléphone ou sur une imageuse, 72 unités représentent un pouce
La plupart des bibliothèques de dessin de pages héritent directement de cette convention. Par exemple, dans HotPDF, TextOut et les appels de chemin sont tous mesurés en points à partir du coin inférieur gauche, de sorte qu'une valeur proche de la hauteur de la page placera le contenu en haut :
// HotPDF, Delphi : l'axe y se mesure depuis le bord bas vers le haut, en points
Pdf.CurrentPage.SetLineWidth(2.0);
Pdf.CurrentPage.MoveTo(100, 700); // près du haut de la page
Pdf.CurrentPage.LineTo(300, 700);
Pdf.CurrentPage.Stroke; // émet les opérateurs moveto/lineto/stroke
Cette séquence d'appels se compile exactement sous la forme des opérateurs m, l et S ci-dessus. La bibliothèque n'est que la dactylo du flux de contenu, rien de plus ; comprendre les opérateurs qu'elle émet vous permet de déduire la cause de l'emplacement d'une forme si elle apparaît là où elle n'est pas attendue
Construire le tracé, puis le dessiner
Le PDF sépare la construction du tracé de son dessin, et cette distinction n'est pas académique. Vous décrivez d'abord une forme à l'aide d'opérateurs de construction sans ajouter de contenu visible, puis vous émettez un unique opérateur de tracé pour décider de ce que vous faites du tracé accumulé. Le même triangle peut être un contour, un remplissage de couleur unie, ou les deux, selon le verbe par lequel vous terminez
Les opérateurs de construction sont peu nombreux. m démarre un nouveau sous-tracé à un point donné. l ajoute un segment de ligne droite. c ajoute une courbe de Bézier cubique à partir de six opérandes (deux points de contrôle et un point final). re est un raccourci pour ajouter un rectangle complet à partir d'un quadruplet x, y, largeur, hauteur. h ferme le sous-tracé actuel vers son point de départ. Aucun d'eux ne laisse d'encre sur la page ; ils ne font qu'accumuler de la géométrie
200 250 m % démarre le sous-tracé
300 350 400 450 500 250 c % courbe de Bézier cubique : deux points de contrôle, puis le point final
150 200 re % un rectangle de 150 x 200, ajouté comme sous-tracé à part entière
h % referme
L'exemple d'origine utilisait la variante y de l'opérateur de courbe, désormais dépréciée ; la forme c avec trois points explicites est celle que vous verrez en pratique et que vous devez utiliser. Une fois le tracé existant, un opérateur de tracé permet de le terminer. Le vocabulaire est restreint et mérite d'être mémorisé, car chaque forme sur chaque page se termine par l'un de ces éléments :
Strace le contour du chemin avec l'épaisseur de ligne et la couleur de contour courantesfremplit l'intérieur en utilisant la couleur de remplissage courante et la règle d'enroulement non nullef*remplit selon la règle pair-impair, ce qui compte pour les formes auto-intersectées et les formes avec trousBremplit puis trace le contour du chemin en une seule opération ;bferme d'abord le cheminnne peint rien : c'est ainsi qu'un chemin devient une région de découpe sans laisser de marque visible
La règle de contournement est l'endroit où les gens se trompent souvent. La règle non nulle (f, B) calcule le nombre de croisements signés d'un rayon à partir du point de test, et remplit les zones où le compte n'est pas nul, de sorte qu'un trou dans un sous-tracé ne reste vide que si sa direction d'enroulement est opposée à celle du tracé extérieur. La règle pair-impair (f*, B*) bascule à chaque croisement, quelle que soit la direction. Si un "beignet" s'affiche plein, c'est que le tracé intérieur s'enroule dans le même sens que le tracé extérieur, et vous devez l'inverser ou basculer vers la règle pair-impair
La couleur est un mode, pas un paramètre
Les couleurs dans le flux de contenu sont persistantes. Une fois que vous avez configuré une couleur, elle reste active jusqu'à ce que vous configuriez une autre couleur ou que vous restauriez un état antérieur, c'est pourquoi un changement de couleur non encapsulé appliquera silencieusement sa teinte à tout ce qui est dessiné après lui. Le PDF conserve également la couleur de remplissage et la couleur de contour comme deux paramètres indépendants, les opérateurs en minuscules étant utilisés pour le remplissage et les majuscules pour le contour. Chaque espace colorimétrique de périphérique possède son propre raccourci :
0.5 g % remplissage DeviceGray, gris moyen (0 = noir, 1 = blanc)
0.2 0.6 0.8 rg % remplissage DeviceRGB
0.8 0.2 0.1 RG % contour DeviceRGB (majuscule = contour)
0.2 0.8 0.0 0.1 k % remplissage DeviceCMYK
DeviceRGB convient à l'affichage à l'écran, DeviceCMYK est attendu pour l'impression de production et DeviceGray est le choix minimal pour le contenu monochrome. Les espaces de périphériques sont pratiques mais non étalonnés : le même triplet RGB peut s'afficher différemment sur deux moniteurs, ce qui est le problème que les espaces colorimétriques basés sur ICC et les intentions de rendu PDF/A tentent de résoudre. Pour les travaux où la couleur est critique, vous utilisez cs et CS pour sélectionner un espace étalonné et sc et scn pour configurer le composant, mais pour les documents ordinaires, les raccourcis de périphériques font le gros du travail. Une bibliothèque encapsulera ces appels typés. Par exemple, HotPDF accepte un seul TColor et émet les opérateurs correspondants :
Pdf.CurrentPage.SetRGBFillColor(clRed);
Pdf.CurrentPage.Rectangle(100, 100, 200, 150); // x, y, largeur, hauteur
Pdf.CurrentPage.Fill;
Pdf.CurrentPage.SetRGBFillColor(RGB(0, 255, 0));
Pdf.CurrentPage.Circle(150, 400, 50); // x, y, rayon
Pdf.CurrentPage.Fill;
État graphique et pile q/Q
Tout le reste en dehors du tracé lui-même réside dans l'état graphique : la matrice de transformation actuelle, les couleurs de remplissage et de contour, l'épaisseur de ligne, le motif de tirets, la zone de détourage, l'alpha. L'état est global et mutable, de sorte que la seule méthode sûre pour effectuer des modifications locales consiste à enregistrer l'état complet, le modifier, dessiner, puis effectuer un retour arrière. C'est ce que font q et Q. q pousse une copie de l'état actuel sur la pile ; Q la dépouille de la pile, abandonnant chaque modification effectuée depuis le q correspondant
q % sauvegarde l'état graphique entier
2 0 0 2 100 100 cm % concatène une transformation : échelle 2x, translation vers (100,100)
0.8 g % remplissage gris, limité à ce bloc
% ... dessin du contenu mis à l'échelle, en gris ...
Q % restauration : transformation et couleur rétablies
Des q et Q déséquilibrés sont une cause classique de dysfonctionnement d'un flux de contenu écrit à la main ou assemblé par morceaux. Un q isolé sans son Q laisse la pile profonde au terme de la page ; un Q en trop la fait passer sous zéro. Dans les deux cas, le visualiseur peut garder en force un vieux détourage ou une vieille transformation, et du contenu disparaît ou atterrit au mauvais endroit. Quand des graphiques s'effacent sans raison que le chemin explique, auditez d'abord la pile d'état
La CTM convertit chaque coordonnée
La matrice de transformation courante (CTM) se situe entre les opérandes et la page réelle. Avant de dessiner quoi que ce soit, chaque coordonnée est multipliée par la CTM, ainsi modifier la matrice change l'emplacement et la manière dont tous les dessins ultérieurs apparaissent, sans toucher aux coordonnées individuelles des tracés. L'opérateur cm concatène une nouvelle matrice à la matrice courante, acceptant six opérandes mappés sur une matrice affine [a b c d e f] :
1 0 0 1 100 50 cm % translation de (100, 50) : e et f portent le décalage
2 0 0 1.5 0 0 cm % échelle x par 2, y par 1,5 : a et d sont les facteurs d'échelle
0.707 0.707 -0.707 0.707 0 0 cm % rotation de 45 degrés (cos/sin dans a, b, c, d)
Deux aspects peuvent dérouter. Premièrement, cm est une composition et non un remplacement, les transformations sont donc cumulatives et l'ordre importe : mettre à l'échelle puis translater diffère de translater puis mettre à l'échelle. Deuxièmement, les rotations et les mises à l'échelle s'effectuent autour de l'origine actuelle, et non du centre de votre forme, donc pour faire pivoter un objet sur place, vous le translatez vers l'origine, effectuez la rotation, puis le translatez en sens inverse, le tout encapsulé dans q/Q. La même matrice est utilisée pour placer les images, ce qui constitue la dernière partie à examiner
Les images et les contenus réutilisables sont des XObjects
Les images trame ne sont pas intégrées dans le flux de contenu. Elles sont stockées en tant que XObjects d'images, qui sont des objets externes avec leur propre dictionnaire (décrivant la largeur, la hauteur, la profondeur de bits, l'espace colorimétrique et le filtre de compression), le flux de contenu se contentant de les référencer. Une photo s'appuyant sur un JPEG se déclare ainsi :
/Photo <<
/Type /XObject
/Subtype /Image
/Width 640
/Height 480
/BitsPerComponent 8
/ColorSpace /DeviceRGB
/Filter /DCTDecode % les données d'image sont un flux JPEG
>>
Un XObject d'image se dessine dans le carré unité : il occupe toujours la région allant de (0, 0) à (1, 1) en espace utilisateur. On ne lui transmet ni position ni taille. À la place, on règle la CTM pour que ce carré unité se projette sur le rectangle voulu, puis on l'invoque avec Do. C'est pourquoi placer une image se fait toujours par une transformation suivie d'une invocation, encapsulées dans un save/restore pour que l'échelle ne contamine pas l'opération suivante :
q
640 0 0 480 50 300 cm % mappe le carré unité sur un cadre 640x480 à (50, 300)
/Photo Do % peint le XObject image
Q
Le même mécanisme Do pilote les XObjects de formulaires (Form XObject), qui conservent sous la forme de leur propre flux de contenu avec boîte englobante une portion réutilisable de graphiques, logo ou tampon répétitif. Définissez-le une fois, appelez-le plusieurs fois avec des CTM différentes, et ces octets n'apparaissent qu'une seule fois dans le fichier. La plupart des bibliothèques masquent cela derrière un seul appel de placement : HotPDF utilise AddImage pour enregistrer le bitmap et ShowImage pour le placer, acceptant des valeurs explicites de x, y, largeur et hauteur au lieu de vous demander de construire manuellement la matrice :
var
Bmp: TBitmap;
ImgIndex: Integer;
begin
Bmp := TBitmap.Create;
try
Bmp.LoadFromFile('logo.bmp');
ImgIndex := Pdf.AddImage(Bmp, icFlate);
// x, y (en bas à gauche), largeur, hauteur, angle de rotation
Pdf.CurrentPage.ShowImage(ImgIndex, 50, 300, 200, 150, 0);
finally
Bmp.Free;
end;
end;
Sous cette ligne unique, la bibliothèque écrit le dictionnaire du XObject d'image, règle la CTM qui dimensionne et positionne le carré unité, et émet Do. Le modèle sous-jacent vaut la peine d'être connu, car il explique chaque résultat étrange : une image étirée est une CTM aux facteurs d'échelle incohérents, un logo identique sur quarante pages est un form XObject invoqué quarante fois, et une image rendue à l'envers est un signe inversé dans la matrice, pas un fichier corrompu
Où cela mène-t-il
Le modèle graphique devient petit dès qu'on en voit la forme. Un flux de contenu est un bytecode suffixe exécuté contre un état mutable ; les coordonnées partent du coin inférieur gauche et passent par la CTM ; les chemins se construisent en silence et se peignent avec un opérateur délibéré ; couleur et réglages de ligne persistent tant qu'on ne les encapsule pas dans q/Q ; images et graphiques réutilisables sont des XObjects placés en transformant un carré unité. Presque tout résultat de rendu déroutant se ramène à l'une de ces cinq règles. Pour voir comment ces opérateurs graphiques s'insèrent dans le modèle d'objets plus vaste, les dictionnaires de pages et la table de références croisées qui pointent vers eux, la présentation technique de la structure de fichier PDF couvre cette couche, et la construction d'un PDF simple de toutes pièces parcourt les octets de bout en bout. Le dessin de texte vit dans sa propre famille d'opérateurs avec ses propres pièges, traités dans l'article complémentaire sur la gestion du texte et des polices en PDF
Les appels de dessin Delphi MoveTo, LineTo, Stroke, Rectangle, Fill, SetRGBFillColor, AddImage et ShowImage présentés ici font tous partie du HotPDF Delphi Component pour Delphi et C++Builder, qui émet ces opérateurs de flux de contenu pour vous