Tehnički članak

Ispis (Printing) PDF dokumenata pomoću PDFium Component-a u Delphiju

PDF koordinate su u točkama (points), koordinate pisača (printer) su u jedinicama uređaja (device units), a to dvoje nema nikakve veze jedno s drugim dok ih namjerno ne pretvorite. To nepodudaranje korijen je većine loših ispisa u Delphi aplikacijama: kod šalje pravu datoteku, ali stranica izlazi izrezana, rastegnuta ili prazna. PDFium Component čisto rješava stranu iscrtavanja (rendering); ožičenje pisača (printer plumbing) je standardni VCL. To dvoje se uklapa sa skromnom količinom koda jednom kada shvatite što koja strana očekuje

Kako radi cjevovod (pipeline) "iscrtaj-pa-ispisuj"

PDFium Component ne razgovara izravno s pisačima. Obrazac je sljedeći: iscrtajte (render) stranicu u TBitmap u rezoluciji koju želite, a zatim prenesite tu bitmapu na platno pisača (printer's canvas) pomoću StretchDIBits. TPdf.RenderPage vraća bitmapu u vlasništvu pozivatelja (caller-owned), tako da vi kontrolirate dimenzije piksela. Proslijedite [rePrinting] u skupu opcija i PDFium prebacuje svoju putanju iscrtavanja na onu koja izostavlja efekte samo za zaslon kao što je LCD subpixel hinting i ispravno rukuje s MediaBoxom stranice za ispis. Izostavite li rePrinting, ono što šaljete na pisač je prikaz na zaslonu (screen render), koji izgleda u redu na monitoru, ali obično proizvodi mekši izlaz na pisačima visoke razlučivosti (high-DPI) jer odluke o hintingu (hinting decisions) donesene za zaslone od 96 DPI ne odgovaraju ispisu na 300 ili 600 DPI

TPdf.Active jedina je prepreka koju treba provjeriti prije dodirivanja bilo kojeg svojstva stranice. Komponenta tiho guta pogreške pri učitavanju (load errors): postavljanje Active := True na oštećenoj datoteci ili datoteci zaštićenoj lozinkom ne pokreće iznimku (exception); jednostavno ostavlja Active kao False. Uvijek ga provjerite nakon dodjele. Čitanje PageCount ili PageWidth na neaktivnom dokumentu vraća nulu, što proizvodi tihe "no-ops" koje je vrlo teško dijagnosticirati kada jednom stignu u red čekanja na ispis (spooler)

Minimalna petlja ispisa

Najjednostavniji radni slučaj učitava datoteku, otvara posao ispisa (print job), prolazi kroz stranice i zatvara se. Jedini nezgodan detalj je da Printer.NewPage ne smije biti pozvan prije prve stranice, otuda i oznaka FirstPage. Prijenos StretchDIBits ide kroz GetDIBSizes i GetDIB kako bi povukao neovisne o uređaju bitove (device-independent bits) iz ručke bitmape (bitmap handle), a zatim ih slika na platno pisač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;

Prosljeđivanje Printer.PageWidth i Printer.PageHeight kao dimenzija bitmape znači da iscrtavate u izvornoj veličini piksela pisača, koja već uzima u obzir DPI uređaja. Poziv StretchDIBits zatim mapira te piksele 1:1 na stranicu. To vam daje najbolju moguću vjernost (fidelity) bez ikakve eksplicitne DPI aritmetike, ali funkcionira samo kada su PDF stranica i fizički papir slučajno iste veličine. Kad se razlikuju, potrebno vam je eksplicitno skaliranje (scaling)

Skaliranje kada se razlikuju veličine stranice i papira

PDF stranica u A4 portretnom (portrait) formatu ne uklapa se automatski na US Letter pisač, a pejzažna (landscape) stranica uložena u portretno orijentirani pisač bit će odrezana (clip). Standardni pristup je izračunati jedinstveni faktor skaliranja iz omjera piksela pisača i PDF točaka, a zatim ga primijeniti na obje dimenzije tako da se sačuva omjer širine i visine (aspect ratio). Pdf.PageWidth i Pdf.PageHeight izlažu trenutne dimenzije stranice u točkama, gdje je jedna točka 1/72 inča. Množenjem ciljnim DPI i dijeljenjem sa 72 pretvara se u piksele na toj rezoluciji. Uzmite Min X i Y omjera kako biste dobili najveće mjerilo koje još uvijek stane unutar područja za ispis (printable area):

// 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 (Rendering) na Dpi = 300 odgovara većini uredskih pisača. Na 600 DPI, bitmapa za jednu A4 stranicu doseže otprilike 34 megapiksela, što je oko 100 MB kao 32-bitna bitmapa; dobitak u kvaliteti za obične tekstualne dokumente je minimalan, a cijena memorije po stranici je značajna. Zadržite 600 DPI za tiskare (print shops) ili tehničke crteže s puno vektora gdje je to zaista važno

Zastavica (flag) reAnnotations u drugom bloku koda neovisna je o rePrinting. Uključite je kada korisnik očekuje da će se pečati, isticanja (highlights) i okviri za komentare pojaviti na papiru. Izostavite je za ispis samo sadržaja. Obje zastavice mogu se slobodno kombinirati

Rotacija stranice

PDFium pohranjuje rotaciju stranice u PDF-u kao unos /Rotate, dostupan putem Pdf.PageRotation, koji vraća vrijednost TRotation (ro0, ro90, ro180, ro270). Koordinatni sustav pisača obrnuto rotira za 90 i 270 stupnjeva u odnosu na zaslon. Ako izravno proslijedite sirovu vrijednost PageRotation funkcije RenderPage bez ikakvog podešavanja, pejzažne stranice ugrađene u portretni dokument ispisivat će se naopako na većini upravljačkih programa (drivers) za Windows pisače. Rješenje je jednostavna zamjena prije poziva za iscrtavanje: preslikajte ro90 u ro270 i ro270 natrag u ro90, ostavljajući ro0 i ro180 nepromijenjenim

Prije isporuke (shipping), provjerite ovo ponašanje na vašem specifičnom ciljnom pisaču. Ponašanje upravljačkog programa u vezi s rotacijom nije ujednačeno među proizvođačima (vendors), a neki upravljački programi primjenjuju vlastitu korekciju rotacije na razini GDI-a. Ako vidite dvostruku rotaciju, uklonite zamjenu; ako uopće ne vidite korekciju, dodajte je. Dokument mješovite orijentacije s izmjeničnim portretnim i pejzažnim stranicama najbrži je način da uhvatite bilo koji način kvara tijekom testiranja

Upravljanje memorijom tijekom dugog ispisa

Svaki poziv na RenderPage dodjeljuje (allocates) novi TBitmap koji posjeduje pozivatelj i mora ga osloboditi (free). U gornjoj petlji, blok try/finally Bitmap.Free ispravno to rješava za jednu po jednu stranicu. Nemojte akumulirati bitmape preko stranica: iscrtavanje dokumenta od 200 stranica s 300-DPI potrošilo bi gigabajte prije nego što prva stranica stigne u red čekanja (spooler). Oslobodite svaku bitmapu prije prijelaza na sljedeću stranicu

Par AllocMem / FreeMem unutar bloka prijenosa slijedi isto pravilo. GetDIBSizes govori vam koliko memorije trebaju DIB zaglavlje (header) i podaci o pikselima; vi dodjeljujete (allocate), popunjavate (fill), slikate (paint) i oslobađate (free) sve unutar dosega jedne stranice. Ako dopustite curenje (leak) u bilo kojem bloku, to će uzrokovati da posao ispisa iscrpi hrpu (heap) procesa na dokumentima dužim od nekoliko desetaka stranica

Ako trebate pokretati poslove ispisa u pozadinskoj niti (background thread), držite TPdf i sve pozive VCL pisača u istoj niti. Sam TPdf nije siguran za niti (thread-safe) među instancama koje dijele globalno stanje PDFium DLL-a; najsigurniji model je jedan TPdf po niti, pri čemu svaka učitava svoju kopiju datoteke

API za iscrtavanje i dokument prikazan ovdje dio je PDFium Component za Delphi i C++Builder