Ein Nutzer füllt ein PDF-Formular in einem Delphi-Viewer aus, drückt Drucken, und das Blatt kommt heraus mit jedem Feld leer und der Letter-großen Seite gedehnt, um A4 zu füllen. Beide Fehler sitzen in der Druckschleife: PDFium Component malt Feldwerte nur in eine Bitmap (oder bäckt sie in eine abgeflachte Kopie), und jede Seite will einen einzigen seitenverhältnis-bewahrenden Skalierungsfaktor
Die Formular-Ebene ist ein zweiter Durchlauf, der eine Bitmap braucht
Auf Windows reicht das Device-Context-Overload TPdf.RenderPage(DC, Left, Top, Width, Height, Rotation, Options) das HDC des Druckers direkt an FPDF_RenderPage durch. Dieser Aufruf malt den Seiteninhalt, plus Markup-Annotationen, wenn reAnnotations gesetzt ist. Die interaktive Formular-Ebene erreicht den DC nie — sie gehört zu PDFiums Form-Fill-Umgebung und wird von FPDF_FFLDraw gezeichnet, deren einziges Ziel ein FPDF_BITMAP ist. Eine HDC-Variante gibt es nicht. Die zwei Bitmap-Overloads von TPdf.RenderPage laufen FPDF_RenderPageBitmap und FPDF_FFLDraw auf denselben Pixels, wann immer das Dokument mit FormFill auf True geöffnet wurde, dem Default. Mit rePrinting in den Optionen entfernt PDFium Component das Feld-Highlight vor dem Formular-Durchlauf und stellt danach Default-Highlight-Farbe und Alpha wieder her, also bleiben die getönten Boxen um editierbare Felder auf dem Bildschirm. rePrinting mappt außerdem auf das FPDF_PRINTING-Flag der Engine, das den Output als Druck markiert — der Fall, den ISO 32000-1 §12.5.3 (Tabelle 165) mit Annotation-Flag-Bit 3 abhandelt: Ein konformer Reader lässt eine Annotation, deren Print-Bit clear ist, vom Papier fern, ganz gleich, wie sie auf dem Bildschirm aussieht
Die Plattform entscheidet den Rest. In der PDFium-Engine zeichnet FPDF_RenderPage über Windows GDI in einen Device Context und existiert nur in Windows-Builds, während FPDF_FFLDraw in eine Bitmap und nichts sonst zeichnet, auf jeder Plattform. Unter Linux und den anderen Nicht-Windows-Zielen gibt es keinen GDI-Aufruf zu machen, also wird das HDC-Overload bedient, indem eine Bitmap gerendert wird — Formular-Ebene eingeschlossen — und auf das Canvas hinter dem Handle gezeichnet. Der Device-Context-Aufruf trägt daher Feldwerte abseits von Windows und lässt sie auf Windows weg. Halten Sie den Zweig explizit in Code, der zwischen Delphi und Lazarus geteilt wird: Wenn das Dokument eine Formular-Umgebung hat und seine Werte aufs Papier gehören, bitten Sie auf jeder Plattform um eine Bitmap
Welchen Weg soll ein Druckauftrag nehmen?
Wählen Sie nach dem, was das Papier tragen muss. Live-Formularwerte zeigen auf den Bitmap-Weg, Vektor-Output auf den HDC-Weg, und eine abgeflachte Kopie liefert beides zum Preis eines Speicherns und Neuladens. SetPdfPrintMode gilt bibliotheksweit, also ändern Sie es zwischen Aufträgen, nie zwischen Seiten
| Weg | Formularwerte auf Papier | Output | Kosten |
|---|---|---|---|
RenderPage(DC, …) | Nein, auf Windows | Vektor-GDI; SetPdfPrintMode wählt EMF (den Default) oder eine PostScript-Stufe | Kleinster Spool, am schnellsten |
RenderPage in eine TBitmap, dann StretchDIBits | Ja, eingeschlossen ungespeicherte Edits | Raster in der Pixelgröße, die Sie anfordern | Width × Height × 4 Bytes pro Seite, plus die DIB-Kopie |
RenderPageBanded | Ja | Raster, ein Streifen je Durchlauf | Ein Band im Speicher statt des ganzen Blatts |
Abgeflachte Kopie, dann RenderPage(DC, …) | Ja, als gebackener Seiteninhalt | Vektor | Speichern, neu laden, GenerateFormAppearances, FlattenAllPages(FLAT_PRINT) |
Ein ganzes A4-Blatt bei 600 dpi sind etwa 4960 × 7016 Device-Pixels, und bei 32 Bits pro Pixel sind das 139,197,440 Bytes für eine Seite; sie in eine DIB für StretchDIBits zu kopieren verdoppelt den Peak, eine schwere Last für einen 32-Bit-Prozess. Für den Print-Button eines Viewers ist der Bitmap-Weg trotzdem der richtige Default: Ein Nutzer, der in ein Feld tippt und Drucken drückt, erwartet, diesen Wert auf dem Papier zu sehen, und Banding hält den Speicher begrenzt. Dokumente, die die Anwendung verlassen, etwa archivierte Abrechnungen, sind besser mit der abgeflachten Kopie bedient — die Werte werden zu gewöhnlichem Seiteninhalt, und der Drucker erhält Vektor-Output
uses
System.SysUtils, System.IOUtils, PDFium, FPdfUtils; // FPdfUtils: FLAT_PRINT
function OpenFlattenedPrintCopy(Source: TPdf; out TempName: string): TPdf;
begin
TempName := TPath.Combine(TPath.GetTempPath, TPath.GetRandomFileName + '.pdf');
if not Source.SaveAs(TempName) then
raise Exception.Create('Could not write the print copy');
Result := TPdf.Create(nil);
try
Result.FileName := TempName; // FormFill bleibt True (der Default)
Result.Password := Source.Password; // die Kopie behält ihre Verschlüsselung
Result.Active := True;
if not Result.Active then // ein gescheiterter Ladevorgang lässt TPdf inaktiv
raise Exception.Create('Could not reopen the print copy');
Result.GenerateFormAppearances; // /AP aus dem aktuellen /V neu aufbauen
if not Result.FlattenAllPages(FLAT_PRINT) then
raise Exception.Create('Flattening failed');
except
Result.Free;
TFile.Delete(TempName);
raise;
end;
end;
Der Flatten-Weg hat eine Reihenfolge-Regel, die mehr zählt als das Flag. FlattenAllPages bäckt den Appearance-Stream jedes Widgets (ISO 32000-1 §12.5.5) in den Seiteninhalt, und ein Appearance-Stream ist das, was der letzte Producer geschrieben hat; der Feldwert in /V spielt keine Rolle. Ein Formular, das von einem Tool gefüllt wurde, das seine Appearances nie neu erzeugte, flacht zum alten Text oder zu nichts ab, also läuft GenerateFormAppearances zuerst: Es baut Textfeld-Appearances aus den aktuellen Werten neu auf und synchronisiert Checkbox- und Radio-/AS-States, und es braucht FormFill auf True, als das Dokument geöffnet wurde. Machen Sie all das an einer Kopie, denn Flattening schreibt die Seiten des TPdf um, auf dem es läuft, und ein offener Viewer will seine Felder editierbar halten. Die Kopie läuft durch den Vektor-Zweig der Druckroutine unten mit ausgeschalteter Formular-Ebene; geben Sie sie frei und löschen Sie die temporäre Datei, sobald der Auftrag endet
Jede Seite in den bedruckbaren Bereich einpassen
Skalieren Sie einmal, mit dem kleineren der beiden Faktoren, und zentrieren Sie das Ergebnis. Auf Printer.PageWidth × Printer.PageHeight zu dehnen verzerst jede Seite, deren Gestalt vom Papier abweicht, eine Letter-Seite (612 × 792 Points) auf A4 oder eine Querformat-Seite auf einem Hochformat-Blatt. Die Druckerseite ist der bedruckbare Bereich in Device-Pixels (HORZRES mal VERTRES), mit den Treiber-Rändern bereits ausgeschlossen. Auf der PDF-Seite melden TPdf.PageWidth und PageHeight die Seite bereits so, wie sie angezeigt wird: Eine A4-Hochformat-Seite, gespeichert mit /Rotate 90, kommt als 842 × 595 Points zurück. ISO 32000-1 §7.7.3.3 (Tabelle 30) sagt, die Seite soll bei Anzeige oder Druck um /Rotate gedreht werden, PDFium wendet das innerhalb des Render-Aufrufs an, und das Rotation-Argument von RenderPage ist nur die extra Drehung, die ein Viewer hinzufügt, wenn der Nutzer die Ansicht dreht. Reichen Sie dort die eigene PageRotation der Seite durch, dreht sich die Seite zweimal. Tauschen Sie Width und Height für das Einpassen nur, wenn diese Extra-Drehung 90 oder 270 Grad ist, dasselbe Paar von Regeln, das Thumbnail-Fit und Zoom unter Rotation regelt; die Routine unten erweitert die Basis-Schleife für das Drucken von PDF-Dokumenten mit PDFium Component um einen Formularwert-Zweig und einen Abbruch-Pfad, der den Auftrag verwirft
uses
Winapi.Windows, System.SysUtils, Vcl.Graphics, Vcl.Forms, Vcl.Printers, PDFium;
procedure FitRect(PageW, PageH: Double; Swap: Boolean;
AreaW, AreaH: Integer; out X, Y, W, H: Integer);
var
T, Scale: Double;
begin
if Swap then
begin
T := PageW; PageW := PageH; PageH := T;
end;
if (PageW <= 0) or (PageH <= 0) then
begin
X := 0; Y := 0; W := AreaW; H := AreaH;
Exit;
end;
Scale := AreaW / PageW;
if AreaH / PageH < Scale then
Scale := AreaH / PageH;
W := Round(PageW * Scale);
H := Round(PageH * Scale);
X := (AreaW - W) div 2;
Y := (AreaH - H) div 2;
end;
procedure BlitBitmap(DC: HDC; Bitmap: TBitmap; X, Y: Integer);
var
InfoSize, ImageSize: DWORD;
Info: PBitmapInfo;
Bits: Pointer;
begin
GetDIBSizes(Bitmap.Handle, InfoSize, ImageSize);
Info := AllocMem(InfoSize);
try
Bits := AllocMem(ImageSize);
try
GetDIB(Bitmap.Handle, 0, Info^, Bits^);
StretchDIBits(DC, X, Y, Bitmap.Width, Bitmap.Height, 0, 0,
Bitmap.Width, Bitmap.Height, Bits, Info^, DIB_RGB_COLORS, SRCCOPY);
finally
FreeMem(Bits);
end;
finally
FreeMem(Info);
end;
end;
function PrintPages(Pdf: TPdf; FromPage, ToPage: Integer; FormLayer: Boolean;
ExtraRotation: TRotation; var Cancel: Boolean): Boolean;
const
Opts: TRenderOptions = [reAnnotations, rePrinting];
var
Page, X, Y, W, H: Integer;
Bitmap: TBitmap;
begin
Result := False;
Printer.BeginDoc;
try
for Page := FromPage to ToPage do
begin
if Page > FromPage then
Printer.NewPage;
Pdf.PageNumber := Page;
FitRect(Pdf.PageWidth, Pdf.PageHeight, ExtraRotation in [ro90, ro270],
Printer.PageWidth, Printer.PageHeight, X, Y, W, H);
if FormLayer then
begin
Bitmap := Pdf.RenderPage(0, 0, W, H, ExtraRotation, Opts);
if Bitmap <> nil then // nil bei einer degenerierten Seitengröße
try
BlitBitmap(Printer.Canvas.Handle, Bitmap, X, Y);
finally
Bitmap.Free;
end;
end
else
Pdf.RenderPage(Printer.Canvas.Handle, X, Y, W, H, ExtraRotation, Opts);
Application.ProcessMessages; // lässt einen Cancel-Button das Flag setzen
if Cancel then
Break;
end;
Result := not Cancel;
finally
if Result then
Printer.EndDoc // übergibt den Auftrag an den Spooler
else
Printer.Abort; // Abbruch oder Exception: nichts druckt
end;
end;
Ein Viewer ruft das mit Pdf.FormFill als FormLayer auf; die abgeflachte Kopie gibt False durch. EndDoc ist der Commit, kein Aufräum-Schritt. Nach BeginDoc reicht Printer.EndDoc alles bisher Gerenderte als gültigen Auftrag an den Spooler weiter, also macht ein bedingungsloses EndDoc im finally aus einem abgebrochenen Druck einen halb gedruckten. Die Routine entscheidet stattdessen im finally anhand des Erfolgs-Flags: EndDoc nach der letzten Seite, Abort nach einem Abbruch oder einer Exception. Der Schleifen-Ausstieg braucht ähnliche Sorgfalt, denn Delphi und Free Pascal werten den Endwert einer for-Schleife beide einmal aus, beim Eintritt. ToPage oder eine Kopienzahl aus einem Cancel-Handler auf null zu setzen stoppt keine Schleife, die bereits läuft; testen Sie das Flag und Break, und bei verschachtelten Kopier- und Sortier-Schleifen aus jeder Ebene ausbrechen. Und da Application.ProcessMessages auch Menü-Shortcuts durchlässt, deaktivieren Sie Open, Save und Print, solange der Auftrag läuft
Den Raster-Weg im Speicher halten
TPdf.RenderPageBanded rendert eine Seite in horizontalen Streifen einer Höhe, die Sie wählen, und reicht jeden Streifen an einen Callback durch. Jeder Streifen ist ein Slice der seitengroßen Seite — nichts wird neu skaliert — und er trägt dieselben Feldwerte wie eine ganzseitige Bitmap. Mit 512-Zeilen-Bändern braucht eine 4960 Pixel breite Seite etwa 10 MB pro Band statt 139 MB für das Blatt. Etwa ein Vierzehntel des Speichers. TPdfBandCallback ist ein Methodenzeiger, der den Band-Index, den Top-Offset des Bands in Seiten-Pixels und die Bitmap erhält und True zurückgibt, um fortzufahren; RenderPageBanded gibt die Bandzahl zurück, 0 bei Fehlschlag
type
TBandPrinter = class
private
FDC: HDC;
FLeft, FTop: Integer;
public
constructor Create(DC: HDC; Left, Top: Integer);
function PrintBand(BandIndex, BandTopY: Integer; Bitmap: TBitmap): Boolean;
end;
constructor TBandPrinter.Create(DC: HDC; Left, Top: Integer);
begin
inherited Create;
FDC := DC;
FLeft := Left;
FTop := Top;
end;
function TBandPrinter.PrintBand(BandIndex, BandTopY: Integer;
Bitmap: TBitmap): Boolean;
begin
BlitBitmap(FDC, Bitmap, FLeft, FTop + BandTopY);
Result := True; // False stoppt die restlichen Bänder
end;
// aufgerufen aus der Seiten-Schleife anstelle des Ganzseiten-Bitmap-Zweigs
procedure PrintPageBanded(Pdf: TPdf; Page, X, Y, W, H: Integer;
ExtraRotation: TRotation);
var
Bands: TBandPrinter;
begin
Bands := TBandPrinter.Create(Printer.Canvas.Handle, X, Y);
try
if Pdf.RenderPageBanded(W, H, 512, Bands.PrintBand, ExtraRotation,
[reAnnotations, rePrinting]) = 0 then
raise Exception.CreateFmt('Page %d could not be rendered', [Page]);
finally
Bands.Free;
end;
end;
Was soll der Host mit ViewerPreferences und Permission-Bits machen?
Lesen Sie sie und handeln Sie in Ihrem eigenen Code danach; PDFium meldet sie und rendert die Seite trotzdem. Ein Producer kann Druck-Hinweise im /ViewerPreferences-Dictionary des Katalogs ablegen (ISO 32000-1 §12.2, Tabelle 150), und TPdf exponiert diejenigen, die einen Druckdialog formen:
PrintScaling(Boolean) ist False, wenn die Datei/PrintScaling /Nonesagt, was in tatsächlicher Größe drucken heißt, und True für/AppDefaultoder einen fehlenden Eintrag. Die Norm will es befolgt haben, selbst wenn kein Druckdialog gezeigt wirdPrintPaperHandling(TPrintPaperHandling) trägt/DuplexalsphSimplex,phDuplexFlipShortEdge,phDuplexFlipLongEdgeoderphUndefined.SetPdfPrintPaperHandlingDevModeschreibt es inDEVMODE.dmDuplexund setztDM_DUPLEX; fürphUndefinedgibt es False zurück und lässt den Record in Ruhe, also überlebt der Treiber-DefaultPrintCopies(Integer) ist/NumCopies, die Kopienzahl für den Dialog
Permission-Bits folgen derselben Regel. Für den Standard-Security-Handler weist ISO 32000-1 §7.6.3.2 (Tabelle 22) Bit 3 von /P dem Drucken zu und, für Revision-3-und-später-Handler, Bit 12 dem Drucken in voller Originaltreue; mit Bit 3 gesetzt und Bit 12 clear ist das Drucken auf eine niedrigstufige Darstellung der Erscheinung beschränkt, möglicherweise degradiert. TPdf.GrantedPermissions dekodiert die Maske in permPrint und permPrintHighQuality, und HasPermission prüft einen Wert; die Engine rendert, was auch immer Sie anfordern, also ist die Durchsetzung Sache des Hosts, normalerweise ein deaktivierter Print-Button. Wenn nur permPrint gewährt ist, ist die Lesart, die zur Norm passt, der Bitmap-Weg in bescheidener Auflösung statt des Vektor- oder PostScript-Outputs, den eine volle Gewährung erlaubt. Die Bits beschreiben das Dokument, wie es geöffnet wurde: Eine Datei, die mit ihrem Owner-Passwort freigeschaltet wurde, meldet vollen Zugriff, und UserHasPermission antwortet für die User-Level-Bits allein
uses
Winapi.Windows, System.SysUtils, Vcl.Dialogs, Vcl.Printers, PDFium;
procedure PreparePrintJob(Pdf: TPdf; Dialog: TPrintDialog;
out ActualSize, HighQuality: Boolean);
var
Device, Driver, Port: array[0..MAX_PATH] of Char;
DevModeHandle: THandle;
DevMode: PDeviceMode;
begin
if not Pdf.HasPermission(permPrint) then
raise Exception.Create('This document does not permit printing');
HighQuality := Pdf.HasPermission(permPrintHighQuality);
ActualSize := not Pdf.PrintScaling; // /PrintScaling /None
Printer.GetPrinter(Device, Driver, Port, DevModeHandle);
if DevModeHandle <> 0 then
begin
DevMode := GlobalLock(DevModeHandle);
try
SetPdfPrintPaperHandlingDevMode(DevMode^, Pdf.PrintPaperHandling);
finally
GlobalUnlock(DevModeHandle);
end;
Printer.SetPrinter(Device, Driver, Port, DevModeHandle); // der Dialog startet davon
end;
Dialog.MinPage := 1;
Dialog.MaxPage := Pdf.PageCount;
Dialog.Options := Dialog.Options + [poPageNums];
if Pdf.PrintCopies > 1 then
Dialog.Copies := Pdf.PrintCopies;
end;
// benutzt statt FitRect, wenn ActualSize True ist
procedure ActualSizeRect(Pdf: TPdf; out X, Y, W, H: Integer);
begin
// 72 Points pro Inch auf der PDF-Seite, Device-Pixels pro Inch auf dem Drucker
W := Round(Pdf.PageWidth / 72 * GetDeviceCaps(Printer.Handle, LOGPIXELSX));
H := Round(Pdf.PageHeight / 72 * GetDeviceCaps(Printer.Handle, LOGPIXELSY));
X := (Printer.PageWidth - W) div 2; // negativ, wenn die Seite größer ist
Y := (Printer.PageHeight - H) div 2; // als der bedruckbare Bereich: Ränder clippen
end;
Will-Print- und Did-Print-Aktionen an der echten Druckgrenze ausführen
Dokumenten-Level-Druck-Skripte laufen, wenn der Host sie laufen lässt. Das Additional-Actions-Dictionary des Katalogs (ISO 32000-1 §12.6.3, Tabelle 197) definiert WP, eine JavaScript-Aktion, die vor dem Drucken ausgeführt wird, und DP, die danach ausgeführt wird. PDFium Component exponiert beide über ExecuteDocumentLifecycleAction(dlaWillPrint, ErrorMessage) und dlaDidPrint und hält sie aus den Render-Aufrufen heraus: Auch Thumbnails, Snapshots und Konversionen rendern, und keines davon sollte für ein Dokument-Skript wie der Druck eines Nutzers aussehen. Die Reihenfolge müssen Sie richtig hinbekommen. Ein Will-Print-Skript, das ein Datum stempelt oder ein "printed by"-Feld füllt, ändert Formularwerte, also führen Sie es aus, bevor die erste Seite gerendert wird und, auf dem Flatten-Weg, bevor die Kopie gespeichert wird; feuern Sie Did-Print erst, nachdem EndDoc den Auftrag commitet hat. Der Aufruf gibt False mit einer Meldung zurück, wenn die Formular-Umgebung fehlt oder LifecycleActionPolicy es verweigert; für unvertrauenswürdige Dateien schaltet lapDisabled Dokument-Aktionen ab. Das breitere Skripting-Bild steht in AcroForm-JavaScript mit Host-Events ausführen
Auf Windows gehört ein Auftrag, der Formularwerte tragen muss, auf den Bitmap-Zweig: RenderPage in eine TBitmap bei gewöhnlichen Auflösungen, RenderPageBanded bei 600 dpi und darüber. Halten Sie das Device-Context-Overload der PDFium Component für abgeflachte Kopien und für Dokumente ohne Felder bereit, wo sein Vektor-Output das bessere Ergebnis ist