Technical Article

Tiskanje dokumentov PDF s komponento PDFium VCL v Delphiju

Koordinate PDF so v točkah (points), tiskalniške koordinate pa v enotah naprave (device units), oba sistema pa nimata nič skupnega, dokler ju namerno ne pretvorite. To neskladje je glavni razlog za večino slabe kakovosti tiskanja v aplikacijah Delphi: koda pošlje pravo datoteko, vendar je natisnjena stran odrezana, raztegnjena ali prazna. PDFium VCL čisto opravi z izrisom strani, medtem ko je tiskalniški del standardni VCL. Oba dela se povežeta z majhno količino kode, ko razumete, kaj pričakuje posamezna stran.

Kako poteka proces izrisa in tiskanja

PDFium VCL ne komunicira neposredno s tiskalniki. Vzorec delovanja je naslednji: stran izrišete v TBitmap pri želeni ločljivosti, nato pa to bitno sliko prenesete na platno (canvas) tiskalnika s pomočjo funkcije StretchDIBits. Metoda TPdf.RenderPage vrne bitno sliko v lasti klicatelja, tako da sami nadzorujete dimenzije slikovnih pik. Če v naboru možnosti posredujete [rePrinting], PDFium preklopi svojo pot izrisa na takšno, ki izpusti učinke, namenjene le zaslonu (kakovostno subpikselno glajenje LCD), in pravilno obravnava MediaBox strani za tiskanje. Če možnost rePrinting izpustite, tiskalniku pošljete zaslonski izris, ki je na monitorju sličen, vendar na tiskalnikih z visoko ločljivostjo (high-DPI) povzroči mehkejši izpis, saj odločitve o glajenju za 96 DPI zaslone niso primerne za tiskanje pri 300 ali 600 DPI.

Lastnost TPdf.Active je edini pogoj, ki ga morate preveriti, preden se dotaknete katere koli lastnosti strani. Komponenta tiho zadrži napake pri nalaganju: nastavitev Active := True na poškodovani ali z geslom zaščiteni datoteki ne sproži izjeme, temveč enostavno pusti Active na False. To vedno preverite po dodelitvi. Branje lastnosti PageCount ali PageWidth na neaktivnem dokumentu vrne nič, kar povzroči tihe napake (no-ops), ki jih je izjemno težko diagnosticirati, ko enkrat dosežejo tiskalniško vrsto (spooler).

Minimalna tiskalniška zanka

Najenostavnejši delujoč primer naloži datoteko, odpre tiskalno opravilo, gre skozi strani in se zapre. Edina posebnost je, da funkcije Printer.NewPage ne smemo poklicati pred prvo stranjo, zato uporabimo zastavico FirstPage. Prenos StretchDIBits poteka prek funkcij GetDIBSizes in GetDIB, da pridobimo od naprave neodvisne bite (DIB) iz ročice bitne slike, ter jih nato nariše na platno tiskalnika v polni velikosti strani:

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;

Če kot dimenziji bitne slike posredujete Printer.PageWidth in Printer.PageHeight, izris izvedete v nativni velikosti slikovnih pik tiskalnika, ki že upošteva DPI naprave. Klic StretchDIBits nato te slikovne pike preslika 1:1 na stran. To vam omogoča najboljšo možno natančnost brez eksplicitne aritmetike DPI, vendar deluje le takrat, ko sta stran PDF in fizični papir enake velikosti. Če se razlikujeta, potrebujete eksplicitno prilagajanje velikosti.

Prilagajanje velikosti ob razlikah med stranjo in papirjem

Stran PDF formata A4 v pokončni orientaciji se ne prilagodi samodejno tiskalniku formata US Letter, ležeča stran, poslana na tiskalnik s pokončno orientacijo, va pa bo odrezana. Standardni pristop je izračun enotnega rasa merila iz razmerja med slikovnimi pikami tiskalnika in točkami PDF, nato pa se ta faktor uporabi za obe dimenziji, da se ohrani razmerje stranic. Lastnosti Pdf.PageWidth in Pdf.PageHeight razkrivata trenutne dimenzije strani v točkah (points), kjer je ena točka enaka 1/72 palca. Množenje s ciljno vrednostjo DPI in deljenje z 72 pretvori vrednost v slikovne pike pri tej ločljivosti. Izberite manjšo (Min) izmed obeh vrednosti razmerij X in Y, da dobite največje merilo, ki še ustreza tiskalnemu območju:

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

