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 MoveTo und LineTo 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 THotPDF.CurrentPage. Zwischen BeginDoc und EndDoc 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 MoveTo und LineTo für beliebige Pfade, Rectangle für Rechtecke, Circle für Kreise und die beiden Maloperatoren Stroke und Fill.
Das Koordinatensystem hat seinen Ursprung unten links
Das ist die eine Sache, die jeden aus der VCL-Welt ins Straucheln bringt. Das TCanvas, 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 Y := 720 sitzt nahe der Oberkante einer US-Letter-Seite, die 792 Punkte hoch ist, und Y := 50 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 TextOut, 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. MoveTo hebt den Stift an und setzt den Startpunkt, ohne etwas zu markieren. Jedes LineTo verlängert den aktuellen Pfad zu einem neuen Punkt. Nichts erscheint auf der Seite, bis man Stroke aufruft, das den angehäuften Pfad mit der aktuellen Strichfarbe und Linienstärke zeichnet und dann den Pfad löscht, sodass das nächste MoveTo frisch beginnt.
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.FileName := 'DrawPaths.pdf';
Pdf.BeginDoc;
// Linienstärke in Punkten, gilt bis zur nächsten Änderung.
Pdf.CurrentPage.SetLineWidth(1.5);
Pdf.CurrentPage.SetRGBStrokeColor(clBlack);
// Eine horizontale Linie nahe der Seitenoberkante (Y von unten gemessen).
Pdf.CurrentPage.MoveTo(72, 720);
Pdf.CurrentPage.LineTo(523, 720);
Pdf.CurrentPage.Stroke; // Pfad übernehmen; vorher wurde nichts gezeichnet
// Eine dickere verbundene Polylinie: drei Segmente in einem Pfad.
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: SetLineWidth setzt sie einmal und jedes nachfolgende Stroke verwendet diesen Wert, bis man ihn wieder ändert, weshalb die Polylinie oben dicker ist als die Linie. Und der Pfad wird nach jedem Stroke zurückgesetzt, sodass ein vergessenes Stroke 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. MoveTo und LineTo nehmen Single-Werte, sodass eine Haarlinie bei 0.5 Punkten oder eine Position bei 72.25 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 0.5 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. Rectangle nimmt eine Position und eine Größe, Circle nimmt einen Mittelpunkt und einen Radius, und beides wird mit Fill übernommen, das das Innere in der aktuellen Füllfarbe malt, oder mit Stroke nur für einen Umriss. Füllfarbe und Strichfarbe sind getrennte Zustandswerte, gesetzt mit SetRGBFillColor und SetRGBStrokeColor, beide nehmen ein einziges TColor. Das bedeutet, man kann Delphis Farbkonstanten und den RGB-Helfer direkt verwenden.
// Rectangle(X, Y, Width, Height): X und Y sind die untere linke Ecke.
Pdf.CurrentPage.SetRGBFillColor(RGB(220, 60, 60));
Pdf.CurrentPage.Rectangle(72, 500, 160, 90);
Pdf.CurrentPage.Fill;
// Circle(X, Y, Radius): X und Y sind der Mittelpunkt.
Pdf.CurrentPage.SetRGBFillColor(clNavy);
Pdf.CurrentPage.Circle(420, 545, 45);
Pdf.CurrentPage.Fill;
// Nur Umriss: eine Strichfarbe und eine Breite setzen, dann Stroke.
Pdf.CurrentPage.SetLineWidth(2);
Pdf.CurrentPage.SetRGBStrokeColor(clBlack);
Pdf.CurrentPage.Rectangle(72, 400, 160, 60);
Pdf.CurrentPage.Stroke;
Die Argumentform bei Rectangle verdient Aufmerksamkeit. Es ist Position-plus-Größe, X, Y, Width, Height, nicht zwei gegenüberliegende Ecken. Das TCanvas.Rectangle, das Delphi-Entwickler kennen, nimmt (Left, Top, Right, Bottom), 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 (X, Y)-Paar ist die untere linke Ecke, konsistent mit dem Seitenursprung. Bei einem Kreis ist (X, Y) 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 Random($FFFFFF) 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 AddImage und ShowImage 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 hier gezeigten Aufrufe MoveTo, LineTo, Stroke, Fill und die Farbaufrufe sind Teil des HotPDF Component für Delphi und C++Builder.