對那張支援工單的簡短回答是:可以,但有限制。HotPDF 2.730.0 能在 Free Pascal 3.2.2 與 Lazarus 4.6 上為 Win64 建置,核心的建立、載入與儲存路徑都能運作。不跟著過來的,是一切依賴靜態連結原生編解碼器目的檔、或依賴 Delphi 匿名方法的東西
這個問題通常以同一種方式抵達:一個團隊為跨平台工具統一採用 Lazarus,或接手了一套 Free Pascal 程式碼庫,然後想要他們已為 Delphi 授權的同一套 PDF 元件。移植一套成熟的 Delphi 函式庫很少是語法問題。有趣的部分是移植暴露了什麼——函式庫悄悄耦合在單一工具鏈上的位置,而在這個案例裡,耦合落在兩個非常具體的地方:隨附編解碼器的目的檔 ABI,以及藏在一個版本符號背後的編譯器功能
Free Pascal 3.2.2 需要什麼才能編譯 HPDFDoc
HotPDF 只在 Delphi 模式下於 Free Pascal 編譯,且只在 Lazarus LCL 單元目錄位於搜尋路徑上時才編譯。兩者都沒有商量餘地。HotPDF.inc 在其 {$IFDEF FPC} 區塊內以 {$MODE DELPHI} 與 {$H+} 切換編譯器,並在 FPC_FULLVERSION 低於 30202 時以 {$FATAL} 拒絕更舊的版本,所以 3.0.x 安裝會大聲失敗,而不是產出一個壞掉的單元。Lazarus 執行階段套件 HotPDFLaz.lpk 編碼了剩下的部分:LCL 作為必要套件,-Mdelphi 作為自訂選項
對只想要主控台輸出的人來說,LCL 需求令人意外,但它是結構性的。HPDFFPCCompat 提供 Free Pascal 沒有對應物的 Delphi VCL 型別,把 TMetafile 與 TMetafileCanvas 對應到 LCL 點陣圖與畫布類別,並把 TRichEdit 取別名為 TMemo,而 HPDFDoc 把 TPNGObject 取別名為 Graphics.TPortableNetworkGraphic。把這些當成編譯期墊片,而非功能對等:一個由點陣圖支撐的中繼檔類別讓單元保持可編譯,它不會讓中繼檔路徑表現得像在 Delphi 上那樣。即使是非 GUI 的冒煙測試也會拉入 Interfaces,而建置腳本為 lcl\units\x86_64-win64 與 lazutils 輸出目錄傳入 -Fu
為什麼 D2009+ 不能兼任版本門檻
把 Free Pascal 建置當成現代編譯器、然後直接定義最新的 Delphi 功能符號,是很誘人的做法。HotPDF 沒有這麼做,理由值得明說:D2009+ 不只代表 Unicode 字串,它還門控著公開 API 以匿名方法表達的單元。Free Pascal 3.2.2 既不支援 Delphi 匿名方法,也不支援那些 API,所以借用該符號會拖進無法編譯的程式碼。因此 HPDFDoc 的 uses 子句帶著兩條分開的條件尾巴,而它們之間的重疊是刻意而非意外
uses
// ...
HPDFJavaScript,
HPDFFormCalcGraph
{$IFDEF FPC}
, HPDFFPCCodecStubs,
HPDFCMS,
HPDFWinCertSigner
{$ENDIF}
{$IFDEF D2009+}
, HPDFXFARuntime,
HPDFCMS,
HPDFWinCertSigner,
HPDFSignVerify,
HPDFSignatureBatch
{$ENDIF};
為什麼原生編解碼器停在連結器?
因為它們是由某一特定工具鏈產出的 Win64 COFF 目的檔,而 Free Pascal 在 Win64 上的兩個連結器都不接受它們:內部連結器不行,外部 GNU ld 路徑也不行。這是目的檔 ABI 問題,不是 Pascal 問題,再多的條件化原始碼也修不了。函式庫採取了唯一誠實的路線。每個拉入靜態編解碼器目的檔的 {$L} 指示都包在 {$IFNDEF FPC} 裡,所以 Free Pascal 建置直接省略它們,然後由 HPDFFPCCodecStubs 把每個缺失的外部符號補成一個會拋出例外而非回傳的樁
// HPDFFPCCodecStubs.pas
function HPDFFPCNativeCodecUnavailable: PtrUInt;
begin
raise ENotSupportedException.Create(
'This native codec is not available in the Free Pascal build');
end;
function HPDFFPCStub_deflate: PtrUInt; cdecl;
public name 'deflate';
begin
Result := HPDFFPCNativeCodecUnavailable;
end;
那個樁表很長,讀一遍就能精確知道哪些能力今天是 Delphi 限定:zlib-ng 與 zopfli 的 deflate 入口、libjpeg 壓縮與解壓縮、OpenJPEG JPEG 2000 編解碼器、libtiff 及其逐壓縮演算法的初始化常式、JBIG2 編碼與解碼、Little-CMS 色彩轉換入口,以及 AES 基元。樁背後的設計選擇比清單本身更重要。連結期缺符號給您的是一面來自您從未碰過的單元的未定義參照之牆;拋出 ENotSupportedException 的樁給您的是一個跑得動的建置、一則指名原因的訊息,以及一份指向呼叫點的堆疊追蹤。這也代表 Free Pascal 建置絕不會在 Delphi 建置產生正確位元組的地方靜靜產生錯誤位元組。還要注意二階效應:在隔離處理程序中執行不受信任的影像編解碼器是只在 Delphi 建置上才會出現的決策,因為 Free Pascal 建置一開始就沒有可沙箱化的處理程序內原生解碼器
壓縮:第一個要改的是 cmNone
在移植任何其他東西之前,把 Compression 設為 cmNone。THPDFCompressionMethod 恰好提供兩個值,cmNone 與 cmFlateDecode,而後者直接路由到在 Free Pascal 建置中是樁的 deflate 入口。先在關閉壓縮的情況下驗證核心物件模型,再決定您還需要什麼。這是隨附冒煙測試採用的順序:建立一份單頁未壓縮文件、重新載入、並斷言回來的頁數是一。未壓縮輸出比較大,而它仍是完全合法的 PDF
program HotPDFLazarusSmoke;
{$mode delphi}
{$H+}
uses
Interfaces, SysUtils, HPDFDoc;
var
Pdf, Reloaded: THotPDF;
OutputFile: string;
PageCount: Integer;
begin
OutputFile := IncludeTrailingPathDelimiter(GetTempDir) +
'HotPDF-FPC-Smoke.pdf';
Pdf := THotPDF.Create(nil);
try
Pdf.FileName := OutputFile;
Pdf.Compression := cmNone; // cmFlateDecode 會碰到設樁的符號
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('Arial', [], 12);
Pdf.CurrentPage.TextOut(72, 72, 0, 'HotPDF Free Pascal smoke test');
Pdf.EndDoc;
finally
Pdf.Free;
end;
Reloaded := THotPDF.Create(nil);
try
PageCount := Reloaded.LoadFromFile(OutputFile);
if PageCount <> 1 then
raise Exception.CreateFmt('Expected one page, got %d', [PageCount]);
finally
Reloaded.Free;
end;
end.
平行頁面算繪會怎麼樣?
它仍然編譯、仍回傳正確的點陣圖,只是不再平行。THotPDF.RenderLoadedPagesParallel 與 THotPDF.RenderLoadedPagesParallelOrdered 建在 TThread.CreateAnonymousThread 與內嵌 procedure 閉包之上,這是 Free Pascal 3.2.2 無法表達的,所以 Free Pascal 分支執行確定性的序列後備:它按順序走訪頁面索引,對每一頁呼叫 RenderLoadedPageToBitmap,並計算成功次數。API 形狀、回傳值與輸出陣列都不變,這正是單一程式碼庫能兩邊建置的原因
var
Bitmaps: THPDFBitmapArray;
Info: THPDFParallelRenderPipelineInfo;
Rendered: Integer;
begin
Rendered := Pdf.RenderLoadedPagesParallel([0, 1, 2, 3], 150, 4,
Bitmaps, Info);
// Delphi:Info.WorkerCount 是記憶體預算允許的數量
// Free Pascal:Info.WorkerCount 永遠是 1,頁面按索引順序
if Info.WorkerCount = 1 then
LogSerialFallback(Rendered, Info.RequestedWorkerCount);
後備不是沉默的,這正是值得圍繞它設計的部分。它誠實地填寫 THPDFParallelRenderPipelineInfo:PageCount 來自請求,RequestedWorkerCount 回聲您要求的數量,WorkerCount 設為 1,而完成與交付計數與實際回來的相符。已經檢視 Info 來決定進度列或記憶體預算的程式碼繼續運作,而且讀到的是真相而非假設。如果您的吞吐量規劃依賴平行算繪管線及其背壓模型,那份規劃是 Delphi 的規劃;在 Free Pascal 上,請以把一頁算繪成點陣圖的單執行緒成本乘以頁數來編列預算
您實際該出貨哪個建置?
按能力選,不要按偏好選。如果您的工作流程是文件組裝、文字與向量繪製、表單填入、載入與儲存,Win64 上的 Free Pascal 建置涵蓋這些,而您應該在開啟任何開關之前先以關閉壓縮的狀態驗證。如果它涉及 JPEG、JPEG 2000、TIFF 或 JBIG2 影像、ICC 色彩轉換、壓縮輸出,或依賴多核心的吞吐量,暫時留在 Delphi 或 C++Builder。這條邊界由一個目的檔 ABI 與一個缺失的語言功能劃出,兩者在原始碼中都清晰可見而非埋在支援矩陣裡,且兩者都以具名錯誤而非錯誤結果失敗
Free Pascal 與 Lazarus 套件與 Delphi 及 C++Builder 單元在同一個發行包中出貨,所以一份授權涵蓋兩邊,您可以在承諾之前先用您自己的文件測試 Lazarus 路徑;HotPDF Delphi PDF Component 產品頁收錄目前的編譯器支援矩陣與完整 API 參考