Teknik Makale

PDF Form Değerlerini Yazdırın, Sayfa Oranlarını Koruyun

Bir kullanıcı, Delphi tabanlı bir viewer'da PDF formunu doldurur, Yazdır'a basar ve kağıt, her alanı boş, Letter boyutundaki sayfa A4'ü dolduracak şekilde gerilmiş olarak çıkar. İki kusur da yazdırma döngüsündedir: PDFium Component, alan değerlerini yalnızca bir bitmap'e boyar (ya da düzleştirilmiş bir kopyaya pişirir) ve her sayfa, eni boyunu koruyan tek bir ölçek çarpanı ister

Form katmanı, bitmap isteyen ikinci bir geçiştir

Windows'ta device-context overload'u TPdf.RenderPage(DC, Left, Top, Width, Height, Rotation, Options), yazıcının HDC'sini doğrudan FPDF_RenderPage'e verir. O çağrı sayfa içeriğini boyar; reAnnotations kuruluysa işaretleme annotation'larını da. Etkileşimli form katmanı DC'ye hiç ulaşmaz — o, PDFium'un form-fill ortamına aittir ve FPDF_FFLDraw tarafından çizilir; onun tek hedefi bir FPDF_BITMAP'dir. HDC varyantı yoktur. TPdf.RenderPage'in iki bitmap overload'u, FPDF_RenderPageBitmap ile FPDF_FFLDraw'u — belge FormFill True ile, yani varsayılanla, açılmışsa — aynı piksellerde çalıştırır. Seçeneklerde rePrinting varken PDFium Component, form geçişinden önce alan vurgusunu kaldırır ve sonra varsayılan vurgu rengini ile alfasını geri getirir; böylece düzenlenebilir alanların etrafındaki renkli kutular ekranda kalır. rePrinting ayrıca motorun FPDF_PRINTING bayrağına eşlenir; bu bayrak çıktıyı yazdırma olarak işaretler — ISO 32000-1 §12.5.3'ün (Table 165) annotation bayrağının 3. bitiyle ele aldığı durum budur: kurallara uyan bir okuyucu, Print biti temiz bir annotation'ı ekranda nasıl görünüyorsa görünsün kağıda koymaz

Geri kalanına platform karar verir. PDFium motorunda FPDF_RenderPage, Windows GDI üzerinden bir device context'e çizer ve yalnızca Windows derlemelerinde vardır; FPDF_FFLDraw ise, her platformda, başka hiçbir şeye değil yalnızca bir bitmap'e çizer. Linux'ta ve öteki Windows dışı hedeflerde GDI çağrısı yapacak bir şey yoktur; bu yüzden HDC overload'u, bir bitmap render edilerek — form katmanı dahil — handle'ın arkasındaki canvas üzerine çizilerek karşılanır. Dolayısıyla device-context çağrısı, alan değerlerini Windows dışında kağıda taşır, Windows'ta ise dışarıda bırakır. Delphi ile Lazarus arasında paylaşılan kodda bu dalı açık tutun: belgenin bir form ortamı varsa ve değerleri kağıda gitmek zorundaysa, her platformda bitmap isteyin

Delphi'de PDFium Component ile form alanlarını yazdırmak: yazıcı HDC'sinde TPdf.RenderPage, form katmanı olmadan FPDF_RenderPage çağırır; TBitmap overload'u ise StretchDIBits'ten önce bir FPDF_FFLDraw geçişi ekler
Windows'ta yalnızca RenderPage'in bitmap overload'u FPDF_FFLDraw form geçişini çalıştırır; yazılan alan değerleri bu yüzden o yolda kağıda ulaşır, device-context yolunda kaybolur

Bir yazdırma işi hangi yolu izlemeli?

Kağıt neyi taşıyacaksa ona göre seçin. Canlı form değerleri bitmap yoluna, vektör çıktı HDC yoluna işaret eder; düzleştirilmiş bir kopya ise ikisini birden verir — bir kaydetme ve bir yeniden yükleme bedeline. SetPdfPrintMode kütüphane geneldir; işler arasında değiştirin, sayfalar arasında asla

