Article technique

Graphiques Vectoriels dans un PDF avec Delphi : Tracés et Dégradés

La plupart du code Delphi qui touche au PDF traite le format comme un conteneur pour deux choses : des suites de texte et quelques bitmaps placés. Cette vision est correcte jusqu'à un certain point, et elle laisse inutilisée la partie la plus performante du format. Une page PDF est un canevas 2D indépendant de la résolution, construit sur le même modèle d'imagerie que PostScript. Elle peut dessiner des lignes, des courbes, des régions remplies, des dégradés et des motifs répétitifs, le tout sous forme de vecteurs qui restent nets à n'importe quel zoom et s'impriment à la pleine résolution du périphérique. Si vous dessinez un logo, un graphique, un filigrane ou une bordure de certificat, le tracé vectoriel est presque toujours la primitive appropriée, et il est plus petit et plus net que l'image tramée vers laquelle de nombreux programmes se tournent à la place

Cet article parcourt le modèle vectoriel tel que le définit la norme ISO 32000-1 et montre les appels PDFlibPas correspondants. Le but 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 à tracés

La norme ISO 32000-1 §8.5 décrit les graphiques en deux phases qui ne se chevauchent jamais. Vous construisez d'abord un tracé, ce qui est de la géométrie pure sans résultat visible. Ensuite, vous peignez ce tracé en une seule opération qui dessine son contour, remplit son intérieur, ou fait les deux. Rien n'apparaît sur la page pendant la construction. Le tracé est une séquence abstraite de points et de segments conservée dans l'état graphique jusqu'à ce qu'un opérateur de peinture le consomme, moment auquel il est rendu et éliminé

Un tracé est constitué d'un ou plusieurs sous-tracés. Un sous-tracé commence à un point et s'agrandit par l'ajout de segments : des lignes droites, des courbes de Bézier cubiques, et sur certaines plateformes des rectangles entiers ajoutés en tant que sous-tracé fermé propre. Dans PDFlibPas, vous ouvrez un tracé avec StartPath, qui définit le point de départ, puis vous l'étendez avec AddLineToPath et AddCurveToPath. Chaque appel fait avancer un point courant implicite, de sorte que le segment suivant continue à partir de l'endroit où le dernier s'est terminé. ClosePath dessine un segment droit final pour revenir au début du sous-tracé, ce qui est important pour le contour (stroke) car cela produit une véritable jonction de ligne au sommet de fermeture au lieu de 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 de Bézier et un point final : AddCurveToPath(CtAX, CtAY, CtBX, CtBY, EndX, EndY). La courbe va du point courant à (EndX, EndY), tirée vers les deux points de contrôle tout au long du trajet. Les arcs circulaires sont disponibles via AddArcToPath(CenterX, CenterY, TotalAngle), où le rayon est pris à partir de la distance entre le point courant et le centre, et le moteur émet l'arc comme une chaîne de segments de Bézier. Les rectangles ont un raccourci, AddBoxToPath(Left, Top, Width, Height), qui ajoute un rectangle fermé complet en tant que sous-tracé propre sans StartPath préalable

Deux règles de remplissage, et pourquoi elles ne sont pas d'accord

Lorsque vous remplissez un tracé qui se croise ou qui contient une boucle interne, le moteur de rendu a besoin d'une règle pour décider quelles régions sont à l'intérieur de la forme et lesquelles sont 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 des contours non nuls (nonzero winding) compte les intersections signées d'un rayon lancé d'un point de test vers l'infini, en ajoutant un pour chaque segment qui croise de gauche à droite et en soustrayant un pour chaque segment qui croise dans l'autre sens ; le point est à l'intérieur lorsque le total n'est pas zéro. La règle pair-impair (even-odd) ignore la direction et se contente de compter les intersections, considérant le point comme étant à l'intérieur lorsque le compte est impair

Le cas classique où elles divergent est une forme avec un trou, 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 creuse 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 des contours non nuls, le trou n'apparaît que si la boucle interne s'enroule dans la direction opposée à celle de la boucle externe ; enroulez-les dans le même sens et les contours se renforcent au lieu de s'annuler, et la région interne se remplit de manière pleine. Une étoile à cinq branches dessinée comme un simple contour auto-sécant montre la même division : la règle pair-impair laisse le pentagone central vide tandis que la règle des contours non nuls le remplit

// 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

