PDF-Koordinaten sind in Punkten, Drucker-Koordinaten in Geräteeinheiten, und beide haben nichts miteinander zu tun, bis Sie sie bewusst konvertieren. Diese Diskrepanz ist die Ursache für die meisten schlechten Druckergebnisse in Delphi-Anwendungen: Der Code sendet die richtige Datei, aber die Seite wird beschnitten, gestreckt oder leer ausgegeben. Die PDFium-Komponente handhabt das Rendering sauber; die Drucker-Infrastruktur ist Standard-VCL. Beide passen mit etwas Code zusammen, sobald man versteht, was jede Seite erwartet
Wie die Rendern-dann-Drucken-Pipeline funktioniert
Die PDFium-Komponente spricht Drucker nicht direkt an. Das Muster lautet: Rendern Sie eine Seite mit der gewünschten Auflösung in ein TBitmap und übertragen Sie diese Bitmap dann mit StretchDIBits auf den Canvas des Druckers. TPdf.RenderPage gibt eine Bitmap zurück, die dem Aufrufer gehört, sodass Sie die Pixelabmessungen kontrollieren. Wenn Sie [rePrinting] in der Optionsmenge übergeben, wechselt PDFium seinen Rendering-Pfad zu einem, der bildschirmspezifische Effekte wie LCD-Subpixel-Hinting weglässt und die MediaBox der Seite für die Druckausgabe korrekt handhabt. Lassen Sie rePrinting weg, und was Sie an den Drucker senden, ist ein Bildschirm-Rendering, das auf einem Monitor gut aussieht, aber tendenziell eine unschärfere Ausgabe auf Hoch-DPI-Druckern erzeugt, da die Hinting-Entscheidungen, die für 96-DPI-Bildschirme getroffen wurden, nicht für den 300- oder 600-DPI-Druck geeignet sind
TPdf.Active ist das einzige Gate, das überprüft werden muss, bevor eine Seiteneigenschaft berührt wird. Die Komponente schluckt Ladefehler stillschweigend: Das Setzen von Active := True bei einer beschädigten oder passwortgeschützten Datei löst keine Ausnahme aus; es belässt Active einfach auf False. Überprüfen Sie es immer nach der Zuweisung. Das Lesen von PageCount oder PageWidth in einem inaktiven Dokument gibt null zurück, was zu stillen No-Ops führt, die sehr schwer zu diagnostizieren sind, sobald sie den Spooler erreichen
Eine minimale Druckschleife
Der einfachste funktionierende Fall lädt eine Datei, öffnet einen Druckauftrag, iteriert durch die Seiten und schließt. Das einzige knifflige Detail ist, dass Printer.NewPage nicht vor der ersten Seite aufgerufen werden darf, daher das Flag FirstPage. Die StretchDIBits-Übertragung läuft über GetDIBSizes und GetDIB, um geräteunabhängige Bits aus dem Bitmap-Handle zu ziehen, und malt sie dann in voller Seitengröße auf den Drucker-Canvas:
procedure PrintPdfFile(const FileName: string);
var
Pdf: TPdf;
I: Integer;
Bitmap: TBitmap;
InfoHeaderSize, ImageSize: DWORD;
InfoHeader: PBitmapInfo;
Image: Pointer;
FirstPage: Boolean;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := FileName;
Pdf.Active := True;
if not Pdf.Active then
Exit; // load failed silently; bail out
Printer.Title := Pdf.Title;
Printer.BeginDoc;
try
FirstPage := True;
for I := 1 to Pdf.PageCount do
begin
if FirstPage then
FirstPage := False
else
Printer.NewPage;
Pdf.PageNumber := I;
// Render at printer resolution; rePrinting adjusts the render path
Bitmap := Pdf.RenderPage(
0, 0,
Printer.PageWidth,
Printer.PageHeight,
ro0,
[rePrinting]
);
try
GetDIBSizes(Bitmap.Handle, InfoHeaderSize, ImageSize);
InfoHeader := AllocMem(InfoHeaderSize);
try
Image := AllocMem(ImageSize);
try
GetDIB(Bitmap.Handle, 0, InfoHeader^, Image^);
StretchDIBits(
Printer.Canvas.Handle,
0, 0, Printer.PageWidth, Printer.PageHeight,
0, 0, Bitmap.Width, Bitmap.Height,
Image, InfoHeader^, DIB_RGB_COLORS, SRCCOPY
);
finally
FreeMem(Image);
end;
finally
FreeMem(InfoHeader);
end;
finally
Bitmap.Free;
end;
end;
finally
Printer.EndDoc;
end;
finally
Pdf.Active := False;
Pdf.Free;
end;
end;
Durch Übergabe von Printer.PageWidth und Printer.PageHeight als Bitmap-Dimensionen rendern Sie in der nativen Pixelgröße des Druckers, was bereits die Geräte-DPI berücksichtigt. Der StretchDIBits-Aufruf ordnet diese Pixel dann 1:1 auf der Seite an. Dies bietet Ihnen die bestmögliche Genauigkeit ohne explizite DPI-Arithmetik, funktioniert aber nur, wenn die PDF-Seite und das physische Papier zufällig dieselbe Größe haben. Wenn sie sich unterscheiden, benötigen Sie eine explizite Skalierung
Skalierung bei unterschiedlichen Seiten- und Papiergrößen
Eine PDF-Seite im A4-Hochformat passt nicht automatisch auf einen US-Letter-Drucker, und eine Querformatseite, die an einen Hochformatdrucker gesendet wird, wird abgeschnitten. Der Standardansatz besteht darin, aus dem Verhältnis von Druckerpixeln zu PDF-Punkten einen einheitlichen Skalierungsfaktor zu berechnen und ihn dann auf beide Dimensionen anzuwenden, damit das Seitenverhältnis erhalten bleibt. Pdf.PageWidth und Pdf.PageHeight geben die aktuellen Seitenabmessungen in Punkten an, wobei ein Punkt 1/72 Zoll entspricht. Multiplizieren mit der Ziel-DPI und Dividieren durch 72 rechnet in Pixel bei dieser Auflösung um. Nehmen Sie das Min der X- und Y-Verhältnisse, um die größte Skalierung zu erhalten, die noch in den druckbaren Bereich passt:
// Fit PDF page to printable area, preserving aspect ratio
var
ScaleX, ScaleY, Scale: Double;
DestWidth, DestHeight: Integer;
Dpi: Integer;
begin
Dpi := 300; // target render resolution
Pdf.PageNumber := PageIndex;
ScaleX := Printer.PageWidth / (Pdf.PageWidth * Dpi / 72);
ScaleY := Printer.PageHeight / (Pdf.PageHeight * Dpi / 72);
Scale := Min(ScaleX, ScaleY);
// Clamp to 1.0 for shrink-to-fit only (no enlargement)
if Scale > 1.0 then Scale := 1.0;
DestWidth := Round(Pdf.PageWidth * Dpi / 72 * Scale);
DestHeight := Round(Pdf.PageHeight * Dpi / 72 * Scale);
Bitmap := Pdf.RenderPage(0, 0, DestWidth, DestHeight, ro0,
[rePrinting, reAnnotations]);
// ... transfer with StretchDIBits as above
end;
Ein Rendering mit Dpi = 300 eignet sich für die meisten Bürodrucker. Bei 600 DPI beläuft sich die Bitmap für eine einzelne A4-Seite auf rund 34 Megapixel, was etwa 100 MB als 32-Bit-Bitmap entspricht; der Qualitätsgewinn bei gewöhnlichen Textdokumenten ist minimal und die Speicherkosten pro Seite sind erheblich. Behalten Sie 600 DPI für Druckereien oder vektorlastige technische Zeichnungen, bei denen es wirklich darauf ankommt
Das Flag reAnnotations im zweiten Codeblock ist unabhängig von rePrinting. Binden Sie es ein, wenn der Benutzer erwartet, dass Stempel, Hervorhebungen und Kommentarboxen auf dem Papier erscheinen. Lassen Sie es für reine Inhaltsausgaben weg. Beide Flags können frei kombiniert werden
Seitendrehung
PDFium speichert die Seitendrehung im PDF als /Rotate-Eintrag, auf den über Pdf.PageRotation zugegriffen werden kann, welches einen TRotation-Wert zurückgibt (ro0, ro90, ro180, ro270). Das Druckkoordinatensystem invertiert Drehungen um 90 und 270 Grad relativ zum Bildschirm. Wenn Sie den rohen PageRotation-Wert ohne jegliche Anpassung direkt an RenderPage übergeben, werden Querformatseiten, die in einem Hochformatdokument eingebettet sind, auf den meisten Windows-Druckertreibern auf dem Kopf gedruckt. Die Lösung ist ein einfacher Tausch vor dem Renderaufruf: Weisen Sie ro90 zu ro270 und ro270 zurück zu ro90 zu, während ro0 und ro180 unverändert bleiben
Überprüfen Sie dieses Verhalten auf Ihrem spezifischen Zieldrucker, bevor Sie ausliefern. Das Treiberverhalten bezüglich der Drehung ist nicht bei allen Anbietern einheitlich, und einige Treiber wenden ihre eigene Rotationskorrektur auf GDI-Ebene an. Wenn Sie eine doppelte Drehung sehen, entfernen Sie den Tausch; wenn Sie überhaupt keine Korrektur sehen, fügen Sie ihn hinzu. Ein Dokument mit gemischter Ausrichtung und abwechselnden Hoch- und Querformatseiten ist der schnellste Weg, beide Fehlermodi beim Testen zu erkennen
Speicherverwaltung bei einem langen Druckauftrag
Jeder Aufruf von RenderPage weist ein neues TBitmap zu, das dem Aufrufer gehört und freigegeben werden muss. In der obigen Schleife handhabt der try/finally Bitmap.Free-Block dies korrekt für eine Seite nach der anderen. Sammeln Sie keine Bitmaps seitenübergreifend an: Ein 300-DPI-Rendering eines 200-seitigen Dokuments würde Gigabytes verbrauchen, bevor die erste Seite den Spooler erreicht. Geben Sie jedes Bitmap frei, bevor Sie zur nächsten Seite übergehen
Das AllocMem / FreeMem-Paar innerhalb des Übertragungsblocks folgt derselben Regel. GetDIBSizes teilt Ihnen mit, wie viel Speicher der DIB-Header und die Pixeldaten benötigen; Sie reservieren, füllen, zeichnen und geben alles innerhalb des Geltungsbereichs einer Seite frei. Lässt man einen der Blöcke lecken, wird der Druckauftrag bei Dokumenten, die länger als ein paar Dutzend Seiten sind, den Prozess-Heap erschöpfen
Wenn Sie Druckaufträge in einem Hintergrundthread ausführen müssen, behalten Sie TPdf und alle VCL-Druckeraufrufe im selben Thread. TPdf selbst ist über Instanzen hinweg, die den globalen Status der PDFium-DLL teilen, nicht threadsicher; das sicherste Modell ist ein TPdf pro Thread, wobei jeder seine eigene Kopie der Datei lädt
Die hier gezeigte Rendering- und Dokumenten-API ist Teil der PDFium-Komponente für Delphi und C++Builder