Die erste Rechnung, die fast jedes Team mit einer PDF-Bibliothek rendert, ist auf dieselbe Weise falsch: Der Kopftext sitzt am unteren Seitenrand, und jede folgende Zeile wandert nach oben. Nichts ist kaputt. Der PDF-Benutzerraum legt den Ursprung gemäß ISO 32000-1 §8.3 in die linke untere Ecke, wobei Y nach oben wächst, also genau entgegengesetzt zur GDI-Canvas, auf der ein VCL-Entwickler seit Jahren zeichnet. HotPDF, die PDF-Erzeugungsbibliothek von losLab für Delphi und C++Builder, legt dieses Koordinatenmodell direkt offen. Die fünf Minuten, die Sie jetzt in sein Verständnis investieren, ersparen später einen Layout-Rewrite. Dieser Artikel führt durch die Ausgabeprimitive, die ein Berichtsgenerator tatsächlich braucht: positionierten Text, Schriften, die Deployments überstehen, Bildplatzierung und Vektorzeichnung.
Textplatzierung und der Ursprung links unten
Der zentrale Aufruf des Seitenobjekts ist TextOut(X, Y, Angle, Text). X und Y positionieren den Text in Punkten von der linken unteren Ecke aus, und Angle rotiert ihn in Grad; so entstehen diagonale DRAFT- und COPY-Stempel ohne weitere Mechanik. Das Idiom, das VCL-geprägte Intuition nutzbar hält, berechnet Y als Seitenhöhe minus Abstand von oben:
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.FileName := 'invoice-0001.pdf';
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('Arial', [fsBold], 16);
Pdf.CurrentPage.TextOut(50, 792 - 50, 0, 'INVOICE'); // 50pt from top of Letter
Pdf.CurrentPage.SetFont('Arial', [], 10);
Pdf.CurrentPage.TextOut(50, 792 - 70, 0, 'Date: 2026-06-11');
Pdf.CurrentPage.TextOut(300, 400, 45, 'COPY'); // rotated stamp
Pdf.AddPage; // CurrentPage now points here
Pdf.CurrentPage.SetFont('Arial', [], 10); // font state does not carry over
Pdf.CurrentPage.TextOut(50, 742, 0, 'Page 2 detail rows');
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
Zwei zustandsbehaftete Verhaltensweisen in diesem Listing verursachen die meisten Seite-zwei-Fehler. AddPage setzt CurrentPage auf die neue Seite, daher ist jede Referenz, die Sie auf das vorherige Seitenobjekt gecacht haben, für Zeichenoperationen jetzt veraltet. Und die Schriftauswahl ist seitenbezogen: Rufen Sie SetFont nach jedem AddPage erneut auf, sonst verwendet das erste TextOut auf der neuen Seite nicht die Schrift, die Sie erwarten. Eine Berichtsschleife sollte „neue Seite“ und „Textzustand wiederherstellen“ als eine einzige Einheit behandeln.
Schriften, die auf dem Server existieren, nicht nur auf Ihrem Desktop
Schriftfehler sind Deployment-Fehler. Auf dem Entwicklungsrechner ist die Corporate Font installiert; das Windows-Dienstkonto auf dem Produktionshost hat sie nicht, und die Ausgabe ersetzt sie stillschweigend. Das defensive Muster ist, Schriften aus Dateien zu laden, die Ihr Installer kontrolliert, statt dem OS-Schriftenverzeichnis zu vertrauen, und genau das erledigt HotPDFs Unicode-Registrierungsaufruf:
Pdf.RegisterUnicodeTTF('C:\ProgramData\MyApp\Fonts\NotoSans.ttf');
Pdf.CurrentPage.SetFont('NotoSans', [], 12);
Pdf.CurrentPage.TextOut(50, 700, 0, WideString('Łódź — Ünïcode test ✓'));
Beachten Sie, dass TextOut direkt einen WideString annimmt. Kundendaten, die irgendetwas jenseits der lokalen Codepage enthalten, also praktisch alle Kundendaten, laufen damit durch denselben Aufruf wie ASCII-Rahmentext, sofern die ausgewählte Schrift die Glyphen abdeckt. Eingebettete Unicode-Schriften setzen außerdem PDF 1.5 oder neuer voraus, behalten Sie diese Versionsuntergrenze also im Blick, falls eine andere Anforderung Sie an eine ältere Version bindet. Für Schriftsysteme, die mehr als Glyph-Lookup benötigen, besonders Arabisch und Hebräisch, beschreibt unser Artikel zu Complex-Script-Text-Shaping mit HotPDF die dafür vorgesehene Rechts-nach-links-Pipeline.
Für den seltenen Fall, dass keine Schriftdatei das Gewünschte darstellen kann, etwa MICR-ähnliche Markierungen oder proprietäre Symbole, unterstützt HotPDF Type-3-Schriften über RegisterType3Font und AddType3Glyph, wobei jede Glyphe ein kleiner Content Stream ist, den Sie definieren. Das ist ein Spezialwerkzeug, aber besser als Symbolgrafiken als hunderte winzige Bilder auszuliefern.
Bilder: die mittleren Argumente sind Breite und Höhe, keine Ecke
HotPDF trennt Bildregistrierung und Platzierung. AddImage nimmt ein TBitmap oder TJPEGImage einmal auf — PNG-Grafiken vorher in ein Bitmap dekodieren — und gibt einen Index zurück; ShowImage platziert diesen Index beliebig oft. Die Signatur ist der Teil, den man zweimal lesen sollte:
var
Png: TPngImage;
Logo: TBitmap;
LogoIdx: Integer;
begin
Png := TPngImage.Create;
Logo := TBitmap.Create;
try
Png.LoadFromFile('brand-logo.png');
Logo.Assign(Png); // decode PNG to a bitmap
LogoIdx := Pdf.AddImage(Logo, icFlate); // lossless for flat-color art
finally
Logo.Free;
Png.Free;
end;
// (Index, X, Y, Width, Height, Angle) — not (X1, Y1, X2, Y2)
Pdf.CurrentPage.ShowImage(LogoIdx, 50, 700, 120, 40, 0);
end;
Die beiden Zahlen nach der Position sind Breite und Höhe, nicht die gegenüberliegende Ecke, und das letzte Argument ist ein Rotationswinkel. Code, der von X1/Y1/X2/Y2 ausgeht, zieht Logos über den Großteil der Seite, ein Fehler, der in der Ausgabe offensichtlich und im Quelltext rätselhaft wirkt. Verwandt damit: KeepImageAspectRatio steht standardmäßig auf True, daher erzeugt eine unpassende Box Letterboxing statt Verzerrung; setzen Sie es nur dann auf False, wenn Strecken wirklich beabsichtigt ist.
Die Trennung von Registrierung und Platzierung ist auch für Performance und Dateigröße wichtig: AddImage bettet die Bitmap-Daten einmal ein, und jedes ShowImage mit demselben Index nutzt dieses eine eingebettete Objekt wieder. Ein 500-seitiger Kontoauszugslauf, der für dasselbe Logo pro Seite AddImage aufruft, bettet das Logo 500-mal ein; derselbe Lauf mit einmaliger Registrierung und wiederverwendetem Index bettet es einmal ein. Cachen Sie die Indizes in einem kleinen Dictionary nach Asset-Pfad, und das Problem tritt nicht auf.
Die Dateigröße wird ebenfalls hier entschieden. Fotografische Inhalte sollten über JPEG-Encoding laufen — übergeben Sie icJpeg an AddImage und setzen Sie JpegQuality auf etwa 85, da die Eigenschaft standardmäßig 100 ist —, was für gescannte Anhänge und Fotos visuell sauber bleibt und nur einen Bruchteil verlustloser Größe benötigt. Behalten Sie PNG für flächige Grafiken wie Logos und Diagramme, bei denen JPEG-Ringing sichtbar ist und Flate-Kompression bereits effizient arbeitet. Ein Kontoauszugslauf, der pro Seite ein Foto mit falschen Einstellungen einbettet, verschickt Gigabytes; derselbe Lauf mit JPEG 85 verschickt ein Zehntel davon, ohne dass jemandes Augen Einwände haben.
Linien, Kästen und Schattierungen mit Pfadprimitiven
Tabellenlinien und Summenkästen brauchen überhaupt keine Bilder; Vektorprimitive liefern bei jedem Zoom schärfere Ausgabe und kosten nahezu nichts in der Dateigröße. Das Modell ist Pfadkonstruktion gefolgt von einem Paint-Operator:
// Horizontal rule under the table header
Pdf.CurrentPage.SetLineWidth(0.75);
Pdf.CurrentPage.MoveTo(50, 660);
Pdf.CurrentPage.LineTo(545, 660);
Pdf.CurrentPage.Stroke;
// Shaded totals box: X, Y, width, height
Pdf.CurrentPage.SetRGBFillColor(RGB(235, 235, 235));
Pdf.CurrentPage.Rectangle(395, 120, 150, 40);
Pdf.CurrentPage.Fill;
Die Reihenfolgedisziplin ist dieselbe wie in rohen PDF-Content-Streams: Paint State setzen, Pfad bauen, dann Stroke oder Fill aufrufen. Ein Pfad, der nie gemalt wird, verschwindet schlicht, was die übliche Erklärung ist, wenn eine Linie „nicht auftaucht“. SetRGBFillColor nimmt ein einzelnes TColor, daher funktionieren VCL-Konstanten — clNavy, clBlack — direkt, und Rectangle folgt derselben Breite-und-Höhe-Konvention wie die Bildplatzierung. Haarlinien verdienen eine Warnung: Linienstärken unter ungefähr einem halben Punkt wirken am Bildschirm elegant und können auf einem 600 dpi-Bürodrucker vollständig ausfallen, daher ist 0.75pt ein sinnvoller Boden für Tabellenlinien, die Papier überstehen müssen.
Paginierung gegen echte Daten, nicht gegen Beispieldaten
Numerische Spalten zeigen eine weitere Gewohnheit, die früh aufgebaut werden sollte: Richten Sie Beträge an ihrer rechten Kante aus, indem Sie die X-Position aus der rechten Spaltengrenze und der gerenderten Breite jedes Werts berechnen, statt Zeichenketten mit Leerzeichen aufzufüllen. Leerzeichen funktionieren nur in Monospace-Schriften, und Finanzberichte werden nie in Monospace-Schriften gesetzt. Formatieren Sie Werte mit Delphis locale-bewussten Routinen wie FormatFloat, bevor Sie messen, damit genau das Tausendertrennzeichen, das die Locale des Kunden erwartet, auch in seiner Breite gemessen wurde.
Der Demo-Datensatz hat zehn kurze Zeilen; in Produktion gibt es einen Kunden, dessen Firmenname 140 Zeichen lang ist, und einen Auszug mit 4,000 Positionen. Eine robuste Berichtsschleife führt einen Y-Cursor nach unten, zieht die Höhe jeder Zeile ab und bricht auf eine neue Seite um, wenn der Cursor den unteren Rand schneiden würde, wobei „nach unten“ in diesem Koordinatensystem abnehmendes Y bedeutet. Legen Sie die Seitenumbruchbehandlung an eine Stelle, führen Sie darin SetFont erneut aus und zeichnen Sie den laufenden Kopf neu; dann verschwinden Ein-Seiten-Versatzfehler. Wenn Ihre Berichte auch Archivierungs- oder Barrierefreiheitsanforderungen erfüllen müssen, sind die hier getroffenen Generierungsentscheidungen, eingebettete Schriften, getaggte Ausgabe, Farbräume, genau das, was die Standards beschränken; lesen Sie den HotPDF-Leitfaden zu PDF/A, PDF/X und PDF/UA, bevor das Template festgeschrieben wird.
FAQ
Warum rendert mein Text am unteren Seitenrand?
Der PDF-Ursprung liegt links unten, und Y wächst nach oben. Konvertieren Sie Positionen relativ zur oberen Kante mit PageHeight - Offset, oder entwerfen Sie Ihren Layoutcode von Anfang an um den Ursprung links unten.
Warum ist die Schrift auf Seite 2 falsch, auf Seite 1 aber korrekt?
Die Schriftauswahl wird nicht über Seiten hinweg übernommen, und AddPage setzt CurrentPage auf die neue Seite. SetFont muss nach jedem AddPage vor dem ersten TextOut erneut aufgerufen werden.
Wie halte ich die Dateigröße bei vielen eingebetteten Fotos vernünftig?
Übergeben Sie icJpeg an AddImage und setzen Sie JpegQuality für fotografische Inhalte in die Nähe von 85; reservieren Sie verlustloses icFlate für flächige Logos und Strichgrafik. Registrieren Sie jedes unterschiedliche Bild einmal mit AddImage und verwenden Sie den Index wieder.
Produktreferenz
Jeder Aufruf in diesem Artikel wird mit der HotPDF Component für Delphi und C++Builder ausgeliefert, die die vollständige Text-, Schrift-, Bild- und Zeichen-API zusammen mit den Formular-, Verschlüsselungs- und Signierfunktionen dokumentiert, die an anderer Stelle in diesem Blog behandelt werden.