La plupart des codes Delphi qui touchent au PDF traitent ce format comme un conteneur pour deux éléments : des blocs de texte et quelques images bitmap placées. Cette vision est correcte dans une certaine mesure, mais elle laisse inexploitée la partie la plus puissante du format. Une page PDF est un canevas 2D indépendant de la résolution, basé sur le même modèle d'imagerie que PostScript. Elle peut dessiner des lignes, des courbes, des zones remplies, des dégradés et des motifs répétitifs, le tout sous forme de vecteurs qui restent nets quel que soit le zoom et s'impriment à la résolution maximale de l'appareil. Si vous dessinez un logo, un graphique, un filigrane ou une bordure de certificat, le chemin vectoriel est presque toujours la bonne primitive, plus légère et plus nette que l'image tramée que de nombreux programmes utilisent à la place.
Cet article parcourt le modèle vectoriel tel que défini par la norme ISO 32000-1 et présente les appels PDFlibPas correspondants. L'objectif est de rendre la spécification concrète, car l'API s'y calque étroitement, et comprendre l'un vous enseigne l'autre.
La page est une machine à chemins
La norme ISO 32000-1 §8.5 décrit les graphiques en deux phases qui ne se chevauchent jamais. Vous construisez d'abord un chemin, ce qui est de la pure géométrie sans résultat visible. Ensuite, vous peignez ce chemin en une seule opération qui trace son contour (stroke), remplit son intérieur (fill), ou fait les deux. Rien n'apparaît sur la page pendant la construction. Le chemin est une séquence abstraite de points et de segments conservée dans l'état graphique jusqu'à ce qu'un opérateur de peinture la consomme, moment auquel elle est rendue puis supprimée.
Un chemin est constitué d'un ou plusieurs sous-chemins. Un sous-chemin commence en un point et s'étend en y ajoutant des segments : des lignes droites, des courbes de Bézier cubiques et, sur certaines plateformes, des rectangles entiers ajoutés comme leur propre sous-chemin fermé. Dans PDFlibPas, vous ouvrez un chemin avec StartPath, qui définit le point de départ, puis vous l'étendez avec AddLineToPath et AddCurveToPath. Chaque appel fait avancer un point actuel implicite, de sorte que le segment suivant commence là où le précédent s'est terminé. ClosePath dessine un dernier segment droit vers le début du sous-chemin, ce qui est important pour le tracé de contour car cela produit une véritable jonction de ligne au sommet de fermeture plutôt que deux extrémités libres.
// A closed quadrilateral, stroked then filled
PDF.SetLineColor(0, 0, 0);
PDF.SetFillColor(0.6, 0.8, 1.0);
PDF.SetLineWidth(1.5);
PDF.StartPath(150, 100); // open the path at the first vertex
PDF.AddLineToPath(220, 140);
PDF.AddLineToPath(180, 210);
PDF.AddLineToPath(110, 170);
PDF.ClosePath; // straight segment back to (150, 100)
PDF.DrawPath(2); // 2 = fill and stroke; path is consumed
Les courbes utilisent AddCurveToPath, qui prend deux points de contrôle Bézier et un point final : AddCurveToPath(CtAX, CtAY, CtBX, CtBY, EndX, EndY). La courbe s'étend du point actuel à (EndX, EndY), attirée par les deux points de contrôle en chemin. Les arcs circulaires sont disponibles via AddArcToPath(CenterX, CenterY, TotalAngle), où le rayon est déterminé par la distance entre le point actuel et le centre, et le moteur émet l'arc sous forme de chaîne de segments de Bézier. Les rectangles disposent d'un raccourci, AddBoxToPath(Left, Top, Width, Height), qui ajoute un rectangle fermé complet comme son propre sous-chemin sans StartPath préalable.
Deux règles de remplissage, et pourquoi elles divergent
Lorsque vous remplissez un chemin qui se croise lui-même ou contient une boucle interne, le moteur de rendu a besoin d'une règle pour décider quelles zones se trouvent à l'intérieur de la forme et lesquelles constituent des trous. La norme ISO 32000-1 §8.5.3.3 en définit deux, et elles peuvent peindre la même géométrie différemment. La règle du nombre de tours non nul (nonzero winding) compte les croisements signés d'un rayon lancé d'un point de test vers l'infini, en ajoutant un pour chaque segment qui traverse de gauche à droite et en soustrayant un pour chaque segment qui traverse dans l'autre sens ; le point est à l'intérieur lorsque le total n'est pas nul. La règle pair-impair (even-odd) ignore la direction et compte simplement les croisements, considérant le point comme intérieur lorsque le nombre de croisements est impair.
Le cas classique où elles divergent est une forme avec un trou, comme un beignet ou une rondelle. Dessinez une limite externe et une limite interne à l'intérieur de celle-ci. Sous la règle pair-impair, la boucle interne découpe toujours un trou, car tout point entre les deux limites est croisé une fois et tout point à l'intérieur de la boucle interne est croisé deux fois. Sous la règle du nombre de tours non nul, le trou n'apparaît que si la boucle interne tourne dans le sens opposé à la boucle externe ; si elles tournent dans le même sens, les enroulements se renforcent au lieu de s'annuler, et la région interne se remplit entièrement. Une étoile à Microsoft/cinq branches dessinée comme un seul contour auto-intersectant montre la même séparation : pair-impair laisse le pentagone central vide tandis que le nombre de tours non nul le remplit.
PDFlibPas sélectionne la règle par l'appel de peinture que vous effectuez, et non par un indicateur. DrawPath effectue le remplissage avec la règle du nombre de tours non nul ; DrawPathEvenOdd avec la règle pair-impair. Les deux prennent le même mode entier : 0 trace le contour uniquement, 1 remplit uniquement et 2 remplit et trace. La règle pair-impair est l'outil le plus simple pour les trous découpés précisément parce qu'elle ne vous oblige pas à gérer la direction du sous-chemin.
// Same two boxes, two fill rules, two different results.
// Nonzero winding: both boxes wind the same way, so the inner one
// does NOT cut a hole and the whole outer box fills solid.
PDF.SetFillColor(0.2, 0.4, 0.8);
PDF.AddBoxToPath(100, 100, 200, 120); // outer
PDF.AddBoxToPath(140, 130, 120, 60); // inner
PDF.DrawPath(1); // 1 = fill, nonzero winding
// Even-odd: the inner box is crossed an even number of times,
// so it punches a clean rectangular hole through the outer box.
PDF.SetFillColor(0.2, 0.4, 0.8);
PDF.AddBoxToPath(100, 300, 200, 120); // outer
PDF.AddBoxToPath(140, 330, 120, 60); // inner cut-out
PDF.DrawPathEvenOdd(1); // 1 = fill, even-odd
Les dégradés axiaux font varier la couleur le long d'une ligne
Une couleur de remplissage unie représente une valeur unique sur toute la région. Un dégradé fait varier la couleur en continu, et le type le plus simple est le dégradé axial, ou linéaire. La norme ISO 32000-1 §8.7.4.5 le spécifie comme un ombrage axial de type 2 : vous indiquez deux points qui définissent un axe, une couleur de départ au premier point et une couleur de fin au second, et le moteur de rendu interpoler la couleur le long de cet axe. Chaque point de la zone remplie prend la couleur de sa projection perpendiculaire sur l'axe, de sorte que le dégradé se déploie en bandes perpendiculaires à la ligne reliant les deux points.
Dans PDFlibPas, un dégradé est une ressource de document nommée que vous créez une fois puis sélectionnez comme peinture active. NewRGBAxialShader l'enregistre. La signature est NewRGBAxialShader(ShaderName, StartX, StartY, StartRed, StartGreen, StartBlue, EndX, EndY, EndRed, EndGreen, EndBlue, Extend) : les deux extrémités de l'axe, les triplets RGB à chaque extrémité sous forme de valeurs comprises entre 0 et 1, et un indicateur Extend. Avec Extend défini sur 1, les couleurs d'extrémité se prolongent sous forme de remplissage uni au-delà des extrémités de l'axe, ce qui est généralement souhaité pour éviter que les angles d'une zone située en dehors de l'axe ne restent non peints ; 0 les laisse intacts. Une fois le shader existant, vous le liez avec SetFillShader pour les régions remplies, SetLineShader pour les contours tracés ou SetTextShader pour le texte. La liaison reste active pour les appels de dessin qui suivent, de sorte que le chemin que vous peignez ensuite prend le dégradé à la place d'une couleur unie.
// Define a vertical gradient once: blue at the bottom to white at the top.
PDF.NewRGBAxialShader('panelGrad',
0, 100, 0.10, 0.25, 0.55, // start point and start RGB
0, 260, 1.00, 1.00, 1.00, // end point and end RGB
1); // 1 = extend ends as solid color
// Select the gradient as the fill, then paint a rectangle with it.
PDF.SetFillShader('panelGrad');
PDF.AddBoxToPath(80, 100, 300, 160);
PDF.DrawPath(1); // 1 = fill, now filled by the shader
Les motifs de pavage répètent une cellule
Là où un dégradé fait varier doucement une seule couleur, un motif de pavage (tiling pattern) répète une petite œuvre d'art sur une région. La norme ISO 32000-1 §8.7.3.1 définit un motif de pavage comme une cellule de motif, un élément de contenu indépendant, que le moteur de rendu réplique sur une grille fixe pour paver la zone peinte. C'est ainsi que vous construisez des hachures pour un remplissage technique, un motif de marque répétitif derrière un en-tête ou un arrière-plan texturé qui reste net en vectoriel et ne pèse presque rien quelle que soit la taille de la zone, car la cellule est stockée une seule fois et référencée partout.
PDFlibPas construit la cellule de motif à partir du contenu de page capturé. Vous capturez une page ou une région avec CapturePage, transformez la capture en un motif nommé avec NewTilingPatternFromCapturedPage(PatternName, CaptureID), et puis sélectionnez ce motif comme remplissage actuel avec SetFillTilingPattern(PatternName). À partir de ce moment, tout chemin que vous remplissez est peint avec la cellule répétitive plutôt qu'avec une couleur unie, exactement comme fonctionne un remplissage par shader mais avec une cellule pavée comme source de peinture. La séquence est plus complexe qu'un simple appel, donc si l'étape de capture ne vous est pas familière, traitez le motif comme une opération en deux étapes : produisez d'abord la cellule capturée, puis liez-la comme remplissage par son nom avant de dessiner la région que vous souhaitez paver.
Assembler les primitives
Les pièces s'assemblent directement. Une forme de Bézier remplie est un chemin de courbes peintes avec DrawPath. Le même contour peint avec DrawPathEvenOdd après l'ajout d'une boucle interne présente un trou que le remplissage par nombre de tours aurait fermé. Un rectangle rempli d'un dégradé est une boîte liée à un shader. L'exemple ci-dessous dessine les trois à la suite pour que la différence entre les deux règles de remplissage soit visible sur une seule page, puis dispose un panneau dégradé sous eux.
// 1. A filled Bezier shape (nonzero winding).
PDF.SetFillColor(0.85, 0.30, 0.25);
PDF.StartPath(120, 480);
PDF.AddCurveToPath(160, 560, 240, 560, 280, 480); // top lobe
PDF.AddCurveToPath(240, 420, 160, 420, 120, 480); // bottom lobe
PDF.ClosePath;
PDF.DrawPath(1); // 1 = fill
// 2. The same outline, plus an inner loop, filled even-odd to show a hole.
PDF.SetFillColor(0.85, 0.30, 0.25);
PDF.StartPath(120, 300);
PDF.AddCurveToPath(160, 380, 240, 380, 280, 300);
PDF.AddCurveToPath(240, 240, 160, 240, 120, 300);
PDF.ClosePath;
PDF.MovePath(180, 300); // new subpath: the hole
PDF.AddArcToPath(200, 300, 360); // a full circle
PDF.ClosePath;
PDF.DrawPathEvenOdd(1); // hole is punched out
// 3. A rectangle filled with an axial gradient.
PDF.NewRGBAxialShader('footerGrad',
60, 100, 0.95, 0.55, 0.10,
60, 200, 0.20, 0.10, 0.40,
1);
PDF.SetFillShader('footerGrad');
PDF.AddBoxToPath(60, 100, 340, 100);
PDF.DrawPath(1);
Deux détails méritent d'être retenus. L'appel de peinture décide de la règle de remplissage, de sorte que le choix entre DrawPath et DrawPathEvenOdd est le choix entre le nombre de tours non nul et pair-impair, et pour les formes avec des trous, la règle pair-impair vous évite de devoir raisonner sur la direction du sous-chemin. De plus, l'état graphique est échantillonné au moment où vous peignez : définissez vos couleurs, votre épaisseur de ligne et votre liaison de shader avant l'appel de peinture, car c'est cet état que le moteur lit. Construisez d'abord, configurez l'état ensuite, peignez en dernier, et le modèle vectoriel se comporte de manière prévisible à chaque fois.
À partir de là, les étapes suivantes naturelles consistent à relire les vecteurs et le texte à partir d'un document existant, sujet traité dans notre article sur l'extraction de texte, d'images et de polices, et à rendre le même modèle de dessin sur un contexte de périphérique Windows pour l'aperçu à l'écran et l'impression, sujet traité dans le guide d'impression et d'aperçu. Les appels de chemin, de shader et de motif décrits ici sont fournis avec la bibliothèque PDF pour Delphi en même temps que les API de texte, d'image, de formulaire et de signature présentées par ailleurs sur ce blog.