YolKağıtta form değerleriÇıktıMaliyet
RenderPage(DC, …)Hayır, Windows'taVektör GDI; SetPdfPrintMode, EMF'i (varsayılan) ya da bir PostScript seviyesini seçerEn küçük kuyruk, en hızlı
RenderPage, bir TBitmap'e; ardından StretchDIBitsEvet, kaydedilmemiş düzenlemeler dahilİstediğiniz piksel boyutunda rasterSayfa başına Width × Height × 4 bayt, artı DIB kopyası
RenderPageBandedEvetRaster, bir kerede bir şeritBütün sayfa yerine bellekte bir bant
Düzleştirilmiş kopya, ardından RenderPage(DC, …)Evet, pişirilmiş sayfa içeriği olarakVektörKaydetme, yeniden yükleme, GenerateFormAppearances, FlattenAllPages(FLAT_PRINT)

600 dpi'da tam bir A4 sayfası kabaca 4960 × 7016 aygıt pikselidir ve piksel başına 32 bitlikte bu, tek bir sayfa için 139,197,440 bayt demektir; onu StretchDIBits için bir DIB'e kopyalamak zirveyi ikiye katlar — 32-bit bir süreç için ağır bir yük. Bir viewer'ın Yazdır düğmesi için bitmap yolu yine de doğru varsayılandır: alanına bir şey yazıp Yazdır'a basan kullanıcı, o değeri kağıtta görmeyi bekler ve banding belleği sınırlı tutar. Arşivlenen ekstreler gibi uygulamadan çıkan belgelerse düzleştirilmiş kopyayla daha iyi hizmet alır — değerler sıradan sayfa içeriğine dönüşür ve yazıcı vektör çıktı alır

uses
  System.SysUtils, System.IOUtils, PDFium, FPdfUtils;  // FPdfUtils: FLAT_PRINT

function OpenFlattenedPrintCopy(Source: TPdf; out TempName: string): TPdf;
begin
  TempName := TPath.Combine(TPath.GetTempPath, TPath.GetRandomFileName + '.pdf');
  if not Source.SaveAs(TempName) then
    raise Exception.Create('Could not write the print copy');
  Result := TPdf.Create(nil);
  try
    Result.FileName := TempName;   // FormFill True kalır (varsayılan)
    Result.Password := Source.Password;  // kopya şifrelemesini korur
    Result.Active := True;
    if not Result.Active then      // başarısız yüklemede TPdf etkin kalmaz
      raise Exception.Create('Could not reopen the print copy');
    Result.GenerateFormAppearances;          // güncel /V'den /AP'yi yeniden kurar
    if not Result.FlattenAllPages(FLAT_PRINT) then
      raise Exception.Create('Flattening failed');
  except
    Result.Free;
    TFile.Delete(TempName);
    raise;
  end;
end;

Düzleştirme yolunun, bayraktan daha önemli bir sıra kuralı vardır. FlattenAllPages, her widget'ın appearance stream'ini (ISO 32000-1 §12.5.5) sayfa içeriğine pişirir ve bir appearance stream, son üreticinin yazdığı her neyse odur; /V'deki alan değerinin bunda payı yoktur. Görünümlerini hiç yeniden üretmemiş bir araçla doldurulmuş bir form, eski metne ya da hiçbir şeye düzleşir; bu yüzden GenerateFormAppearances önce koşar: text-field görünümlerini güncel değerlerden yeniden kurar ve checkbox ile radio /AS durumlarını eşitler; bunun için de belgenin FormFill True ile açılmış olması gerekir. Bütün bunları bir kopyada yapın; çünkü düzleştirme, üzerinde koştuğu TPdf'in sayfalarını yeniden yazar ve açık bir viewer, alanlarının düzenlenebilir kalmasını ister. Kopya, aşağıdaki yazdırma rutininin vektör kolundan form katmanı kapatılmış olarak geçer; iş bitince serbest bırakın ve geçici dosyayı silin

