Der meiste Delphi-Code, der mit PDF arbeitet, behandelt das Format als Container für zwei Dinge: Textpassagen und einige platzierte Bitmaps. Diese Sichtweise ist zwar im Wesentlichen richtig, lässt aber den leistungsfähigsten Teil des Formats ungenutzt. Eine PDF-Seite ist eine auflösungsunabhängige 2D-Zeichenfläche, die auf demselben Bildgebungsmodell wie PostScript basiert. Sie kann Linien, Kurven, gefüllte Bereiche, Verläufe und sich wiederholende Muster zeichnen – und das alles als Vektoren, die bei jedem Zoom scharf bleiben und mit der vollen Auflösung des Geräts gedruckt werden. Wenn Sie ein Logo, ein Diagramm, ein Wasserzeichen oder einen Zertifikatsrahmen zeichnen, ist der Vektorpfad fast immer das richtige Primitiv, da er kleiner und schärfer ist als das gerasterte Bild, zu dem viele Programme stattdessen greifen.
Dieser Artikel führt durch das Vektormodell, wie es in ISO 32000-1 definiert ist, und zeigt die entsprechenden PDFlibPas-Aufrufe. Das Ziel ist es, die Spezifikation greifbar zu machen, da die API eng an sie angelehnt ist und das Verständnis des einen das Erlernen des anderen erleichtert.
Die Seite als Pfad-Maschine
ISO 32000-1 §8.5 beschreibt Grafiken in zwei Phasen, die sich niemals überschneiden. Zuerst konstruieren Sie einen Pfad, was reine Geometrie ohne sichtbares Ergebnis ist. Dann zeichnen Sie diesen Pfad in einer einzigen Operation, die seine Kontur nachzeichnet (stroke), sein Inneres füllt (fill) oder beides tut. Während der Konstruktion erscheint nichts auf der Seite. Der Pfad is eine abstrakte Folge von Punkten und Segmenten, die im Grafikzustand gehalten wird, bis ein Zeichen-Operator sie konsumiert. An diesem Punkt wird sie gerendert und verworfen.
Ein Pfad besteht aus einem oder mehreren Unterpfaden. Ein Unterpfad beginnt an einem Punkt und wächst durch das Anhängen von Segmenten: gerade Linien, kubische Bezier-Kurven und auf einigen Plattformen ganze Rechtecke, die als eigener geschlossener Unterpfad hinzugefügt werden. In PDFlibPas öffnen Sie einen Pfad mit StartPath, was den Startpunkt festlegt, und erweitern ihn dann mit AddLineToPath und AddCurveToPath. Jeder Aufruf verschiebt einen impliziten aktuellen Punkt, sodass das nächste Segment dort fortgesetzt wird, wo das letzte endete. ClosePath zeichnet ein abschließendes gerades Segment zurück zum Anfang des Unterpfads, was für das Nachzeichnen der Kontur wichtig ist, da es eine echte Linienverbindung am schließenden Scheitelpunkt erzeugt und nicht zwei lose Endkappen.
// A closed quadrilateral, stroked then filled
PDF.SetLineColor(0, 0, 0);
PDF.SetFillColor(0.6, 0.8, 1.0);
PDF.SetLineWidth(1.5);
PDF.StartPath(150, 100); // open the path at the first vertex
PDF.AddLineToPath(220, 140);
PDF.AddLineToPath(180, 210);
PDF.AddLineToPath(110, 170);
PDF.ClosePath; // straight segment back to (150, 100)
PDF.DrawPath(2); // 2 = fill and stroke; path is consumed
Kurven verwenden AddCurveToPath, was zwei Bezier-Kontrollpunkte und einen Endpunkt übernimmt: AddCurveToPath(CtAX, CtAY, CtBX, CtBY, EndX, EndY). Die Kurve verläuft vom aktuellen Punkt bis zu (EndX, EndY), entlang des Weges gezogen zu den beiden Kontrollpunkten. Kreisbögen sind über AddArcToPath(CenterX, CenterY, TotalAngle) verfügbar, wobei der Radius aus dem Abstand zwischen dem aktuellen Punkt und dem Zentrum ermittelt wird und die Engine den Bogen als Kette von Bezier-Segmenten ausgibt. Rechtecke haben eine Abkürzung, AddBoxToPath(Left, Top, Width, Height), die ein vollständiges geschlossenes Rechteck als eigenen Unterpfad ohne vorheriges StartPath anhängt.
Zwei Füllregeln und warum sie sich unterscheiden
Wenn Sie einen Pfad füllen, der sich selbst schneidet oder eine innere Schleife enthält, benötigt der Renderer eine Regel, um zu entscheiden, welche Bereiche innerhalb der Form liegen und welche Löcher sind. ISO 32000-1 §8.5.3.3 definiert zwei Füllregeln, und sie können dieselbe Geometrie unterschiedlich einfärben. Die Nonzero-Winding-Regel zählt die vorzeichenbehafteten Schnitte eines Strahls, der von einem Testpunkt ins Unendliche geworfen wird: Sie addiert eins für jedes Segment, das von links nach rechts kreuzt, und subtrahiert eins für jedes, das andersherum kreuzt. Der Punkt liegt im Inneren, wenn die Summe nicht Null ist. Die Even-Odd-Regel ignoriert die Richtung und zählt einfach die Schnittpunkte, wobei der Punkt als innen definiert wird, wenn die Anzahl ungerade ist.
Der klassische Fall, in dem sie voneinander abweichen, ist eine Form mit einem Loch, wie ein Donut oder eine Unterlegscheibe. Zeichnen Sie eine äußere Begrenzung und eine innere Begrenzung darin. Unter der Even-Odd-Regel stanzt die innere Schleife immer ein Loch aus, da jeder Punkt zwischen den beiden Begrenzungen einmal und jeder Punkt innerhalb der inneren Schleife zweimal gekreuzt wird. Unter der Nonzero-Winding-Regel entsteht das Loch nur, wenn die innere Schleife in die entgegengesetzte Richtung der äußeren Schleife verläuft; verlaufen sie in dieselbe Richtung, verstärken sich die Wicklungen, anstatt sich aufzuheben, und der innere Bereich wird ausgefüllt. Ein fünfzackiger Stern, der als eine einzige, sich selbst schneidende Kontur gezeichnet wird, zeigt dieselbe Spaltung: Even-Odd lässt das zentrale Fünfeck leer, während Nonzero-Winding es füllt.
PDFlibPas wählt die Regel über den Aufruf aus, den Sie zum Zeichnen tätigen, nicht über ein Flag. DrawPath füllt nach der Nonzero-Winding-Regel; DrawPathEvenOdd füllt nach der Even-Odd-Regel. Beide übernehmen denselben Integer-Modus: 0 zeichnet nur die Kontur nach, 1 füllt nur und 2 füllt und zeichnet die Kontur. Die Even-Odd-Regel ist das einfachere Werkzeug für ausgestanzte Löcher, eben weil Sie sich nicht um die Richtung des Unterpfads kümmern müssen.
// Same two boxes, two fill rules, two different results.
// Nonzero winding: both boxes wind the same way, so the inner one
// does NOT cut a hole and the whole outer box fills solid.
PDF.SetFillColor(0.2, 0.4, 0.8);
PDF.AddBoxToPath(100, 100, 200, 120); // outer
PDF.AddBoxToPath(140, 130, 120, 60); // inner
PDF.DrawPath(1); // 1 = fill, nonzero winding
// Even-odd: the inner box is crossed an even number of times,
// so it punches a clean rectangular hole through the outer box.
PDF.SetFillColor(0.2, 0.4, 0.8);
PDF.AddBoxToPath(100, 300, 200, 120); // outer
PDF.AddBoxToPath(140, 330, 120, 60); // inner cut-out
PDF.DrawPathEvenOdd(1); // 1 = fill, even-odd
Axiale Verläufe variieren die Farbe entlang einer Linie
Eine flache Füllfarbe hat einen einzigen Wert im gesamten Bereich. Ein Verlauf variiert die Farbe kontinuierlich, und die einfachste Art ist der axiale oder lineare Verlauf. ISO 32000-1 §8.7.4.5 spezifiziert dies als axiale Schattierung des Typs 2: Sie geben zwei Punkte an, die eine Achse definieren, eine Startfarbe am ersten Punkt und eine Endfarbe am zweiten Punkt, und der Renderer interpoliert die Farbe entlang dieser Achse. Jeder Punkt im gefüllten Bereich nimmt die Farbe seiner senkrechten Projektion auf die Achse an, sodass der Verlauf in Bändern rechtwinklig zur Verbindungslinie zwischen den beiden Punkten verläuft.
In PDFlibPas ist ein Verlauf eine benannte Dokumentenressource, die Sie einmal erstellen und dann als aktiven Zeichenstift auswählen. NewRGBAxialShader registriert ihn. Die Signatur lautet NewRGBAxialShader(ShaderName, StartX, StartY, StartRed, StartGreen, StartBlue, EndX, EndY, EndRed, EndGreen, EndBlue, Extend): die beiden Achsenendpunkte, die RGB-Tripel an jedem Ende als Werte im Bereich von 0 bis 1 und ein Extend-Flag. Wenn Extend auf 1 gesetzt ist, setzen sich die Endfarben über die Achsenendpunkte hinaus als solide Füllung fort, was meistens gewünscht ist, damit die Ecken eines Bereichs außerhalb der Achse nicht ungezeichnet bleiben; 0 lässt sie unberührt. Sobald der Shader existiert, binden Sie ihn mit SetFillShader für gefüllte Bereiche, SetLineShader für nachgezeichnete Konturen oder SetTextShader für Text. Die Bindung bleibt für die folgenden Zeichenaufrufe aktiv, sodass der Pfad, den Sie als Nächstes zeichnen, den Verlauf anstelle einer flachen Farbe annimmt.
// Define a vertical gradient once: blue at the bottom to white at the top.
PDF.NewRGBAxialShader('panelGrad',
0, 100, 0.10, 0.25, 0.55, // start point and start RGB
0, 260, 1.00, 1.00, 1.00, // end point and end RGB
1); // 1 = extend ends as solid color
// Select the gradient as the fill, then paint a rectangle with it.
PDF.SetFillShader('panelGrad');
PDF.AddBoxToPath(80, 100, 300, 160);
PDF.DrawPath(1); // 1 = fill, now filled by the shader
Die Achse verläuft hier vertikal, von y=100 bis y=260 bei festem x, sodass die Farbbänder horizontal verlaufen und das Rechteck an seiner Basis von Blau zu Weiß an der Spitze verläuft. Da der Shader über den Namen referenziert wird, kann eine Definition eine beliebige Anzahl von Formen auf der Seite füllen, und das Zurückschalten auf eine flache Farbe ist nur ein weiterer SetFillColor-Aufruf vor dem nächsten Pfad.
Kachelmuster wiederholen eine Zelle
Während ein Verlauf eine einzelne Farbe sanft variiert, wiederholt ein Kachelmuster ein kleines Kunstwerk über einen Bereich hinweg. ISO 32000-1 §8.7.3.1 definiert ein Kachelmuster als eine Musterzelle, ein unabhängiges Stück Inhalt, das der Renderer auf einem festen Raster repliziert, um den gezeichneten Bereich zu kacheln. Auf diese Weise erstellen Sie Schraffuren für technische Füllungen, ein sich wiederholendes Markenmotiv hinter einer Kopfzeile oder einen strukturierten Hintergrund, der vektorscharf bleibt und unabhängig von der Größe des Bereichs fast nichts wiegt, da die Zelle einmal gespeichert und überall referenziert wird.
PDFlibPas erstellt die Musterzelle aus erfassten Seiteninhalten. Sie erfassen eine Seite oder einen Bereich mit CapturePage, wandeln die Erfassung mit NewTilingPatternFromCapturedPage(PatternName, CaptureID) in ein benanntes Muster um und wählen dieses Muster dann mit SetFillTilingPattern(PatternName) als aktuelle Füllung aus. Von diesem Zeitpunkt an wird jeder Pfad, den Sie füllen, mit der sich wiederholenden Zelle anstelle einer flachen Farbe gezeichnet – genau wie eine Shader-Füllung funktioniert, jedoch mit einer gekachelten Zelle als Zeichenquelle. Die Sequenz ist komplexer als ein einzelner Aufruf. Wenn Ihnen der Erfassungsschritt also unvertraut ist, betrachten Sie das Muster als zweistufige Operation: Erstellen Sie zuerst die erfasste Zelle und binden Sie sie dann vor dem Zeichnen des zu kachelnden Bereichs namentlich als Füllung.
Zusammenfügen der Primitive
Die Teile lassen sich direkt zusammensetzen. Ein gefüllter Bezier-Blob ist ein Pfad aus Kurven, der mit DrawPath gezeichnet wird. Dieselbe Kontur, nach dem Hinzufügen einer inneren Schleife mit DrawPathEvenOdd gezeichnet, zeigt ein Loch, das die Winding-Füllung geschlossen hätte. Ein mit einem Verlauf gefülltes Rechteck ist eine Box, die an einen Shader gebunden ist. Das folgende Beispiel zeichnet alle drei nacheinander, sodass der Unterschied zwischen den beiden Füllregeln auf einer einzigen Seite sichtbar wird, und legt dann ein Verlaufs-Panel darunter.
// 1. A filled Bezier shape (nonzero winding).
PDF.SetFillColor(0.85, 0.30, 0.25);
PDF.StartPath(120, 480);
PDF.AddCurveToPath(160, 560, 240, 560, 280, 480); // top lobe
PDF.AddCurveToPath(240, 420, 160, 420, 120, 480); // bottom lobe
PDF.ClosePath;
PDF.DrawPath(1); // 1 = fill
// 2. The same outline, plus an inner loop, filled even-odd to show a hole.
PDF.SetFillColor(0.85, 0.30, 0.25);
PDF.StartPath(120, 300);
PDF.AddCurveToPath(160, 380, 240, 380, 280, 300);
PDF.AddCurveToPath(240, 240, 160, 240, 120, 300);
PDF.ClosePath;
PDF.MovePath(180, 300); // new subpath: the hole
PDF.AddArcToPath(200, 300, 360); // a full circle
PDF.ClosePath;
PDF.DrawPathEvenOdd(1); // hole is punched out
// 3. A rectangle filled with an axial gradient.
PDF.NewRGBAxialShader('footerGrad',
60, 100, 0.95, 0.55, 0.10,
60, 200, 0.20, 0.10, 0.40,
1);
PDF.SetFillShader('footerGrad');
PDF.AddBoxToPath(60, 100, 340, 100);
PDF.DrawPath(1);
Zwei Details sollte man sich merken. Der Zeichenaufruf entscheidet über die Füllregel: Die Wahl zwischen DrawPath und DrawPathEvenOdd ist also die Wahl zwischen Nonzero-Winding und Even-Odd. Bei Formen mit Löchern erspart Ihnen die Even-Odd-Regel das Nachdenken über die Richtung des Unterpfads. Zudem wird der Grafikzustand in dem Moment abgefragt, in dem Sie zeichnen: Setzen Sie Ihre Farben, die Linienbreite und die Shader-Bindung vor dem Zeichenaufruf, da dies der Zustand ist, den die Engine liest. Erst konstruieren, dann den Zustand konfigurieren, zuletzt zeichnen – so verhält sich das Vektormodell jedes Mal vorhersehbar.
Von hier aus sind die nächsten logischen Schritte das Auslesen von Vektoren und Text aus einem bestehenden Dokument, was in unserem Artikel über Text-, Bild- und Schriftartenextraktion behandelt wird, sowie das Rendern desselben Zeichnungsmodells auf einen Windows-Gerätekontext für Bildschirmvorschau und Druck, was im Leitfaden für Druck und Vorschau beschrieben wird. Die hier beschriebenen Pfad-, Shader- und Musteraufrufe werden als Teil der Delphi PDF-Bibliothek zusammen mit den APIs für Text, Bilder, Formulare und Signaturen ausgeliefert, die an anderer Stelle in diesem Blog behandelt werden.