Article technique

Dessin sur canevas HotPDF dans Delphi : chemins vectoriels et couleur

HotPDF dessine des graphiques vectoriels en construisant un chemin (path) sur la page actuelle, puis en demandant qu'il soit peint. Il n'y a pas d'étape bitmap entre les deux. Une ligne que vous dessinez avec MoveTo et LineTo se retrouve sous la forme d'opérateurs de chemin PDF dans le flux de contenu, elle reste donc un vrai vecteur : nette à un zoom de 50 %, nette à 1600 %, et pour une fraction de la taille que coûterait une version pixellisée. Pour les diagrammes, les règles (lignes) de tableau, les axes de graphique et les décorations de formulaire, c'est exactement ce que vous voulez, et l'API qui se trouve derrière est suffisamment petite pour être apprise en une seule session

Toute la surface de dessin réside sur THotPDF.CurrentPage. Entre BeginDoc et EndDoc, vous définissez la couleur et la largeur de ligne sur cet objet de page, vous posez la géométrie et vous appelez un opérateur de peinture pour la valider (commit). Les quatre primitives que vous utiliserez le plus sont MoveTo et LineTo pour des chemins arbitraires, Rectangle pour des boîtes, Circle pour des disques, et les deux opérateurs de peinture Stroke et Fill

Le système de coordonnées est en bas à gauche

C'est la chose qui fait trébucher tous ceux qui arrivent de la VCL. Le TCanvas avec lequel vous peignez les contrôles place l'origine dans le coin supérieur gauche avec Y croissant vers le bas. Le PDF fait l'inverse. HotPDF mesure à partir du coin inférieur gauche de la page en points (1/72 de pouce), avec Y augmentant à mesure que vous montez. Un point à Y := 720 se trouve près du haut d'une page US Letter, qui mesure 792 points de haut, et Y := 50 se trouve près du bas. Si votre premier dessin ressort inversé verticalement, c'est la raison : le code porté à partir de graphiques d'écran suppose la mauvaise direction et sort par le bord inférieur

La même convention régit TextOut, de sorte que le texte et les formes partagent un modèle mental une fois que vous l'avez intériorisé. Planifiez une mise en page en décidant de l'endroit où se trouve le bas de chaque élément, pas le haut, et le reste suit

Chemins : MoveTo, LineTo, Stroke

Un chemin tracé (stroked path) est un stylo levé, placé et fait glisser. MoveTo lève le stylo et définit le point de départ sans rien marquer. Chaque LineTo étend le chemin actuel jusqu'à un nouveau point. Rien n'apparaît sur la page jusqu'à ce que vous appeliez Stroke, qui dessine le chemin accumulé en utilisant la couleur de contour et la largeur de ligne actuelles, puis efface le chemin pour que le prochain MoveTo reparte à zéro

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'DrawPaths.pdf';
    Pdf.BeginDoc;

    // Line width is in points and applies until you change it.
    Pdf.CurrentPage.SetLineWidth(1.5);
    Pdf.CurrentPage.SetRGBStrokeColor(clBlack);

    // A horizontal rule near the top of the page (Y measured from bottom).
    Pdf.CurrentPage.MoveTo(72, 720);
    Pdf.CurrentPage.LineTo(523, 720);
    Pdf.CurrentPage.Stroke;          // commit the path; nothing drew before this

    // A thicker connected polyline: three segments in one path.
    Pdf.CurrentPage.SetLineWidth(3);
    Pdf.CurrentPage.SetRGBStrokeColor(RGB(30, 90, 200));
    Pdf.CurrentPage.MoveTo(72, 640);
    Pdf.CurrentPage.LineTo(172, 690);
    Pdf.CurrentPage.LineTo(272, 620);
    Pdf.CurrentPage.LineTo(372, 680);
    Pdf.CurrentPage.Stroke;

    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

Deux détails permettent d'économiser un temps de débogage précieux. La largeur de la ligne est un état, pas un argument : SetLineWidth la définit une fois et chaque Stroke ultérieur utilise cette valeur jusqu'à ce que vous la changiez à nouveau, c'est pourquoi la polyligne ci-dessus est plus épaisse que la règle (rule). Et le chemin se réinitialise après chaque Stroke, donc un Stroke oublié signifie que la géométrie que vous avez si soigneusement disposée ne s'affichera jamais. Si une forme manque dans la sortie, l'appel de peinture est le premier endroit où regarder

Les coordonnées sont des points, et les points sont fractionnaires. MoveTo et LineTo acceptent des valeurs Single, de sorte qu'un trait fin (hairline) à 0.5 points ou une position à 72.25 est légal et significatif, et non arrondi à l'unité entière la plus proche. Cette précision compte dans deux directions opposées. Une largeur de ligne inférieure à environ 0.5 peut s'afficher comme la ligne la plus fine possible dépendant de l'appareil (device-dependent thinnest-possible line) qui disparaît à l'écran et réapparaît à l'impression, donc une règle visible requiert une largeur que vous définissez intentionnellement plutôt que la valeur par défaut. À l'autre extrémité, l'accrochage (snapping) des règles de tableau et des lignes de quadrillage à des coordonnées en points entiers empêche une grille dense de paraître légèrement inégale là où les lignes adjacentes s'arrondissent différemment. Décidez de l'espacement de la grille en points dès le départ et le reste de la mise en page en hérite

