Teknik Makale

losLab PDF Kütüphanesi ile Delphi'de RTF'den PDF'e

RTF, kimsenin planlamadığı yerlerde ortaya çıkacak kadar uzun ömürlüdür: eski rapor oluşturucular, adres mektup birleştirme boru hatları, modern kelime işlemcilerinden önceki yasal belge arşivleri. Bunu anında PDF'e dönüştürmek yinelenen bir gereksinimdir ve Windows'ta gerçekten işe yarayan yaklaşım özel bir RTF ayrıştırıcısı değil, Windows'un kendisinin TRichEdit ve EM_FORMATRANGE aracılığıyla zaten sağladığı işleme yoludur. losLab PDF Library, bu ardışık düzene doğrudan yerleşen sanal bir device context ortaya koyar

Mekanizma: sanal DC ve EM_FORMATRANGE

Rich Edit denetimleri içeriklerini yalnızca fiziksel bir yazıcı için değil, herhangi bir device context için sayfalara ayırabilir. EM_FORMATRANGE mesajı, denetime bir karakter aralığını belirli bir DC'ye yerleştirmesini söyler ve sığdırmayı başardığı son karakterin konumunu döndürür. Bunu art arda çağırın, her seferinde cpMin'i ilerletin; sayfa sayfa çıktı elde edersiniz. losLab PDF Library'nin GetCanvasDC'si, belirlediğiniz sayfa ölçülerine göre boyutlandırılmış bellek içi bir DC sağlar; bir sayfayı içine render ettikten sonra LoadFromCanvasDC, sonucu yeni tek sayfalık bir PDF belgesi olarak yakalar. Ardışık düzenin tamamı budur

En baştan doğru yapılması gereken bir şey var: TRichEdit denetimi hedef sayfayla eşleşecek şekilde boyutlandırılmalıdır. Denetim DC ölçülerinden küçük ya da büyükse sayfalandırma, PDF'ye girenle uyuşmaz. A4 çıktısı için standart yaklaşım, RTF dosyasını yüklemeden önce denetimin piksel ölçülerini, DC'yi boyutlandırmak için kullandığınız aynı milimetre-to-piksel dönüşümüyle, daha sonra LoadFromCanvasDC'ye geçireceğiniz DPI'da 210 x 297 mm'ye eşitlemektir

PDF: RTF'den PDF'ye hat: boyutlandırılmış bir Rich Edit denetimi, metnini EM_FORMATRANGE yoluyla sanal bir tuval DC'ye sayfalar ve her geçiş tek bir PDF sayfası olarak yakalanır
EM_FORMATRANGE, bir RTF metni aralığını sanal tuval DC'sine dizer ve LoadFromCanvasDc sonucu yakalar; son karaktere ulaşılanla dek yinelenir

Delphi uygulaması

Aşağıdaki kod, kütüphanenin ActiveX sürümü için type library içe aktarımı olan PDFlibAX_TLB içe aktarma birimini kullanır. Form bir TRichEdit ve bir düğme barındırır; formun OnCreate işleyicisi denetimi boyutlandırıp RTF'yi yükler, düğme tıklaması ise dönüştürme döngüsünü sürer

unit MainUnit;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, ComCtrls, PDFlibAX_TLB, ActiveX;

type
  TForm1 = class(TForm)
    RichEdit1: TRichEdit;
    Button1: TButton;
    procedure FormCreate(Sender: TObject);
    procedure Button1Click(Sender: TObject);
  private
    function PrintRtfBox(hDc: HDC; rtfBox: TRichEdit;
      FirstChar: Integer): Integer;
  end;

var
  Form1: TForm1;
  PdfDoc: IPDFlibrary;

implementation

{$R *.dfm}

procedure TForm1.FormCreate(Sender: TObject);
begin
  PdfDoc := CoPDFlibrary.Create;
  // Sayfalandırmanın DC ile eşleşmesi için denetimi ekran DPI'sında A4 boyutuna getirin
  RichEdit1.Width  := MulDiv(210, Screen.PixelsPerInch * 10, 254);
  RichEdit1.Height := MulDiv(297, Screen.PixelsPerInch * 10, 254);
  RichEdit1.Lines.LoadFromFile(
    ExtractFilePath(Application.ExeName) + 'document.rtf');
end;

procedure TForm1.Button1Click(Sender: TObject);
var
  Dc: HDC;
  PageNumber, LastChar, PdfDocId: Integer;
begin
  PageNumber := 1;
  LastChar   := 0;
  repeat
    // A4 boyutunda sanal bir DC alın
    Dc := PdfDoc.GetCanvasDC(
      MulDiv(210, Screen.PixelsPerInch * 10, 254),
      MulDiv(297, Screen.PixelsPerInch * 10, 254));
    // RTF içeriğinin sonraki sayfasını DC'ye render edin
    LastChar := PrintRtfBox(Dc, RichEdit1, LastChar);
    // DC içeriklerini bir PDF belgesi olarak yakalayın
    PdfDoc.LoadFromCanvasDC(Screen.PixelsPerInch, 0);
    PdfDocId := PdfDoc.SelectedDocument;
    PdfDoc.SaveToFile(
      ExtractFilePath(Application.ExeName)
      + 'Output' + IntToStr(PageNumber) + '.pdf');
    PdfDoc.RemoveDocument(PdfDocId);
    Inc(PageNumber);
  until LastChar = 0;
  // Denetimin DC için cache'lediği biçimlendirme verisini bırakmasını sağlayın
  SendMessage(RichEdit1.Handle, EM_FORMATRANGE, 0, 0);
