PDFlibPas 把增強型中繼檔逐記錄轉換成真正的 PDF 頁面內容,而不是點陣化,這正是匯入的圖表或 CAD 圖面在任何縮放倍率下都保持清晰的原因。該轉換器約 6500 行,原本針對 VCL 撰寫,所以當程式庫新增 Free Pascal 目標時,它被歸類為不可移植並以 stub 取代。這個歸類是錯的,而它錯的方式,恰好是一堂關於如何在決定圍繞相依性重寫之前先審查相依性的課
這 6500 行實際用到的 VCL 介面其實很小:一個使用其像素格式、串流儲存、handle、canvas 與掃描線的點陣圖類別;一個只取寬、高與 handle 的中繼檔類別;以及帶兩個常數的色彩型別。每一項都由程式庫自己的圖形單元提供,而這個單元存在的目的,正是讓非 VCL 建置有對應物。轉換器根本沒有被 VCL 卡住,卡住它的是 Free Pascal 的 Windows 單元
沿程式碼真正依賴的軸線做拆分
所以改動不是重新實作,而是一個條件:從「非 VCL 建置時編譯 stub」改為「非 Windows 目標時編譯 stub」。這才是正確的軸線,把原因說清楚就能看出差別。增強型中繼檔是 Windows 容器;轉換器從頭到尾都是 Windows GDI 記錄的剖析器。主應用程式用 VCL、別的 widget 集合,還是完全不用 widget 集合,與這些記錄能不能被解讀毫無關係;而目標是不是 Windows,則與它息息相關
選對軸線的好處是自然掉出來的。C++Builder 建置(本程式庫在其下未定義 Windows 平台符號)保留拋出例外的 stub,行為與過去完全一致。macOS 保留 stub,這是正確的,因為那裡沒有 GDI 記錄可剖析。Delphi VCL 建置不受影響。而使用非 VCL widget 集合的 Windows 建置,順帶獲得了向量 EMF 匯入,沒有人需要為此寫一行實作。對齊真實相依性的條件編譯,把平台工作變成一行改動;對齊錯誤相依性的條件編譯,把它變成一場永遠排不上時程的重寫
Free Pascal 缺的是宣告,不是邏輯
真正缺的,是 Delphi 的 Windows 單元有提供而 Free Pascal 沒有的 Win32 宣告。把它們集中到一個相容單元,而不是在轉換器裡散布條件編譯,讓剖析器保持可讀。這份清單頗有啟發性,因為它顯示兩套 RTL 的標頭涵蓋有多不均:113 個中繼檔記錄型別常數、兩個延伸文字輸出旗標、三個漸層填滿模式常數、一個 handle 表指標型別、漸層頂點與基本圖元記錄的別名,以及三個 Free Pascal 完全未宣告的記錄型別,涵蓋 Alpha 混色、透明 blit 與色彩管理模式
這些項目單獨看都不起眼,但剖析器要能編譯,每一項都必須正確;相容單元是它們自然的家,因為可以對照標頭文件整批做差異比對
無聲畫出錯誤圖像的那一個
其中兩個宣告不只是缺席,而是存在且對這個用途是錯的,這是即使您永遠不碰中繼檔也值得記住的部分
Free Pascal 宣告筆刷建立記錄時內嵌了執行階段筆刷結構,宣告延伸畫筆記錄時內嵌了執行階段畫筆結構。這兩個執行階段結構都把 hatch 成員宣告成指標大小的整數,因為在實際的 GDI 呼叫中,該成員可以攜帶 handle。但中繼檔永遠儲存 32 位元形式,因為記錄配置是序列化檔案格式的一部分,不會隨行程位元數改變
在 32 位元建置下兩者一致,什麼事都不會發生。在 Win64 上,指標大小的成員是 8 位元組,而檔案裡是 4 位元組,於是 hatch 成員之後的每個欄位都從錯誤的偏移讀取。沒有例外、沒有剖析錯誤、沒有警告。中繼檔只是畫錯:色彩來自錯誤的位元組、畫筆寬度來自錯誤的位元組,圖像看起來像渲染錯誤,而不是結構配置錯誤。Delphi 正是為此隨附了明確 32 位元的兩個結構變體,相容單元也以同樣方式重新宣告
// 在 Win64 上錯誤:Hatch 是指標大小,檔案卻儲存 32 位元,
// 後續每個欄位都位移四個位元組,且沒有任何錯誤
type
TLogBrushRuntime = record
lbStyle: UINT;
lbColor: COLORREF;
lbHatch: ULONG_PTR; // 在 64 位元行程中為 8 位元組
end;
// 正確:序列化配置,無論位元數都是固定寬度
type
TLogBrush32 = record
lbStyle: UINT;
lbColor: COLORREF;
lbHatch: DWORD; // 永遠 4 位元組,與中繼檔中的儲存一致
end;
通用規則:任何既作為執行階段 API 引數、又作為序列化欄位配置出現的結構,都需要兩份宣告,而且序列化那份必須從頭到尾使用固定寬度型別。檔案格式裡的指標大小成員,永遠是等著 64 位元建置引爆的錯誤
函式簽章差異放進包裝函式,而不是每個呼叫點
其餘差異都是普通的函式簽章不符,吸收它們的方式是轉發包裝函式,而不是在每個呼叫點放條件編譯。變換合併函式在 Free Pascal 下接受指標,而 Delphi 下接受參照參數,所以包裝函式收參照、傳位址。它還會先把兩個來源引數複製到區域變數,因為轉換器有些呼叫點的目的地矩陣同時也是來源之一,對一個邊讀邊寫的函式把同一位址傳兩次,會得到一個只在旋轉內容上才顯露出來的微妙錯誤變換
function CombineTransformCompat(var Dest: TXForm;
const A, B: TXForm): BOOL;
var
SrcA, SrcB: TXForm;
begin
// 先複製:呼叫端確實可能把 Dest 同時當成 A 或 B 傳入
SrcA := A;
SrcB := B;
{$IFDEF FPC}
Result := Windows.CombineTransform(@Dest, @SrcA, @SrcB);
{$ELSE}
Result := Windows.CombineTransform(Dest, SrcA, SrcB);
{$ENDIF}
end;
矩形與點型別是另一種情況。Free Pascal 把中繼檔的矩形與點記錄視為與一般圖形記錄不同的型別,所以八個指派位置需要在配置相同的記錄之間明確轉型。兩個編譯器都接受轉型寫法,所以這些位置完全不需要條件編譯,為此犧牲一點美觀值得
這對 Free Pascal 部署意味著什麼
向量 EMF 匯入在 Windows 上的 Free Pascal 下可用,產生與 Delphi 建置相同的頁面內容:路徑是路徑、漸層是圖樣內容、文字是文字。Windows 之外,點陣路徑仍是答案,這是格式的限制,而不是移植的限制。轉換器餵入的座標與裁剪狀態在內容串流 CTM 與裁剪狀態追蹤器文章中有描述,它發出的向量基本圖元則在向量圖形、著色器與漸層中涵蓋
如果您正在審查自己的程式碼庫尋找同樣的機會,有用的練習就是促成這篇文章的那個:列出您實際用到的、來自您以為依賴的框架的成員。答案往往比匯入清單暗示的短得多,而真正的約束通常完全在別處。基於裝置內容的匯入路徑一般性地描述於列印預覽與裝置內容文章,平台與工具鏈涵蓋範圍列於 losLab PDF Developer Library 產品頁