Formes pleines et couleur

Les primitives fermées peuvent être remplies (filled) au lieu d'être soulignées (outlined). Rectangle prend une position et une taille, Circle prend un centre et un rayon, et l'un ou l'autre est validé avec Fill, qui peint l'intérieur dans la couleur de remplissage actuelle, ou avec Stroke pour un contour uniquement. La couleur de remplissage et la couleur de contour sont des éléments d'état distincts, définis avec SetRGBFillColor et SetRGBStrokeColor, qui prennent tous deux un seul TColor. Cela signifie que vous pouvez réutiliser directement les constantes de couleur de Delphi et l'assistant RGB

// Rectangle(X, Y, Width, Height): X and Y are the lower-left corner.
Pdf.CurrentPage.SetRGBFillColor(RGB(220, 60, 60));
Pdf.CurrentPage.Rectangle(72, 500, 160, 90);
Pdf.CurrentPage.Fill;

// Circle(X, Y, Radius): X and Y are the center.
Pdf.CurrentPage.SetRGBFillColor(clNavy);
Pdf.CurrentPage.Circle(420, 545, 45);
Pdf.CurrentPage.Fill;

// Outline only: set a stroke color and a width, then Stroke.
Pdf.CurrentPage.SetLineWidth(2);
Pdf.CurrentPage.SetRGBStrokeColor(clBlack);
Pdf.CurrentPage.Rectangle(72, 400, 160, 60);
Pdf.CurrentPage.Stroke;

Faites attention à la forme de l'argument sur Rectangle. Il s'agit de la position plus la taille, X, Y, Width, Height, et non de deux coins opposés. Le TCanvas.Rectangle que les développeurs Delphi connaissent prend (Left, Top, Right, Bottom), donc la mémoire musculaire donnera à HotPDF un deuxième coin là où il attend une largeur et une hauteur, et la boîte sortira de la mauvaise taille. La paire (X, Y) est le coin inférieur gauche, conformément à l'origine de la page. Pour un cercle, (X, Y) est le centre et le troisième argument est le rayon en points

Un choix de couleur que l'exemple d'origine a raté

Une ancienne version de cet exemple a généré des couleurs avec Random($FFFFFF) sur chaque forme. Cela a l'air vivant, et c'est un mauvais instinct pour les documents générés. Un PDF que vous créez à partir de code est généralement quelque chose que vous souhaitez également tester, et les couleurs de remplissage aléatoires rendent la sortie impossible à comparer d'une exécution à l'autre : une différence octet par octet (byte-for-byte diff) par rapport à un fichier correct connu échoue à chaque fois, sans véritable raison. Choisissez des couleurs explicites. Lorsque vous souhaitez de la variété dans une série de formes, pilotez-la à partir de vos données ou d'un tableau de palettes fixe, de sorte que la même entrée produise toujours le même fichier. Le déterminisme vaut plus que la nouveauté lorsque l'artefact se déplace dans un pipeline de publication

Assembler les primitives : une boîte de légende (callout box)

Chaque primitive est simple en soi ; la récompense se voit lorsqu'une poignée d'entre elles se composent pour former ce dont un rapport a réellement besoin. Une boîte de légende (callout), la boîte annotée qui pointe vers une figure et l'explique, utilise tout ce qui a été couvert jusqu'à présent : un rectangle rempli avec une bordure, une ligne de pointeur tracée, un point pour ancrer le pointeur et du texte disposé à l'intérieur de la boîte en utilisant les mêmes coordonnées inférieures gauches que celles utilisées par les formes. FillAndStroke gagne sa place ici, en peignant l'intérieur et le contour d'un chemin en un seul commit au lieu de construire le rectangle deux fois

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'Callout.pdf';
    Pdf.BeginDoc;

    // 1. The box: pale fill plus a visible border, one path, one commit.
    //    Rectangle is lower-left corner plus size, Y measured from the bottom
    Pdf.CurrentPage.SetRGBFillColor(RGB(255, 244, 214));   // pale amber panel
    Pdf.CurrentPage.SetRGBStrokeColor(RGB(180, 130, 40));  // darker rim
    Pdf.CurrentPage.SetLineWidth(1);
    Pdf.CurrentPage.Rectangle(90, 600, 240, 70);
    Pdf.CurrentPage.FillAndStroke;

    // 2. The pointer: one stroked segment from the box edge down
    //    toward the thing being annotated
    Pdf.CurrentPage.SetLineWidth(1.5);
    Pdf.CurrentPage.MoveTo(90, 615);        // left edge of the box
    Pdf.CurrentPage.LineTo(66, 546);
    Pdf.CurrentPage.Stroke;

    // 3. A filled dot anchors the pointer at its target
    Pdf.CurrentPage.SetRGBFillColor(RGB(180, 130, 40));
    Pdf.CurrentPage.Circle(64, 542, 3);
    Pdf.CurrentPage.Fill;

    // 4. The label, positioned relative to the box's lower-left corner.
    //    Text and shapes share one coordinate system, so the offsets
    //    are plain arithmetic against (90, 600)
    Pdf.CurrentPage.SetFont('Arial', [fsBold], 10);
    Pdf.CurrentPage.TextOut(102, 645, 0, 'Check this total');
    Pdf.CurrentPage.SetFont('Arial', [], 9);
    Pdf.CurrentPage.TextOut(102, 628, 0, 'The rounding rule changed in the');
    Pdf.CurrentPage.TextOut(102, 616, 0, 'June release; verify against v2.1');

    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