end;

function TForm1.PrintRtfBox(hDc: HDC; rtfBox: TRichEdit;
  FirstChar: Integer): Integer;
var
  Fr: TFormatRange;
  Dpi, PageWidthTw, PageHeightTw, NextCharPosition: Integer;
begin
  // rc ile rcPage piksel değil twip'tir (1/1440 inç): sayfa boyutunu, DC'yi ve
  // denetimi boyutlandıran aynı DPI ile çevirin
  Dpi          := Screen.PixelsPerInch;
  PageWidthTw  := MulDiv(rtfBox.Width,  1440, Dpi);
  PageHeightTw := MulDiv(rtfBox.Height, 1440, Dpi);

  Fr.hdc         := hDc;
  Fr.hdcTarget   := hDc;
  Fr.rcPage      := Rect(0, 0, PageWidthTw, PageHeightTw);
  Fr.rc          := Fr.rcPage;  // bütün sayfaya çiz; kenar boşlukları için daraltın
  Fr.chrg.cpMin  := FirstChar;
  Fr.chrg.cpMax  := -1;

  NextCharPosition :=
    SendMessage(rtfBox.Handle, EM_FORMATRANGE, 1, LPARAM(@Fr));
  if NextCharPosition < Length(rtfBox.Text) then
    Result := NextCharPosition
  else
    Result := 0;  // son sayfayı işaret eder
end;

end.

Döngü ne yapıyor

PrintRtfBox, TFormatRange yapısını doldurur ve SendMessage aracılığıyla Zengin Metin denetimine geçirir. Denetim cpMin'den başlayarak karakterleri işler, DC dolduğunda durur ve sığmayan ilk karakterin konumunu döndürür. Dönüş değeri toplam metin uzunluğuna eşit veya bu uzunluğu aştığında, her karakter işlenmiş olur ve işlev sıfır döndürerek repeat...until döngüsünü sonlandırır

Her yineleme Output1.pdf, Output2.pdf diye giden bir PDF dosyası üretir. Bunun yerine tek bir çok sayfalı belge istiyorsanız, AddToFileList artı MergeFileList sayfa başına dosyaları sonradan bir araya getirebilir; ya da döngüyü, her yeni sayfa belgesini MergeDocument ile birincisine katlayacak şekilde yeniden kurabilirsiniz. Yukarıdaki yineleme başına SaveToFile ve ardından RemoveDocument deseni, tepe belleği bir sayfalık içerikte sınırlı tutar; çok uzun RTF dosyaları için bu önemlidir

İnsanları yanıltan boyutlandırma ayrıntıları

LoadFromCanvasDC'ye giden DPI bağımsız değişkeni, kütüphaneye kaç pikselin bir inç yaptığını söyler: sayfa, tuval genişliğinin DPI'ya bölümü kadar 72 punto genişlikte çıkar; yükseklik için de aynısı. Bunu yanlış alırsanız görüntü ekranda doğru görünürken metin çıktıda yanlış boyutta belirir

rc ve rcPage'in birimi, bu tekniğin çoğu Delphi örneğinin yanlış aldığı ayrıntıdır. EM_FORMATRANGE iki dikdörtgeni de, çizim yaptığı DC piksellerle çalışsa da twip cinsinden okur; inçin 1/1440'ı. Onları piksel değerleriyle doldurursanız denetim, 96 DPI'da on beş kat küçük bir sayfaya metin dizer: geçiş başına bir avuç sözcük ve on sayfalık bir belgeyi yüzlerce neredeyse boş PDF'ye çeviren bir döngü. DC'yi boyutlandıran DPI ile MulDiv(Pixels, 1440, Dpi) üzerinden çevirmek, sayfa kırılımını tuvalin gerçekte bittiği yere koyar. rcPage bütün sayfadır; rc metnin işgal edebileceği alandır, dolayısıyla kenar boşlukları rc'nin onun içinde daraltılmasından gelir; o da twip ile. Döngü bittiğinde nil lParam'lı bir EM_FORMATRANGE daha, denetime DC için cache'lediği biçimlendirme verisini serbest bırakmasını söyler

PDF: EM_FORMATRANGE için iç içe rcPage ve rc dikdörtgenleri; sınır çizgilerinin sayfalar boyunca çoğaltılmasını önleyen fazladan yüz piksellik kenar boşluğuyla
rcPage, bileşenin çizim dikdörtgeninin ötesine uzanır; böylece sayfa kırılımına düşen bir satır bölünemez ya da çoğaltılamaz

