RTF 已經存在了足夠長的時間,以至於它出現在沒有人預期的地方:舊版報表產生器、郵件合併管道,以及早於現代文書處理器的法律文件封存;即時將其轉換為 PDF 是一項反覆出現的需求,而在 Windows 上實際可行的方法不是專用的 RTF 解析器,而是 Windows 本身已透過 TRichEdit 和 EM_FORMATRANGE 提供的轉譯路徑;losLab PDF 函式庫 DLL 版本公開了一個虛擬裝置內容,可直接嵌入該管道中
機制:虛擬 DC 和 EM_FORMATRANGE
Rich Edit 控制項可以針對任何裝置內容(而不僅僅是實體印表機)進行內容分頁;EM_FORMATRANGE 訊息會指示控制項將字元範圍排版至給定的 DC 中,並傳回它成功容納的最後一個字元的位置;重複呼叫它,每次遞增 cpMin,即可取得逐頁輸出;losLab PDF 函式庫的 GetCanvasDC 提供一個記憶體內 DC,其大小調整為您指定的任何頁面尺寸;在將頁面轉譯至其中之後,LoadFromCanvasDc 會將結果擷取為 PDF 頁面;這就是整個管道
有一點需要預先做對:TRichEdit 控制項的尺寸必須調整為與目標頁面相符;如果控制項小於或大於 DC 尺寸,分頁將無法與最終 PDF 中的內容對齊;對於 A4 輸出,標準方法是在載入 RTF 檔案之前,使用您用來調整 DC 大小的相同縮放輔助程式,將控制項的像素尺寸設定為與 96 DPI 下的 210 x 297 mm 相符
Delphi 實作
以下使用 PDFlibAX_TLB 匯入單元,它封裝了該函式庫的 DLL 版本;表單裝載了一個 TRichEdit 和一個按鈕;表單的 OnCreate 處理常式會調整控制項大小並載入 RTF,而按鈕按一下則會驅動轉換迴圈
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.
迴圈正在做什麼
PrintRtfBox 填寫 TFormatRange 結構,並透過 SendMessage 將其傳遞給 Rich Edit 控制項;該控制項從 cpMin 開始轉譯字元,在 DC 填滿時停止,並傳回第一個無法容納的字元位置;當傳回值等於或超過總文字長度時,表示每個字元都已轉譯完畢,且函式傳回零,這會終止 repeat...until 迴圈
每次反覆運算會產生一個名為 Output1.pdf、Output2.pdf 等的 PDF 檔案;如果您想要單一的多頁文件,該函式庫的頁面附加 API 允許您事後進行組合,或者您可以重構迴圈以在單一文件工作階段中呼叫 AddPage;上述每次反覆運算呼叫 SaveToFile 緊接在後呼叫 RemovePdfDocument 的模式將記憶體峰值限制在一頁內容的範圍內,這對於非常長的 RTF 檔案非常重要
容易出錯的尺寸細節
LoadFromCanvasDc 的 96 DPI 引數會告知函式庫該 DC 是以何種螢幕解析度轉譯的,以便它可以計算 PDF 頁面正確的點到像素對應;如果弄錯了這一點,即使影像在螢幕上看起來是正確的,輸出中的文字也會以錯誤的大小顯示
新增至 RcPage.Right 和 RcPage.Bottom 的 +100 是超出控制項可見邊緣的微小邊界;Rich Edit 使用 rcPage 矩形來決定在何處拆分頁面,如果沒有該邊界,剛好落在邊界上的行可能會在兩個頁面上重複出現;這不是一個魔術常數,您需要它足夠大,以便頁面邊界乾淨地落在控制項的版面配置區域內,而不是最後一個像素上
最後,控制項必須已經附加到可見的表單視窗時 FormCreate 執行,以便在第一次呼叫 SendMessage 之前其視窗控制代碼是有效的;如果表單尚未顯示,在執行期動態建立的 TRichEdit 在轉譯迴圈開始之前需要明確呼叫 HandleNeeded
處理字型和 RTF 功能
因為轉譯是由 Windows Rich Edit 引擎執行的,所以字型替代遵循其用於顯示和列印的相同規則;RTF 檔案中引用且已安裝在電腦上的字型將會忠實地轉譯,缺失的字型將會被悄悄替代,這可能會改變行長和分頁;對於生產批次轉換,這值得進行明確的測試:載入一份包含您的 RTF 來源使用的每種字體的文件,並確認輸出頁數與您從手動列印預覽中預期的頁數相符
表格、內嵌影像和大多數 RTF 格式化功能都可以正常運作,無需任何額外處理,因為 Rich Edit 會以原生方式轉譯它們;可能令人驚訝的一個領域是使用自訂段落間距或以 twips 表示的首行縮排文字:Rich Edit 的內部座標系統是以 twips (1/1440 英吋) 為單位,而您在 TFormatRange 中設定的 DC 座標則是以目前 DPI 的像素為單位;控制項會在內部進行轉換,但如果您是透過程式設計方式建構 RTF,則應驗證您的邊距值是否使用正確的單位
DPI 感知與高 DPI 顯示器
在以 150% 縮放比例 (144 DPI) 執行的顯示器上,ScaleX(210, mmPixel) 將會傳回比 100% 顯示器上更大的像素計數;PDF 函式庫會記錄您傳遞給 GetCanvasDC 的任何像素尺寸,並使用 LoadFromCanvasDc 中的 DPI 引數來回推計算 PDF 中的實體頁面大小;只要您傳遞的 DPI 值與您應用程式正在執行的 DPI 相符,無論顯示器縮放比例如何,輸出頁面大小都將是正確的
如果您的應用程式不具備 DPI 感知(舊的預設設定),Windows 會縮放螢幕 DC,且您的像素計算在高 DPI 電腦上將會出錯;最簡單的修正方法是在應用程式資訊清單中宣告 DPI 感知,應用程式接著會接收真實的裝置像素,且您傳遞給 LoadFromCanvasDc 的 96 應替換為從 GetDeviceCaps(GetDC(0), LOGPIXELSX) 取得的實際顯示 DPI;上面的程式碼範例硬編碼了 96,因為它適用於 100% 縮放環境並能保持範例簡短
輸出結構:每頁一個檔案對比合併文件
上述迴圈會將每個頁面寫入個別的 PDF 檔案;這是否是您想要的,取決於下游用途;報表產生系統通常需要個別頁面,因為它們稍後會透過合併或重新排序頁面來組合最終文件;如果您一開始就想要單一的 PDF,該函式庫允許您在單一工作階段中建立具有多個頁面的文件:在迴圈外部建立一次文件,在迴圈內部呼叫頁面新增方法而不是 SaveToFile,並在迴圈結束後儲存完整文件;這可以避免中間檔案,是大多數單一文件轉換案例的正確結構
對於大型 RTF 檔案,在迴圈中新增一些進度回饋是值得的,因為轉換率大致與頁數成正比,而 200 頁的文件可能需要幾秒鐘;repeat...until 結構很容易擴充:在每次反覆運算後追蹤進度列更新中的字元偏移量,使用 LastChar 除以來自 RichEdit1.GetTextLen 的總字元數
此處顯示的 GetCanvasDC 和 LoadFromCanvasDc 方法是適用於 Delphi 和 C++Builder 的 losLab PDF Library 的一部分