Technisch artikel

PDF-formulierwaarden printen en paginaverhoudingen houden

Een gebruiker vult een PDF-formulier in een Delphi-viewer in, drukt op Print, en het vel komt naar buiten met elk veld leeg en de Letter-pagina uitgerekt om A4 te vullen. Beide gebreken zitten in de print-lus: PDFium Component schildert veldwaarden alleen in een bitmap (of bakt ze in een afgevlakte kopie), en elke pagina wil één schaalfactor die de verhouding bewaart

De formulierlaag is een tweede pass die een bitmap nodig heeft

Op Windows geeft de device-context-overload TPdf.RenderPage(DC, Left, Top, Width, Height, Rotation, Options) de HDC van de printer rechtstreeks aan FPDF_RenderPage. Die aanroep schildert de pagina-inhoud, plus markup-annotations als reAnnotations is gezet. De interactieve formulierlaag bereikt de DC nooit — hij hoort bij de form-fill-omgeving van PDFium en wordt getekend door FPDF_FFLDraw, wiens enige doel een FPDF_BITMAP is. Er is geen HDC-variant. De twee bitmap-overloads van TPdf.RenderPage draaien FPDF_RenderPageBitmap en FPDF_FFLDraw op dezelfde pixels zodra het document werd geopend met FormFill op True, de default. Met rePrinting in de opties verwijdert PDFium Component de veld-highlight vóór de form-pass en herstelt daarna de default-highlightkleur en -alpha, zodat de getinte kaders rond bewerkbare velden op het scherm blijven. rePrinting wijst ook naar de flag FPDF_PRINTING van de engine, die de uitvoer als print markeert — het geval dat ISO 32000-1 §12.5.3 (Tabel 165) aanpakt met annotation-flag bit 3: een conforme reader laat een annotation wiens Print-bit gewist is van het papier, hoe hij er op het scherm ook uitziet

Het platform beslist de rest. In de PDFium-engine tekent FPDF_RenderPage via Windows GDI naar een device context en bestaat alleen in Windows-builds, terwijl FPDF_FFLDraw naar een bitmap tekent en naar niets anders, op elk platform. Op Linux en de andere non-Windows-doelen is er geen GDI-aanroep te maken, dus de HDC-overload wordt bediend door een bitmap te renderen — formulierlaag incluis — en die op het canvas achter de handle te tekenen. De device-context-aanroep draagt veldwaarden dus mee buiten Windows en laat ze op Windows achterwege. Houd de branch expliciet in code die tussen Delphi en Lazarus wordt gedeeld: heeft het document een formulieromgeving en horen zijn waarden op papier, vraag dan op elk platform om een bitmap

PDFium Component formuliervelden printen in Delphi: TPdf.RenderPage op de printer-HDC roept FPDF_RenderPage aan zonder de formulierlaag, terwijl de TBitmap-overload een FPDF_FFLDraw-pass toevoegt vóór StretchDIBits
Op Windows draait alleen de bitmap-overload van RenderPage de FPDF_FFLDraw-formpass, wat is waarom getypte veldwaarden op die route het papier bereiken en op de device-context-route verdwijnen

Welke route hoort een printtaak te nemen?

Kies op grond van wat het papier moet dragen. Levende formulierwaarden wijzen naar de bitmaproute, vectoruitvoer naar de HDC-route, en een afgevlakte kopie geeft u beide voor de prijs van een opslag en een herlading. SetPdfPrintMode werkt op bibliotheksniveau, dus stel hem bij tussen taken in, nooit tussen pagina's

RouteFormulierwaarden op papierUitvoerKost
RenderPage(DC, …)Nee, op WindowsVector GDI; SetPdfPrintMode kiest EMF (de default) of een PostScript-niveauKleinste spool, snelste
RenderPage naar een TBitmap, dan StretchDIBitsJa, inclusief niet-opgeslagen bewerkingenRaster op de pixelformaat die u vraagtBreedte × Hoogte × 4 bytes per pagina, plus de DIB-kopie
RenderPageBandedJaRaster, één strook per keerEén band in het geheugen in plaats van het hele vel
Afgevlakte kopie, dan RenderPage(DC, …)Ja, als ingebakken pagina-inhoudVectorOpslaan, herladen, GenerateFormAppearances, FlattenAllPages(FLAT_PRINT)

