Teknisk artikkel

Skrive ut PDF-dokumenter med PDFium-komponent i Delphi

PDF-koordinater er i punkter, skriverkoordinater er i enhetsenheter (device units), og de to har ingenting med hverandre å gjøre før du konverterer dem bevisst. Det misforholdet er roten til de fleste dårlige utskrifter i Delphi-applikasjoner: koden sender den riktige filen, men siden kommer ut beskåret, strukket eller tom. PDFium-komponenten håndterer gjengivelsessiden rent; skriver-rørarbeidet er standard VCL. De to passer sammen med en beskjeden mengde kode når du først forstår hva hver side forventer

Hvordan gjengi-så-skrive-ut-rørledningen fungerer

PDFium-komponenten snakker ikke direkte med skrivere. Mønsteret er: gjengi en side til en TBitmap ved den oppløsningen du vil ha, overfør deretter det bitkartet til skriverens lerret med StretchDIBits. TPdf.RenderPage returnerer et oppringereid (caller-owned) bitkart, så du kontrollerer pikseldimensjonene. Send [rePrinting] i alternativsettet og PDFium bytter gjengivelsesbanen sin til en som utelater kun-skjerm-effekter som LCD-subpiksel-hinting, og håndterer sidens MediaBox riktig for utskrift. Utelater du rePrinting, er det du sender til skriveren en skjermgjengivelse, som ser fint ut på en skjerm, men som pleier å produsere mykere (softer) utdata på høyoppløselige skrivere fordi hinting-beslutningene gjort for 96 DPI-skjermer ikke passer til 300 eller 600 DPI-utskrift

TPdf.Active er den eneste porten å sjekke før du berører noen sideegenskap. Komponenten svelger lastefeil i det stille: å sette Active := True på en skadet eller passordbeskyttet fil hever ikke et unntak; det lar rett og slett Active være False. Sjekk den alltid etter tilordningen. Å lese PageCount eller PageWidth på et inaktivt dokument returnerer null, noe som produserer stille nulloperasjoner som er svært vanskelige å diagnostisere når de først når utskriftskøen (spooler)

En minimal utskriftsløkke

Det enkleste fungerende tilfellet laster inn en fil, åpner en utskriftsjobb, itererer sider og lukker. Den eneste vriene detaljen er at Printer.NewPage ikke må kalles før den første siden, derav FirstPage-flagget. StretchDIBits-overføringen går gjennom GetDIBSizes og GetDIB for å trekke enhetsuavhengige bits fra bitkarthåndtaket, og maler dem deretter på skriverlerretet i full sidestørrelse:

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;

Å sende Printer.PageWidth og Printer.PageHeight som bitkartdimensjoner betyr at du gjengir i skriverens opprinnelige (native) pikselstørrelse, som allerede tar høyde for enhetens DPI. StretchDIBits-kallet kartlegger (maps) deretter de pikslene 1:1 inn på siden. Dette gir deg den beste oppnåelige troskapen uten noen eksplisitt DPI-aritmetikk, men det fungerer bare når PDF-siden og det fysiske papiret tilfeldigvis er samme størrelse. Når de er forskjellige, trenger du eksplisitt skalering

Skalering når side- og papirstørrelser er forskjellige

En PDF-side i A4 stående (portrait) passer ikke automatisk til en US Letter-skriver, og en liggende (landscape) side matet til en stående-orientert skriver vil bli beskåret. Standardtilnærmingen er å beregne en jevn skaleringsfaktor fra forholdet mellom skriverpiksler og PDF-punkter, og deretter bruke den på begge dimensjoner slik at sideforholdet bevares. Pdf.PageWidth og Pdf.PageHeight eksponerer de gjeldende sidedimensjonene i punkter, der ett punkt er 1/72 tomme. Å multiplisere med en mål-DPI og dele på 72 konverterer til piksler ved den oppløsningen. Ta Min av X- og Y-forholdene for å få den største skalaen som fortsatt passer innenfor det utskrivbare området:

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

Å gjengi på Dpi = 300 passer de fleste kontorskrivere. Ved 600 DPI kommer bitkartet for en enkelt A4-side opp i omtrent 34 megapiksler, som er omtrent 100 MB som et 32-biters bitkart; gevinsten i kvalitet for vanlige tekstdokumenter er minimal og minnekostnaden per side er betydelig. Behold 600 DPI for trykkerier eller vektortunge tekniske tegninger der det genuint betyr noe

reAnnotations-flagget i den andre kodeblokken er uavhengig av rePrinting. Inkluder det når brukeren forventer at stempler, uthevinger og kommentarbokser skal vises på papir. Utelat det for kun-innhold-utdata. Begge flaggene kan kombineres fritt

Siderotasjon

PDFium lagrer siderotasjon i PDF-en som en /Rotate-oppføring, tilgjengelig via Pdf.PageRotation, som returnerer en TRotation-verdi (ro0, ro90, ro180, ro270). Skriverkoordinatsystemet inverterer 90 og 270 graders rotasjoner relativt til skjermen. Hvis du sender den rå PageRotation-verdien direkte til RenderPage uten noen justering, vil liggende sider innebygd i et stående dokument skrives ut opp-ned på de fleste Windows-skriverdrivere. Løsningen er et enkelt bytte (swap) før gjengivelseskallet: kartlegg ro90 til ro270 og ro270 tilbake til ro90, og la ro0 og ro180 være uendret

Bekreft denne oppførselen på din spesifikke målskriver før du gir den ut (shipping). Driveradferd rundt rotasjon er ikke ensartet på tvers av leverandører, og noen drivere bruker sin egen rotasjonskorreksjon på GDI-nivået. Hvis du ser dobbel rotasjon, fjern byttet; hvis du ikke ser noen korreksjon i det hele tatt, legg det til. Et dokument med blandet orientering med vekslende stående og liggende sider er den raskeste måten å fange noen av feilmodusene på under testing

Minnehåndtering over en lang utskriftsjobb

Hvert kall til RenderPage tildeler en ny TBitmap som oppringeren (caller) eier og må frigjøre. I løkken ovenfor håndterer try/finally Bitmap.Free-blokken dette riktig for én side av gangen. Ikke akkumuler bitkart over sider: en 300-DPI-gjengivelse av et 200-siders dokument ville konsumert gigabyte før den første siden i det hele tatt når utskriftskøen. Frigjør hvert bitkart før du går videre til neste side

AllocMem / FreeMem-paret inni overføringsblokken følger samme regel. GetDIBSizes forteller deg hvor mye minne DIB-hodet (header) og pikseldataene trenger; du tildeler, fyller, maler og frigjør alt innenfor omfanget (scope) til én side. Å la noen av blokkene lekke vil føre til at utskriftsjobben tømmer prosesshaugen (process heap) på dokumenter lengre enn noen få dusin sider

Hvis du trenger å kjøre utskriftsjobber på en bakgrunnstråd, hold TPdf og alle VCL-skriverkall på samme tråd. TPdf i seg selv er ikke trådsikker på tvers av forekomster som deler PDFium-DLL-ens globale tilstand; den tryggeste modellen er én TPdf per tråd, der hver laster inn sin egen kopi av filen

Gjengivelses- og dokument-API-et vist her er en del av PDFium-komponenten for Delphi og C++Builder