Technisch artikel

PDF-documenten afdrukken met de PDFium-component in Delphi

PDF-coördinaten worden weergegeven in punten, printercoördinaten in apparaateenheden, en de twee hebben niets met elkaar te maken totdat u ze opzettelijk converteert. Deze wanverhouding is de oorzaak van de meeste slechte afdrukken in Delphi-toepassingen: de code verzendt het juiste bestand, maar de pagina komt er bijgesneden, uitgerekt of blanco uit. De PDFium-component handelt de renderzijde netjes af; het loodgieterswerk voor de printer is standaard VCL. De twee passen goed samen met een bescheiden hoeveelheid code als u eenmaal begrijpt wat elke partij verwacht

Hoe de render-dan-afdrukken pijplijn werkt

De PDFium-component praat niet rechtstreeks met printers. Het patroon is: render een pagina naar een TBitmap met de door u gewenste resolutie, en draag die bitmap vervolgens over naar het canvas van de printer met StretchDIBits. TPdf.RenderPage retourneert een bitmap die eigendom is van de aanroeper, dus u beheert de pixelafmetingen. Geef [rePrinting] door in de optieset en PDFium schakelt zijn renderpad over naar een pad dat alleen-scherm-effecten, zoals LCD subpixel hinting, weglaat en de MediaBox van de pagina correct afhandelt voor de afdrukuitvoer. Laat u rePrinting weg, dan verzendt u een schermweergave naar de printer, wat er op een monitor prima uitziet, maar bij hoge-DPI-printers vaak resulteert in een zachtere uitvoer omdat de hintingbeslissingen die zijn genomen voor 96 DPI-schermen niet geschikt zijn voor afdrukken met 300 of 600 DPI

TPdf.Active is de enige poort die u moet controleren voordat u een pagina-eigenschap aanraakt. De component slikt laadfouten stilletjes in: het instellen van Active := True op een beschadigd of met een wachtwoord beveiligd bestand veroorzaakt geen uitzondering; het laat Active simpelweg op False staan. Controleer het altijd na de toewijzing. Het lezen van PageCount of PageWidth van een inactief document retourneert nul, wat resulteert in stille, niet-uitgevoerde bewerkingen (no-ops) die erg moeilijk te diagnosticeren zijn zodra ze de spooler bereiken

Een minimale afdruklus

Het eenvoudigste werkende geval laadt een bestand, opent een afdruktaak, itereert de pagina's en sluit af. Het enige lastige detail is dat Printer.NewPage niet mag worden aangeroepen vóór de eerste pagina, vandaar de flag FirstPage. De StretchDIBits-overdracht verloopt via GetDIBSizes en GetDIB om apparaatonafhankelijke bits uit de bitmap-handle te halen, waarna ze op volledige paginagrootte op het printercanvas worden getekend:

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;

Het doorgeven van Printer.PageWidth en Printer.PageHeight als de bitmapafmetingen betekent dat u rendert op de oorspronkelijke pixelgrootte van de printer, waarbij al rekening is gehouden met de DPI van het apparaat. De aanroep StretchDIBits wijst deze pixels vervolgens 1:1 toe aan de pagina. Dit geeft u de best haalbare getrouwheid zonder enige expliciete DPI-rekenkunde, maar het werkt alleen als de PDF-pagina en het fysieke papier toevallig even groot zijn. Als ze verschillen, hebt u expliciete schaling nodig

Schalen wanneer pagina- en papierformaten verschillen

Een PDF-pagina in A4 staand formaat (portrait) past niet automatisch op een US Letter-printer, en een liggende pagina (landscape) die naar een printer in staande stand wordt gestuurd, zal worden afgesneden. De standaardaanpak is om een uniforme schaalfactor te berekenen uit de verhouding tussen printerpixels en PDF-punten, en deze vervolgens toe te passen op beide afmetingen, zodat de beeldverhouding behouden blijft. Pdf.PageWidth en Pdf.PageHeight geven de huidige pagina-afmetingen in punten weer, waarbij één punt overeenkomt met 1/72 inch. Door dit te vermenigvuldigen met een doel-DPI en te delen door 72, wordt geconverteerd naar pixels in die resolutie. Neem Min van de X- en Y-verhoudingen om de grootste schaal te krijgen die nog steeds binnen het afdrukbare gebied past:

