Technischer Artikel

Vektorzeichnen mit HotPDF in Delphi: Pfade und Farbe

HotPDF zeichnet Vektorgrafik, indem es auf der aktuellen Seite einen Pfad aufbaut und dann verlangt, dass er gemalt wird. Dazwischen liegt kein Bitmap-Schritt. Eine Linie, die Sie mit MoveTo und LineTo ziehen, landet als PDF-Pfadoperator im Content-Stream und bleibt damit echter Vektor: scharf bei 50 Prozent Zoom, scharf bei 1600 Prozent, und ein Bruchteil der Größe, die eine gerasterte Fassung kosten würde. Für Diagramme, Tabellenlinien, Diagrammachsen und Formularschmuck ist genau das gewünscht, und die API dahinter ist klein genug, um sie an einem Nachmittag zu lernen

Die gesamte Zeichenfläche liegt an THotPDF.CurrentPage. Zwischen BeginDoc und EndDoc setzen Sie an diesem Seitenobjekt Farbe und Linienbreite, legen Geometrie an und rufen einen Maloperator auf, der sie festschreibt. Die vier Primitiven, die Sie am häufigsten brauchen, sind MoveTo und LineTo für beliebige Pfade, Rectangle für Kästen, Circle für Scheiben sowie die beiden Maloperatoren Stroke und Fill

Das Koordinatensystem beginnt unten links

Das ist die eine Sache, über die jeder stolpert, der aus der VCL kommt. Das TCanvas, mit dem Sie Steuerelemente zeichnen, legt den Ursprung in die obere linke Ecke, und Y wächst nach unten. PDF macht es umgekehrt. HotPDF misst von der unteren linken Ecke der Seite in Punkt (1/72 Zoll), und Y wächst nach oben. Ein Punkt bei Y := 720 sitzt nahe dem oberen Rand einer US-Letter-Seite, die 792 Punkt hoch ist, und Y := 50 sitzt nahe dem unteren Rand. Wenn Ihre erste Zeichnung senkrecht gespiegelt herauskommt, liegt es daran: Aus der Bildschirmgrafik übernommener Code nimmt die falsche Richtung an und läuft unten aus der Seite

Dieselbe Konvention gilt für TextOut, also teilen Text und Formen ein Denkmodell, sobald Sie es verinnerlicht haben. Planen Sie ein Layout, indem Sie festlegen, wo die Unterkante jedes Elements sitzt, nicht die Oberkante, und der Rest ergibt sich

Vergleich zwischen dem Ursprung oben links auf der Bildschirm-Canvas und dem Ursprung unten links im PDF: Derselbe Punkt nahe dem oberen Rand einer US-Letter-Seite lautet im TCanvas-Code Y = 72, in HotPDF-Punkten aber Y = 720, sodass übernommener Code ohne gespiegeltes Y verkehrt herum zeichnet
HotPDF misst von der unteren linken Ecke in Punkt, sodass eine Stelle nahe dem oberen Rand der Seite mit 612 mal 792 den Wert Y = 720 trägt — dieselbe physische Stelle, die TCanvas-Code mit einem kleinen, nach unten gemessenen Y adressiert

Pfade: MoveTo, LineTo, Stroke

Ein gestrichener Pfad ist ein Stift, der gehoben, aufgesetzt und gezogen wird. MoveTo hebt den Stift und setzt den Startpunkt, ohne etwas zu markieren. Jedes LineTo verlängert den aktuellen Pfad zu einem neuen Punkt. Auf der Seite erscheint nichts, bis Sie Stroke aufrufen, das den angesammelten Pfad mit der aktuellen Strichfarbe und Linienbreite zeichnet und den Pfad danach leert, sodass das nächste MoveTo frisch beginnt

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

    // Die Linienbreite ist in Punkt und gilt, bis Sie sie ändern.
    Pdf.CurrentPage.SetLineWidth(1.5);
    Pdf.CurrentPage.SetRGBStrokeColor(clBlack);

    // Eine waagerechte Linie nahe dem Seitenkopf (Y vom unteren Rand gemessen).
    Pdf.CurrentPage.MoveTo(72, 720);
    Pdf.CurrentPage.LineTo(523, 720);
    Pdf.CurrentPage.Stroke;          // Pfad festschreiben; vorher wurde nichts gezeichnet

    // Ein dickerer zusammenhängender Linienzug: 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 sparen echte Fehlersuchzeit. Die Linienbreite ist Zustand, kein Argument: SetLineWidth setzt sie einmal, und jedes folgende Stroke verwendet diesen Wert, bis Sie ihn wieder ändern, weshalb der Linienzug oben dicker ist als die Linie. Und der Pfad wird nach jedem Stroke zurückgesetzt, sodass ein vergessenes Stroke bedeutet, dass die sorgsam angelegte Geometrie überhaupt nicht erscheint. Fehlt eine Form in der Ausgabe, ist der Malaufruf die erste Stelle, an der Sie nachsehen

