Article technique

Dessin sur canevas HotPDF en Delphi : chemins vectoriels et couleur

HotPDF dessine des graphiques vectoriels en construisant un tracé sur la page active, puis en demandant sa mise en peinture. Il n'y a pas d'étape matricielle intermédiaire. Une ligne tracée avec MoveTo et LineTo se traduit par des opérateurs de tracé PDF dans le flux de contenu, restant ainsi un véritable vecteur : net à un zoom de 50 %, net à 1600 %, et représentant une fraction de la taille qu'aurait coûtée une version pixélisée. Pour les diagrammes, les bordures de tableaux, les axes de graphiques et les décorations de formulaires, c'est exactement ce qu'il vous faut, et l'API associée est suffisamment concise 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, positionnez la géométrie, puis appelez un opérateur de peinture pour l'appliquer. Les quatre primitives que vous utiliserez le plus sont MoveTo et LineTo pour les tracés personnalisés, Rectangle pour les rectangles, Circle pour les cercles, ainsi que les deux opérateurs de peinture Stroke et Fill.

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

C'est le point qui déconcerte tous les développeurs habitués à la VCL. Le TCanvas avec lequel vous dessinez des contrôles place l'origine dans le coin supérieur gauche, avec l'axe Y orienté vers le bas. Le format PDF fait le contraire. HotPDF mesure à partir du coin inférieur gauche de la page en points (1/72 de pouce), l'axe Y augmentant au fur et à mesure que vous montez. Un point à Y := 720 se situe près du haut d'une page au format US Letter, qui mesure 792 points de haut, et Y := 50 se trouve près du bas. Si votre premier dessin apparaît inversé verticalement, en voici la raison : le code porté depuis des graphiques d'écran suppose une mauvaise direction et dépasse la bordure inférieure.

La même convention s'applique à TextOut. Ainsi, le texte et les formes partagent le même modèle mental une fois assimilé. Planifiez votre mise en page en décidant de l'emplacement du bas de chaque élément, et non du haut, et le reste suivra naturellement.

Tracés : MoveTo, LineTo, Stroke

Un tracé contour (stroked path) correspond à un stylo que l'on lève, pose et déplace. MoveTo lève le stylo et définit le point de départ sans rien tracer. Chaque LineTo prolonge le tracé actuel vers un nouveau point. Rien n'apparaît sur la page tant que vous n'appelez pas Stroke, qui dessine le tracé accumulé en utilisant la couleur de contour et la largeur de ligne actuelles, puis réinitialise le tracé pour que le prochain MoveTo commence à 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 de gagner un temps précieux lors du débogage. La largeur de 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 modifiez à nouveau, c'est pourquoi la polyligne ci-dessus est plus épaisse que la ligne simple. De plus, le tracé est réinitialisé après chaque Stroke. Ainsi, un Stroke oublié signifie que la géométrie que vous avez si soigneusement définie ne s'affichera pas du tout. Si une forme manque dans le résultat final, l'appel de peinture est le premier endroit à vérifier.

Les coordonnées sont exprimées en points, et les points peuvent être fractionnaires. MoveTo et LineTo acceptent des valeurs de type Single, de sorte qu'un trait ultra-fin de 0.5 point ou une position à 72.25 est valide et prise en compte, sans être arrondie à l'unité entière la plus proche. Cette précision a son importance dans deux directions opposées. Une largeur de ligne inférieure à environ 0.5 peut s'afficher sous la forme d'une ligne ultra-fine dépendante de l'appareil qui disparaît à l'écran et réapparaît à l'impression. Une ligne visible nécessite donc une largeur définie explicitement plutôt que par défaut. À l'inverse, l'alignement des bordures de tableau et des lignes de grille sur des coordonnées en points entiers évite qu'une grille dense ne paraisse légèrement irrégulière à cause d'arrondis différents sur des lignes adjacentes. Déterminez l'espacement de la grille en points dès le départ, et le reste de la mise en page en héritera.

Formes remplies et couleur

Les primitives fermées peuvent être remplies au lieu d'être simplement détourées. Rectangle prend une position et une taille, Circle prend un centre et un rayon, et chacun est validé avec Fill (qui peint l'intérieur avec la couleur de remplissage actuelle) ou avec Stroke (pour obtenir uniquement le contour). La couleur de remplissage et la couleur de contour sont des états distincts, définis respectivement par SetRGBFillColor et SetRGBStrokeColor, qui acceptent tous deux un seul paramètre TColor. Cela signifie que vous pouvez réutiliser directement les constantes de couleur de Delphi et la fonction d'assistance 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 aux arguments attendus par Rectangle. Il s'agit de la position suivie de la taille : X, Y, Width, Height, et non pas de deux coins opposés. La méthode TCanvas.Rectangle familière aux développeurs Delphi prend en paramètre (Left, Top, Right, Bottom). La mémoire musculaire risque donc de transmettre à HotPDF un second coin là où il attend une largeur et une hauteur, ce qui donnerait un rectangle de taille incorrecte. La paire (X, Y) désigne le coin inférieur gauche, conformément à l'origine de la page. Pour un cercle, (X, Y) représente le centre et le troisième argument est le rayon en points.

Un choix de couleur erroné dans l'exemple original

Une ancienne version de cet exemple générait des couleurs de manière aléatoire avec Random($FFFFFF) pour chaque forme. C'est attrayant visuellement, mais c'est une mauvaise idée pour des documents générés. Un document PDF construit par programmation est généralement destiné à être testé, et des couleurs de remplissage aléatoires rendent impossible la comparaison d'un rendu à l'autre : une comparaison binaire octet par octet par rapport à un fichier de référence valide échouera systématiquement, sans raison valable. Choisissez des couleurs explicites. Si vous souhaitez varier les couleurs d'une série de formes, pilotez-les à partir de vos données ou d'un tableau de palette fixe, afin que les mêmes entrées produisent toujours le même fichier. Le déterminisme a bien plus de valeur que la nouveauté lorsque le fichier généré passe par un pipeline de déploiement.

Où le dessin vectoriel est avantageux, et où il ne l'est pas

Utilisez ces appels de tracé et de forme lorsque la géométrie est générée dynamiquement : lignes de grille et barres de graphique, lignes de séparation d'un tableau de facture, encadrés sur un diagramme, ou logo représenté par quelques tracés. Tout cela s'adapte sans aucun flou et n'ajoute presque rien à la taille du fichier, car un rectangle ne représente que quelques nombres plutôt que des milliers de pixels. La réciproque est également vraie. Si vous disposez d'une photographie ou d'une capture d'écran, intégrez-la plutôt sous forme d'image avec AddImage et ShowImage : recréer un bitmap avec des tracés vectoriels n'apporte aucun bénéfice. Les courbes complexes dépassent également le cadre de cet article. Les primitives présentées se limitent aux segments droits, rectangles et cercles, qui couvrent la grande majorité des besoins de rapports. Tout ce qui requiert des courbes de Bézier libres relève d'une autre partie de l'API.

La dernière bonne habitude à adopter est la vérification. Une géométrie générée peut s'afficher correctement sur votre machine et échouer chez un client, généralement à cause d'une substitution de police dans les textes intégrés ou d'une hypothèse erronée sur les dimensions de la page. Ouvrez le fichier final à plusieurs niveaux de zoom pour vous assurer que les contours restent nets, et vérifiez que chaque forme se trouve bien dans les marges prévues. Avec un schéma de couleurs déterministe, cette vérification peut être automatisée en comparant le résultat à un PDF de référence plutôt qu'en effectuant un contrôle visuel.

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