Artikel Teknis

Konversi RTF ke PDF di Delphi dengan Pustaka PDF losLab

RTF sudah ada cukup lama sehingga muncul di tempat-tempat yang tidak direncanakan siapa pun: generator laporan lama, pipeline gabungan surat, arsip dokumen hukum yang sudah ada sebelum word processor modern. Mengonversinya ke PDF secara langsung adalah kebutuhan yang berulang, dan pendekatan yang benar-benar berhasil di Windows bukanlah parser RTF khusus melainkan jalur rendering yang sudah disediakan Windows melalui TRichEdit dan EM_FORMATRANGE. Edisi DLL dari losLab PDF Library mengekspos device context virtual yang terhubung langsung ke pipeline tersebut

Mekanismenya: DC virtual dan EM_FORMATRANGE

Kontrol Rich Edit dapat membuat paginasi kontennya untuk device context apa pun, bukan hanya printer fisik. Pesan EM_FORMATRANGE memerintahkan kontrol untuk menata letak sekumpulan karakter ke DC yang diberikan dan mengembalikan posisi karakter terakhir yang berhasil dimuat. Panggil berulang kali, majukan cpMin setiap kali, dan Anda mendapatkan output halaman demi halaman. GetCanvasDC milik losLab PDF Library menyediakan DC dalam memori yang berukuran sesuai dimensi halaman yang Anda tentukan; setelah merender sebuah halaman ke dalamnya, LoadFromCanvasDc menangkap hasilnya sebagai halaman PDF. Itulah seluruh pipeline-nya

Satu hal yang perlu diperhatikan sejak awal: kontrol TRichEdit harus diubah ukurannya agar sesuai dengan halaman target. Jika kontrol lebih kecil atau lebih besar dari dimensi DC, paginasi tidak akan selaras dengan apa yang ada di PDF. Untuk output A4, pendekatan standar adalah mengatur dimensi piksel kontrol agar sesuai dengan 210 x 297 mm pada 96 DPI sebelum memuat file RTF, menggunakan pembantu skala yang sama yang akan Anda gunakan untuk mengatur ukuran DC

Implementasi Delphi

Berikut ini menggunakan unit impor PDFlibAX_TLB, yang membungkus edisi DLL dari pustaka. Form menampung sebuah TRichEdit dan sebuah tombol; handler OnCreate form mengatur ukuran kontrol dan memuat RTF, dan klik tombol menjalankan loop konversi

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: TPDFLibrary;

implementation

{$R *.dfm}

procedure TForm1.FormCreate(Sender: TObject);
begin
  PdfDoc := TPDFLibrary.Create(Self);
  // Size the control to A4 at screen DPI so pagination matches the DC
  RichEdit1.Width  := Round(ScaleX(210, mmPixel));
  RichEdit1.Height := Round(ScaleY(297, mmPixel));
  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
    // Obtain a virtual DC sized to A4
    Dc := PdfDoc.GetCanvasDC(
      Round(ScaleX(210, mmPixel)),
      Round(ScaleY(297, mmPixel)));
    // Render the next page of RTF content into the DC
    LastChar := PrintRtfBox(Dc, RichEdit1, LastChar);
    // Capture the DC contents as a PDF document
    PdfDoc.LoadFromCanvasDc(96, 0);
    PdfDocId := PdfDoc.SelectedPdfDocument;
    PdfDoc.SaveToFile(
      ExtractFilePath(Application.ExeName)
      + 'Output' + IntToStr(PageNumber) + '.pdf');
    PdfDoc.RemovePdfDocument(PdfDocId);
    Inc(PageNumber);
  until LastChar = 0;
end;

function TForm1.PrintRtfBox(hDc: HDC; rtfBox: TRichEdit;
  FirstChar: Integer): Integer;
var
  RcDrawTo, RcPage: TRect;
  Fr: TFormatRange;
  NextCharPosition: Integer;
begin
  RcPage.Left   := 0;
  RcPage.Top    := 0;
  RcPage.Right  := rtfBox.Left + rtfBox.Width  + 100;
  RcPage.Bottom := rtfBox.Top  + rtfBox.Height + 100;

  RcDrawTo.Left   := rtfBox.Left;
  RcDrawTo.Top    := rtfBox.Top;
  RcDrawTo.Right  := rtfBox.Left + rtfBox.Width;
  RcDrawTo.Bottom := rtfBox.Top  + rtfBox.Height;

  Fr.hdc         := hDc;
  Fr.hdcTarget   := hDc;
  Fr.rc          := RcDrawTo;
  Fr.rcPage      := RcPage;
  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;  // signals last page
end;

end.

Apa yang dilakukan loop

PrintRtfBox mengisi struktur TFormatRange dan meneruskannya ke kontrol Rich Edit melalui SendMessage. Kontrol merender karakter mulai dari cpMin, berhenti saat DC penuh, dan mengembalikan posisi karakter pertama yang tidak muat. Saat nilai kembalian sama dengan atau melebihi panjang teks total, setiap karakter telah dirender dan fungsi mengembalikan nol, yang mengakhiri loop repeat...until

Setiap iterasi menghasilkan satu file PDF bernama Output1.pdf, Output2.pdf, dan seterusnya. Jika Anda menginginkan satu dokumen multi-halaman, API penambahan halaman dari pustaka memungkinkan Anda merakitnya setelah selesai, atau Anda dapat merestrukturisasi loop untuk memanggil AddPage dalam satu sesi dokumen. Pola SaveToFile per iterasi diikuti oleh RemovePdfDocument di atas menjaga memori puncak dibatasi pada konten satu halaman, yang penting untuk file RTF yang sangat panjang