Delphi'de PDFium Component düzleştirilmiş yazdırma kopyası: geçici bir dosyaya SaveAs, FormFill ile yeniden açma, GenerateFormAppearances, FlattenAllPages(FLAT_PRINT) ve ardından device context üzerinde RenderPage ile vektör yazdırma
GenerateFormAppearances'i FlattenAllPages'ten önce çağırmak, pişirilen appearance stream'lerini son üreticinin /AP içinde bıraktığı her neyse onun yerine güncel alan değerleriyle eşitler

Her sayfayı yazdırılabilir alana sığdırmak

Bir kez ölçekleyin, iki çarpanın küçüğüyle, ve sonucu ortalayın. Printer.PageWidth × Printer.PageHeight'a gerilmek, biçimi kağıttan farklı olan her sayfayı — A4 üzerinde bir Letter sayfasını (612 × 792 point) ya da dikey bir kağıtta yatay sayfayı — bozar. Yazıcı tarafı, aygıt piksellerinde yazdırılabilir alandır (HORZRES ile VERTRES); sürücü marjları çoktan dışarıda. PDF tarafında TPdf.PageWidth ile PageHeight, sayfayı zaten görüntülendiği gibi raporlar: /Rotate 90 ile saklanmış bir A4 dikey sayfa 842 × 595 point olarak geri gelir. ISO 32000-1 §7.7.3.3 (Table 30), sayfanın görüntülenirken ya da yazdırılırken /Rotate ile döndürülmesi gerektiğini söyler; PDFium bunu render çağrısının içinde uygular ve Rotation argümanı — RenderPage çağrısında — yalnızca viewer'ın görünümü döndürdüğünde eklediği ek turdur. Oraya sayfanın kendi PageRotation'ını geçerseniz sayfa iki kez döner. Genişlik ile yüksekliği yalnızca o ek döndürme 90 ya da 270 dereceyken takas edin — döndürme altında thumbnail sığdırma ve zoom'u yöneten aynı kural çifti; aşağıdaki rutin, PDFium Component ile PDF belgelerini yazdırmak temel döngüsünü bir form-değeri dalı ve işi çöpe atan bir iptal yoluyla genişletir

Delphi yazdırmasında PDFium Component sayfa sığdırma: Printer.PageWidth ile PageHeight'a gerilen bir Letter sayfası karşısında, yazdırılabilir alanda ortalanmış en-boy koruyan tek ölçek; /Rotate, TPdf.PageWidth içinde zaten uygulanmış
İki çarpanın küçüğüyle ölçeklemek her sayfayı bozulmamış ve ortalı tutar; genişlik ile yükseklik yalnızca ek bir ro90 ya da ro270 viewer döndürmesinde takas edilir
uses
  Winapi.Windows, System.SysUtils, Vcl.Graphics, Vcl.Forms, Vcl.Printers, PDFium;

procedure FitRect(PageW, PageH: Double; Swap: Boolean;
  AreaW, AreaH: Integer; out X, Y, W, H: Integer);
var
  T, Scale: Double;
begin
  if Swap then
  begin
    T := PageW; PageW := PageH; PageH := T;
  end;
  if (PageW <= 0) or (PageH <= 0) then
  begin
    X := 0; Y := 0; W := AreaW; H := AreaH;
    Exit;
  end;
  Scale := AreaW / PageW;
  if AreaH / PageH < Scale then
    Scale := AreaH / PageH;
  W := Round(PageW * Scale);
  H := Round(PageH * Scale);
  X := (AreaW - W) div 2;
  Y := (AreaH - H) div 2;
end;

procedure BlitBitmap(DC: HDC; Bitmap: TBitmap; X, Y: Integer);
var
  InfoSize, ImageSize: DWORD;
  Info: PBitmapInfo;
  Bits: Pointer;