Izris pri Dpi = 300 je primeren za večino pisarniških tiskalnikov. Pri 600 DPI bitna slika za eno stran A4 obsega približno 34 megapikslov, kar pomeni približno 100 MB as a 32-bit bitmap; pridobitev na kakovosti pri običajnih besedilnih dokumentih je minimalna, poraba pomnilnika na stran pa znatna. Ločljivost 600 DPI ohranite za tiskarne ali tehnične risbe z veliko vektorskimi elementi, kjer je to res pomembno.

Zastavica reAnnotations v drugem bloku kode je neodvisna od rePrinting. Vključite jo, ko uporabnik pričakuje, da se bodo na papirju natisnili štampiljke, označbe in komentarji. Za izpis same vsebine jo izpustite. Obe zastavici lahko poljubno kombinirate.

Zasuk strani

PDFium shranjuje zasuk strani v dokumentu PDF kot vnos /Rotate, ki je dostopen prek Pdf.PageRotation in vrne vrednost TRotation (ro0, ro90, ro180, ro270). Koordinatni sistem tiskalnika obrne zasuka za 90 in 270 stopinj glede na zaslon. Če surovo vrednost PageRotation predate neposredno metodi RenderPage brez prilagoditve, se bodo ležeče strani v pokončnem dokumentu na večini Windows tiskalniških gonilnikov natisnile obrnjene na glavo. Rešitev je preprosta zamenjava pred klicem izrisa: preslikajte ro90 v ro270 in ro270 nazaj v ro90, medtem ko ro0 in ro180 pustite nespremenjena.

Pred izdajo programske opreme preverite to delovanje na svojem ciljnem tiskalniku. Delovanje gonilnikov glede zasuka ni enotno pri vseh proizvajalcih, nekateri gonilniki pa uporabijo lasten popravek zasuka na ravni GDI. Če opazite dvojni zasuk, odstranite zamenjavo; če popravka sploh ni, jo dodajte. Dokument z mešano orientacijo, kjer se izmenjujejo pokončne in ležeče strani, je najhitrejši način za zaznavanje obeh načinov odpovedi med testiranjem.

Upravljanje pomnilnika med dolgim tiskalnim opravilom

Vsak klic metode RenderPage dodeli nov TBitmap, katerega lastnik je klicatelj in ga mora sprostiti. V zgornji zanki blok try/finally Bitmap.Free to pravilno upravlja za vsako stran posebej. Ne kopičite bitnih slik skozi strani: izris 200-stranskega dokumenta pri 300 DPI bi porabil več gigabajtov pomnilnika, še preden bi prva stran sploh dosegla tiskalno vrsto. Preden nadaljujete na naslednjo stran, sprostite vsako posamezno bitno sliko.

Par AllocMem / FreeMem znotraj bloka za prenos sledi istemu pravilu. Funkcija GetDIBSizes pove, koliko pomnilnika potrebujeta glava DIB in podatki o slikovnih pikah; alokacijo, polnjenje, risanje in sproščanje izvedete znotraj obsega posamezne strani. Uhajanje pomnilnika v katerem koli od teh blokov bo povzročilo, da tiskalno opravilo porabi celoten kopični pomnilnik (heap) procesa pri dokumentih, daljših od nekaj deset strani.

Če morate tiskalna opravila izvajati v ozadni niti, ohranite TPdf in vse klice VCL tiskalnika v isti niti. TPdf sam po sebi ni nitno varen (thread-safe) med instancami, ki si delijo globalno stanje knjižnice PDFium DLL; najbolj varen model je ena instanca TPdf na nit, pri čemer vsaka naloži svojo kopijo datoteke.

API za izrisovanje in dokumente, prikazan tukaj, je del komponente PDFium VCL Component za Delphi in C++Builder.