HotPDF 是原生 Delphi 與 C++Builder PDF 元件,會直接解譯每一筆 GDI 記錄並轉換為 PDF 運算子來匯入 Windows EMF 與 WMF 中繼檔,而不是將檔案扁平化為點陣圖:漸層填滿會成為 PDF 軸向陰影圖樣,網紋筆刷會成為 PDF 平鋪圖樣,集中式路徑狀態閘門則會阻止格式錯誤的記錄破壞輸出。任何 TChart、GDI+ 表面或一般 TCanvas 能以增強型中繼檔匯出的圖表,都可能走這條路徑;當有人放大頁面或將其送至高解析度印表機時,差異便會立即顯現
多數 Delphi 開發者預設採用的替代方式,是先將中繼檔點陣化為點陣圖,再放到頁面上,而代價往往稍後才會出現:螢幕上清晰的長條圖,在 PDF 以 600 DPI 列印或投影到會議室螢幕時會立刻變得明顯鋸齒化;如果填滿樣式沒有被保留,使用網紋填滿的 CAD 區域也會縮成單一的扁平灰色矩形。將中繼檔當成程式而非圖片來讀取,便能避開這兩個問題;但要正確實作這條較困難的路徑,因此報表交付前值得先了解以下陷阱
為什麼要解譯中繼檔,而不是將它扁平化為點陣圖
HotPDF 讓 EMF 與 WMF 匯入維持向量路徑,是因為 Windows 中繼檔是已記錄的 GDI 繪圖呼叫序列,而不是圖片;將這些呼叫重播為 PDF 路徑、文字與陰影運算子,才能讓結果像頁面的其他內容一樣縮放。應用程式會呼叫 THPDFPage.ShowMetafile 及其 ShowMetafileEx 對應入口,兩者都會將中繼檔交給 THPDFWmf,由該類別逐筆走訪並轉換 GDI 記錄。這種區分並非絕對,HotPDF 也不掩飾這一點:真正屬於點陣資料的中繼檔記錄,例如 StretchDIBits 點陣圖傳送,會透過 AddImage 與 ShowImage 嵌入為真正的 PDF Image XObject;這正是頁面上其他圖片所使用的同一組呼叫,而不是硬塞進無法表達相片的路徑運算子。線條、填滿與文字維持向量;來源中原本就是像素的內容,在輸出中也維持像素。最簡單的呼叫只需要已載入的中繼檔:
var
Pdf: THotPDF;
Chart: TMetafile;
begin
Pdf := THotPDF.Create(nil);
Chart := TMetafile.Create;
try
Chart.LoadFromFile('quarterly-revenue.emf'); // exported from TChart or GDI+
Pdf.FileName := 'quarterly-report.pdf';
Pdf.BeginDoc;
Pdf.CurrentPage.ShowMetafile(Chart);
Pdf.EndDoc;
finally
Chart.Free;
Pdf.Free;
end;
end;
解譯器如何將 GDI 座標轉換為 PDF 頁面空間
HotPDF 會以單次走訪中繼檔自身的記錄串流來處理,而不是重新實作一套 GDI。THPDFWmf.Analyse 會透過 Win32 GetEnhMetaFileHeader 呼叫讀取中繼檔標頭、重設內部繪圖狀態,並呼叫 EnumEnhMetafile;這是中繼檔檢視器也會使用的相同列舉 API,因此每筆 EMR_* 記錄都會依原始記錄順序送達 THPDFWmf.ExecuteRecord。GDI 會依中繼檔自身映射模式所選定的裝置或邏輯單位,由上至下表示座標;PDF 頁面則在使用者空間點中由下至上,這套座標系統也涵蓋於HotPDF 路徑與填滿的畫布繪圖模型。每個記錄處理常式都會透過 ScaleX 與 ScaleY 解決這項差異,而這兩者會呼叫 ProjectX 與 ProjectY,重播 GDI 對異向性與等向性映射模式使用的視窗至視埠公式;因此,無論來源應用程式設定了什麼視窗與視埠範圍,記錄為五個邏輯單位寬的形狀都能以正確寬度落在 PDF 點中
GDI 漸層填滿如何成為 PDF 陰影圖樣
只要 GDI 以兩種矩形模式之一記錄 EMR_GRADIENTFILL 記錄,它就會成為真正的 PDF Type 2 軸向陰影圖樣(ISO 32000-1 §8.7.4.5)。THPDFWmf.VEMRGradientFill 會直接從原始位元組緩衝區讀取記錄自身的配置,遵循 MS-EMF §2.3.1.6 結構:先是由 16 位元 RGBA 角點組成的頂點陣列,接著是每個參照其中兩個頂點的矩形清單。對於 GRADIENT_FILL_RECT_H,色彩會沿矩形水平中線由左至右漸變;對於 GRADIENT_FILL_RECT_V,色彩會沿垂直中線由上至下漸變。無論哪一種模式,兩個角點色彩與投影後的矩形座標都會直接送入 THotPDF.RegisterAxialGradient,由它回傳圖樣名稱;頁面再繪製矩形並透過該圖樣(SetFillPattern)填滿,而不是呼叫扁平的 SetRGBFillColor,因此試算表樣式的帶狀標頭或圖表的漸層繪圖區能保留其混合效果,而不會塌縮為單一平均色
Gouraud 三角形模式是明確存在的缺口。當記錄的 ulMode 欄位回報 GRADIENT_FILL_TRIANGLE 時,VEMRGradientFill 會辨識出該模式、記錄三角形模式尚未實作,並略過矩形,而不是猜測雙色近似。任意三角形網格上的逐頂點、逐像素插值,無法化約為雙色標的軸向或放射狀陰影;要正確表達,必須輸出 PDF Type 4 或 Type 5 網格陰影,這也是HotPDF 頁面轉譯器讀回 PDF 時仍會留白的同一個陰影系列缺口。兩條無關的程式路徑落在相同邊界:網格陰影在寫入端與讀取端都是缺口,而使用 Gouraud 三角形呈現平滑放射光暈的來源圖表,會退回最後使用的實心筆刷,而不是轉譯近似效果
網紋筆刷會成為平鋪圖樣,而不是扁平灰色
GDI 網紋筆刷會在 PDF 中保留其紋理,因為 THPDFWmf.SetBrushColor 會先檢查 CurrentBrush.lbStyle 是否為 BS_HATCHED,才回退到實心填滿,並將該情況導向 SetHatchBrushPattern。該方法會寫入由描邊線條運算子組成的 8×8 單位 PDF 內容串流:m、l 與 S,並依 GDI 網紋樣式選擇線條:HS_HORIZONTAL 與 HS_VERTICAL 使用單一水平或垂直線,HS_FDIAGONAL 與 HS_BDIAGONAL 使用三條平行對角線,而 HS_CROSS 與 HS_DIAGCROSS 則使用水平加垂直或兩組對角線的組合。THotPDF.RegisterTilingPattern 會將該內容串流註冊為有色平鋪圖樣(PaintType 1、ISO 32000-1 §8.7.3.1),並以 8 單位的 XStep 與 YStep 重複;頁面會像使用軸向陰影一樣透過 SetFillPattern 填滿。依賴網紋填滿區分材料的 CAD 平面圖或工程圖,會在 PDF 中保留這種視覺語言,而不會讓每個區域都失去差異並變成相同灰色
並非每種筆刷都能獲得這項處理,CAD 匯入交付前值得了解這個缺口。EMR_CREATEDIBPATTERNBRUSHPT 是自訂點陣圖樣式筆刷的記錄,不是 GDI 的六種標準網紋樣式;它只會註冊其控制代碼,讓後續的 SELECTOBJECT 與 DELETEOBJECT 記錄保持一致。HotPDF 目前尚未提供任意圖磚影像的 PDF Pattern 資源管線,因此選取該筆刷時會回退至實心色彩,而不是來源紋理。如果填滿結果呈現扁平色彩,但原始內容明顯使用重複影像紋理,來源筆刷幾乎可以確定是自訂 DIB 圖樣,而不是標準網紋;這是最值得先手動檢查的情況。為這類圖面設定匯入選項時,仍使用相同的選項物件:
var
Pdf: THotPDF;
Drawing: TMetafile;
Options: THPDFEmfOptions;
begin
Pdf := THotPDF.Create(nil);
Drawing := TMetafile.Create;
Options := THPDFEmfOptions.Create;
try
Drawing.LoadFromFile('floor-plan.emf');
Options.Assign(Pdf.EmfOptions); // start from the document-wide defaults
Options.Redraw := False; // interpret the original EMF bytes, no GDI re-record pass
Options.ShowNullBrush := True; // keep explicitly unfilled CAD regions visible
Options.UseFrame := True; // clip output to the frame the EMF header declares
Pdf.FileName := 'floor-plan.pdf';
Pdf.BeginDoc;
Pdf.CurrentPage.ShowMetafileEx(Drawing, Options);
Pdf.EndDoc;
finally
Options.Free;
Drawing.Free;
Pdf.Free;
end;
end;
什麼機制能阻止格式錯誤的中繼檔破壞頁面
HotPDF 的作法是在 ExecuteRecord 頂端設置單一閘門,而不是在約八十個記錄處理常式中重複防禦性檢查。由 EMR_BEGINPATH 開始、以 EMR_ENDPATH 或 EMR_ABORTPATH 結束的 GDI 路徑區段,會由私有 PathContinue 屬性追蹤,而該屬性由 FPathContinue 欄位支援。當區段開啟時,ExecuteRecord 只允許路徑建構記錄通過,包括移動、線段、多重折線、多邊形、多重貝茲曲線與多重繪製變體,以及 CLOSEFIGURE 和少量轉換與 DC 狀態記錄,例如 SETWORLDTRANSFORM、SAVEDC 與 RESTOREDC。任何其他記錄類型在區段開啟時抵達 ExecuteRecord,例如游離的 EXTTEXTOUT 或點陣圖傳送,都會在抵達瞬間由單一 Exit 集中丟棄
設置這道閘門,是因為手工編寫、工具產生或單純損毀的中繼檔,其路徑區段不保證只包含格式完整檔案會放在開始與結束記錄之間的內容。若文字輸出記錄落在 EMR_BEGINPATH 與 EMR_ENDPATH 之間,沒有閘門時,它可能污染正在建立的路徑幾何,或在應該純粹建立路徑的序列中輸出 PDF 顯示文字運算子;這兩種失敗模式都可能只在第三方工具產生的一個格式錯誤輸入上出現,而不是一般測試套件碰巧涵蓋的內容。將檢查集中在 ExecuteRecord,代表個別的 VEMR* 處理常式不必各自防禦在錯誤時機被呼叫;閘門會在分派前決定一次,而不是在分派後決定八十次
在同一頁面將向量圖表置於文字與影像旁
報表頁面很少只放置圖表,而 ShowMetafile 能像其他繪圖呼叫一樣,與 HotPDF 的其他頁面運算子組合使用。以 TextOut 繪製的標題、以 EMF 匯入且使用網紋填滿的長條圖,以及透過 ShowImage 放置的標誌,都能在同一頁面的同一個內容串流中呈現,並各自保留原生精度;這種組合方式也涵蓋於HotPDF 在報表中配置文字、字型與影像的指南:
Pdf.CurrentPage.SetFont('Arial', [fsBold], 14);
Pdf.CurrentPage.TextOut(50, 760, 0, 'Q2 Regional Sales');
Pdf.CurrentPage.ShowMetafile(RegionChart); // hatch-filled bars, still vector
Pdf.CurrentPage.ShowImage(LogoIndex, 450, 760, 90, 30, 0);
本文說明的 EMF 與 WMF 解譯器、為漸層填滿註冊的軸向陰影圖樣,以及網紋筆刷的平鋪圖樣對應,都是標準HotPDF 元件的一部分,適用於 Delphi 與 C++Builder;這是一個原生 VCL 程式庫,所有這些功能都不依賴外部 DLL