PDFlibPas sélectionne la règle par l'appel que vous effectuez pour peindre, non par un indicateur. DrawPath remplit avec la règle des contours non nuls ; DrawPathEvenOdd remplit avec la règle pair-impair. Tous deux prennent le même mode entier : 0 trace le contour uniquement, 1 remplit uniquement, et 2 remplit et trace le contour. La règle pair-impair est l'outil le plus simple pour les trous découpés précisément parce qu'elle ne nécessite pas de gérer la direction du sous-tracé

Les dégradés axiaux font varier la couleur le long d'une ligne

Une couleur de remplissage plate est 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 donnez 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 interpole la couleur le long de cet axe. Chaque point dans la région remplie prend la couleur de sa projection perpendiculaire sur l'axe, de sorte que le dégradé s'écoule en bandes à angle droit par rapport à la ligne entre 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 points finaux de l'axe, les triplets RVB à chaque extrémité sous forme de valeurs dans la plage de 0 à 1, et un indicateur Extend. Avec Extend défini à 1, les couleurs de fin se poursuivent comme un remplissage uni au-delà des points finaux de l'axe, ce que vous souhaitez généralement pour que les coins d'une région en dehors de l'axe ne restent pas 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 tracé que vous peignez ensuite prend le dégradé au lieu d'une couleur plate

// 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

L'axe ici est vertical, de y=100 à y=260 à un x fixe, de sorte que les bandes de couleur s'écoulent horizontalement et que le rectangle s'estompe du bleu à sa base vers le blanc à son sommet. Parce que le shader est identifié par son nom, une définition peut remplir n'importe quel nombre de formes sur la page, et revenir à une couleur plate n'est qu'un autre appel SetFillColor avant le tracé suivant

Les motifs de pavage répètent une cellule

Là où un dégradé fait varier une couleur unique en douceur, un motif de pavage (tiling pattern) répète une petite œuvre d'art à travers 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 reproduit sur une grille fixe pour paver la zone peinte. C'est ainsi que vous construisez des hachures pour un remplissage d'ingénierie, un motif de marque répétitif derrière un en-tête, ou un arrière-plan texturé qui reste net en tant que vecteur et ne pèse presque rien quelle que soit la taille de la zone, car la cellule est stockée une fois et référencée partout

PDFlibPas construit la cellule de motif à partir d'un 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), puis sélectionnez ce motif comme remplissage courant avec SetFillTilingPattern(PatternName). À partir de ce moment, tout tracé que vous remplissez est peint avec la cellule répétitive plutôt qu'avec une couleur plate, exactement comme fonctionne un remplissage de shader mais avec une cellule pavée comme source de peinture. La séquence est plus complexe qu'un seul appel, donc si l'étape de capture n'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 nom avant de dessiner la région que vous souhaitez paver

Rassembler les primitives

Les éléments se composent directement. Une forme de Bézier remplie est un tracé de courbes peint avec DrawPath. Le même contour peint avec DrawPathEvenOdd après avoir ajouté une boucle interne montre un trou que le remplissage des contours (winding) aurait fermé. Un rectangle rempli d'un dégradé est une boîte liée à un shader. L'exemple ci-dessous dessine les trois en séquence pour que la différence entre les deux règles de remplissage soit visible sur une page, puis dépose un panneau de dégradé en dessous

// 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, le choix entre DrawPath et DrawPathEvenOdd est donc le choix entre la règle des contours non nuls et la règle pair-impair, et pour les formes avec des trous, la règle pair-impair vous évite de raisonner sur la direction des sous-tracés. Et l'état graphique est échantillonné au moment où vous peignez : définissez vos couleurs, la largeur de ligne et la liaison du shader avant l'appel de peinture, car c'est l'état que le moteur lit. Construisez d'abord, configurez l'état, peignez en dernier, et le modèle vectoriel se comportera de manière prévisible à chaque fois

À partir de là, les prochaines étapes naturelles consistent à relire les vecteurs et le texte d'un document existant, ce qui est couvert dans notre article sur l'extraction de texte, d'image et de police, et à rendre le même modèle de dessin dans un contexte de périphérique Windows pour un aperçu à l'écran et une impression, couvert dans le guide d'impression et d'aperçu. Les appels de tracé, de shader et de motif décrits ici sont fournis dans le cadre de la Bibliothèque PDF Delphi aux côtés des API de texte, d'image, de formulaire et de signature couvertes ailleurs sur ce blog