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 PDF Library for Delphi 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 PDF Library for Delphi, 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
// Un quadrilatère fermé, tracé puis rempli
PDF.SetLineColor(0, 0, 0);
PDF.SetFillColor(0.6, 0.8, 1.0);
PDF.SetLineWidth(1.5);
PDF.StartPath(150, 100); // ouvre le path au premier sommet
PDF.AddLineToPath(220, 140);
PDF.AddLineToPath(180, 210);
PDF.AddLineToPath(110, 170);
PDF.ClosePath; // segment droit de retour vers (150, 100)
PDF.DrawPath(2); // 2 = fill et stroke ; le path est consommé
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
// Les deux mêmes boîtes, deux règles de remplissage, deux résultats différents
// Nonzero winding : les deux boîtes tournent dans le même sens, donc celle de l'intérieur
// ne creuse PAS de trou et la boîte extérieure se remplit en plein
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 : la boîte intérieure est traversée un nombre pair de fois,
// donc elle perce un trou rectangulaire net dans la boîte extérieure
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
PDF Library for Delphi 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 PDF Library for Delphi, 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
// Définit une fois un dégradé vertical : bleu en bas vers blanc en haut
PDF.NewRGBAxialShader('panelGrad',
0, 100, 0.10, 0.25, 0.55, // point de départ et RGB de départ
0, 260, 1.00, 1.00, 1.00, // point d'arrivée et RGB d'arrivée
1); // 1 = prolonge les extrémités en couleur pleine
// Sélectionne le dégradé comme fill, puis peint un rectangle avec
PDF.SetFillShader('panelGrad');
PDF.AddBoxToPath(80, 100, 300, 160);
PDF.DrawPath(1); // 1 = fill, désormais rempli par le 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
PDF Library for Delphi 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. Une forme Bezier remplie (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. Le même contour, plus une boucle intérieure, rempli en even-odd pour montrer un trou
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); // le trou est percé
// 3. Un rectangle rempli avec un dégradé axial
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