Technischer Artikel

PDF-Formularwerte drucken und Seitenproportionen halten

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

PDFium Component druckt Formularfelder in Delphi: TPdf.RenderPage auf dem Drucker-HDC ruft FPDF_RenderPage ohne die Formular-Ebene, während das TBitmap-Overload einen FPDF_FFLDraw-Durchlauf vor StretchDIBits hinzufügt
Auf Windows läuft nur das Bitmap-Overload von RenderPage den FPDF_FFLDraw-Formular-Durchlauf, weshalb getippte Feldwerte auf diesem Weg das Papier erreichen und auf dem Device-Context-Weg verschwinden

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

WegFormularwerte auf PapierOutputKosten
RenderPage(DC, …)Nein, auf WindowsVektor-GDI; SetPdfPrintMode wählt EMF (den Default) oder eine PostScript-StufeKleinster Spool, am schnellsten
RenderPage in eine TBitmap, dann StretchDIBitsJa, eingeschlossen ungespeicherte EditsRaster in der Pixelgröße, die Sie anfordernWidth × Height × 4 Bytes pro Seite, plus die DIB-Kopie
RenderPageBandedJaRaster, ein Streifen je DurchlaufEin Band im Speicher statt des ganzen Blatts
Abgeflachte Kopie, dann RenderPage(DC, …)Ja, als gebackener SeiteninhaltVektorSpeichern, 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

PDFium Component abgeflachte Druckkopie in Delphi: SaveAs in eine temporäre Datei, wieder öffnen mit FormFill, GenerateFormAppearances, FlattenAllPages(FLAT_PRINT), dann Vektor-Druck durch RenderPage auf dem Device Context
GenerateFormAppearances vor FlattenAllPages aufzurufen bringt die gebackenen Appearance-Streams dazu, den aktuellen Feldwerten zu entsprechen, statt dem, was der letzte Producer in /AP hinterließ

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

PDFium Component Page Fit für Delphi-Druck: eine Letter-Seite gedehnt auf Printer.PageWidth und PageHeight gegen eine seitenverhältnis-bewahrende Skalierung, zentriert im bedruckbaren Bereich, mit /Rotate bereits in TPdf.PageWidth
Skalieren mit dem kleineren der beiden Faktoren hält jede Seite unverzerrt und zentriert, und Width und Height tauschen nur bei einer extra ro90- oder ro270-Viewer-Rotation
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 /None sagt, was in tatsächlicher Größe drucken heißt, und True für /AppDefault oder einen fehlenden Eintrag. Die Norm will es befolgt haben, selbst wenn kein Druckdialog gezeigt wird
  • PrintPaperHandling (TPrintPaperHandling) trägt /Duplex als phSimplex, phDuplexFlipShortEdge, phDuplexFlipLongEdge oder phUndefined. SetPdfPrintPaperHandlingDevMode schreibt es in DEVMODE.dmDuplex und setzt DM_DUPLEX; für phUndefined gibt es False zurück und lässt den Record in Ruhe, also überlebt der Treiber-Default
  • PrintCopies (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