Een vol A4-vel op 600 dpi is ongeveer 4960 × 7016 device pixels, en op 32 bits per pixel is dat 139.197.440 bytes voor één pagina; hem naar een DIB kopiëren voor StretchDIBits verdubbelt de piek, een zware last voor een 32-bit-proces. Voor de Print-knop van een viewer blijft de bitmaproute de juiste default: een gebruiker die in een veld typt en op Print drukt verwacht die waarde op papier te zien, en banding houdt het geheugen begrensd. Documenten die de applicatie verlaten, zoals gearchiveerde afschriften, zijn beter geholpen met de afgevlakte kopie — de waarden worden gewone pagina-inhoud en de printer ontvangt vectoruitvoer

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 blijft True (de default)
    Result.Password := Source.Password;  // de kopie houdt zijn versleuteling
    Result.Active := True;
    if not Result.Active then      // een mislukte load laat TPdf inactief
      raise Exception.Create('Could not reopen the print copy');
    Result.GenerateFormAppearances;          // /AP herbouwen vanuit de huidige /V
    if not Result.FlattenAllPages(FLAT_PRINT) then
      raise Exception.Create('Flattening failed');
  except
    Result.Free;
    TFile.Delete(TempName);
    raise;
  end;
end;

De flatten-route heeft een ordeningsregel die zwaarder weegt dan de flag. FlattenAllPages bakt de appearance stream van elke widget (ISO 32000-1 §12.5.5) in de pagina-inhoud, en een appearance stream is precies wat de laatste producent schreef; de veldwaarde in /V speelt geen rol. Een formulier gevuld door een tool die zijn appearances nooit opnieuw genereerde vlakt af tot de oude tekst of tot niets, dus GenerateFormAppearances draait eerst: hij herbouwt tekstveld-appearances vanuit de huidige waarden en synchroniseert de checkbox- en radio-/AS-states, en hij heeft nodig dat FormFill True stond toen het document werd geopend. Doe dit alles op een kopie, want afvlakken herschrijft de pagina's van welke TPdf hij ook op draait, en een open viewer wil zijn velden bewerkbaar houden. De kopie gaat door de vector-branch van de print-routine hieronder met de formulierlaag uit; geef hem vrij en verwijder het tijdelijke bestand zodra de taak eindigt

PDFium Component afgevlakte printkopie in Delphi: SaveAs naar een tijdelijk bestand, heropenen met FormFill, GenerateFormAppearances, FlattenAllPages(FLAT_PRINT), en dan vectorprinten via RenderPage op de device context
GenerateFormAppearances vóór FlattenAllPages aanroepen maakt dat de ingebakken appearance streams bij de huidige veldwaarden passen in plaats van bij wat de laatste producent in /AP achterliet

Elke pagina passen op het afdrukbare gebied

Schaal één keer, met de kleinste van de twee factoren, en centreer het resultaat. Uitrekken naar Printer.PageWidth × Printer.PageHeight vervormt elke pagina waarvan de vorm van het papier verschilt, een Letter-pagina (612 × 792 points) op A4 of een liggende pagina op een staand vel. De printerkant is het afdrukbare gebied in device pixels (HORZRES bij VERTRES), met de marges van de driver al uitgesloten. Aan de PDF-kant melden TPdf.PageWidth en PageHeight de pagina al zoals getoond: een A4-staand blad opgeslagen met /Rotate 90 komt terug als 842 × 595 points. ISO 32000-1 §7.7.3.3 (Tabel 30) zegt dat de pagina bij tonen of printen met /Rotate geroteerd hoort te worden, PDFium past dat binnen de render-aanroep toe, en het argument Rotation van RenderPage is alleen de extra draai die een viewer toevoegt wanneer de gebruiker de weergave roteert. Geef daar de eigen PageRotation van de pagina door en de pagina draait twee keer. Wissel breedte en hoogte voor de pasvorm alleen wanneer die extra rotatie 90 of 270 graden is, hetzelfde paar regels dat thumbnail-pasvorm en zoom bij rotatie stuurt; de routine hieronder breidt de basislus voor PDF-documenten printen met PDFium Component uit met een formulierwaarde-branch en een annuleerpad dat de taak weggooit

