Technical Article

Štampanje PDF dokumenata pomoću PDFium VCL u Delphi-ju

PDF koordinate su u tačkama (points), koordinate štampača su u jedinicama uređaja, i to dvoje nemaju ništa zajedničko dok ih namerno ne konvertujete. To neslaganje je koren većine loših rezultata štampanja u Delphi aplikacijama: kod šalje ispravnu datoteku, ali stranica izlazi skraćena, rastegnuta ili prazna. PDFium VCL čisto rukuje stranom iscrtavanja; vodovod štampača je standardni VCL. Ovo dvoje se uklapa sa skromnom količinom koda kada razumete šta svaka strana očekuje.

Kako funkcioniše cevovod iscrtaj-pa-štampaj

PDFium VCL ne razgovara direktno sa štampačima. Šablon je: iscrtajte stranicu u TBitmap u rezoluciji koju želite, a zatim prenesite tu bitmapu na platno štampača pomoću StretchDIBits. TPdf.RenderPage vraća bitmapu koju poseduje pozivalac, tako da vi kontrolišete dimenzije piksela. Prenesite [rePrinting] u skup opcija i PDFium prebacuje svoju putanju iscrtavanja na onu koja izostavlja efekte samo za ekran kao što je LCD subpixel hinting, i ispravno rukuje MediaBox-om stranice za izlaz štampanja. Ostavite rePrinting po strani i ono što šaljete štampaču je iscrtavanje ekrana, što izgleda u redu na monitoru, ali ima tendenciju da proizvede mekši izlaz na štampačima visoke rezolucije jer odluke o hintingu donete za ekrane od 96 DPI ne odgovaraju štampanju od 300 ili 600 DPI.

TPdf.Active je jedina kapija koju treba proveriti pre dodirivanja bilo kog svojstva stranice. Komponenta tiho guta greške učitavanja: postavljanje Active := True na oštećenu datoteku ili datoteku zaštićenu lozinkom ne podiže izuzetak; to jednostavno ostavlja Active kao False. Uvek ga proverite nakon dodele. Čitanje PageCount ili PageWidth na neaktivnom dokumentu vraća nulu, što proizvodi tihe neuspehe koje je veoma teško dijagnostikovati kada stignu do spooler-a.

Minimalna petlja štampanja

Najjednostavniji radni slučaj učitava datoteku, otvara posao štampanja, prolazi kroz stranice i zatvara ga. Jedini lukav detalj jeste da se Printer.NewPage ne sme pozvati pre prve stranice, otuda i zastavica FirstPage. Prenos StretchDIBits ide kroz GetDIBSizes i GetDIB da bi se izvukli bitovi nezavisni od uređaja iz ručke bitmape, a zatim ih crta na platnu štampača u punoj veličini stranice:

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;

Prenošenje Printer.PageWidth i Printer.PageHeight kao dimenzija bitmape znači da iscrtavate u izvornoj veličini piksela štampača, što već uzima u obzir DPI uređaja. Poziv StretchDIBits zatim mapira te piksele 1:1 na stranicu. Ovo vam daje najbolju moguću vernost bez ikakve eksplicitne DPI aritmetike, ali radi samo kada su PDF stranica i fizički papir slučajno iste veličine. Kada se razlikuju, potrebno vam je eksplicitno skaliranje.

Skaliranje kada se veličine stranice i papira razlikuju

PDF stranica na A4 uspravno ne odgovara automatski US Letter štampaču, a položena stranica prosleđena štampaču u uspravnoj orijentaciji biće odsečena. Standardni pristup je izračunavanje jedinstvenog faktora skaliranja iz odnosa piksela štampača i PDF tačaka, a zatim ga primeniti na obe dimenzije kako bi se sačuvao odnos širine i visine. Pdf.PageWidth i Pdf.PageHeight izlažu trenutne dimenzije stranice u tačkama, gde je jedna tačka 1/72 inča. Množenje sa ciljnim DPI i deljenje sa 72 pretvara u piksele u toj rezoluciji. Uzmite Min od X i Y odnosa da biste dobili najveće skaliranje koje se i dalje uklapa u oblast za štampanje:

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

