Műszaki cikk

PDF dokumentumok nyomtatása PDFium Component-lel Delphiben

A PDF koordináták pontokban (points) vannak, a nyomtató koordinátái eszközelemekben (device units), és a kettőnek semmi köze egymáshoz, amíg ön szándékosan át nem számítja őket. Ez az eltérés a gyökere a legtöbb rossz nyomtatási kimenetnek a Delphi alkalmazásokban: a kód a megfelelő fájlt küldi, de az oldal levágva, megnyújtva vagy üresen jön ki. A PDFium Component tisztán kezeli a renderelési oldalt; a nyomtató bekötése (plumbing) szabványos VCL. A kettő szerény mennyiségű kóddal illeszkedik egymáshoz, amint megérti, mit vár el mindkét oldal

Hogyan működik a renderelés-aztán-nyomtatás csővezeték (pipeline)

A PDFium Component nem beszélget közvetlenül a nyomtatókkal. A minta a következő: rendereljen egy oldalt egy TBitmap-be a kívánt felbontáson, majd vigye át azt a bittérképet (bitmap) a nyomtató vásznára (canvas) a StretchDIBits segítségével. A TPdf.RenderPage egy hívó által birtokolt (caller-owned) bittérképet ad vissza, így ön irányítja a pixelméreteket. Adja át a [rePrinting] értéket a beállításhalmazban (options set), és a PDFium olyan renderelési útvonalra vált, amely kihagyja a csak képernyőre vonatkozó effekteket, mint például az LCD alpixel hinting, és helyesen kezeli az oldal MediaBox-át a nyomtatási kimenethez. Hagyja ki a rePrinting-et, és amit a nyomtatónak küld, az egy képernyős renderelés lesz, ami jól mutat egy monitoron, de hajlamos lágyabb (softer) kimenetet produkálni a magas DPI-s nyomtatókon, mivel a 96 DPI-s képernyőkre hozott hinting döntések nem felelnek meg a 300 vagy 600 DPI-s nyomtatásnak

A TPdf.Active az egyetlen kapu, amelyet ellenőrizni kell, mielőtt bármilyen oldaltulajdonsághoz nyúlna. A komponens csendben elnyeli a betöltési hibákat: az Active := True beállítása egy sérült vagy jelszóval védett fájlon nem vet fel kivételt (exception); egyszerűen False értéken hagyja az Active-ot. A hozzárendelés (assignment) után mindig ellenőrizze. A PageCount vagy a PageWidth olvasása egy inaktív dokumentumon nullát ad vissza, ami csendes semmittevéseket (no-ops) eredményez, amiket nagyon nehéz diagnosztizálni, amint elérik a sorkezelőt (spooler)

Egy minimális nyomtatási hurok

A legegyszerűbb működő eset betölt egy fájlt, megnyit egy nyomtatási feladatot, végigiterál (iterates) az oldalakon, és bezár. Az egyetlen trükkös részlet az, hogy a Printer.NewPage-et nem szabad az első oldal előtt meghívni, innen a FirstPage jelző (flag). A StretchDIBits átvitel (transfer) a GetDIBSizes-en és a GetDIB-en megy keresztül, hogy eszközfüggetlen (device-independent) biteket húzzon ki a bittérkép leíróból (bitmap handle), majd a teljes oldalméretben ráfesse azokat a nyomtató vásznára:

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;

A Printer.PageWidth és a Printer.PageHeight átadása bittérkép méretként azt jelenti, hogy a nyomtató natív pixelméretén renderel, ami már figyelembe veszi az eszköz DPI-jét. A StretchDIBits hívás ezután ezeket a pixeleket 1:1 arányban leképezi (maps) az oldalra. Ez a legjobb elérhető hűséget (fidelity) adja explicit DPI aritmetika nélkül, de csak akkor működik, ha a PDF oldal és a fizikai papír véletlenül azonos méretű. Ha eltérnek, explicit skálázásra van szüksége

Skálázás, ha az oldal és a papír mérete eltér

Egy álló (portrait) A4-es PDF oldal nem illeszkedik automatikusan egy US Letter nyomtatóhoz, és egy fekvő (landscape) oldal, amelyet egy álló tájolású (portrait-oriented) nyomtatóba táplálnak be, le lesz vágva (clip). A szabványos megközelítés az, hogy egy egységes léptéktényezőt (scale factor) számolunk a nyomtató pixelek és a PDF pontok (points) arányából, majd ezt alkalmazzuk mindkét dimenzióra, hogy a képarány megmaradjon. A Pdf.PageWidth és a Pdf.PageHeight pontokban (points) teszi közzé az aktuális oldalméreteket, ahol egy pont (point) az 1/72 hüvelyk (inch). Egy cél (target) DPI-vel megszorozva és 72-vel elosztva képpontokká alakul azon a felbontáson. Vegye az X és Y arányok Min értékét, hogy megkapja a legnagyobb léptéket, amely még belefér a nyomtatható területre:

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

