PDFium hat den Ruf einer Viewer-Engine, dem Renderer hinter dem PDF-Tab von Chrome. Daher sollte zunächst klargestellt werden, dass PDFium Component auch ein Dokument erstellen kann, das zuvor nie existiert hat. Die Autorenseite umhüllt die Seitenobjekt-API von PDFium: Sie erstellen ein leeres Dokument, fügen Seiten mit expliziten Abmessungen hinzu und platzieren Text, Vektorpfade und Bilder an den von Ihnen gewählten Koordinaten auf jeder Seite. Es gibt keine Seitenbeschreibungssprache zu erlernen und keinen Druckertreiber in der Schleife. Sie rufen Methoden auf, die Bibliothek setzt PDF-Objekte zusammen, und SaveAs serialisiert das Ergebnis
Was Sie nicht erhalten, ist eine Layout-Engine. Das ist wichtig genug, um es vorab zu erwähnen, denn es prägt jedes der folgenden Beispiele. PDFium Component platziert Inhalte dort, wo Sie es anweisen, in absoluten Koordinaten, und nirgendwo sonst. Es wird keinen Absatz umbrechen, Text nicht über einen Seitenumbruch fließen lassen oder eine Tabelle aus Zeilen und Spalten berechnen. Das ist Ihre Aufgabe. Wenn Sie etwas erwartet haben, das Fließtext wie ein Textverarbeitungsprogramm umbricht, sollten Sie sich jetzt neu orientieren: Dies ist eine präzise Low-Level-Platzierungs-API, die dem Zeichnen auf einer Leinwand (Canvas) näher kommt als dem Setzen eines Dokuments. Für generierte Rechnungen, Zertifikate, Etiketten und Berichtsseiten, bei denen Sie bereits wissen, wohin jedes Element gehört, ist genau diese Präzision das, was Sie brauchen
Das Minimum, das eine Datei erzeugt
Drei Aufrufe stehen zwischen einem leeren TPdf und einem gespeicherten PDF: Dokument erstellen, eine Seite hinzufügen, es ausschreiben. Alles andere ist Inhalt, den Sie dazwischen schichten
uses
Vcl.Graphics, // for clBlack and TColor
PDFium; // TPdf lives here
procedure CreateBlankPdf(const FileName: string);
var
Pdf: TPdf;
begin
Pdf := TPdf.Create(nil);
try
Pdf.CreateDocument; // empty in-memory document
Pdf.AddPage(0, 595, 842); // A4 portrait, in points
Pdf.AddText('First page', 'Arial', 18, 50, 780);
Pdf.SaveAs(FileName); // serialize to disk
finally
Pdf.Active := False;
Pdf.Free;
end;
end;
Ein Detail bringt Leute zum Stolpern, die ältere Codeausschnitte gesehen haben: Sie weisen nach CreateDocument nicht Pdf.Active := True zu. Die Eigenschaft Active meldet, ob ein Dokument-Handle existiert, und CreateDocument hat bereits eines erstellt, sodass die Eigenschaft in dem Moment auf True steht, in dem dieser Aufruf zurückkehrt. Es noch einmal zu setzen, ist bestenfalls eine Nulloperation (no-op) und schlimmstenfalls irreführend für den nächsten Leser. Active macht sich beim Beenden bezahlt: Die Zuweisung von False gibt das zugrunde liegende Dokument vor Free frei, was die saubere Reihenfolge für den Abbau ist. Behandeln Sie CreateDocument und das Öffnen durch Laden einer Datei als sich gegenseitig ausschließend. Die Bibliothek weigert sich, ein neues Dokument in einem TPdf zu erstellen, in dem bereits eines geöffnet ist; eine Wiederverwendung bedeutet also, dass zuerst das aktuelle Dokument geschlossen werden muss
Koordinaten beginnen unten links
Das zweite Argumentenpaar für AddText und für jeden Platzierungsaufruf ist ein Punkt im PDF-Benutzerbereich. Der Ursprung liegt in der unteren linken Ecke der Seite, X verläuft nach rechts und Y verläuft nach oben. Eine Einheit entspricht einem Punkt (1/72 Zoll). Eine A4-Seite ist also 595 mal 842 Einheiten groß und US Letter 612 mal 792. Diese nach oben verlaufende Y-Achse ist die häufigste Ursache für die Verwirrung „Mein Text befindet sich außerhalb der Seite“, da Bildschirm- und Bitmap-Koordinaten den Ursprung oben platzieren, wobei Y nach unten zunimmt. Auf einer 842 Punkte hohen Seite befindet sich eine Überschrift nahe dem oberen Rand bei etwa Y 780, nicht Y 60. Wenn ein Zeichenvorgang (Run) an einer unerwarteten Stelle landet, ist die Seitenhöhe minus Ihr Y fast immer die Zahl, die Sie eigentlich gemeint haben
AddPage verwendet eine Einfügeposition als erstes Argument, ausgedrückt 1-basiert, wobei 0 eine praktische Kurzform für „Anfang des Dokuments“ ist. Übergeben Sie 0 oder 1 für die erste Seite, und die Seite wird ganz vorne eingefügt; übergeben Sie den Wert, der der aktuellen Anzahl entspricht, an die Sie anhängen, um am Ende hinzuzufügen. Die neu hinzugefügte Seite wird auch zur aktuellen Seite – der Seite, auf die nachfolgende Zeichenaufrufe abzielen –, es gibt also nach dem Hinzufügen keinen separaten „Diese Seite auswählen“-Schritt. Wenn Sie mehrere Seiten hinzufügen und später wieder auf einer früheren Seite zeichnen müssen, setzen Sie PageNumber, um den Cursor zu verschieben; solange Sie die Seiten in der Reihenfolge füllen, in der Sie sie erstellen, können Sie es so belassen
Text schreiben und die Schriftart-Regel, die leise zubeißt
Die AddText-Signatur enthält alles, was ein einzelner Zeichenvorgang (Run) benötigt: die Zeichenfolge, einen Schriftartnamen, eine Größe in Punkten, den X- und Y-Anker, dann optionale Farbe, ein Alpha-Byte für Transparenz und einen Drehwinkel in Grad
procedure WriteHeader(Pdf: TPdf; const Title, Author: string);
begin
// Title in black, default opacity, no rotation
Pdf.AddText(Title, 'Arial', 20, 50, 780);
// A lighter byline 24 points below it
Pdf.AddText('By ' + Author, 'Arial', 11, 50, 756, clGray);
// A faint diagonal draft stamp across the page
Pdf.AddText('DRAFT', 'Arial', 64, 180, 380, clGray, $30, 45.0);
end;
Das Alpha-Byte reicht von $00 (unsichtbar) bis $FF (deckend), was den Entwurfsstempel zu einem Wasserzeichen statt zu einem massiven Block macht: $30 entspricht etwa neunzehn Prozent Deckkraft, genug, um hindurchzulesen. Der Winkel dreht den Text gegen den Uhrzeigersinn um seinen Anker, sodass 45 Grad den klassischen Stempel von Ecke zu Ecke ergeben. Nichts davon erfordert eine separate Wasserzeichen-Funktion. Ein Wasserzeichen ist einfach ein großer, halbtransparenter, gedrehter AddText-Aufruf, und ob es vor oder nach dem Haupttext gezeichnet wird, entscheidet darüber, ob es hinter oder über dem Inhalt liegt
Schriftarten verdienen einen achtsamen Hinweis, denn der Fehlermodus ist unauffällig. Wenn Sie einen Schriftartnamen übergeben, fordert PDFium Component vom Betriebssystem die TrueType-Daten dieser Schriftart an und bettet sie in das Dokument ein. Aus diesem Grund wird eine auf Ihrem Rechner erstellte Datei auf einem Rechner, auf dem die Schriftart nie installiert war, identisch gerendert. Der Haken ist, was passiert, wenn der Name nicht aufgelöst werden kann: ein Tippfehler oder eine Schrift, die auf dem Build-Rechner schlicht nicht vorhanden ist. Es gibt keine Ausnahme (Exception). Die Bibliothek greift darauf zurück, ein Textobjekt zu erstellen, das den Namen nur als Bezeichnung trägt, ohne dass etwas eingebettet wird, und überlässt es dem Viewer, das zu ersetzen, was er für ähnlich hält. Der Text erscheint in Ihren Tests, sieht plausibel aus und verschiebt Metriken oder Glyphen in dem Moment, in dem die Datei irgendwo geöffnet wird, wo andere Schriftarten installiert sind. Verwenden Sie Namen, von denen Sie wissen, dass sie auf dem generierenden Rechner vorhanden sind, behandeln Sie die Schriftartenliste als Deployment-Abhängigkeit und öffnen Sie ein Beispiel in einem Viewer auf einem sauberen System, bevor Sie der Ausgabe vertrauen
Vektorformen: Einen Pfad erstellen, dann übernehmen
Linien, Rechtecke und gefüllte Bereiche werden über einen Pfad gezeichnet. Sie öffnen einen mit CreatePath, was gleichzeitig den Startpunkt und das gesamte Styling festlegt: Füllmodus, Füll- und Strichfarben mit ihren eigenen Alpha-Bytes, Strichstärke, Linienenden (Caps) und Verbindungen (Joins). Dann erweitern Sie ihn mit LineTo, BezierTo und ClosePath, und schließlich übernimmt AddPath (commit) den fertigen Pfad auf die Seite. Der Commit-Schritt wird leicht vergessen und erzeugt nichts, wenn Sie ihn überspringen
procedure DrawDivider(Pdf: TPdf; X, Y, Width: Single);
begin
// A thin horizontal rule. The rectangle overload sets a box directly:
// X, Y, Width, Height, then fill mode and colors.
Pdf.CreatePath(X, Y, Width, 0.5, fmNone, clBlack, $FF,
True, clBlack, $FF, 1.0);
Pdf.AddPath;
end;
procedure DrawTriangle(Pdf: TPdf);
begin
// Point overload: start at the first vertex, line to the rest, close.
Pdf.CreatePath(200, 300, fmWinding, clBlue, $80, True, clNavy, $FF, 2.0);
Pdf.LineTo(300, 300);
Pdf.LineTo(250, 400);
Pdf.ClosePath;
Pdf.AddPath; // nothing is drawn until this runs
end;
Zwei Überladungen decken die häufigsten Fälle ab. Die Variante mit vier Koordinaten verwendet X, Y, Breite und Höhe und liefert Ihnen in einem Aufruf ein achsenausgerichtetes Rechteck, auf das Sie zurückgreifen, um eine Linie, einen Zellrahmen oder ein gefülltes Hintergrundfeld zu zeichnen. Die Variante mit zwei Koordinaten legt nur einen Startpunkt fest, und Sie zeichnen den Rest des Umrisses selbst mit LineTo und BezierTo nach. Der Füllmodus steuert, wie sich überlappende Bereiche gezeichnet werden: fmWinding (Nonzero Winding) eignet sich für die meisten massiven Formen, fmAlternate (Even-Odd) behandelt Ausschnitte und sich selbst überschneidende Umrisse, und fmNone hinterlässt einen nur gestrichelten Pfad ohne Füllung, wie ihn die obige Trennlinie verwendet
Tabellen sind Pfade und Text, von Hand zusammengesetzt
Da es kein Tabellen-Grundelement (Primitive) gibt, ist eine Tabelle eine Schleife. Sie bestimmen die X-Versätze der Spalten und die Zeilenhöhe, schreiben jede Zelle mit AddText und zeichnen die Linien mit Rechteckpfaden. Die Berechnung liegt bei Ihnen, aber sie ist simpel, und einmal geschrieben, lässt sie sich auf jedes beliebige Raster verallgemeinern, das Sie benötigen
procedure DrawTable(Pdf: TPdf; Left, Top: Double);
const
ColX: array[0..2] of Double = (0, 110, 210); // column offsets
RowH = 20;
var
Y: Double;
Row: Integer;
begin
// Header row
Pdf.AddText('Item', 'Arial', 10, Left + ColX[0], Top);
Pdf.AddText('Qty', 'Arial', 10, Left + ColX[1], Top);
Pdf.AddText('Price', 'Arial', 10, Left + ColX[2], Top);
// Rule under the header
Pdf.CreatePath(Left, Top - 5, 260, 0.5, fmNone, clBlack, $FF);
Pdf.AddPath;
// Data rows, stepping Y downward each iteration
Y := Top;
for Row := 1 to 3 do
begin
Y := Y - RowH;
Pdf.AddText('Item ' + IntToStr(Row), 'Arial', 9, Left + ColX[0], Y);
Pdf.AddText(IntToStr(Row * 2), 'Arial', 9, Left + ColX[1], Y);
Pdf.AddText('$' + IntToStr(Row * 10) + '.00', 'Arial', 9, Left + ColX[2], Y);
end;
end;
Beachten Sie, dass Y bei jedem Durchlauf um die Zeilenhöhe nach unten rückt – wiederum, weil oben positiv ist. Hier zeigt sich auch das Fehlen der Textmessung: Nichts hindert einen langen Elementnamen daran, in die nächste Spalte überzulaufen, da die Bibliothek nicht weiß, wie breit Ihre Zeichenfolge gerendert wurde. Bei Ausgaben mit festem Format, bei denen Sie die Daten kontrollieren, dimensionieren Sie die Spalten großzügig und fahren fort. Für echte variable Inhalte müssen Sie entweder die Eingaben beschränken oder die Glyphenbreiten vor der Platzierung selbst messen; an diesem Punkt beginnt sich eine dedizierte Composition-Bibliothek bezahlt zu machen
Bilder und mehrere Seiten
Rasterinhalte kommen über die Bild-Hilfsfunktionen herein. AddPicture nimmt ein geladenes TPicture und platziert es an einem Punkt, mit einer optionalen Breite und Höhe zur Skalierung; AddImage akzeptiert direkt einen Dateipfad oder ein TBitmap, und AddJpegImage streamt JPEG-Bytes ohne den Umweg über ein Bitmap. Wie bei allem anderen sind die Platzierungskoordinaten die untere linke Ecke des Bildes im Benutzerbereich, und Breite und Höhe sind die Größe auf der Seite in Punkten, nicht die Pixelabmessungen der Quelle
procedure CreateMultiPageReport(const FileName: string; PageCount: Integer);
var
Pdf: TPdf;
P: Integer;
begin
Pdf := TPdf.Create(nil);
try
Pdf.CreateDocument;
for P := 1 to PageCount do
begin
Pdf.AddPage(P, 595, 842); // append; the new page becomes current
Pdf.AddText('Page ' + IntToStr(P) + ' of ' + IntToStr(PageCount),
'Arial', 10, 50, 30); // footer near the bottom edge
// ... draw this page's body here ...
end;
Pdf.SaveAs(FileName);
finally
Pdf.Active := False;
Pdf.Free;
end;
end;
Ein mehrseitiges Dokument ist das Einzelseiten-Muster in einer Schleife. Jedes AddPage hängt eine Seite an und macht sie zur aktuellen, sodass der Hauptteil (Body) und die Fußzeile, die Sie als Nächstes zeichnen, auf der Seite landen, die Sie gerade hinzugefügt haben. Sie weisen PageNumber innerhalb dieser Schleife nicht neu zu, da das Hinzufügen einer Seite den Cursor bereits dorthin bewegt hat; Sie benötigen PageNumber nur, wenn Sie zu einer Seite außerhalb der Erstellungsreihenfolge zurückkehren. Rufen Sie SaveAs einmal am Ende auf, nachdem die letzte Seite gefüllt ist. Wenn Sie ein Archivierungsprofil anstelle einer einfachen Datei benötigen, stellt dasselbe Dokumentobjekt SaveAsPdfA und die anderen Konformitätsvarianten zur Verfügung. Die Wahl des Ausgabestandards ist also ein anderer Speicheraufruf, kein anderer Erstellungspfad
Wofür sich dies eignet
Um ehrlich zu sein: Die Authoring-API von PDFium Component ist eine originalgetreue, dünne Schicht über dem Seitenobjektmodell von PDFium – echte Dokumentenerstellung, echte eingebettete Schriftarten, echte Vektor- und Rasterinhalte, serialisiert in eine normgerechte Datei. Es ist keine Document-Engine mit Textumbruch (Reflow) und gibt auch nicht vor, eine zu sein. Die Trennlinie ist das Textlayout. Wenn Ihre Ausgabe vorlagenbasiert ist – Rechnungen, Zertifikate, Etiketten, Dashboards, die auf einem festen Raster gerendert werden –, ist das Modell mit absoluten Koordinaten direkt und schnell, und der Code bleibt lesbar. Wenn Ihre Ausgabe aus längeren Texten besteht, die von selbst umgebrochen und paginiert werden müssen, müssten Sie auf diesen Aufrufen aufbauend eine Layout-Engine neu entwickeln – dafür ist es das falsche Werkzeug. Zu wissen, auf welcher Seite dieser Trennlinie Sie stehen, macht den Großteil der Entscheidung aus
Die hier beschriebenen Erstellungsmethoden sind Teil der PDFium Component für Delphi, die diesen Authoring-Pfad mit den Rendering- und Textextraktions-Funktionen kombiniert, für die PDFium bekannter ist