Das Ticket war drei Zeilen lang: „Die Vorschau zeigt das Formular zentriert. Die gedruckte Seite ist nach oben links verschoben, und der Rand ist auf zwei Seiten abgeschnitten.“ Mit dem Rendering-Code war nichts falsch. Die Vorschau zeichnete die Seite relativ zum Papierbogen, während der Ursprung des Drucker-Device-Contexts an der Ecke des bedruckbaren Bereichs liegt — und der betreffende Laserdrucker die äußeren 4,2 mm des Blatts nicht erreichen konnte. Jede Delphi-Druckfunktion kollidiert irgendwann mit diesem Unterschied, und der günstige Zeitpunkt, ihn zu behandeln, liegt vor dem ersten Kunden, der ein Formular mit Rahmen druckt. losLab PDF Library (PDFlibPas) deckt den gesamten Pfad mit Device-Context-Renderingaufrufen, einer virtuellen Druckerkonfigurationsschicht und Vorschaubitmaps ab, die aus den Metriken des Druckers selbst erzeugt werden.
Papiergeometrie ist nicht Druckgeometrie
Drei Rechtecke beschreiben jedes Druckziel, und sie zu verwechseln erzeugt genau das Ticket oben. Das Papierrechteck ist der physische Bogen. Das bedruckbare Rechteck ist der Bereich, den die Druckmechanik erreichen kann. Der Offset zwischen ihren Ursprüngen ist der Hardware-Rand, und er unterscheidet sich pro Druckermodell und manchmal pro Fach. Die Druckschicht der Bibliothek modelliert alle drei: Die zugrunde liegende Klasse TPLPrinter legt PageWidth und PageHeight für den bedruckbaren Bereich offen, FullPageWidth und FullPageHeight für den Bogen sowie PrintOffsetX und PrintOffsetY für den Abstand, alles in Gerätepixeln bei der von GetDPI gemeldeten Auflösung. Eine ehrliche Vorschau skaliert dieselben drei Rechtecke auf Bildschirmauflösung herunter, statt die Seite in irgendein Rechteck zu malen, das das Control gerade besitzt.
Bildschirmvorschau über RenderPageToDC
Für ein On-Screen-Vorschaucontrol zeichnet RenderPageToDC(DPI, Page, DC) eine Seite des geladenen Dokuments direkt auf jeden GDI-Device-Context — Canvas eines TPaintBox, Offscreen-Bitmap oder Metafile-DC. Das DPI-Argument setzt den Zoom: 96 entspricht auf einem klassischen Display ungefähr 100%, und eine Verdopplung verdoppelt die gerenderte Größe.
procedure TPreviewForm.PreviewBoxPaint(Sender: TObject);
begin
// these three are sticky library state, not per-call parameters:
FPdf.SetRenderDCOffset(FOffsetX, FOffsetY);
FPdf.SetRenderDCErasePage(1);
FPdf.SetRenderCropType(0);
FPdf.RenderPageToDC(FPreviewDpi, FCurrentPage, PreviewBox.Canvas.Handle);
end;
Die Falle ist, dass der DC-Renderpfad von persistierendem Bibliothekszustand statt von Per-Call-Parametern gesteuert wird. SetRenderDCOffset, SetRenderDCErasePage und SetRenderCropType bleiben jeweils gültig, bis sie geändert werden. Eine Thumbnail-Schleife, die nach einer vom Benutzer angepassten Zoomansicht läuft, erbt also jeden Offset oder Crop, den der vorherige Codepfad stehen ließ — und das Symptom ist eine Vorschau, die nur in bestimmten Navigationssequenzen driftet, was elend zu reproduzieren ist. Allen relevanten Zustand wie oben am Anfang des Paint-Handlers zu setzen, kostet nichts und entfernt die ganze Bugklasse. Ein zweiter Multiplikator liegt daneben: Die effektive Ausgabeauflösung ist Render Scale mal DPI-Argument, und SetRenderScale steht standardmäßig auf 1.0, bleibt nach Änderung aber erhalten, sodass ein Exportfeature, das ihn angepasst hat, jede spätere Vorschau still neu skaliert.
Scrollende Viewer und Teilrepaints haben eine eigene Variante: RenderPageToDCClip nimmt eine Clip-Spezifikation zusammen mit dem Device Context, sodass die Invalidierung eines Fensterbands nur dieses Band neu malt, statt die ganze Seite erneut zu rastern. Bei hohem Zoom auf großformatigen Seiten ist dieser Unterschied die Linie zwischen einem Viewer, der dem Scrollbalken folgt, und einem, der hinterher schmiert.
Ein Druckjob, der zur Vorschau passt
Die Druckseite arbeitet über einen virtuellen Drucker: NewCustomPrinter klont einen Systemdrucker in eine bibliotheksprivate Konfiguration, und SetupPrinter passt diesen Klon an — Papier mit Einstellung 1 (eine DMPAPER_*-Konstante) und Orientierung mit Einstellung 11 —, ohne den maschinenweiten DevMode zu berühren. Ein Dienst kann A4-Etiketten drucken, während der Standarddrucker des Hosts auf Letter bleibt, und danach muss nichts wiederhergestellt werden.
var
Pdf: TPDFlib;
Virt: WideString;
Opt: Integer;
begin
Pdf := TPDFlib.Create;
try
if Pdf.LoadFromFile('report.pdf', '') <> 1 then
raise Exception.Create('load failed');
Virt := Pdf.NewCustomPrinter(Pdf.GetDefaultPrinterName);
Pdf.SetupPrinter(Virt, 1, 9); // setting 1 = paper, DMPAPER_A4
Pdf.SetupPrinter(Virt, 11, 1); // setting 11 = orientation, 1 = portrait
Opt := Pdf.PrintOptions(1, 1, 'Monthly Report'); // fit to paper, auto-rotate + center
Pdf.PrintDocument(Virt, 1, Pdf.PageCount, Opt);
finally
Pdf.Free;
end;
end;
PrintOptions verdient genaues Lesen: Es gibt ein Options-Handle zurück, das an PrintDocument oder PrintPages übergeben werden muss. Es ist kein ambienter Zustand. Optionen zu bauen und das Handle nicht zu übergeben ist ein stiller Fehler — der Job druckt mit Defaults, und niemand bemerkt es, bis Fit-to-Paper erwartet wurde und eine übergroße Seite abgeschnitten herauskommt. Das Page-Scaling-Argument trägt die Policy: Keine Skalierung erhält Maßhaltigkeit für Formulare, die mit Linealen gemessen werden; Fit-to-Paper skaliert alles; Shrink-Large-Pages greift nur ein, wenn eine Seite den bedruckbaren Bereich überschreitet — meist die richtige Vorgabe für gemischte Dokumentensätze. Das Auto-Rotate-and-Center-Flag behandelt Querformatseiten ohne separaten Codepfad.
Anwendungen, die bereits einen TPrinter über den VCL-Dialogfluss verwalten, können ihn direkt übergeben: PrintDocumentToPrinterObject und PrintPagesToPrinterObject akzeptieren die konfigurierte TPrinter-Instanz. So bleibt der Standarddruckdialog die benutzerseitige Konfigurationsoberfläche, während die Bibliothek das Seitenrendering übernimmt. Die beiden Ansätze mischen sich schlecht in einem Codepfad — wählen Sie den virtuellen Drucker für unbeaufsichtigte Dienste und die TPrinter-Route für interaktive Anwendungen, dann bleibt der Geometrievertrag eindeutig.
Unbeaufsichtigte Umgebungen bekommen Ausgabe auch ohne physisches Gerät: Die Page-Range-Druckaufrufe besitzen Print-to-File-Varianten, die praktische Antwort für Regressionstests der Druckgeometrie auf einem Buildserver ohne Treiberwarteschlange. Rendern Sie dasselbe Dokument mit denselben Optionen bei jedem Build in ein Dateiartefakt, und eine Geometrie-Regression wird zum Diff statt zum Kundenbericht.
Vorschaubitmaps mit den Metriken des Druckers selbst
Eine bei 96 DPI gegen eine angenommene Seitengröße gerenderte Vorschau beantwortet die falsche Frage. GetPrintPreviewBitmapToString baut die Vorschau mit demselben Custom Printer und demselben Options-Handle wie der spätere Job, also wirken Papiergröße, Orientierung, Skalierungspolitik, Rotation und Hardware-Offset mit — zurück kommt, was der Bogen zeigen wird.
procedure ShowPrinterTruePreview(Pdf: TPDFlib; const Virt: WideString; Opt: Integer);
var
Data: AnsiString;
Strm: TMemoryStream;
Bmp: TBitmap;
begin
Data := Pdf.GetPrintPreviewBitmapToString(Virt, 1, Opt, 1200, 0);
Strm := TMemoryStream.Create;
try
Strm.WriteBuffer(PAnsiChar(Data)^, Length(Data));
Strm.Position := 0;
Bmp := TBitmap.Create;
try
Bmp.LoadFromStream(Strm);
PreviewImage.Picture.Assign(Bmp);
finally
Bmp.Free;
end;
finally
Strm.Free;
end;
end;
Das MaxDimension-Argument begrenzt die lange Kante der Bitmap: 1200 Pixel sind für einen Vorschaudialog angenehm scharf und halten den Speicher selbst bei E-Size-Engineering-Zeichnungen moderat, wo ein Vollauflösungsrendering bei 600 DPI in Gigabytes laufen würde.
Druckerauswahlen des Benutzers merken
Druckdialoge, die ihre Einstellungen zwischen Sitzungen vergessen, erzeugen eigene Supporttickets. Das DevMode-Paar — GetPrinterDevModeToString und SetPrinterDevModeFromString — serialisiert die vollständige Treiberkonfiguration eines Druckers in einen opaken String, den Sie in Benutzereinstellungen speichern und in der nächsten Sitzung wiederherstellen können, einschließlich treiberspezifischer Optionen, die keine generische API modelliert. Persistieren Sie den Drucker über den Namen aus GetPrinterNames, nie über den Listenindex: Die Reihenfolge ändert sich bei jedem hinzugefügten oder entfernten Drucker, und GetDefaultPrinterName deckt den Fallback ab, wenn das gemerkte Gerät verschwunden ist.
Die Fachauswahl rundet die Persistenzgeschichte ab: GetPrinterBins meldet die Papierquellen, die ein Treiber offenlegt. Das zählt für Briefkopf-Workflows, bei denen Seite eins aus dem Briefkopffach und der Rest aus Normalpapier kommt — eine Policy, die Benutzer zusammen mit allem anderen gemerkt erwarten.
Fragen aus Druckprojekten
Warum ist die gedruckte Seite relativ zu meiner Vorschau verschoben?
Fast immer wegen des Hardware-Rands: Der Ursprung des Drucker-DC ist die Ecke des bedruckbaren Bereichs, nicht die Papierecke. Modellieren Sie den Offset explizit in der Vorschau oder erzeugen Sie Vorschauen mit GetPrintPreviewBitmapToString, damit die Druckergeometrie eingebacken ist.
Wie drucke ich eine Seitenauswahl wie 2-5 und 12?
PrintPages akzeptiert einen Bereichsstring — übergeben Sie den Namen des virtuellen Druckers, '2-5,12' und das Options-Handle. Dieselbe Bereichssyntax treibt die Print-to-File-Varianten.
Können Vorschau und Druckjob unterschiedliche Rendering-Engines verwenden?
Sie können, sollten es aber nicht: Die Engine-Auswahl gilt für Bildschirm- und Druckziele, und das Mischen von Engines führt genau den Fidelity-Drift wieder ein, den eine druckergetreue Vorschau beseitigt. Die Abwägungen zwischen Built-in, Cairo und PDFium werden in Multi-Engine-PDF-Rendering in Delphi behandelt.
Dokumente, die vor dem Drucken zu groß für bequemes Laden sind, können über den Direct-Access-Pfad aus Large PDF Merge, Split und Direct Access geöffnet werden, der Seiten aus einem Datei-Handle auf einen Device Context rendert, ohne den Dokumentbaum aufzubauen. Die vollständige Druck-API-Referenz steht auf der Produktseite losLab PDF Library for Delphi.