A Dpi = 300 felbontáson történő renderelés a legtöbb irodai nyomtatónak megfelel. 600 DPI-nél egyetlen A4-es oldal bittérképe nagyjából 34 megapixelig fut, ami egy 32 bites bittérképként körülbelül 100 MB; a minőségnövekedés (gain in quality) közönséges szöveges dokumentumok esetén minimális, a memóriaköltség oldalanként viszont jelentős. Tartsa meg a 600 DPI-t a nyomdák (print shops) vagy a vektor-nehéz (vector-heavy) műszaki rajzok számára, ahol ez valóban számít

A második kódblokkban szereplő reAnnotations jelző (flag) független a rePrinting-től. Foglalja bele, ha a felhasználó elvárja, hogy a bélyegzők (stamps), a kiemelések (highlights) és a megjegyzésdobozok (comment boxes) megjelenjenek a papíron. Hagyja ki, ha csak tartalmi (content-only) kimenetre van szüksége. A két jelző szabadon kombinálható

Oldal elforgatása (Page rotation)

A PDFium a PDF-ben az oldal elforgatását (rotation) egy /Rotate bejegyzésként tárolja, amely a Pdf.PageRotation-ön keresztül érhető el, ez pedig egy TRotation értéket ad vissza (ro0, ro90, ro180, ro270). A nyomtató koordinátarendszere a képernyőhöz képest megfordítja a 90 és 270 fokos elforgatásokat. Ha a nyers (raw) PageRotation értéket közvetlenül a RenderPage-nek adja át mindenféle korrekció (adjustment) nélkül, akkor egy álló (portrait) dokumentumba ágyazott fekvő (landscape) oldalak a legtöbb Windows nyomtató-illesztőprogramon (printer drivers) fejjel lefelé fognak nyomtatódni. A javítás egy egyszerű csere (swap) a renderelési hívás előtt: képezze le az ro90-et ro270-re, az ro270-et pedig vissza ro90-re, miközben az ro0-t és ro180-at változatlanul hagyja

Ellenőrizze ezt a viselkedést az ön konkrét célnyomtatóján (target printer), mielőtt kiadná (shipping). Az illesztőprogramok (driver) elforgatás körüli viselkedése nem egységes a gyártók között, és egyes illesztőprogramok a saját elforgatási korrekciójukat alkalmazzák a GDI szintjén. Ha dupla elforgatást lát, távolítsa el a cserét (swap); ha egyáltalán nem lát korrekciót, adja hozzá. Egy vegyes tájolású (mixed-orientation), álló és fekvő oldalakat váltakozva tartalmazó dokumentum a leggyorsabb módja bármelyik hibamód (failure mode) elcsípésének a tesztelés során

Memóriakezelés (Memory management) egy hosszú nyomtatási feladat során

Minden RenderPage hívás egy új TBitmap-et foglal le, amelyet a hívó birtokol, és neki kell felszabadítania (free). A fenti hurokban a try/finally Bitmap.Free blokk ezt helyesen kezeli egyszerre egy oldalon. Ne halmozza fel a bittérképeket az oldalakon keresztül: egy 200 oldalas dokumentum 300 DPI-s renderelése gigabájtokat emésztene fel, mielőtt az első oldal elérné a sorkezelőt (spooler). Szabadítson fel minden bittérképet, mielőtt továbblépne a következő oldalra

Az átviteli blokkon (transfer block) belüli AllocMem / FreeMem páros ugyanazt a szabályt követi. A GetDIBSizes megmondja, hogy mennyi memóriára van szüksége a DIB fejlécnek és a pixeladatoknak; ön lefoglalja, kitölti, ráfesti és felszabadítja mindezt egyetlen oldal hatókörén (scope) belül. Ha bármelyik blokkot is szivárogni hagyja (leak), az a nyomtatási feladat számára kimeríti a folyamat kupacát (process heap) a néhány tucat oldalnál hosszabb dokumentumok esetében

Ha háttérszálon (background thread) kell futtatnia nyomtatási feladatokat, tartsa a TPdf-et és az összes VCL nyomtató hívást ugyanazon a szálon. A TPdf önmagában nem szálbiztos (thread-safe) a PDFium DLL globális állapotán osztozó példányokon (instances) keresztül; a legbiztonságosabb modell egy TPdf szálanként, mindegyik betöltve a fájl saját másolatát

Az itt bemutatott renderelési és dokumentum API a Delphihez és C++Builderhez készült PDFium Component része