begin
  GetDIBSizes(Bitmap.Handle, InfoSize, ImageSize);
  Info := AllocMem(InfoSize);
  try
    Bits := AllocMem(ImageSize);
    try
      GetDIB(Bitmap.Handle, 0, Info^, Bits^);
      StretchDIBits(DC, X, Y, Bitmap.Width, Bitmap.Height, 0, 0,
        Bitmap.Width, Bitmap.Height, Bits, Info^, DIB_RGB_COLORS, SRCCOPY);
    finally
      FreeMem(Bits);
    end;
  finally
    FreeMem(Info);
  end;
end;

function PrintPages(Pdf: TPdf; FromPage, ToPage: Integer; FormLayer: Boolean;
  ExtraRotation: TRotation; var Cancel: Boolean): Boolean;
const
  Opts: TRenderOptions = [reAnnotations, rePrinting];
var
  Page, X, Y, W, H: Integer;
  Bitmap: TBitmap;
begin
  Result := False;
  Printer.BeginDoc;
  try
    for Page := FromPage to ToPage do
    begin
      if Page > FromPage then
        Printer.NewPage;
      Pdf.PageNumber := Page;
      FitRect(Pdf.PageWidth, Pdf.PageHeight, ExtraRotation in [ro90, ro270],
        Printer.PageWidth, Printer.PageHeight, X, Y, W, H);
      if FormLayer then
      begin
        Bitmap := Pdf.RenderPage(0, 0, W, H, ExtraRotation, Opts);
        if Bitmap <> nil then          // bozuk sayfa boyutu için nil
        try
          BlitBitmap(Printer.Canvas.Handle, Bitmap, X, Y);
        finally
          Bitmap.Free;
        end;
      end
      else
        Pdf.RenderPage(Printer.Canvas.Handle, X, Y, W, H, ExtraRotation, Opts);
      Application.ProcessMessages;     // İptal düğmesinin bayrağı kurmasına izin verir
      if Cancel then
        Break;
    end;
    Result := not Cancel;
  finally
    if Result then
      Printer.EndDoc                   // işi kuyruklayıcıya teslim eder
    else
      Printer.Abort;                   // iptal ya da istisna: hiçbir şey yazdırılmaz
  end;
end;

Bir viewer bunu, Pdf.FormFill'i FormLayer olarak geçirerek çağırır; düzleştirilmiş kopya False geçirir. EndDoc bir temizlik adımı değil, taahhüdün kendisidir. BeginDoc'tan sonra Printer.EndDoc, o ana dek render edilen her şeyi geçerli bir iş olarak kuyruklayıcıya verir; dolayısıyla koşulsuz bir EndDoc, finally içinde, iptal edilmiş bir yazdırmayı yarım yazdırılmışa çevirir. Rutin, kararı finally içinde başarı bayrağından okur: son sayfadan sonra EndDoc, iptal ya da istisnadan sonra Abort. Döngü çıkışı da benzer bir özen ister; çünkü Delphi ile Free Pascal, bir for döngüsünün bitiş değerini girişte bir kez hesaplar. Bir İptal işleyicisinden ToPage'i ya da kopya sayısını sıfıra çekmek, hâlâ koşan bir döngüyü durdurmaz; bayrağı sınayın ve Break alın, iç içe kopya ve harmanlama döngülerinde her seviyeden çıkın. Application.ProcessMessages menü kısayollarının da tetiklenmesine izin verdiği için, iş koşarken Open, Save ve Print'i kapatın

Raster yolunu bellek içinde tutmak

TPdf.RenderPageBanded, bir sayfayı seçtiğiniz yükseklikteki yatay şeritler halinde render eder ve her şeridi bir callback'e verir. Her şerit, tam boyutlu sayfanın bir dilimidir — hiçbir şey yeniden ölçeklenmez — ve tam sayfalık bir bitmap'le aynı alan değerlerini taşır. 512 satırlık bantlarla, 4960 piksel genişliğinde bir sayfa bant başına kabaca 10 MB ister; sayfanın 139 MB'ına karşı. Kabaca belleğin on dörtte biri. TPdfBandCallback, bant indeksini, bandın sayfa pikselleri cinsinden üst ofsetini ve bitmap'i alan bir method pointer'dır ve devam etmek için True döndürür; RenderPageBanded bant sayısını, başarısızlıkta 0'ı döndürür

