Eine PDF-Seite für die Druckvorschau auf einen Windows-Device-Context zu rendern, bringt drei Koordinatensysteme in dieselbe Codezeile, und sie sind sich selten einig. Die PDF-Seite wird in Punkten gemessen, mit dem Ursprung unten links. Der Bildschirm-DC wird in Pixeln gemessen, mit dem Ursprung oben links und einem Zoomfaktor, den Sie selbst wählen. Der Drucker-DC, den die Vorschau vorhersagen soll, misst Pixel in der Geräteauflösung, setzt seinen Ursprung aber an die Ecke des bedruckbaren Bereichs und nicht an die Ecke des Blatts. Stimmt auch nur eines davon nicht, sieht die Vorschau gut aus, während die gedruckte Seite verschoben, skaliert oder an einer Kante beschnitten herauskommt. Das übliche Symptom ist ein umrandetes Formular, das in der Vorschau zentriert erscheint und dann mit abgeschnittener oberer und linker Linie gedruckt wird, weil der Laserdrucker die äußersten Millimeter nicht bedrucken kann und niemand der Vorschau davon erzählt hat. losLab PDF Library (PDF Library for Delphi) deckt den gesamten Weg ab, mit Renderaufrufen auf Device Contexts, einer Konfigurationsschicht für virtuelle Drucker und Vorschau-Bitmaps aus den Metriken des Druckers selbst, und genau das macht die Vorschau bei diesem Rand ehrlich
Papiergeometrie ist nicht bedruckbare Geometrie
Zwei Rechtecke beschreiben jedes Druckziel, und im Versatz zwischen ihnen stecken die meisten Vorschaufehler. Das Papierrechteck ist das physische Blatt. Das bedruckbare Rechteck ist der kleinere Bereich, den die Druck-Engine tatsächlich erreichen kann, eingerückt um einen Hardwarerand, der je nach Druckermodell und manchmal je nach Schacht unterschiedlich ausfällt. Die Druckschicht der Bibliothek misst beides. Die zugrunde liegende Klasse TPLPrinter stellt PageWidth und PageHeight für den bedruckbaren Bereich bereit, FullPageWidth und FullPageHeight für das ganze Blatt sowie PrintOffsetX und PrintOffsetY für den Abstand zwischen ihren Ursprüngen, alles in Gerätepixeln bei der Auflösung, die GetDPI meldet. Eine ehrliche Vorschau rechnet genau diese Zahlen auf die Bildschirmauflösung herunter, statt die Seite in irgendein Rechteck zu malen, das das Steuerelement gerade hat. Lassen Sie diesen Schritt aus, nimmt die Vorschau stillschweigend einen Rand von null an, und das ist der eine Wert, den kein echter Drucker verwendet
Bildschirmvorschau über RenderPageToDC
Für ein Vorschau-Steuerelement auf dem Bildschirm zeichnet RenderPageToDC(DPI, Page, DC) eine Seite des geladenen Dokuments direkt auf einen beliebigen GDI-Device-Context, sei es eine TPaintBox-Canvas, eine Bitmap im Speicher oder ein Metafile-DC. Das Argument DPI bestimmt den Zoom. 96 entspricht ungefähr einer 100-Prozent-Ansicht auf einem klassischen Display, und eine Verdoppelung verdoppelt die gerenderte Größe
procedure TPreviewForm.PreviewBoxPaint(Sender: TObject);
begin
// diese drei sind persistenter Bibliothekszustand, keine Parameter pro Aufruf:
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 persistentem Bibliothekszustand gesteuert wird und nicht von Parametern pro Aufruf. SetRenderDCOffset, SetRenderDCErasePage und SetRenderCropType bleiben jeweils bestehen, bis sie etwas ändert, sodass eine Thumbnail-Schleife, die nach einer Zoomänderung des Anwenders läuft, den Versatz oder den Zuschnitt erbt, den der vorherige Codepfad hinterlassen hat. Das Symptom ist eine Vorschau, die nur bei bestimmten Navigationsfolgen verrutscht, und das ist ungefähr so mühsam zu reproduzieren, wie ein Fehler nur werden kann. Den gesamten relevanten Zustand oben im Paint-Handler zu setzen, wie oben gezeigt, kostet nichts und beseitigt die ganze Fehlerklasse. Ein zweiter Multiplikator versteckt sich gleich daneben. Die effektive Ausgabeauflösung ist die Renderskalierung mal dem DPI-Argument, und obwohl SetRenderScale standardmäßig auf 1.0 steht, bleibt auch dieser Wert nach einer Änderung bestehen, sodass eine Exportfunktion, die ihn hochgesetzt hat, jede spätere Vorschau still umskaliert, bis ihn etwas zurücksetzt
Für scrollende Viewer und partielle Neuzeichnungen gibt es eine eigene Variante. RenderPageToDCClip nimmt neben dem Device-Context eine Clip-Angabe entgegen, sodass das Invalidieren eines einzelnen Fensterstreifens nur diesen Streifen neu zeichnet, statt die ganze Seite neu zu rastern. Bei hohem Zoom auf großformatigen Seiten ist das der Unterschied zwischen einem Viewer, der dem Scrollbalken folgt, und einem, der hinter ihm herschmiert
Ein Druckauftrag, der zur Vorschau passt
Die Druckseite arbeitet über einen virtuellen Drucker. NewCustomPrinter klont einen Systemdrucker in eine bibliothekseigene Konfiguration, und SetupPrinter passt diesen Klon an, ohne den maschinenweiten DevMode anzurühren: Papier geht als Einstellung 1 hinein (eine DMPAPER_*-Konstante) und die Ausrichtung als Einstellung 11. Der Gewinn ist Isolation. Ein Dienst kann A4-Etiketten drucken, während der Standarddrucker des Hosts auf Letter bleibt, und hinterher 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); // Einstellung 1 = Papier, DMPAPER_A4
Pdf.SetupPrinter(Virt, 11, 1); // Einstellung 11 = Ausrichtung, 1 = Hochformat
Opt := Pdf.PrintOptions(1, 1, 'Monthly Report'); // ans Papier anpassen, automatisch drehen + zentrieren
Pdf.PrintDocument(Virt, 1, Pdf.PageCount, Opt);
finally
Pdf.Free;
end;
end;
PrintOptions verdient sorgfältiges Lesen. Die Funktion liefert ein Options-Handle zurück, das Sie an PrintDocument oder PrintPages übergeben müssen; es ist kein ambienter Zustand. Die Optionen zu bauen und dann das Handle nicht zu übergeben, schlägt stillschweigend fehl. Der Auftrag druckt mit Vorgaben, und niemand merkt es, bis eine Anpassung ans Papier erwartet wurde und stattdessen eine übergroße Seite beschnitten herauskam. Im Argument für die Seitenskalierung steckt genau diese Richtlinie. Keine Skalierung bewahrt die Maßhaltigkeit, was bei Formularen zählt, die mit dem Lineal nachgemessen werden. Anpassen ans Papier skaliert alles auf das Blatt. Große Seiten verkleinern lässt normale Seiten in Ruhe und greift nur ein, wenn eine Seite den bedruckbaren Bereich überschreitet, was für einen gemischten Dokumentbestand meist die richtige Vorgabe ist. Das Flag für automatisches Drehen und Zentrieren erledigt Querformatseiten ohne einen zweiten Codepfad
Anwendungen, die bereits über den VCL-Dialogfluss einen TPrinter verwalten, können ihn direkt übergeben. PrintDocumentToPrinterObject und PrintPagesToPrinterObject nehmen die konfigurierte TPrinter-Instanz entgegen, sodass der Standarddruckdialog die Konfigurationsoberfläche für den Anwender bleibt, während die Bibliothek das Rendern der Seiten übernimmt. Beide Ansätze in einem Codepfad zu mischen, holt gerade die Geometrieabweichung zurück, die diese ganze Arbeit beseitigen sollte, also entscheiden Sie sich für einen. Der Weg über den virtuellen Drucker passt zu unbeaufsichtigten Diensten, der Weg über TPrinter zu interaktiven Anwendungen
Selektive Ausgabe funktioniert genauso. PrintPages nimmt einen Bereichsstring entgegen, sodass die Übergabe des virtuellen Druckernamens, von '2-5,12' und des Options-Handles die Seiten 2 bis 5 und 12 druckt, ohne den Geometrievertrag zu brechen, und dieselbe Syntax steuert die Varianten für die Ausgabe in eine Datei. Diese Dateivarianten sind die praktische Antwort für eine unbeaufsichtigte Umgebung ohne angeschlossenes Gerät: Regressionstests der Druckgeometrie auf einem Buildserver, der überhaupt keine Treiberwarteschlange hat. Rendern Sie bei jedem Build dasselbe Dokument über dieselben Optionen in ein Dateiartefakt, und aus einer Geometrieregression wird ein Diff statt einer Kundenmeldung drei Wochen später
Vorschau-Bitmaps mit den Metriken des Druckers
Eine Vorschau, die mit 96 DPI gegen eine angenommene Seitengröße gerendert wird, beantwortet die falsche Frage. Sie zeigt, wie die Seite aussieht, nicht, was dieser Drucker auf dieses Papier bringen wird. GetPrintPreviewBitmapToString schließt diese Lücke, indem die Vorschau aus demselben benutzerdefinierten Drucker und demselben Options-Handle aufgebaut wird wie der spätere Auftrag, sodass Papierformat, Ausrichtung, Skalierungsrichtlinie, Drehung und der Hardwareversatz alle in die Bitmap einfließen. Zurück kommt das, was auf dem Blatt zu sehen sein 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 Argument MaxDimension begrenzt die lange Kante der Bitmap. 1200 Pixel bleiben für einen Vorschaudialog scharf und halten den Speicherbedarf selbst bei technischen Zeichnungen im Format E moderat, wo ein Rendern in voller Auflösung mit den 600 DPI des Druckers in die Gigabyte ginge
Die Druckerauswahl des Anwenders merken
Druckdialoge, die ihre Einstellungen zwischen Sitzungen vergessen, erzeugen ihre eigenen Supportfälle. Das DevMode-Paar GetPrinterDevModeToString und SetPrinterDevModeFromString serialisiert die vollständige Treiberkonfiguration eines Druckers in eine undurchsichtige Zeichenkette, die Sie in den Benutzereinstellungen ablegen und in der nächsten Sitzung wiederherstellen können, samt der treiberspezifischen Optionen, die keine generische API abzubilden versucht. Speichern Sie den Drucker über den Namen aus GetPrinterNames, niemals über den Listenindex. Die Reihenfolge ändert sich, sobald ein Drucker hinzukommt oder verschwindet, sodass ein gespeicherter Index beim nächsten Verschieben der Liste still auf das falsche Gerät zeigt. GetDefaultPrinterName deckt den Rückfall ab, wenn das gemerkte Gerät ganz verschwunden ist
Die Schachtauswahl rundet die Persistenz ab. GetPrinterBins meldet die Papierquellen, die ein Treiber anbietet, und das zählt bei Briefbogen-Workflows, in denen Seite eins aus dem Briefbogenschacht kommt und der Rest aus normalem Papier. Das ist eine Richtlinie, von der Anwender erwarten, dass die Anwendung sie sich zusammen mit allem anderen merkt, und ein Druckauftrag auf dem falschen Papier wirkt wie ein Fehler, selbst wenn jedes Byte des PDF korrekt war
Eine Engine für Vorschau und Druck
Eine letzte Entscheidung bestimmt still die Wiedergabetreue. Die Wahl der Rendering-Engine gilt für Bildschirm- und Druckerziele gleichermaßen, also ist die Versuchung groß, mit einer schnellen Engine vorzuschauen und mit einer genauen zu drucken. Widerstehen Sie ihr. Vorschau und Auftrag durch verschiedene Engines zu treiben, holt genau die Abweichung zurück, die eine druckertreue Vorschau beseitigen sollte, und zwar so, dass sie erst auf Papier auffällt. Die Abwägungen zwischen der eingebauten Engine, Cairo und PDFium sind in Multi-Engine-PDF-Rendering in Delphi beschrieben; wählen Sie eine und nutzen Sie sie auf beiden Seiten
Dokumente, die zu groß sind, um vor dem Drucken bequem geladen zu werden, lassen sich über den Direktzugriffspfad öffnen, der in große PDFs zusammenführen, teilen und direkt zugreifen beschrieben ist; er rendert Seiten aus einem Dateihandle auf einen Device-Context, ohne den Dokumentbaum aufzubauen. Die vollständige Referenz der Druck-API steht auf der Produktseite von losLab PDF Library for Delphi