Einen Bericht zu erzeugen läuft darauf hinaus, drei Dinge auf einer Seite zu platzieren und sie darüber einig werden zu lassen, wo sie sitzen: Text an bekannten Koordinaten, Schriften, die auf dem Server genauso gerendert werden wie auf Ihrem Desktop, und passend dimensionierte Bilder. Alles andere, was eine Berichtsbibliothek tut, ist um diese drei herum angeordnet. HotPDF, die PDF-Erzeugungsbibliothek von losLab für Delphi und C++Builder, bietet Ihnen jedes davon als direkten Aufruf am Seitenobjekt, und die einzige echte Reibung ist das darunterliegende Koordinatensystem, das in die entgegengesetzte Richtung läuft wie der VCL-Canvas, den Sie gewohnt sind. Klären Sie diese Orientierung zuerst, und der Rest der Layoutarbeit hört auf, sich gegen Sie zu wehren
Textplatzierung und der Ursprung unten links
Fast jedermanns erster Bericht kommt auf dem Kopf heraus. Der Titel landet nahe der Unterkante, und jede Zeile darunter klettert nach oben. Nichts funktioniert falsch. Der PDF-Benutzerraum, definiert in ISO 32000-1 §8.3, legt den Ursprung in die untere linke Ecke mit nach oben wachsendem Y, was das Spiegelbild des GDI-Canvas ist, bei dem Y von oben links nach unten wächst. Fünf Minuten, die man damit verbringt, seinen Frieden damit zu schließen, ersparen ein Layout, das man sonst neu schreiben würde, sobald die Zahlen keinen Sinn mehr ergeben
Der zentrale Aufruf des Seitenobjekts ist TextOut(X, Y, Angle, Text). X und Y positionieren den Text in Punkt von der unteren linken Ecke, und Angle dreht ihn in Grad, womit sich ein diagonaler DRAFT- oder COPY-Stempel ohne jede Sonderbehandlung zeichnen lässt. Der Trick, mit dem VCL-geschulte Intuition weiter funktioniert, besteht darin, Y als Seitenhöhe minus dem gewünschten Abstand vom oberen Rand auszudrücken:
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'); // 50 pt vom oberen Rand einer Letter-Seite
Pdf.CurrentPage.SetFont('Arial', [], 10);
Pdf.CurrentPage.TextOut(50, 792 - 70, 0, 'Date: 2026-06-11');
Pdf.CurrentPage.TextOut(300, 400, 45, 'COPY'); // gedrehter Stempel
Pdf.AddPage; // CurrentPage zeigt jetzt hierher
Pdf.CurrentPage.SetFont('Arial', [], 10); // der Schriftzustand wird nicht übernommen
Pdf.CurrentPage.TextOut(50, 742, 0, 'Page 2 detail rows');
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
Die beiden zustandsbehafteten Verhaltensweisen in diesem Listing sind für die meisten Fehler verantwortlich, die erst auf Seite zwei auftreten. AddPage richtet CurrentPage auf die gerade erstellte Seite aus, sodass eine zuvor zwischengespeicherte Seitenreferenz nicht mehr dort zeichnet, wo Sie es erwarten. Auch die Schriftauswahl gilt pro Seite und nicht pro Dokument. Wenn Sie das SetFont nach einem AddPage auslassen, fällt das erste TextOut auf der frischen Seite auf den Standard zurück, mit dem die Seite begonnen hat, nicht auf die fette Überschriftenschrift, die Sie drei Seiten zuvor gesetzt haben. Die sichere Gewohnheit besteht darin, „neue Seite beginnen“ und „Textzustand wiederherstellen“ als einen untrennbaren Schritt in der Berichtsschleife zu behandeln
Schriften, die auf dem Server existieren, nicht nur auf Ihrem Desktop
Die meisten Schriftprobleme sind in Wirklichkeit verkleidete Deployment-Probleme. Auf Ihrem Entwicklungsrechner ist die Unternehmensschrift installiert, also sieht der Bericht auf Ihrem Bildschirm richtig aus und wird ausgeliefert. Der Produktionshost führt den Auftrag unter einem Dienstkonto aus, für das diese Schrift nie installiert wurde, der Renderer ersetzt sie still durch etwas, das er finden kann, und das Erste, was jemand davon hört, ist ein Kunde, der fragt, warum sich der Briefkopf geändert hat. Der Ausweg besteht darin, dem Schriftverzeichnis des Betriebssystems nicht mehr zu vertrauen und die Schrift aus einer Datei zu laden, die Ihr Installer auf die Platte legt. Der Unicode-Registrierungsaufruf von HotPDF nimmt einen Pfad entgegen und tut genau das:
Pdf.RegisterUnicodeTTF('C:\ProgramData\MyApp\Fonts\NotoSans.ttf');
Pdf.CurrentPage.SetFont('NotoSans', [], 12);
Pdf.CurrentPage.TextOut(50, 700, 0, WideString('Łódź - Ünïcode test ✓'));
TextOut akzeptiert direkt einen WideString, was wichtiger ist, als es zunächst aussieht. Ein Kundenname mit Akzent, eine deutsche Straße, eine polnische Stadt: Das sind keine Randfälle, sondern der normale Inhalt einer Kundentabelle, und sie laufen durch denselben Aufruf wie die ASCII-Beschriftungen, die Sie fest codieren, solange die registrierte Schrift die Glyphen tatsächlich enthält. Eine Versionsbeschränkung geht mit eingebetteten Schriften einher: Das Dokument muss PDF 1.5 oder neuer sein, sodass, wenn eine unabhängige Anforderung Sie an eine ältere Version bindet, genau das still brechen wird. Von rechts nach links laufende Schriftsysteme wie Arabisch und Hebräisch brauchen echtes Shaping statt eines direkten Glyphen-Lookups, und das hat seine eigene Pipeline; siehe unseren Artikel über Text-Shaping für komplexe Schriftsysteme mit HotPDF
Wenn keine installierte Schrift ausdrücken kann, was Sie brauchen, denken Sie an MICR-Zeichen auf einem Scheck oder einen proprietären Symbolsatz, füllen Type-3-Schriften die Lücke. Sie definieren jede Glyphe als kleinen Content-Stream über RegisterType3Font und AddType3Glyph. Es ist eine spezialisierte Ecke der API, nach der Sie selten greifen werden, aber sie ist weit sauberer, als Hunderte winziger Symbol-Bitmaps über eine Seite zu verstreuen
Bilder: Die mittleren Argumente sind Breite und Höhe, keine Ecke
Die Bildbehandlung teilt sich in zwei Schritte, und sie getrennt zu halten ist der ganze Sinn. AddImage nimmt ein TBitmap oder TJPEGImage entgegen, bettet es einmal ein und gibt einen Index zurück. PNG-Grafiken müssen vorher in eine Bitmap decodiert werden. ShowImage zeichnet diesen Index dann, wo und wie oft Sie möchten. Die Argumentreihenfolge von ShowImage ist die eine Stelle, an der es sich lohnt, langsamer zu lesen:
var
Png: TPngImage;
Logo: TBitmap;
LogoIdx: Integer;
begin
Png := TPngImage.Create;
Logo := TBitmap.Create;
try
Png.LoadFromFile('brand-logo.png');
Logo.Assign(Png); // PNG in eine Bitmap decodieren
LogoIdx := Pdf.AddImage(Logo, icFlate); // verlustfrei für flächige Grafiken
finally
Logo.Free;
Png.Free;
end;
// (Index, X, Y, Width, Height, Angle): nicht (X1, Y1, X2, Y2)
Pdf.CurrentPage.ShowImage(LogoIdx, 50, 700, 120, 40, 0);
end;
Die beiden Zahlen nach der Position sind eine Breite und eine Höhe. Sie sind nicht die Koordinaten der gegenüberliegenden Ecke, und das letzte Argument ist ein Drehwinkel in Grad. Liest man die Signatur als X1/Y1/X2/Y2-Box, erstreckt sich ein 120 mal 40 großes Logo, das bei (50, 700) platziert wird, stattdessen von dort bis (120, 40) und breitet sich über den Großteil der Seite aus. Die Ausgabe macht den Fehler offensichtlich, während der Quellcode völlig vernünftig aussieht, und genau das lässt einen Nachmittag verstreichen. KeepImageAspectRatio ist standardmäßig True, sodass eine Box mit falschen Proportionen das Bild mit Rändern einpasst, statt es zu verzerren; setzen Sie es nur dann auf False, wenn Sie tatsächlich strecken möchten
Die Trennung zwischen Registrieren und Platzieren zahlt sich bei langen Läufen aus. Weil AddImage die Pixel einmal einbettet und jedes ShowImage mit diesem Index auf dasselbe eingebettete Objekt zurückzeigt, entscheidet der Ort, an dem Sie AddImage aufrufen, über die Dateigröße. Rufen Sie es innerhalb der Seitenschleife für einen 500-seitigen Kontoauszug auf, und dasselbe Logo wird 500-mal eingebettet. Rufen Sie es einmal vor der Schleife auf, behalten Sie den Index, und das Logo wird ein einziges Mal gespeichert. Ein kleines Dictionary mit dem Asset-Pfad als Schlüssel reicht aus, um sicherzustellen, dass jedes unterschiedliche Bild genau einmal registriert wird
Die Wahl des Codecs ist der andere Größenhebel. Fotografischer Inhalt, gescannte Anhänge und dergleichen gehören in JPEG: Übergeben Sie icJpeg an AddImage und senken Sie JpegQuality auf etwa 85, denn die Eigenschaft beginnt bei 100, und der Unterschied bei 85 ist auf einer gedruckten Seite unsichtbar. Flächige Grafiken wie Logos, Diagramme und Strichzeichnungen gehören in icFlate, wo verlustfreie Kompression bereits kompakt ist und JPEG sichtbare Artefakte um die harten Kanten schmieren würde. Ein Auszugslauf, der auf jede Seite ein Foto in voller Qualität drückt, kann auf Gigabytes anschwellen; derselbe Inhalt bei JPEG 85 landet bei etwa einem Zehntel der Größe, und kein Leser bemerkt es
Linien, Boxen und Schattierung mit Pfadprimitiven
Die horizontale Linie unter einem Tabellenkopf und die graue Box hinter einer Summenzahl müssen keine Bilder sein. Zeichnen Sie sie als Vektoren, und sie bleiben bei jeder Zoomstufe scharf, drucken sauber und fügen der Datei fast nichts hinzu. HotPDF folgt demselben Modell wie rohe PDF-Content-Streams: einen Pfad aufbauen und dann einen Operator aufrufen, der ihn malt
// Horizontale Linie unter dem Tabellenkopf
Pdf.CurrentPage.SetLineWidth(0.75);
Pdf.CurrentPage.MoveTo(50, 660);
Pdf.CurrentPage.LineTo(545, 660);
Pdf.CurrentPage.Stroke;
// Schattierte Summenbox: X, Y, Breite, Höhe
Pdf.CurrentPage.SetRGBFillColor(RGB(235, 235, 235));
Pdf.CurrentPage.Rectangle(395, 120, 150, 40);
Pdf.CurrentPage.Fill;
Die Reihenfolge ist nicht optional: den Malzustand setzen, den Pfad konstruieren, dann Stroke oder Fill aufrufen. Ein Pfad, den Sie aufbauen, aber nie malen, trägt nichts zur Seite bei, was fast immer die Antwort ist, wenn eine Linie „nicht angezeigt wird“. SetRGBFillColor nimmt einen einzelnen TColor entgegen, sodass die vertrauten VCL-Konstanten wie clNavy und clBlack direkt passen, und Rectangle verwendet dieselben Breiten- und Höhenargumente wie die Bildplatzierung statt zweier Ecken. Eine Warnung zu dünnen Linien: Alles unter etwa einem halben Punkt kann auf einem Monitor elegant aussehen und dann auf einem 600-dpi-Bürodrucker verschwinden, sodass 0,75 pt eine vernünftige Untergrenze für jede Linie ist, die den Druck überstehen muss
Seitenumbruch gegen echte Daten, nicht gegen Beispieldaten
Ein Detail, das man richtig hinbekommen sollte, bevor das Layout sich verfestigt: Numerische Spalten sollten an ihrer rechten Kante ausgerichtet werden, und der Weg dazu besteht darin, die gerenderte Breite jedes Werts zu messen und ihn von der Spaltengrenze aus zurück zu positionieren, nicht die Zeichenkette mit führenden Leerzeichen aufzufüllen. Auffüllen mit Leerzeichen richtet sich nur in einer Festbreitenschrift aus, und niemand setzt einen Finanzbericht in einer Festbreitenschrift. Lassen Sie die Werte zuerst durch Delphis gebietsschemaabhängige Routinen wie FormatFloat laufen, damit das Tausendertrennzeichen, dessen Breite Sie messen, dasselbe ist, das das Gebietsschema des Kunden tatsächlich anzeigt
Die Gefahr beim Seitenumbruch besteht darin, dass Sie ihn gegen den Demo-Datensatz schreiben, bei dem zehn kurze Zeilen auf eine Seite passen und die Schleife nie umbrechen muss. Die Produktion liefert Ihnen einen Kunden, dessen Firmenname 140 Zeichen lang ist, und einen Auszug mit 4.000 Positionen, und jetzt muss die Schleife jedes Mal korrekt umbrechen. Das Muster, das standhält, ist ein einzelner Y-Cursor, der sich nach unten bewegt, während Sie die Höhe jeder Zeile abziehen, und eine Prüfung, die in dem Moment eine neue Seite beginnt, in dem der Cursor den unteren Rand überschreiten würde. Nach unten bedeutet hier abnehmendes Y, was die eine Stelle ist, an der der Ursprung unten links kontraintuitiv bleibt. Halten Sie all das in einer Routine, die auf der neuen Seite auch SetFont erneut ausgibt und die laufende Kopfzeile neu zeichnet, und die Um-eine-Seite-daneben-Fehler fassen nie Fuß. Wenn dieselben Berichte auch Archivierungs- oder Barrierefreiheitsregeln erfüllen müssen, sind die Entscheidungen, die Sie genau hier treffen, welche Schriften Sie einbetten, ob die Ausgabe getaggt ist, welche Farbräume Sie verwenden, diejenigen, die diese Standards überwachen; der HotPDF-Leitfaden zu PDF/A, PDF/X und PDF/UA lohnt sich zu lesen, bevor die Vorlage sich verfestigt
Jeder hier gezeigte Aufruf, die Textpositionierung, Schriftregistrierung, Bildeinbettung und das Pfadzeichnen, wird mit der HotPDF Delphi Component für Delphi und C++Builder ausgeliefert, deren Referenz die vollständige Ausgabe-API zusammen mit den Formular-, Verschlüsselungs- und Signierfunktionen dokumentiert, neben denen sie steht