type
  TBandPrinter = class
  private
    FDC: HDC;
    FLeft, FTop: Integer;
  public
    constructor Create(DC: HDC; Left, Top: Integer);
    function PrintBand(BandIndex, BandTopY: Integer; Bitmap: TBitmap): Boolean;
  end;

constructor TBandPrinter.Create(DC: HDC; Left, Top: Integer);
begin
  inherited Create;
  FDC := DC;
  FLeft := Left;
  FTop := Top;
end;

function TBandPrinter.PrintBand(BandIndex, BandTopY: Integer;
  Bitmap: TBitmap): Boolean;
begin
  BlitBitmap(FDC, Bitmap, FLeft, FTop + BandTopY);
  Result := True;                      // False, kalan bantları durdurur
end;

// sayfa döngüsünden, tam sayfalık bitmap dalının yerine çağrılır
procedure PrintPageBanded(Pdf: TPdf; Page, X, Y, W, H: Integer;
  ExtraRotation: TRotation);
var
  Bands: TBandPrinter;
begin
  Bands := TBandPrinter.Create(Printer.Canvas.Handle, X, Y);
  try
    if Pdf.RenderPageBanded(W, H, 512, Bands.PrintBand, ExtraRotation,
      [reAnnotations, rePrinting]) = 0 then
      raise Exception.CreateFmt('Page %d could not be rendered', [Page]);
  finally
    Bands.Free;
  end;
end;

Konak, ViewerPreferences ile izin bitlerine ne yapmalı?

Kendi kodunuzda okuyun ve ona göre davranın; PDFium bunları raporlar ve sayfayı koşulsuz render eder. Bir üretici, kataloğun /ViewerPreferences sözlüğüne (ISO 32000-1 §12.2, Table 150) yazdırma ipuçları saklayabilir ve TPdf, yazdırma penceresini biçimlendirenleri ortaya çıkarır:

  • PrintScaling (Boolean), dosya /PrintScaling /None diyorsa — yani gerçek boyutta yazdırma demekse — False'tur; /AppDefault için ya da girdi hiç yokken True'dur. Standart, yazdırma penceresi hiç gösterilmese bile buna uyulmasını ister
  • PrintPaperHandling (TPrintPaperHandling), /Duplex'i phSimplex, phDuplexFlipShortEdge, phDuplexFlipLongEdge ya da phUndefined olarak taşır. SetPdfPrintPaperHandlingDevMode, bunu DEVMODE.dmDuplex içine yazar ve DM_DUPLEX'i kurar; phUndefined için False döndürür ve kayda dokunmaz, böylece sürücü varsayılanı sağ kalır
  • PrintCopies (Integer), /NumCopies'dir; pencereye yazılacak kopya sayısı

İzin bitleri aynı kuralı izler. Standart güvenlik işleyicisi için ISO 32000-1 §7.6.3.2 (Table 22), /P'nin 3. bitini yazdırmaya ve revision 3 ile sonraki işleyicilerde 12. biti tam sadakatle yazdırmaya ayırır; 3. bit kurulu, 12. bit temizken yazdırma, görünümün düşük düzeyli bir temsiliyle — muhtemelen kalite kaybıyla — sınırlıdır. TPdf.GrantedPermissions maskeyi permPrint ile permPrintHighQuality'ye çözer, HasPermission tek bir değeri sınar; motor ne istersek onu render eder, o yüzden zorunlu kılma konağın işidir — çoğunlukla kapatılmış bir Yazdır düğmesi. Yalnızca permPrint verilmişse, standartla uyuşan okuma, tam iznin açtığı vektör ya da PostScript çıktı değil, mütevazı bir çözünürlükte bitmap yoludur. Bitler belgeyi açıldığı haliyle anlatır: owner şifresiyle açılan bir dosya tam erişim raporlar ve UserHasPermission yalnızca kullanıcı düzeyi bitler için cevap verir