Iscrtavanje na Dpi = 300 odgovara većini kancelarijskih štampača. Na 600 DPI, bitmapa za jednu A4 stranicu iznosi otprilike 34 megapiksela, što je oko 100 MB kao 32-bitna bitmapa; dobitak u kvalitetu za obične tekstualne dokumente je minimalan, a trošak memorije po stranici je značajan. Zadržite 600 DPI za štamparije ili vektorski intenzivne tehničke crteže gde to zaista ima značaja.

Zastavica reAnnotations u drugom bloku koda je nezavisna od rePrinting. Uključite je kada korisnik očekuje da se pečati, isticanja i okviri sa komentarima pojave na papiru. Izostavite je za izlaz koji sadrži samo osnovni sadržaj. Obe zastavice se mogu slobodno kombinovati.

Rotacija stranice

PDFium čuva rotaciju stranice u PDF-u kao unos /Rotate, dostupan preko Pdf.PageRotation, koji vraća vrednost TRotation (ro0, ro90, ro180, ro270). Koordinatni sistem štampača invertuje rotacije od 90 i 270 stepeni u odnosu na ekran. Ako prenesete sirovu vrednost PageRotation direktno u RenderPage bez ikakvog prilagođavanja, položene stranice ugrađene u uspravan dokument odštampaće se naopako na većini upravljačkih programa za štampače u Windows-u. Rešenje je jednostavna zamena pre poziva iscrtavanja: mapirajte ro90 u ro270 i ro270 nazad u ro90, ostavljajući ro0 i ro180 nepromenjenim.

Potvrdite ovo ponašanje na vašem specifičnom štampaču pre isporuke. Ponašanje drajvera oko rotacije nije jedinstveno među proizvođačima, a neki drajveri primenjuju sopstvenu korekciju rotacije na GDI nivou. Ako vidite dvostruku rotaciju, uklonite zamenu; ako ne vidite nikakvu korekciju, dodajte je. Dokument sa mešovitom orijentacijom sa naizmeničnim uspravnim i položenim stranicama jeste najbrži način da uhvatite bilo koji od neuspeha tokom testiranja.

Upravljanje memorijom kroz dug posao štampanja

Svaki poziv RenderPage dodeljuje novu TBitmap koju pozivalac poseduje i mora je osloboditi. U gornjoj petlji, blok try/finally Bitmap.Free ispravno rukuje ovim za jednu po jednu stranicu. Nemojte akumulirati bitmape kroz stranice: iscrtavanje od 300 DPI za dokument od 200 stranica potrošilo bi gigabajte pre nego što prva stranica stigne do spooler-a. Oslobodite svaku bitmapu pre prelaska na sledeću stranicu.

Par AllocMem / FreeMem unutar bloka prenosa prati isto pravilo. GetDIBSizes vam govori koliko je memorije potrebno za zaglavlje DIB-a i podatke o pikselima; vi alocirate, popunjavate, crtate i oslobađate, sve u okviru opsega jedne stranice. Puštanje bilo kog bloka da procuri prouzrokovaće da posao štampanja iscrpi gomilu procesa (heap) na dokumentima dužim od nekoliko desetina stranica.

Ako treba da pokrećete poslove štampanja na pozadinskoj niti, držite TPdf i sve VCL pozive štampača na istoj niti. TPdf sam po sebi nije bezbedan za niti (thread-safe) kroz instance koje dele globalno stanje PDFium DLL-a; najsigurniji model je jedan TPdf po niti, pri čemu svaka učitava sopstvenu kopiju datoteke.

API za iscrtavanje i dokumente prikazan ovde deo je PDFium VCL komponente za Delphi i C++Builder.