Remarquez le peu de gestion d'état dont le composite a besoin. La couleur de remplissage, la couleur de contour et la largeur de la ligne sont toutes définies immédiatement avant la forme qui les utilise, de sorte que chaque bloc du dessin se lit comme une unité autonome et peut être réorganisé ou extrait dans un assistant (helper) sans entraîner avec lui d'état caché. Enveloppez cela dans une procédure prenant le point d'ancrage et les chaînes, et vous obtenez une annotation de diagramme réutilisable pour le coût de quarante lignes

Là où le dessin vectoriel s'avère payant, et là où il ne l'est pas

Optez pour ces appels de chemin et de forme lorsque la géométrie est générée : lignes de quadrillage et barres de graphique, lignes tracées d'un tableau de facturation, boîtes de légende sur un diagramme, marque de logo exprimée comme une poignée de chemins. Tout cela évolue sans flou et n'ajoute presque rien à la taille du fichier, car un rectangle représente quelques chiffres plutôt que des milliers de pixels. Le revers de la médaille est également honnête. Si ce que vous avez réellement est une photographie ou une capture d'écran, dessinez-la plutôt comme une image avec AddImage et ShowImage ; le traçage d'un bitmap avec des appels vectoriels ne vous apporte rien. Les segments droits, les rectangles et les cercles ci-dessus effectuent la grande majorité du travail de rapport réel, et les trois raffinements que les développeurs demandent ensuite (les courbes, les motifs de tirets et la transparence) se trouvent sur le même objet de page

Courbes, tirets et transparence en bref

Les courbes de forme libre (Freeform curves) étendent le même mécanisme de chemin que vous possédez déjà. CurveToC(X1, Y1, X2, Y2, X3, Y3) ajoute un segment de Bézier cubique du point actuel à (X3, Y3), se pliant vers les deux points de contrôle, et les variantes abrégées CurveToV et CurveToY couvrent les cas où un point de contrôle coïncide avec un point final. Un chemin peut mélanger librement des segments LineTo et CurveToC avant qu'un seul Stroke ou Fill ne le valide, c'est ainsi que sont construits les coins arrondis et les lignes de graphique lisses

Les traits tiretés sont un état, exactement comme la largeur de la ligne. SetDash([3, 3], 0) fait passer chaque trait (stroke) suivant à un motif à trois points allumés et trois points éteints, le tableau (array) énonçant les longueurs de cycle allumé/éteint (on/off run lengths) en points et le deuxième argument phasant l'endroit où le cycle commence ; NoDash ramène le stylo à une ligne continue. Définissez-le, tracez les lignes de quadrillage qui le souhaitent, et réinitialisez-le avant la règle continue (solid rule) suivante, sinon le tiret infecte discrètement tout ce qui suit

La transparence passe par un état graphique nommé plutôt que par un argument de couleur, car l'alpha dans les PDF est une propriété du dictionnaire d'état graphique. Enregistrez-en un sur le document avec RegisterExtGState, en passant un alpha de remplissage (fill alpha) et un alpha de contour (stroke alpha) entre 0 et 1, puis appliquez le nom qu'il renvoie avec CurrentPage.SetGraphicsState ; les remplissages et les traits peignent à l'opacité enregistrée à partir de ce point. C'est une cérémonie plus lourde que les setters de couleur, et cela en vaut la peine la première fois qu'une barre de surbrillance (highlight bar) doit reposer sur du texte sans le masquer

L'habitude restante qui vaut la peine d'être conservée est la vérification. La géométrie générée peut réussir sur votre machine et échouer sur celle d'un client, généralement à cause d'une substitution de police dans tout texte que vous incorporez ou d'une hypothèse de taille de page qui ne tient pas. Ouvrez le fichier terminé à quelques niveaux de zoom pour confirmer que les bords restent nets, et vérifiez que chaque forme atterrit à l'intérieur de la boîte de marge que vous avez prévue. Avec un schéma de couleurs déterministe, cette vérification peut être automatisée par rapport à un PDF de référence plutôt que d'être évaluée à l'œil nu

Les appels MoveTo, LineTo, Stroke, Fill et de couleur présentés ici font partie du Composant HotPDF pour Delphi et C++Builder