Teknik Makale

Delphi'de PDFium Bileşeni İle PDF Belgeleri Yazdırma

PDF koordinatları punto (points) cinsindendir, yazıcı koordinatları cihaz birimleri (device units) cinsindendir ve siz onları kasıtlı olarak dönüştürene kadar ikisinin birbiriyle hiçbir ilgisi yoktur. Bu uyuşmazlık, Delphi uygulamalarındaki çoğu kötü yazdırma çıktısının (print output) temelidir: kod doğru dosyayı gönderir ancak sayfa kırpılmış, uzatılmış (stretched) veya boş çıkar. PDFium Bileşeni oluşturma tarafını (rendering side) temiz bir şekilde halleder; yazıcı tesisatı (plumbing) standart VCL'dir. Her bir tarafın ne beklediğini anladığınızda, ikisi mütevazı (modest) bir kod miktarıyla birbirine uyar

Oluştur-sonra-yazdır (render-then-print) boru hattı nasıl çalışır

PDFium Bileşeni yazıcılarla doğrudan konuşmaz. Model (pattern) şudur: bir sayfayı istediğiniz çözünürlükte bir TBitmap'e oluşturun (render), ardından bu bit eşlemi (bitmap) StretchDIBits ile yazıcının tuvaline (canvas) aktarın. TPdf.RenderPage çağıranın sahip olduğu (caller-owned) bir bit eşlem döndürür, böylece piksel boyutlarını (pixel dimensions) siz kontrol edersiniz. Seçenekler kümesinde (options set) [rePrinting] öğesini (pass) geçirin ve PDFium, oluşturma yolunu LCD alt piksel ipucu (subpixel hinting) gibi yalnızca ekran etkilerini atlayan bir yola geçirir (switches) ve yazdırma çıktısı (print output) için sayfanın MediaBox'ını doğru bir şekilde işler. rePrinting'i dışarıda bırakırsanız yazıcıya gönderdiğiniz şey bir ekran oluşturmadır; bu bir monitörde iyi görünür, ancak 96 DPI ekranlar için alınan ipucu (hinting) kararları 300 veya 600 DPI baskıya uymadığı için yüksek DPI'lı yazıcılarda daha yumuşak (softer) çıktı üretme eğilimindedir

TPdf.Active, herhangi bir sayfa özelliğine dokunmadan önce kontrol edilmesi gereken tek kapıdır. Bileşen yükleme hatalarını sessizce yutar (swallows): hasarlı veya parola korumalı bir dosyada Active := True ayarlanması bir istisna fırlatmaz (does not raise an exception); sadece Active değerini False olarak bırakır. Atamadan sonra her zaman kontrol edin. Etkin olmayan (inactive) bir belgede PageCount veya PageWidth değerini okumak sıfır döndürür, bu da yazıcı biriktiricisine (spooler) ulaştığında teşhis edilmesi çok zor olan sessiz hareketsizlikler (silent no-ops) üretir

Minimal bir yazdırma döngüsü

En basit çalışma örneği bir dosyayı yükler, bir yazdırma işini (print job) açar, sayfaları yineler (iterates) ve kapatır. Tek zor ayrıntı, ilk sayfadan önce Printer.NewPage yönteminin çağrılmaması gerektiğidir, bu nedenle FirstPage bayrağı (flag) kullanılır. StretchDIBits aktarımı, bit eşlem (bitmap) tanıtıcısından (handle) cihazdan bağımsız bitleri çekmek (pull) için GetDIBSizes ve GetDIB yöntemlerinden geçer, ardından bunları yazıcı tuvaline (printer canvas) tam sayfa boyutunda boyar:

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;

Bit eşlem (bitmap) boyutları (dimensions) olarak Printer.PageWidth ve Printer.PageHeight değerlerini geçmek, halihazırda cihazın DPI'sini hesaba katan (accounts for) yazıcının yerel piksel (native pixel) boyutunda oluşturduğunuz (render) anlamına gelir. Ardından StretchDIBits çağrısı (call), bu pikselleri sayfaya 1:1 eşler (maps). Bu size herhangi bir açık (explicit) DPI aritmetiği olmadan elde edilebilecek en iyi doğruluğu (fidelity) verir, ancak yalnızca PDF sayfası ile fiziksel kağıt tesadüfen (happen to) aynı boyutta olduğunda çalışır. Farklı olduklarında açık ölçeklendirmeye (explicit scaling) ihtiyacınız vardır

Sayfa ve kağıt boyutları farklı olduğunda ölçeklendirme

A4 dikey (portrait) biçimindeki bir PDF sayfası, bir US Letter yazıcıya otomatik olarak sığmaz ve dikey yönelimli (portrait-oriented) bir yazıcıya beslenen yatay (landscape) bir sayfa kırpılır (clip). Standart yaklaşım, yazıcı piksellerinin PDF puntolarına oranından tek biçimli bir ölçek katsayısı (uniform scale factor) hesaplamak (compute) ve ardından en boy oranının (aspect ratio) korunması için bunu her iki boyuta da (dimensions) uygulamaktır. Pdf.PageWidth ve Pdf.PageHeight, geçerli sayfa boyutlarını bir noktanın 1/72 inç olduğu noktalar (points) cinsinden ortaya koyar. Hedef DPI ile çarpıp 72'ye bölmek (dividing) o çözünürlükteki piksellere dönüştürür. Yazdırılabilir alan içine hala sığan en büyük ölçeği (scale) elde etmek için X ve Y oranlarının (ratios) Min değerini alın:

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

