Technischer Artikel

HotPDF Canvas-Zeichnung in Delphi: Vektorpfade und Farbe

HotPDF zeichnet Vektorgrafiken, indem es einen Pfad auf der aktuellen Seite aufbaut und dann darum bittet, ihn zu malen. Es gibt keinen Bitmap-Zwischenschritt. Eine Linie, die man mit und zeichnet, landet als PDF-Pfadoperatoren im Content-Stream, bleibt also ein echter Vektor: scharf bei 50% Zoom, scharf bei 1600%, und ein Bruchteil der Größe, die eine gerasterte Version kosten würde. Für Diagramme, Tabellenlinien, Diagrammachsen und Formulardekoration ist das genau das Richtige, und die API dahinter ist klein genug, um sie in einer Sitzung zu lernen

Die gesamte Zeichenfläche lebt auf . Zwischen und setzt man Farbe und Linienstärke auf diesem Seitenobjekt, legt Geometrie ab und ruft einen Maloperator auf, um sie zu übernehmen. Die vier Grundelemente, die man am häufigsten verwenden wird, sind und für beliebige Pfade, für Rechtecke, für Kreise und die beiden Maloperatoren und

Das Koordinatensystem hat seinen Ursprung unten links

Das ist die eine Sache, die jeden aus der VCL-Welt ins Straucheln bringt. Das , mit dem man Steuerelemente bemalt, legt den Ursprung in die obere linke Ecke, wobei Y nach unten wächst. PDF macht das Gegenteil. HotPDF misst von der unteren linken Ecke der Seite in Punkten (1/72 Zoll), mit zunehmendem Y beim Aufwärtsbewegen. Ein Punkt bei sitzt nahe der Oberkante einer US-Letter-Seite, die 792 Punkte hoch ist, und sitzt nahe der Unterkante. Wenn die erste Zeichnung vertikal gespiegelt herauskommt, liegt es daran: Aus der Bildschirmgrafik portierter Code nimmt die falsche Richtung an und läuft am unteren Rand heraus

Dieselbe Konvention gilt für , sodass Text und Formen ein einziges gedankliches Modell teilen, sobald man es verinnerlicht hat. Plane ein Layout, indem man entscheidet, wo die Unterkante jedes Elements sitzt, nicht die Oberkante, und der Rest folgt

Pfade: MoveTo, LineTo, Stroke

Ein gestrichener Pfad ist ein angehobener, aufgesetzter und gezogener Stift. hebt den Stift an und setzt den Startpunkt, ohne etwas zu markieren. Jedes verlängert den aktuellen Pfad zu einem neuen Punkt. Nichts erscheint auf der Seite, bis man aufruft, das den angehäuften Pfad mit der aktuellen Strichfarbe und Linienstärke zeichnet und dann den Pfad löscht, sodass das nächste frisch beginnt

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;

Zwei Details ersparen echte Debugging-Zeit. Linienstärke ist Zustand, kein Argument: setzt sie einmal und jedes nachfolgende verwendet diesen Wert, bis man ihn wieder ändert, weshalb die Polylinie oben dicker ist als die Linie. Und der Pfad wird nach jedem zurückgesetzt, sodass ein vergessenes bedeutet, dass die so sorgfältig abgelegte Geometrie überhaupt nicht gerendert wird. Wenn eine Form in der Ausgabe fehlt, ist der Malaufruf der erste Ort, an dem man nachschaut

Die Koordinaten sind Punkte, und Punkte sind Dezimalzahlen. und nehmen -Werte, sodass eine Haarlinie bei Punkten oder eine Position bei legal und sinnvoll ist, nicht auf die nächste ganze Einheit gerundet. Diese Präzision spielt in zwei entgegengesetzten Richtungen. Eine Linienstärke unter etwa kann als gerätabhängig dünnstmögliche Linie rendern, die auf dem Bildschirm verschwindet und beim Drucken wieder erscheint, sodass eine sichtbare Linie eine bewusst gesetzte Breite braucht und nicht den Standardwert. Am anderen Ende hält das Einrasten von Tabellenlinien und Gitterlinien auf ganzzahlige Punktkoordinaten ein dichtes Gitter davon ab, leicht ungleichmäßig auszusehen, wo benachbarte Linien unterschiedlich runden. Den Gitterabstand in Punkten vorab festlegen, und der Rest des Layouts erbt ihn

Gefüllte Formen und Farbe

