PDFlibPas 将增强型图元文件逐条记录地转换为真正的 PDF 页面内容,而不是栅格化,这正是导入的图表或 CAD 图纸在任何缩放级别都保持清晰的原因。这个转换器约 6500 行,编写时依赖 VCL,因此当库新增 Free Pascal 目标时,它被归为不可移植并替换成了存根。这个归类是错的,而错在哪里恰好是一堂有用的课:在决定绕开某个依赖重写之前,先审计它
这 6500 行代码实际用到的 VCL 面其实很小:一个位图类(用到其像素格式、流保存、句柄、画布与扫描线);一个图元文件类(用到其宽度、高度与句柄);以及颜色类型和两个常量。这些全部已由库自己的 graphics 单元提供——该单元存在的意义正是让非 VCL 构建有等价物。转换器根本没有卡在 VCL 上,而是卡在 Free Pascal 的 Windows 单元上
沿着代码真正依赖的轴做拆分
所以改动不是重新实现,而只是一条条件:从"无 VCL 构建时编译存根"改成"非 Windows 构建时编译存根"。这才是正确的轴,把理由说清楚,差别就一目了然。增强型图元文件是 Windows 容器;这个转换器从头到尾都是 Windows GDI 记录的解析器。宿主应用用 VCL、用别的控件库还是完全不用控件,与这些记录能否被解析毫无关系;而目标平台是不是 Windows,则与它息息相关
选对轴,好处是顺带而来的。C++Builder 构建(本库中它未定义 Windows 平台符号)保留抛异常的存根,行为与之前完全一致。macOS 正确地保留存根,因为那里没有 GDI 记录可解析。Delphi VCL 构建不受影响。而非 VCL 控件库的 Windows 构建则作为副产品获得了矢量 EMF 导入,没有人需要为此写一行实现。与真实依赖对齐的条件编译把平台工作变成一行改动;对齐到错误的轴,它就变成一个永远排不上期的重写项目
Free Pascal 的缺口是声明,不是逻辑
真正缺失的,是 Delphi 的 Windows 单元有而 Free Pascal 单元没有的 Win32 声明。把它们收进一个单独的兼容单元,而不是把条件编译撒满转换器,才保住了解析器的可读性。这份清单很有启发,因为它展示了两个 RTL 之间头文件覆盖的不均衡:113 个图元文件记录类型常量、两个扩展文本输出标志、三个渐变填充模式常量、一个句柄表指针类型、渐变顶点与图元记录的别名,以及三个 Free Pascal 完全未声明的记录类型,覆盖 alpha 混合、透明位块传输与颜色管理模式
这些声明单独看都无趣。但解析器要编译通过,它们必须全部正确;而兼容单元是它们自然的家,因为可以把它当作一个整体与头文件文档做 diff
那个悄悄画错图的声明
其中两个声明不只是缺失,而是存在但对这个用途是错的——即使你从不碰图元文件,这一部分也值得记住
Free Pascal 在画笔创建记录里内嵌了运行时画笔结构,在扩展画笔记录里内嵌了运行时画笔结构。这两个运行时结构都把 hatch 成员声明为指针宽度的整数,因为在实际的 GDI 调用中该成员可以携带句柄。然而图元文件始终存储 32 位形式,因为记录布局是序列化文件格式的一部分,不随进程位数改变
32 位构建下两者一致,无事发生。Win64 上,指针宽度的成员是 8 字节,而文件里只有 4 字节,hatch 成员之后的每个字段都从错误的偏移读取。没有异常、没有解析错误、没有警告。图元文件只是画错了:颜色来自错误的字节,画笔宽度来自错误的字节,整幅图像渲染 bug 而不像结构布局 bug。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 部署意味着什么
在 Free Pascal 下的 Windows 平台,矢量 EMF 导入正常工作,产出与 Delphi 构建相同的页面内容:路径还是路径,渐变还是图案内容,文本还是文本。Windows 之外仍走栅格路径,这是格式的限制而非移植的缺陷。转换器写入的坐标与裁剪状态在内容流 CTM 与裁剪状态跟踪一文中描述,它输出的矢量图元则见矢量图形、着色器与渐变
如果你在审计自己的代码库寻找同类机会,有用的练习正是引发这一切的那一步:列出你实际用到的、来自你以为所依赖的框架的成员。答案往往比 import 清单暗示的短得多,而真正的约束通常完全在别处。基于设备上下文的导入路径一般见打印预览与设备上下文一文,平台与工具链覆盖情况列在 losLab PDF Developer Library 产品页