Die Koordinaten sind Punkte, und Punkte sind gebrochen. MoveTo und LineTo nehmen Single-Werte entgegen, also ist eine Haarlinie bei 0.5 Punkt oder eine Position bei 72.25 zulässig und bedeutsam, nicht auf die nächste ganze Einheit gerundet. Diese Genauigkeit zählt in zwei entgegengesetzte Richtungen. Eine Linienbreite unter etwa 0.5 kann als geräteabhängig dünnstmögliche Linie erscheinen, die auf dem Bildschirm verschwindet und im Druck wieder auftaucht, eine sichtbare Linie will also eine Breite, die Sie bewusst setzen, statt der Vorgabe. Am anderen Ende hält das Ausrichten von Tabellenlinien und Gitterlinien auf ganze Punktkoordinaten ein dichtes Gitter davon ab, leicht ungleichmäßig zu wirken, wo benachbarte Linien unterschiedlich runden. Legen Sie den Gitterabstand in Punkt vorab fest, und der Rest des Layouts erbt ihn

Gefüllte Formen und Farbe

Geschlossene Primitiven lassen sich füllen statt umranden. Rectangle nimmt Position und Größe, Circle nimmt Mittelpunkt und Radius, und beide werden entweder mit Fill festgeschrieben, das das Innere in der aktuellen Füllfarbe malt, oder mit Stroke für den bloßen Umriss. Füllfarbe und Strichfarbe sind getrennte Zustände, gesetzt mit SetRGBFillColor und SetRGBStrokeColor, die beide ein einzelnes TColor entgegennehmen. Das heißt, Sie können die Farbkonstanten von Delphi und den Helfer RGB unmittelbar weiterverwenden

Pfadmodell von HotPDF: MoveTo, LineTo, Rectangle und Circle bauen im Speicher einen unsichtbaren aktuellen Pfad auf, und erst das Festschreiben mit Stroke, Fill oder FillAndStroke malt ihn mit dem beständigen Grafikzustand aus Strichfarbe, Füllfarbe und Linienbreite, bevor der Puffer geleert wird
Geometrie sammelt sich still im aktuellen Pfad, bis ein Maloperator sie mit der gespeicherten Strichfarbe, Füllfarbe und Linienbreite festschreibt — ein vergessener Malaufruf lässt die Form ungezeichnet
// 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: Strichfarbe und Breite setzen, dann Stroke.
Pdf.CurrentPage.SetLineWidth(2);
Pdf.CurrentPage.SetRGBStrokeColor(clBlack);
Pdf.CurrentPage.Rectangle(72, 400, 160, 60);
Pdf.CurrentPage.Stroke;

Achten Sie auf die Form der Argumente von Rectangle. 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), also reicht das Muskelgedächtnis HotPDF eine zweite Ecke, wo es Breite und Höhe erwartet, und der Kasten kommt in der falschen Größe heraus. Das Paar (X, Y) ist die untere linke Ecke, passend zum Seitenursprung. Bei einem Kreis ist (X, Y) der Mittelpunkt, und das dritte Argument ist der Radius in Punkt

Eine Farbentscheidung, die das ursprüngliche Beispiel falsch traf