Geschlossene Grundelemente können gefüllt statt umrissen werden. nimmt eine Position und eine Größe, nimmt einen Mittelpunkt und einen Radius, und beides wird mit übernommen, das das Innere in der aktuellen Füllfarbe malt, oder mit nur für einen Umriss. Füllfarbe und Strichfarbe sind getrennte Zustandswerte, gesetzt mit und , beide nehmen ein einziges . Das bedeutet, man kann Delphis Farbkonstanten und den -Helfer direkt verwenden

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

Die Argumentform bei verdient Aufmerksamkeit. Es ist Position-plus-Größe, , nicht zwei gegenüberliegende Ecken. Das , das Delphi-Entwickler kennen, nimmt , sodass Muskelgedächtnis HotPDF eine zweite Ecke gibt, wo es eine Breite und eine Höhe erwartet, und das Rechteck kommt in der falschen Größe heraus. Das -Paar ist die untere linke Ecke, konsistent mit dem Seitenursprung. Bei einem Kreis ist der Mittelpunkt und das dritte Argument ist der Radius in Punkten

Eine Farbwahl, die das ursprüngliche Beispiel falsch gemacht hat

Eine ältere Version dieses Beispiels setzte Farben mit bei jeder Form. Das sieht lebendig aus, und es ist der falsche Instinkt für generierte Dokumente. Ein PDF, das man aus Code aufbaut, ist üblicherweise auch etwas, das man testen möchte, und zufällige Füllfarben machen die Ausgabe von Lauf zu Lauf unmöglich zu vergleichen: Ein Byte-für-Byte-Diff gegen eine bekannte gute Datei schlägt jedes Mal fehl, ohne echten Grund. Explizite Farben wählen. Wenn man Abwechslung über eine Folge von Formen möchte, aus den Daten oder einem festen Palettenarray ableiten, sodass dieselbe Eingabe immer dieselbe Datei erzeugt. Determinismus ist mehr wert als Neuheit, wenn das Artefakt durch eine Release-Pipeline läuft

Wo Vektorzeichnung sich lohnt und wo nicht

Diese Pfad- und Formaufrufe verwenden, wenn die Geometrie generiert wird: Diagrammgitterlinien und -balken, die Linien einer Rechnungstabelle, Beschriftungsfelder auf einem Diagramm, ein Logozeichen aus einer Handvoll Pfaden. All das skaliert ohne Unschärfe und fügt der Dateigröße fast nichts hinzu, weil ein Rechteck aus wenigen Zahlen statt aus Tausenden von Pixeln besteht. Die Kehrseite ist ebenso ehrlich. Wenn man tatsächlich ein Foto oder einen Screenshot hat, als Bild mit und zeichnen; eine Bitmap mit Vektoraufrufen nachzeichnen bringt nichts. Komplexe Kurven liegen ebenfalls außerhalb des hiesigen Rahmens. Die obigen Grundelemente sind gerade Segmente, Rechtecke und Kreise, die den Großteil echter Berichtsarbeit tragen; alles, was freie Bézierkurven benötigt, ist ein separater Teil der API

Die verbleibende erwähnenswerte Gewohnheit ist die Verifikation. Generierte Geometrie kann auf dem eigenen Rechner bestehen und beim Kunden versagen, meist durch Schriftartenersatz in gemischtem Text oder eine Seitengrößenannahme, die nicht zutrifft. Die fertige Datei bei einigen Zoomstufen öffnen, um zu bestätigen, dass die Kanten sauber bleiben, und prüfen, dass jede Form innerhalb des beabsichtigten Randrahmens liegt. Mit einem deterministischen Farbschema kann diese Prüfung gegen eine Referenz-PDF automatisiert werden statt auf Augenschein

Die Grundelemente zusammensetzen: ein Beschriftungsfeld

Jedes Grundelement ist für sich schlicht; der Nutzen zeigt sich, wenn einige davon zu etwas zusammenspielen, das ein Bericht wirklich braucht. Ein Beschriftungsfeld, also der kommentierte Kasten, der auf eine Abbildung zeigt und sie erklärt, verwendet alles bisher Besprochene: ein gefülltes Rechteck mit Rand, eine gezeichnete Hinweislinie, einen Punkt als Anker für die Linie und Text im Kasten, der dieselben Koordinaten unten links verwendet wie die Formen selbst. FillAndStroke hat hier seinen Platz, weil es das Innere und die Kontur eines einzigen Pfads in einem Schritt malt, statt das Rechteck zweimal zu bauen

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'callout-box.pdf';
    Pdf.BeginDoc;
    Pdf.CurrentPage.SetLineWidth(1);

    // Box background and border
    Pdf.CurrentPage.SetRGBFillColor(255, 255, 224);
    Pdf.CurrentPage.SetRGBStrokeColor(90, 90, 90);
    Pdf.CurrentPage.MoveTo(300, 500);
    Pdf.CurrentPage.LineTo(470, 500);
    Pdf.CurrentPage.LineTo(470, 580);
    Pdf.CurrentPage.LineTo(300, 580);
    Pdf.CurrentPage.ClosePath;
    Pdf.CurrentPage.FillAndStroke;

    // Pointer line
    Pdf.CurrentPage.SetRGBStrokeColor(90, 90, 90);
    Pdf.CurrentPage.MoveTo(300, 540);
    Pdf.CurrentPage.LineTo(250, 520);
    Pdf.CurrentPage.Stroke;

    // Anchor dot
    Pdf.CurrentPage.SetRGBFillColor(90, 90, 90);
    Pdf.CurrentPage.Ellipse(246, 516, 4, 4, True, True);

    Pdf.CurrentPage.SetFont('Arial', [], 10);
    Pdf.CurrentPage.TextOut(312, 552, 0, 'Key figure: throughput increased');
    Pdf.CurrentPage.TextOut(312, 536, 0, 'after batching the page writes');

    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;

