对那张支持工单的简短回答是:能,但有限制。HotPDF 2.730.0 在 Free Pascal 3.2.2 和 Lazarus 4.6 上为 Win64 构建,核心的创建、加载、保存路径可用。不随行的,是任何落在静态链接原生编解码目标文件或 Delphi 匿名方法上的东西
这个问题通常以同一种方式到来:一个团队为跨平台工具标准化到 Lazarus,或接手了一个 Free Pascal 代码库,想要他们已为 Delphi 购买授权的同一个 PDF 组件。移植一个成熟的 Delphi 库很少是语法问题。有意思的是移植暴露了库曾悄悄耦合在单一工具链的哪些位置,而在这个案例里耦合坐在两个非常具体的地方:捆绑编解码器的目标文件 ABI,以及藏在一个版本符号背后的编译器特性
HPDFDoc 编译之前,Free Pascal 3.2.2 需要什么
HotPDF 只在 Delphi 模式下、且 Lazarus LCL 单元目录在搜索路径上时才在 Free Pascal 下编译。两条都不可谈判。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 目标文件,而 Win64 上两个 Free Pascal 链接器都不消费它们:内部链接器不行,外部 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 组件产品页载有当前编译器支持矩阵和完整 API 参考