PDFium Component page fit voor Delphi-printing: een Letter-pagina uitgerekt naar Printer.PageWidth en PageHeight tegen één verhouding-bewarend schaalfactor gecentreerd in het afdrukbare gebied, met /Rotate al verwerkt in TPdf.PageWidth
Schalen met de kleinste van de twee factoren houdt elke pagina onvervormd en gecentreerd, en breedte en hoogte wisselen alleen bij een extra ro90- of ro270-viewerrotatie
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 bij een degenerate paginagrootte
        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;     // laat een Cancel-knop de vlag zetten
      if Cancel then
        Break;
    end;
    Result := not Cancel;
  finally
    if Result then
      Printer.EndDoc                   // draagt de taak aan de spooler over
    else
      Printer.Abort;                   // annuleren of exception: er wordt niets geprint
  end;
end;

Een viewer roept dit aan met Pdf.FormFill als FormLayer; de afgevlakte kopie geeft False door. EndDoc is de commit, geen opruimstap. Na BeginDoc draagt Printer.EndDoc alles wat tot dan is gerenderd als geldige taak aan de spooler over, dus een onvoorwaardelijke EndDoc in finally maakt van een geannuleerde print een halfgeprinte. De routine beslist in finally vanuit de succesvlag: EndDoc na de laatste pagina, Abort na een annulering of een exception. De lusuitgang heeft vergelijkbare zorg nodig, want Delphi en Free Pascal evalueren allebei de eindwaarde van een for-lus één keer, bij binnenkomst. ToPage of een kopieaantal op nul zetten vanuit een Cancel-handler stopt een lus die al draait niet; toets de vlag en doe Break, en bij geneste kopie- en collatie-lussen op elk niveau uitbreken. En omdat Application.ProcessMessages ook menusneltoetsen laat vuren, schakel Openen, Opslaan en Printen uit terwijl de taak draait

De raster-route binnen het geheugen houden

TPdf.RenderPageBanded rendert een pagina in horizontale stroken van een hoogte die u kiest en geeft elke strook door aan een callback. Elke strook is een plak van de pagina op ware grootte — er wordt niets herschaald — en hij draagt dezelfde veldwaarden als een bitmap van de hele pagina. Met banden van 512 rijen heeft een pagina van 4960 pixels breed ongeveer 10 MB per band nodig in plaats van 139 MB voor het vel. Ruwweg een veertiende van het geheugen. TPdfBandCallback is een method pointer die de bandindex ontvangt, de bovenoffset van de band in paginapixels en de bitmap, en True teruggeeft om door te gaan; RenderPageBanded geeft het bandaantal terug, 0 bij mislukking

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 stopt de resterende banden
end;

// aangeroepen vanuit de paginalus in plaats van de hele-pagina-bitmap-branch
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;

Wat hoort de host te doen met ViewerPreferences en permissiebits?

Lees ze en handel ernaar in uw eigen code; PDFium meldt ze en rendert de pagina toch. Een producent kan printhints opslaan in de dictionary /ViewerPreferences van de catalog (ISO 32000-1 §12.2, Tabel 150), en TPdf legt degenen bloot die een printdialoog vormgeven:

  • PrintScaling (Boolean) is False wanneer het bestand /PrintScaling /None zegt, wat printen op ware grootte betekent, en True voor /AppDefault of een ontbrekende entry. De standaard wil hem gehonoreerd zelfs wanneer er geen printdialoog wordt getoond
  • PrintPaperHandling (TPrintPaperHandling) draagt /Duplex als phSimplex, phDuplexFlipShortEdge, phDuplexFlipLongEdge of phUndefined. SetPdfPrintPaperHandlingDevMode schrijft hem in DEVMODE.dmDuplex en zet DM_DUPLEX; voor phUndefined geeft hij False terug en laat het record met rust, dus de driver-default overleeft
  • PrintCopies (Integer) is /NumCopies, het kopieaantal voor de dialoog