Eine ältere Fassung dieses Beispiels säte die Farben mit Random($FFFFFF) für jede Form. Das wirkt lebendig und ist für erzeugte Dokumente der falsche Reflex. Ein PDF, das Sie aus Code bauen, wollen Sie meist auch testen, und zufällige Füllfarben machen die Ausgabe von Lauf zu Lauf unvergleichbar: Ein byteweiser Vergleich gegen eine als gut bekannte Datei scheitert jedes Mal, ohne echten Grund. Wählen Sie ausdrückliche Farben. Wenn Sie über eine Reihe von Formen Abwechslung wollen, speisen Sie sie aus Ihren Daten oder einer festen Palettenliste, sodass dieselbe Eingabe stets dieselbe Datei ergibt. Determinismus ist mehr wert als Neuheit, sobald das Artefakt eine Freigabepipeline durchläuft

Die Primitiven zusammensetzen: ein Anmerkungskasten

Jede Primitive ist für sich einfach; der Gewinn zeigt sich, wenn eine Handvoll davon zu etwas zusammenwächst, das ein Bericht tatsächlich braucht. Ein Anmerkungskasten, der auf eine Abbildung zeigt und sie erklärt, nutzt alles bisher Behandelte: ein gefülltes Rechteck mit Rand, eine gestrichene Zeigerlinie, einen Punkt, der den Zeiger verankert, und Text im Kasten, gesetzt in denselben Koordinaten von unten links, die auch die Formen benutzen. FillAndStroke verdient hier seinen Platz, denn es malt Inneres und Umriss eines Pfades in einem einzigen Commit, statt das Rechteck zweimal zu bauen

Aufbau eines Anmerkungskastens aus HotPDF-Primitiven: ein mit FillAndStroke festgeschriebenes Rechteck, eine gestrichene Zeigerlinie aus MoveTo und LineTo, ein gefüllter Ankerpunkt und TextOut-Beschriftungen, die dasselbe Raster von unten links teilen wie die Formen
Vier Commits bauen den Anmerkungskasten — FillAndStroke malt Fläche und Rand gemeinsam, Zeiger und Punkt verwenden Strich- und Füllzustand weiter, und jeder Beschriftungsversatz ist schlichte Arithmetik gegen die Kastenecke (90, 600)
var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'Callout.pdf';
    Pdf.BeginDoc;

    // 1. Der Kasten: blasse Füllung plus sichtbarer Rand, ein Pfad, ein Commit.
    //    Rectangle nimmt untere linke Ecke plus Größe, Y vom unteren Rand
    Pdf.CurrentPage.SetRGBFillColor(RGB(255, 244, 214));   // blasse Bernsteinfläche
    Pdf.CurrentPage.SetRGBStrokeColor(RGB(180, 130, 40));  // dunklerer Rand
    Pdf.CurrentPage.SetLineWidth(1);
    Pdf.CurrentPage.Rectangle(90, 600, 240, 70);
    Pdf.CurrentPage.FillAndStroke;

    // 2. Der Zeiger: ein gestrichenes Segment von der Kastenkante hinab
    //    zu dem, was erläutert werden soll
    Pdf.CurrentPage.SetLineWidth(1.5);
    Pdf.CurrentPage.MoveTo(90, 615);        // linke Kante des Kastens
    Pdf.CurrentPage.LineTo(66, 546);
    Pdf.CurrentPage.Stroke;

    // 3. Ein gefüllter Punkt verankert den Zeiger an seinem Ziel
    Pdf.CurrentPage.SetRGBFillColor(RGB(180, 130, 40));
    Pdf.CurrentPage.Circle(64, 542, 3);
    Pdf.CurrentPage.Fill;

    // 4. Die Beschriftung, relativ zur unteren linken Ecke des Kastens.
    //    Text und Formen teilen ein Koordinatensystem, die Versätze
    //    sind also schlichte Arithmetik gegen (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;

Beachten Sie, wie wenig Zustandsverwaltung das Zusammengesetzte braucht. Füllfarbe, Strichfarbe und Linienbreite werden jeweils unmittelbar vor der Form gesetzt, die sie verwendet, sodass jeder Block der Zeichnung als eigenständige Einheit liest und sich umstellen oder in einen Helfer herauslösen lässt, ohne verborgenen Zustand mitzuschleppen. Packen Sie das in eine Prozedur, die den Ankerpunkt und die Zeichenketten entgegennimmt, und Sie haben für vierzig Zeilen eine wiederverwendbare Diagrammanmerkung