Son olarak, denetim FormCreate çalıştırıldığında görünür bir form penceresine eklenmiş olmalıdır, böylece pencere tanıtıcısı SendMessage'a ilk çağrı yapılmadan önce geçerli olur. Çalışma zamanında dinamik olarak oluşturulan bir TRichEdit form henüz gösterilmediyse işleme döngüsü başlamadan önce açık bir HandleNeeded çağrısına ihtiyaç duyar

Yazı tiplerini ve RTF özelliklerini işleme

Oluşturma Windows Rich Edit motoru tarafından yapıldığı için, yazı tipi değiştirme aynı görüntüleme ve yazdırma kurallarını izler. Makinede yüklü olan RTF dosyasında başvurulan yazı tipleri aslına uygun olarak oluşturulur; eksik olan yazı tipleri sessizce değiştirilir, bu da satır uzunluklarını ve sayfalandırmayı değiştirebilir. Üretim toplu dönüştürmesi için bunu açıkça test etmeye değer: RTF kaynaklarınızın kullandığı her bir yazı karakteri ile bir belge yükleyin ve çıktı sayfa sayısının manuel bir baskı önizlemesinden beklediğinizle eşleştiğini onaylayın

Tablolar, gömülü görüntüler ve çoğu Zengin Metin biçimlendirme özelliği, Rich Edit onları doğal olarak render ettiği için ek işlem olmadan çalışır. Şaşırtabilecek tek alan, twip cinsinden ifade edilen özel paragraf aralığı ya da ilk satır girintisi kullanan metindir: Rich Edit twip (1/1440 inç) ile ölçer, TFormatRange'deki dikdörtgenlerle aynı birim; altındaki DC ise geçerli DPI'da piksellerle çalışır. Denetim çizerken birini diğerine eşler; ama RTF'yi programatik kuruyorsanız kenar boşluğu değerlerinizin de twip olduğunu doğrulamalısınız

DPI farkındalığı ve yüksek DPI ekranlar

%150 ölçeklemeyle (144 DPI) çalışan bir ekranda, milimetre-to-piksel dönüşümü %100'lük ekrandan daha büyük bir piksel sayısı döndürür. PDF Library, GetCanvasDC'ye geçirdiğiniz piksel ölçülerini kaydeder ve PDF'deki fiziksel sayfa boyutunu geriye hesaplamak için LoadFromCanvasDC'nin DPI bağımsız değişkenini kullanır. Geçirdiğiniz DPI değeri, milimetreleri piksele çevirirken kullandığınız değer olduğu sürece çıktı sayfa boyutu, ekran ölçeklemesinden bağımsız olarak doğrudur

Uygulamanız DPI-farkında değilse (eski varsayılan), Windows ekran DC'sini ölçekler ve piksel hesaplarınız yüksek DPI makinelerde yanlış olur. En basit çözüm, uygulama manifestosunda DPI farkındalığı bildirmektir; uygulama böylece gerçek aygıt piksellerini alır ve örneğin LoadFromCanvasDC'ye geçirdiği Screen.PixelsPerInch değeri, %100 ölçekleme varsaymak yerine gerçek ekran DPI'sını bildirir

Çıktı yapısı: tek bir birleştirilmiş belgeye karşı sayfa başına bir dosya

Yukarıdaki döngü her sayfayı ayrı bir PDF dosyasına yazar. Bunun istediğiniz şey olup olmadığı akış aşağısı kullanıma bağlıdır. Rapor üretim sistemleri çoğu zaman tek tek sayfalara ihtiyaç duyar; çünkü nihai belgeyi daha sonra sayfaları birleştirerek ya da yeniden sıralayarak kurarlar. Baştan tek bir PDF istiyorsanız kütüphane onu tek oturumda kurmanıza izin verir: ilk LoadFromCanvasDC çağrısının oluşturduğu belgenin ID'sini saklayın, sonraki her geçişte onu SelectDocument ile seçin ve SaveToFile yerine yeni sayfanın belge ID'siyle MergeDocument çağırın; sonra döngü çıktıktan sonra bir kez kaydedin. Bu, ara dosyaları ortadan kaldırır ve çoğu tek belgeli dönüştürme senaryosu için doğru yapıdır

PDF: Sonraki birleştirme için geçiş başına bir PDF dosyası yazmak ile döngü içinde sayfa ekleyerek tek bir çok sayfalı PDF inşa etmek arasında seçim
Aynı işleme döngüsü ya ayrı Output dosyalarını ya da tek bir büyüyen belgeyi besler; akıntı aşağısı esnekliği ara dosyalara takas eder

Büyük RTF dosyaları için, dönüştürme hızı kabaca sayfa sayısıyla orantılı olduğundan ve 200 sayfalık bir belge birkaç saniye sürebildiğinden döngüye bir ilerleme geri bildirimi eklemeye değer. repeat...until yapısının genişletilmesi kolaydır: her yinelemeden sonra RichEdit1.GetTextLen ile toplam karakter sayısına bölünen LastChar değerini kullanarak bir ilerleme çubuğu güncellemesinde karakter sapmasını izleyin

Burada gösterilen GetCanvasDC ve LoadFromCanvasDC yöntemleri, Delphi ve C++Builder için losLab PDF Library'nin parçasıdır