Permissiebits volgen dezelfde regel. Voor de standaard security handler wijst ISO 32000-1 §7.6.3.2 (Tabel 22) bit 3 van /P toe aan printen en, voor revision 3 en latere handlers, bit 12 aan printen op volle getrouwheid; met bit 3 gezet en bit 12 gewist is printen beperkt tot een lage weergave van de appearance, mogelijk gedegradeerd. TPdf.GrantedPermissions decodeert het masker naar permPrint en permPrintHighQuality, en HasPermission toetst één waarde; de engine rendert wat u ook vraagt, dus afdwingen is het werk van de host, normaal een uitgeschakelde Print-knop. Wanneer alleen permPrint is toegekend is de lezing die bij de standaard past de bitmaproute op een bescheiden resolutie in plaats van de vector- of PostScript-uitvoer die een volledige toekenning toestaat. De bits beschrijven het document zoals geopend: een bestand dat met zijn owner password werd opengemaakt meldt volledige toegang, en UserHasPermission antwoordt alleen over de user-level-bits

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);  // de dialoog begint ermee
  end;

  Dialog.MinPage := 1;
  Dialog.MaxPage := Pdf.PageCount;
  Dialog.Options := Dialog.Options + [poPageNums];
  if Pdf.PrintCopies > 1 then
    Dialog.Copies := Pdf.PrintCopies;
end;

// gebruikt in plaats van FitRect wanneer ActualSize True is
procedure ActualSizeRect(Pdf: TPdf; out X, Y, W, H: Integer);
begin
  // 72 points per inch aan de PDF-kant, device pixels per inch op de printer
  W := Round(Pdf.PageWidth / 72 * GetDeviceCaps(Printer.Handle, LOGPIXELSX));
  H := Round(Pdf.PageHeight / 72 * GetDeviceCaps(Printer.Handle, LOGPIXELSY));
  X := (Printer.PageWidth - W) div 2;  // negatief wanneer de pagina groter is
  Y := (Printer.PageHeight - H) div 2; // dan het afdrukbare gebied: randen vallen af
end;

Will-print- en did-print-actions draaien op de echte printgrens

Printscripts op documentniveau draaien wanneer de host ze draait. De additional-actions-dictionary van de catalog (ISO 32000-1 §12.6.3, Tabel 197) definieert WP, een JavaScript-action vóór het printen, en DP, erna. PDFium Component legt beide bloot via ExecuteDocumentLifecycleAction(dlaWillPrint, ErrorMessage) en dlaDidPrint, en houdt ze buiten de render-aanroepen: thumbnails, snapshots en conversies renderen ook, en geen ervan hoort voor een documentscript als de print van een gebruiker te lijken. De volgorde moet u zelf goed neerzetten. Een will-print-script dat een datum stempelt of een veld "printed by" vult verandert formulierwaarden, dus draai hem voordat de eerste pagina wordt gerenderd en, op de flatten-route, voordat de kopie wordt opgeslagen; vuur did-print pas af nadat EndDoc de taak heeft overgedragen. De aanroep geeft False terug met een melding wanneer de formulieromgeving ontbreekt of LifecycleActionPolicy hem weigert; voor onbetrouwbare bestanden schakelt lapDisabled document-actions uit. Het bredere scripting-verhaal staat in AcroForm JavaScript draaien met host-events

Op Windows hoort een taak die formulierwaarden moet dragen op de bitmap-branch: RenderPage naar een TBitmap op gewone resoluties, RenderPageBanded op 600 dpi en hoger. Houd de device-context-overload van PDFium Component voor afgevlakte kopieën en voor documenten zonder velden, waar zijn vectoruitvoer het betere resultaat is