Wo sich Vektorzeichnen auszahlt und wo nicht

Greifen Sie zu diesen Pfad- und Formaufrufen, wenn die Geometrie erzeugt wird: Gitterlinien und Balken von Diagrammen, die Linien einer Rechnungstabelle, Anmerkungskästen an einer Abbildung, eine Bildmarke aus einer Handvoll Pfade. All das skaliert ohne Unschärfe und trägt fast nichts zur Dateigröße bei, denn ein Rechteck sind ein paar Zahlen statt Tausender Pixel. Die Kehrseite ist ebenso ehrlich. Wenn Sie in Wahrheit ein Foto oder einen Bildschirmabzug haben, zeichnen Sie ihn stattdessen mit AddImage und ShowImage als Bild; eine Bitmap mit Vektoraufrufen nachzuziehen, bringt Ihnen nichts. Die geraden Segmente, Rechtecke und Kreise von oben tragen den weitaus größten Teil echter Berichtsarbeit, und die drei Verfeinerungen, nach denen Entwickler als Nächstes fragen, Kurven, Strichmuster und Transparenz, sitzen am selben Seitenobjekt

Kurven, Strichmuster und Transparenz in Kürze

Freie Kurven erweitern dieselbe Pfadmechanik, die Sie schon haben. CurveToC(X1, Y1, X2, Y2, X3, Y3) hängt ein kubisches Bezier-Segment vom aktuellen Punkt nach (X3, Y3) an, das sich zu den beiden Kontrollpunkten neigt, und die Kurzformen CurveToV und CurveToY decken die Fälle ab, in denen ein Kontrollpunkt mit einem Endpunkt zusammenfällt. Ein Pfad darf Segmente aus LineTo und CurveToC frei mischen, bevor ein einzelnes Stroke oder Fill ihn festschreibt, und so entstehen abgerundete Ecken und weiche Diagrammlinien

Gestrichelte Striche sind Zustand, genau wie die Linienbreite. SetDash([3, 3], 0) schaltet jeden folgenden Strich auf ein Muster von drei Punkt an und drei Punkt aus, wobei die Liste die Längen der An- und Aus-Abschnitte in Punkt nennt und das zweite Argument die Phase bestimmt, an der der Zyklus beginnt; NoDash gibt dem Stift die durchgezogene Linie zurück. Setzen Sie es, streichen Sie die Gitterlinien, die es wollen, und setzen Sie es vor der nächsten durchgezogenen Linie zurück, sonst steckt das Strichmuster still alles Folgende an

Transparenz läuft über einen benannten Grafikzustand statt über ein Farbargument, denn Alpha ist in PDF eine Eigenschaft des Grafikzustands-Dictionary. Registrieren Sie einen am Dokument mit RegisterExtGState und übergeben Sie ein Füll- und ein Strich-Alpha zwischen 0 und 1, und wenden Sie dann den zurückgegebenen Namen mit CurrentPage.SetGraphicsState an; Füllungen und Striche ab diesem Punkt malen in der registrierten Deckkraft. Das ist mehr Zeremonie als bei den Farbsettern und lohnt sich beim ersten Mal, wenn ein Hervorhebungsbalken über Text liegen muss, ohne ihn zu verdecken

Die verbleibende Gewohnheit, die sich lohnt, ist die Prüfung. Erzeugte Geometrie kann auf Ihrer Maschine bestehen und auf der eines Kunden scheitern, meist wegen einer Schriftersetzung im beigemischten Text oder einer Annahme zur Seitengröße, die nicht hält. Öffnen Sie die fertige Datei in einigen Zoomstufen, um zu bestätigen, dass die Kanten sauber bleiben, und prüfen Sie, ob jede Form innerhalb des gewünschten Randkastens landet. Mit einem deterministischen Farbschema lässt sich diese Prüfung gegen ein Referenz-PDF automatisieren, statt sie mit dem Auge zu machen

Die hier gezeigten Aufrufe MoveTo, LineTo, Stroke, Fill und die Farbaufrufe gehören zur HotPDF Delphi Component für Delphi und C++Builder