Dpi = 300'de oluşturma (Rendering) çoğu ofis yazıcısına uyar. 600 DPI'da tek bir A4 sayfası için bit eşlem kabaca 34 megapiksele çıkar ki bu 32 bitlik bir bit eşlem (bitmap) olarak yaklaşık 100 MB'tır; sıradan (ordinary) metin belgeleri için kalitedeki kazanım (gain) minimumdur ve sayfa başına bellek maliyeti (memory cost) önemlidir (significant). Gerçekten (genuinely) önemli olduğu basımevleri (print shops) veya vektör ağırlıklı (vector-heavy) teknik çizimler için 600 DPI'ı tutun

İkinci kod bloğundaki reAnnotations bayrağı (flag), rePrinting bayrağından bağımsızdır. Kullanıcı, pulların (stamps), vurguların (highlights) ve yorum kutularının (comment boxes) kağıtta görünmesini beklediğinde bunu dahil edin. Sadece içeriğe (content-only) yönelik çıktı (output) için atlayın (Omit it). Her iki bayrak da serbestçe (freely) birleştirilebilir

Sayfa döndürme

PDFium, sayfa dönüşünü (page rotation) PDF'te /Rotate girdisi (entry) olarak depolar ve TRotation değeri (ro0, ro90, ro180, ro270) döndüren Pdf.PageRotation aracılığıyla erişilebilir. Yazıcı koordinat sistemi, ekrana göre 90 ve 270 derecelik dönüşleri tersine çevirir (inverts). Ham (raw) PageRotation değerini herhangi bir ayarlama yapmadan doğrudan RenderPage'e geçirirseniz, dikey (portrait) bir belgeye gömülü (embedded) yatay (landscape) sayfalar çoğu Windows yazıcı sürücüsünde baş aşağı yazdırılacaktır. Düzeltme (fix), oluşturma çağrısından (render call) önce basit bir takastır (swap): ro90ro270'ye ve ro270'yi tekrar ro90'a eşleyin, ro0 ve ro180'i değişmeden (unchanged) bırakın

Nakliyeden (shipping) önce bu davranışı kendi özel hedef yazıcınızda (specific target printer) doğrulayın. Döndürme (rotation) etrafındaki sürücü davranışı tedarikçiler (vendors) arasında tekdüze (uniform) değildir ve bazı sürücüler GDI düzeyinde (level) kendi döndürme düzeltmelerini uygular. Çift döndürme (double rotation) görürseniz, takası (swap) kaldırın; hiçbir düzeltme görmezseniz ekleyin. Dönüşümlü (alternating) dikey (portrait) ve yatay (landscape) sayfalara sahip karışık yönelimli (mixed-orientation) bir belge, test sırasında her iki arıza modunu (failure mode) da yakalamanın en hızlı (quickest) yoludur

Uzun bir yazdırma işi (print job) boyunca bellek yönetimi (Memory management)

RenderPage'e yapılan her çağrı, çağıranın sahip olduğu ve serbest bırakması (must free) gereken yeni bir TBitmap tahsis eder (allocates). Yukarıdaki döngüde, try/finally Bitmap.Free bloğu (block) bunu bir kerede bir sayfa için (one page at a time) doğru bir şekilde işler. Sayfalar (pages) arasında bit eşlemleri (bitmaps) biriktirmeyin (accumulate): 200 sayfalık bir belgenin 300-DPI oluşturması (render), ilk sayfa biriktiriciye (spooler) ulaşmadan önce gigabaytlar (gigabytes) tüketir. Sonraki (next) sayfaya geçmeden (advancing) önce her bir bit eşlemi serbest bırakın (Free)

Aktarım (transfer) bloğu içindeki AllocMem / FreeMem çifti de (pair) aynı kuralı (rule) izler. GetDIBSizes size DIB başlığı (header) ve piksel verilerinin ne kadar belleğe ihtiyaç duyduğunu söyler; hepsini (all) bir sayfanın (page) kapsamı (scope) içinde ayırır (allocate), doldurur (fill), boyar (paint) ve serbest bırakırsınız (free). Her iki bloğun (block) sızdırmasına (leak) izin vermek, yazdırma işinin (print job), birkaç düzine (dozen) sayfadan daha uzun belgelerde (documents) işlem (process) yığınını (heap) tüketmesine (exhaust) neden olur

Bir arka plan iş parçacığında (background thread) yazdırma işlerini çalıştırmanız gerekiyorsa, TPdf'yi ve tüm VCL yazıcı (printer) çağrılarını (calls) aynı iş parçacığında (thread) tutun. TPdf'nin kendisi, PDFium DLL'nin küresel durumunu (global state) paylaşan (sharing) örnekler (instances) arasında iş parçacığı (thread) güvenli (thread-safe) değildir; en güvenli (safest) model, her biri (each) dosyanın kendi kopyasını yükleyen (loading) iş parçacığı başına bir TPdf'dir

Burada (here) gösterilen oluşturma (rendering) ve belge (document) API'si, Delphi ve C++Builder için PDFium Bileşeni'nin bir parçasıdır (part of)