Die Koordination solcher zusammengesetzten Elemente braucht weniger Zustandsverwaltung, als man denkt. Füllfarbe, Strichfarbe und Linienstärke werden jeweils unmittelbar vor dem Element gesetzt, das sie verwendet, sodass jeder Zeichenblock als geschlossene Einheit lesbar bleibt und sich ohne versteckten Zustand umsortieren oder in eine Hilfsfunktion ziehen lässt

Wo Vektorzeichnung sich lohnt und wo nicht

Greifen Sie zu diesen Pfad- und Formaufrufen, wenn die Geometrie generiert wird: Diagrammgitter und Balken, die Linien einer Rechnungstabelle, Beschriftungsfelder auf einem Diagramm, ein Logozeichen aus einer Handvoll Pfade. Alles skaliert ohne Unschärfe und fügt der Dateigröße fast nichts hinzu, weil ein Rechteck aus wenigen Zahlen statt aus Tausenden von Pixeln besteht. Die Kehrseite ist schlicht. Wenn Sie tatsächlich ein Foto oder einen Screenshot haben, zeichnen Sie es als Bild mit AddImage und ShowImage statt es mit Vektoraufrufen nachzuzeichnen; das bringt nichts. Die geraden Segmente, Rechtecke und Kreise tragen den Großteil der echten Berichtsarbeit, und die drei Erweiterungen, nach denen Entwickler als Nächstes fragen - Kurven, Strichmuster und Transparenz - sitzen auf demselben Seitenobjekt

Kurven, Strichmuster und Transparenz in Kurzform

Freiformkurven erweitern dieselbe Pfadmaschine, die Sie bereits verwenden. CurveToC(X1, Y1, X2, Y2, X3, Y3) hängt ein kubisches Bézier-Segment vom aktuellen Punkt zu (X3, Y3) an und biegt sich zu den beiden Kontrollpunkten hin, während die Kurzformen CurveToV und CurveToY die Fälle abdecken, in denen ein Kontrollpunkt mit einem Endpunkt zusammenfällt. Ein Pfad kann LineTo- und CurveToC-Segmente frei mischen, bevor ein einzelnes Stroke oder Fill ihn abschließt, und genau so entstehen abgerundete Ecken und weiche Diagrammlinien

Gestrichelte Striche sind Zustand, genau wie die Linienstärke. SetDash([3, 3], 0) schaltet jeden nachfolgenden Strich auf ein Muster aus drei Punkten an und drei Punkten aus um, wobei das Array die An- und Auslängen in Punkten beschreibt und das zweite Argument den Startpunkt des Zyklus festlegt; NoDash setzt den Stift wieder auf eine durchgezogene Linie zurück. Setzen, die Gitterlinien damit zeichnen und vor der nächsten durchgezogenen Linie zurücksetzen, sonst infiziert das Strichmuster still alles, was danach kommt

Transparenz lebt ebenfalls im Grafikzustand der Seite. Eine Füllung oder eine Kontur kann einen Alpha-Wert tragen, wenn das Ausgabziel das erlaubt, sodass derselbe Vektorpfad ein Diagramm oder ein Foto überlagern kann, ohne auf einen Raster-Umweg ausweichen zu müssen. Das ist der praktische Wert, die Pfad-API als System zu lernen und nicht als lose Sammlung einzelner Aufrufe: dieselben wenigen Grundelemente greifen später in weitere Funktionen hinein, ohne ihre Form zu ändern

Die hier gezeigten Aufrufe , , , und die Farbaufrufe sind Teil des HotPDF Component für Delphi und C++Builder