PDF 座標以點(points)為單位,印表機座標以裝置單位為單位,除非您刻意轉換,否則這兩者毫無關聯;這種不匹配是 Delphi 應用程式中大多數列印輸出不良的根本原因:程式碼傳送了正確的檔案,張頁面卻被裁剪、拉伸或呈現空白;PDFium 元件可以乾淨地處理轉譯端,而印表機的部分則是標準的 VCL;一旦您了解兩端所預期的內容,只需少量的程式碼即可將兩者結合在一起
先轉譯後列印的工作流程如何運作
PDFium 元件不直接與印表機溝通;其模式為:以您想要的解析度將頁面轉譯為 TBitmap,然後使用 StretchDIBits 將該點陣圖傳輸到印表機的畫布上;TPdf.RenderPage 會傳回呼叫者擁有的點陣圖,因此您可以控制像素尺寸;在選項集中傳遞 [rePrinting],PDFium 就會將其轉譯路徑切換為省略螢幕專用效果(例如 LCD 次像素微調)的路徑,並針對列印輸出正確地處理頁面的 MediaBox;如果不加入 rePrinting,您傳送到印表機的將會是螢幕轉譯結果,這在螢幕上看起來很好,但在高 DPI 的印表機上往往會產生較為模糊的輸出,因為針對 96 DPI 螢幕所做的微調決策並不適合 300 或 600 DPI 的列印
在修改任何頁面屬性之前,TPdf.Active 是唯一需要檢查的關卡;該元件會靜默地吞掉載入錯誤:在損毀或受密碼保護的檔案上設定 Active := True 不會引發異常,它只會讓 Active 保持為 False;請務必在指派後進行檢查;在未作用的文件上讀取 PageCount 或 PageWidth 會傳回零,這會產生靜默的無效操作,一旦進入排程器就非常難以診斷
最簡列印迴圈
最簡單的運作案例是載入檔案、開啟列印工作、反覆檢視頁面並關閉;唯一棘手的細節是,在第一頁之前郵件不能呼叫 Printer.NewPage,因此需要 FirstPage 旗標;StretchDIBits 的傳輸是透過 GetDIBSizes 和 GetDIB 從點陣圖控制代碼中取得裝置獨立的位元,然後以全頁大小繪製到印表機的畫布上:
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;
將 Printer.PageWidth 和 Printer.PageHeight 作為點陣圖尺寸傳遞,代表您是以印表機的原生像素大小進行轉譯,這已經考量到了裝置的 DPI;接著 StretchDIBits 呼叫會將這些像素 1:1 對應到頁面上;這能在不進行任何明確 DPI 計算的情況下,提供最佳的保真度,但只有在 PDF 頁面和實體紙張剛好大小相同時才有用;當兩者不同時,您需要進行明確的縮放
頁面與紙張大小不同時的縮放
直向 A4 的 PDF 頁面不會自動符合 US Letter 印表機,且送到直向印表機的橫向頁面會被裁剪;標準的方法是從印表機像素與 PDF 點的比例計算出一個均一的縮放比例,然後套用到兩個維度以保留長寬比;Pdf.PageWidth 和 Pdf.PageHeight 會以點為單位公開當前頁面的尺寸,其中一個點為 1/72 英吋;乘以目標 DPI 並除以 72 即可轉換為該解析度下的像素;取 X 和 Y 比例的 Min 即可得到仍能放入可列印區域的最大縮放比例:
// 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 進行轉譯適用於大多數辦公室印表機;在 600 DPI 下,單張 A4 頁面的點陣圖大約有 3400 萬像素,作為 32 位元點陣圖約佔 100 MB,這對一般文字文件的品質提升非常有限,但每頁的記憶體成本卻相當顯著;請將 600 DPI 保留給印刷廠或以向量為主的技術工程圖等真正需要的場景
第二個程式碼區塊中的 reAnnotations 旗標是獨立於 rePrinting 的;當使用者期望在紙上顯示印章、高亮度標記和註解方塊時,請加入它;若只需要內容輸出則予以省略;這兩個旗標可以自由組合
頁面旋轉
PDFium 將頁面旋轉以 /Rotate 項目的形式儲存在 PDF 中,可透過 Pdf.PageRotation 存取,它會傳回 TRotation 值(ro0、ro90、ro180、ro270);印表機座標系統會相對於螢幕反轉 90 度和 270 度的旋轉;如果您在沒有進行任何調整的情況下,直接將原始的 PageRotation 值傳遞給 RenderPage,在大多數 Windows 印表機驅動程式上,內嵌在直向文件中的橫向頁面列印出來將會上下顛倒;解決方法是在轉譯呼叫之前進行簡單的對調:將 ro90 對應到 ro270,並將 ro270 對應回 ro90,保持 ro0 和 ro180 不變
在出貨前,請先在您特定的目標印表機上驗證此行為;不同廠商的驅動程式在旋轉方面的行為並不一致,有些驅動程式會在 GDI 層級套用自己的旋轉修正;如果您看到雙重旋轉,請移除對調,如果您根本沒有看到任何修正,請加入它;在測試期間,直向和橫向頁面交替的混合方向文件是捕捉這兩種失敗模式最快的方法
長列印工作中的記憶體管理
每次呼召 RenderPage 都會分配一個呼叫者擁有且必須釋放的新 TBitmap;在上述的迴圈中,try/finally Bitmap.Free 區塊一次正確地處理一頁;請勿跨頁面累積點陣圖:一份 200 頁文件的 300 DPI 轉譯結果,在第一頁到達排程器之前就會消耗數 GB 的記憶體,請在進入下一頁之前釋放每個點陣圖
傳輸區塊內的 AllocMem / FreeMem 對也遵循相同的規則;GetDIBSizes 會告訴您 DIB 標頭和像素資料需要多少記憶體;您在一頁的範圍內完成分配、填滿、繪製和釋放;如果任一區塊發生洩漏,在超過數十頁的文件上,列印工作將會耗盡程序的堆積記憶體
如果您需要在背景執行緒上執行列印工作,請將 TPdf 和所有 VCL 印表機呼叫保持在同一個執行緒上;TPdf 本身在共用 PDFium DLL 全域狀態的執行個體之間並非執行緒安全的,最安全的模型是每個執行緒一個 TPdf,每個 TPdf 各自載入該檔案的副本
此處顯示的轉譯和文件 API 是適用於 Delphi 和 C++Builder 的 PDFium 元件 的一部分