uses
  Winapi.Windows, System.SysUtils, Vcl.Dialogs, Vcl.Printers, PDFium;

procedure PreparePrintJob(Pdf: TPdf; Dialog: TPrintDialog;
  out ActualSize, HighQuality: Boolean);
var
  Device, Driver, Port: array[0..MAX_PATH] of Char;
  DevModeHandle: THandle;
  DevMode: PDeviceMode;
begin
  if not Pdf.HasPermission(permPrint) then
    raise Exception.Create('This document does not permit printing');
  HighQuality := Pdf.HasPermission(permPrintHighQuality);
  ActualSize := not Pdf.PrintScaling;            // /PrintScaling /None

  Printer.GetPrinter(Device, Driver, Port, DevModeHandle);
  if DevModeHandle <> 0 then
  begin
    DevMode := GlobalLock(DevModeHandle);
    try
      SetPdfPrintPaperHandlingDevMode(DevMode^, Pdf.PrintPaperHandling);
    finally
      GlobalUnlock(DevModeHandle);
    end;
    Printer.SetPrinter(Device, Driver, Port, DevModeHandle);  // pencere buradan başlar
  end;

  Dialog.MinPage := 1;
  Dialog.MaxPage := Pdf.PageCount;
  Dialog.Options := Dialog.Options + [poPageNums];
  if Pdf.PrintCopies > 1 then
    Dialog.Copies := Pdf.PrintCopies;
end;

// ActualSize True iken FitRect yerine kullanılır
procedure ActualSizeRect(Pdf: TPdf; out X, Y, W, H: Integer);
begin
  // PDF tarafında inç başına 72 point, yazıcıda inç başına aygıt pikseli
  W := Round(Pdf.PageWidth / 72 * GetDeviceCaps(Printer.Handle, LOGPIXELSX));
  H := Round(Pdf.PageHeight / 72 * GetDeviceCaps(Printer.Handle, LOGPIXELSY));
  X := (Printer.PageWidth - W) div 2;  // sayfa yazdırılabilir alandan büyükken
  Y := (Printer.PageHeight - H) div 2; // negatif olur: kenarlar kırpılır
end;

Will-print ile did-print eylemlerini gerçek yazdırma sınırında koşturmak

Belge düzeyindeki yazdırma script'leri, konak onları koşturduğunda koşar. Kataloğun additional-actions sözlüğü (ISO 32000-1 §12.6.3, Table 197), yazdırmadan önce icra edilen bir JavaScript eylemi olan WP'yi ve yazdırmadan sonra icra edilen DP'yi tanımlar. PDFium Component ikisini de ExecuteDocumentLifecycleAction(dlaWillPrint, ErrorMessage) ile dlaDidPrint üzerinden açar ve render çağrılarının dışında tutar: thumbnail'ler, anlık görüntüler ve dönüştürmeler de render eder ve hiçbiri bir belge script'ine kullanıcının yazdırması gibi görünmemelidir. Sırayı doğru kurmak size düşer. Tarih damgası vuran ya da "printed by" alanını dolduran bir will-print script'i form değerlerini değiştirir; bu yüzden onu ilk sayfa render edilmeden önce, düzleştirme yolunda ise kopya kaydedilmeden önce koşturun; did-print'i yalnızca EndDoc işi taahhüt ettikten sonra ateşleyin. Çağrı, form ortamı yokken ya da LifecycleActionPolicy reddederken mesajla False döndürür; güvenilmeyen dosyalar için lapDisabled, belge eylemlerini kapatır. Daha geniş script'leme resmi, konak olaylarıyla AcroForm JavaScript koşturmak yazısındadır

Windows'ta form değerlerini taşımak zorunda olan bir iş, bitmap koluna aittir: sıradan çözünürlüklerde RenderPage, bir TBitmap'e; 600 dpi ve üzerinde RenderPageBanded. PDFium Component'in device-context overload'unu düzleştirilmiş kopyalar için ve alanı olmayan belgeler için saklayın; orada vektör çıktısı daha iyi sonuçtur