// 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;

Rederen met Dpi = 300 is geschikt voor de meeste kantoorprinters. Bij 600 DPI loopt de bitmap voor een enkele A4-pagina op tot ruwweg 34 megapixels, wat overeenkomt met ongeveer 100 MB als een 32-bits bitmap; de kwaliteitsverbetering voor gewone tekstdocumenten is minimaal, terwijl de geheugenkosten per pagina aanzienlijk zijn. Bewaar 600 DPI voor drukkerijen of zware technische lijntekeningen waar dit echt van belang is

De flag reAnnotations in het tweede codeblok staat los van rePrinting. Neem dit op als de gebruiker verwacht dat stempels, markeringen en commentaarvakken op het papier verschijnen. Laat het weg voor uitsluitend inhoudelijke uitvoer. Beide flags kunnen vrijelijk worden gecombineerd

Paginarotatie

PDFium slaat de paginarotatie in de PDF op als een /Rotate-item, toegankelijk via Pdf.PageRotation, dat een TRotation-waarde retourneert (ro0, ro90, ro180, ro270). Het coördinatensysteem van de printer keert rotaties van 90 en 270 graden om ten opzichte van het scherm. Als u de onbewerkte PageRotation-waarde rechtstreeks doorgeeft aan RenderPage zonder enige aanpassing, zullen liggende pagina's ingebed in een staand document ondersteboven worden afgedrukt door de meeste Windows-printerstuurprogramma's. De oplossing is een eenvoudige omwisseling vóór de renderaanroep: map ro90 naar ro270 en ro270 weer naar ro90, en laat ro0 en ro180 ongewijzigd

Verifieer dit gedrag op uw specifieke doelprinter voordat u software uitbrengt. Het gedrag van het stuurprogramma met betrekking tot rotatie is niet uniform bij alle leveranciers, en sommige stuurprogramma's passen hun eigen rotatiecorrectie toe op GDI-niveau. Als u dubbele rotatie ziet, verwijdert u de omwisseling; ziet u helemaal geen correctie, dan voegt u deze toe. Een document met gemengde afdrukstanden (afwisselend staande en liggende pagina's) is de snelste manier om beide faalwijzen te ontdekken tijdens het testen

Geheugenbeheer tijdens een lange afdruktaak

Elke aanroep van RenderPage reserveert een nieuwe TBitmap die eigendom is van de aanroeper en die moet worden vrijgegeven. In de bovenstaande lus handelt het try/finally Bitmap.Free-blok dit correct af voor één pagina tegelijk. Bouw geen bitmaps op over verschillende pagina's: een 300-DPI render van een document met 200 pagina's zou gigabytes verbruiken voordat de eerste pagina de spooler bereikt. Geef elke bitmap vrij voordat u doorgaat naar de volgende pagina

Het AllocMem / FreeMem-paar in het overdrachtsblok volgt dezelfde regel. GetDIBSizes vertelt u hoeveel geheugen de DIB-header en pixelgegevens nodig hebben; u reserveert, vult, tekent en geeft alles vrij, dit alles binnen de scope van één pagina. Als u een van beide blokken laat lekken, zal de afdruktaak de heap van het proces uitputten bij documenten die langer zijn dan een paar dozijn pagina's

Als u afdruktaken moet uitvoeren op een achtergrondthread, houd TPdf en alle VCL-printeraanroepen dan op dezelfde thread. TPdf zelf is thread-onveilig voor instanties die de algemene staat (global state) van de PDFium DLL delen; het veiligste model is één TPdf per thread, waarbij elke thread zijn eigen exemplaar van het bestand laadt

De API voor het renderen en documenten die hier wordt weergegeven, maakt deel uit van de PDFium-component voor Delphi en C++Builder