Detail ukuran yang sering menjebak

Argumen 96 DPI untuk LoadFromCanvasDc memberitahu pustaka pada resolusi layar berapa DC dirender, sehingga dapat menghitung pemetaan poin-ke-piksel yang benar untuk halaman PDF. Jika nilai ini salah, teks akan muncul pada ukuran yang salah dalam output meskipun gambar terlihat benar di layar

Nilai +100 yang ditambahkan ke RcPage.Right dan RcPage.Bottom adalah margin kecil melampaui tepi kontrol yang terlihat. Rich Edit menggunakan rect rcPage untuk memutuskan di mana memisahkan halaman; tanpa margin, baris yang jatuh tepat di batas dapat digandakan di dua halaman. Ini bukan konstanta ajaib: Anda ingin cukup besar agar batas halaman jatuh dengan bersih di dalam area tata letak kontrol daripada pada piksel terakhir

Terakhir, kontrol harus sudah terpasang ke jendela form yang terlihat saat FormCreate berjalan agar handle jendela valid sebelum panggilan pertama ke SendMessage. Sebuah TRichEdit yang dibuat secara dinamis saat runtime memerlukan panggilan HandleNeeded eksplisit sebelum loop render dimulai jika form belum pernah ditampilkan

Menangani font dan fitur RTF

Karena rendering dilakukan oleh mesin Windows Rich Edit, substitusi font mengikuti aturan yang sama yang digunakannya untuk tampilan dan pencetakan. Font yang direferensikan dalam file RTF yang terinstal di mesin akan dirender dengan benar; font yang tidak ada akan disubstitusi diam-diam, yang dapat menggeser panjang baris dan paginasi. Untuk konversi batch produksi, ini layak diuji secara eksplisit: muat dokumen dengan setiap jenis huruf yang digunakan sumber RTF Anda dan konfirmasikan bahwa jumlah halaman output sesuai dengan yang Anda harapkan dari pratinjau cetak manual

Tabel, gambar tertanam, dan sebagian besar fitur pemformatan Rich Text bekerja tanpa penanganan ekstra karena Rich Edit merendernya secara native. Satu area yang bisa mengejutkan adalah teks yang menggunakan spasi paragraf kustom atau inden baris pertama yang dinyatakan dalam twips: sistem koordinat internal Rich Edit dalam twips (1/1440 inci), sementara koordinat DC yang Anda atur di TFormatRange dalam piksel pada DPI saat ini. Kontrol mengonversi secara internal, tetapi jika Anda membangun RTF secara programatis, Anda harus memverifikasi bahwa nilai margin Anda dalam unit yang benar

Kesadaran DPI dan layar DPI tinggi

Pada layar yang berjalan pada skalasi 150% (144 DPI), ScaleX(210, mmPixel) akan mengembalikan jumlah piksel yang lebih besar daripada di layar 100%. Pustaka PDF merekam dimensi piksel apa pun yang Anda teruskan ke GetCanvasDC dan menggunakan argumen DPI di LoadFromCanvasDc untuk menghitung kembali ukuran halaman fisik dalam PDF. Selama nilai DPI yang Anda teruskan cocok dengan DPI yang sedang dijalankan aplikasi Anda, ukuran halaman output akan benar terlepas dari skalasi layar

Jika aplikasi Anda tidak memiliki kesadaran DPI (default lama), Windows menskalakan DC layar dan perhitungan piksel Anda akan salah pada mesin DPI tinggi. Perbaikan paling sederhana adalah mendeklarasikan kesadaran DPI dalam manifest aplikasi; aplikasi kemudian menerima piksel perangkat nyata dan angka 96 yang Anda teruskan ke LoadFromCanvasDc harus diganti dengan DPI tampilan aktual yang diperoleh dari GetDeviceCaps(GetDC(0), LOGPIXELSX). Sampel kode di atas mengkodekan keras 96 karena sesuai untuk lingkungan skalasi 100% dan menjaga contoh tetap singkat

Struktur output: satu file per halaman versus dokumen gabungan

Loop di atas menulis setiap halaman ke file PDF terpisah. Apakah itulah yang Anda inginkan tergantung pada penggunaan hilir. Sistem pembuatan laporan sering memerlukan halaman individual karena mereka merakit dokumen akhir nanti dengan menggabungkan atau menyusun ulang halaman. Jika Anda menginginkan PDF tunggal sejak awal, pustaka memungkinkan Anda membuat dokumen dengan beberapa halaman dalam satu sesi: buat dokumen sekali di luar loop, panggil metode penambahan halaman alih-alih SaveToFile di dalam loop, dan simpan dokumen lengkap setelah loop selesai. Ini menghindari file perantara dan merupakan struktur yang tepat untuk sebagian besar skenario konversi dokumen tunggal

Untuk file RTF berukuran besar, ada baiknya menambahkan umpan balik kemajuan dalam loop, karena tingkat konversi kira-kira sebanding dengan jumlah halaman dan dokumen 200 halaman dapat memakan waktu beberapa detik. Struktur repeat...until mudah diperluas: lacak offset karakter dalam pembaruan bilah kemajuan setelah setiap iterasi, menggunakan LastChar dibagi dengan jumlah karakter total dari RichEdit1.GetTextLen

Metode GetCanvasDC dan LoadFromCanvasDc yang ditunjukkan di sini adalah bagian dari losLab PDF Library untuk Delphi dan C++Builder