在 PDFium Component for Delphi 里,打开 TPdfLibraryConfiguration 的 BrotliEnabled 或 IsolatePerDocument 曾会把随附的 Skia 构建无声切到 AGG 渲染器,因为这两个选项把 FPDF_LIBRARY_CONFIG 抬到了 PDFium 按字面读 m_RendererType 的版本。自 v3.123.0 起,默认渲染器保持 DLL 自己的默认;自 v3.125.0 起,DLL 兑现不了的 Skia 或 Fontations 请求会抛可捕获的 EPdfError,而不是弄死进程
两个 bug 都不声不响。第一个产出看着没问题的页面——只是出自另一个光栅化器,抗锯齿和文字边缘与你出厂测试的那份构建略有出入。第二个倒是声势浩大地自报家门:从原生初始化内部把宿主进程带走。两者同源:一个带版本的 C 结构,字段只有在版本号点头时才作数,而它的零值不是「未设置」,是真实的选择
FPDF_LIBRARY_CONFIG 怎么决定 PDFium 用哪个渲染器?
FPDF_InitLibraryWithConfig 只在结构的 Version 字段为 4 或更高时才看 m_RendererType,而从那个版本起它就按写的字面用值。版本 4 以下 PDFium 无视该字段、取构建默认——以 PDF_USE_SKIA 编译的构建是 Skia,其余地方是 AGG
后面的每个字段都循同一模式。结构一次长出一个能力,每个能力都带着新版本号一起到来。PDFium Component 在 LoadLibrary 里从你的 TPdfLibraryConfiguration 搭建原生结构,版本只抬到你设置的选项所要求的程度
| 结构版本 | 新增字段 | 何时写入 |
|---|---|---|
| 2 | m_pIsolate, m_v8EmbedderSlot | 恒定写入;V8Isolate, V8EmbedderSlot |
| 3 | m_pPlatform | V8Platform 非 nil |
| 4 | m_RendererType | Renderer 非 prpDefault |
| 5 | m_FontLibraryType | FontBackend 非 pfbpDefault |
| 6 | m_BrotliEnabled | BrotliEnabled = True |
| 7 | m_IsolatePerDocument | IsolatePerDocument = True |
陷阱在最后两行。版本是累积的:version 6 结构同时也是 version 4 和 version 5 结构,所以 PDFium 会读 m_RendererType 和 m_FontLibraryType,哪怕你只想要 Brotli。那一刻这两个字段里坐着什么,什么就成了渲染器和字体后端,不管你有没有打算选
打开 Brotli 为什么会把渲染器换成 AGG?
v3.123.0 之前,PDFium Component 对 prpDefault 往 m_RendererType 里写 FPDF_RENDERERTYPE_AGG,于是任何把结构推到 version 6 或 7 的配置都会在 Skia 构建上强加 AGG。组件随附的 pdfium.dll 和 pdfium.v8.dll 运行时是 Skia 构建,所以挨打的是默认部署,不是什么冷门配置
这段映射落笔时看着无害。version 2 或 3 时字段从不被读,prpDefault 确实意味着「DLL 爱咋咋」。BrotliEnabled(version 6) 或 IsolatePerDocument(version 7) 一进场,同一段代码就把「没有偏好」变成了显式的 AGG 请求。什么都没失败。PDFium 正常初始化、渲染每一页、不返回任何错误码——因为在它看来,调用方要了 AGG,也得到了 AGG
像素哈希能在截图看不见的地方让这场调包现形。同一份示例文档的首页在三种配置下渲染,结果是:
- 默认配置:哈希
502D77C3711B4ACF BrotliEnabled= True 且Renderer留在prpDefault:哈希F75B5EB4728ADE87- 显式
prpAgg:哈希F75B5EB4728ADE87,与 Brotli 那次完全相同
v3.123.0 的修复是公开函数 PdfNativeRendererType,它把 TPdfRendererPreference 解析成写进 m_RendererType 的值。prpAgg 和 prpSkia 一一对应。prpDefault 现在在加载的 DLL 导出 FPDF_RenderPageSkia 时映射为 Skia,否则 AGG。那个导出与 Skia 默认本身在同一个 PDF_USE_SKIA 条件下编译,这使它成为唯一一个能从 DLL 外部观测到的构建属性。修复之后,Brotli 配置产出与默认配置相同的哈希
字体后端从没闹过同样的毛病。m_FontLibraryType 从 version 5 起被读,它的零值 FPDF_FONTBACKENDTYPE_FREETYPE 恰好也是字段完全不被读时 PDFium 的默认。所以对 pfbpDefault 写 FreeType 正好复刻原生默认。零值不一定错,只是从来不会自动对
有了 v3.123.0 或更新版本,你自然会写的那段启动代码终于言行一致:
uses
PDFium;
procedure ConfigurePdfiumAtStartup;
var
Config: TPdfLibraryConfiguration;
begin
// 必须在任何东西加载原生库之前运行
Config := TPdfLibraryConfiguration.Default;
Config.BrotliEnabled := True; // 把 FPDF_LIBRARY_CONFIG 提到 version 6
// Renderer 保持 prpDefault:导出 FPDF_RenderPageSkia 的构建解析为 Skia,
// 仅 AGG 构建解析为 AGG
SetLength(Config.UserFontPaths, 1);
Config.UserFontPaths[0] := 'C:\ProgramData\MyApp\Fonts';
ConfigurePdfLibrary(Config);
end;
记住 BrotliEnabled 只有在 DLL 本身以 PDF_ENABLE_BROTLI 构建时,才能让 PDF 2.0 的 /BrotliDecode 流变得可解。标志是请求,在不支持 Brotli 的构建上没有任何效果。TPdfLibraryConfiguration.Hardened 与 Default 相同,只是 AllowMachineTime 为 False,挡住文档 JavaScript 读真实时钟;作为服务端处理不可信文件的起点很合适
要了 DLL 里没有的后端会怎样?
构建里缺渲染器或字体后端时,PDFium 不返回错误:FPDF_InitLibraryWithConfig 会挂掉一个原生 CHECK,在 Windows 上表现为断点异常,调用周围没有结构化异常处理就终止进程。头文件把话挑明了,警告不支持的值「will similarly fail with an immediate crash」
两个具体情形:仅 AGG 构建收到 FPDF_RENDERERTYPE_SKIA,无 Fontations 构建收到 FPDF_FONTBACKENDTYPE_FONTATIONS。随附的 Skia 运行时属于第二类:它用 Skia 渲染、字体却用 FreeType。对它同时要 prpSkia 加 pfbpFontations,Delphi 这边得到 External exception 80000003。调试器或异常处理器碰巧接住时,局面照样无可挽回:
- PDFium 处于半初始化状态
- 进程级配置已经封印,
ConfigurePdfLibrary拒绝修正后的配置 - 同一进程内换配置重试已无可能
这是与 Brotli bug 相反的失败。那边字段里坐着一个没人选的值,PDFium 无声接受;这边字段里是调用方刻意选的值,PDFium 连商量都不给。两者都是包装层必须在原生调用之前解决的问题,因为调用之后已无可捕获之物
PDFium Component 如何预检 Skia 与 Fontations
从 v3.125.0 起,LoadLibrary 在绑定 DLL 导出之后、调用 FPDF_InitLibraryWithConfig 之前校验配置,把不支持的渲染器或字体后端变成一条 EPdfError,消息点名肇事的设置和可选替代。DLL 被卸载、配置解除封印,调用方可以另选设置再次加载
判定本身住在纯函数 PdfLibraryConfigurationSupportError 里:输入配置加两个描述构建的布尔值,组合安全时返回空串。因为它不碰任何原生状态,你可以在自己的测试里用任意能力组合调它。LoadLibrary 内部那两个布尔值来自不同种类的证据,信任等级也理应不同:
- Skia 由
FPDF_RenderPageSkia导出的在场与否判定,与PdfNativeRendererType用的是同一信号。导出与 Skia 渲染器在同一条件下编译,检查因此精确 - Fontations 没有自己的导出。它留下的唯一痕迹是被拽进二进制的 Rust 字体 crate,所以 PDFium Component 在加载的库文件里扫 crate 名
skrifa和read-fonts(也作read_fonts)。扫描只在请求pfbpFontations时进行,读不了的文件按「无 Fontations」计
Fontations 检查是启发式的,它只可能往一个方向出错:被剥光这些字符串的 Fontations 构建会被拒之门外,尽管它本可以工作。这个取舍是故意的。假拒成本是你捕获一个异常、回落 FreeType;假受成本是整个进程
解封与检查同样要紧。LoadLibrary 在加载最开头就封印配置,没有重置的话,一次能力拒绝会让 ConfigurePdfLibrary 对每次重试都回以 EPdfError「PDFium library configuration is already sealed」。拒绝路径先调 UnloadLibrary;此刻调 FPDF_DestroyLibrary 是安全的,因为 PDFium 尚未初始化、立即返回。其他加载失败——比如 DLL 缺失或架构不匹配——保持封印,所以重试循环必须分清两者:
uses
SysUtils, PDFium;
function StartPdfiumPreferringSkia: TPdfRendererPreference;
var
Config: TPdfLibraryConfiguration;
begin
Config := TPdfLibraryConfiguration.Default;
Config.Renderer := prpSkia;
ConfigurePdfLibrary(Config);
try
PDFium.LoadLibrary; // 带单元限定:Windows.LoadLibrary 同名
Result := prpSkia;
except
on E: EPdfError do
begin
// 能力拒绝会卸载 DLL 并解除配置封印。
// 根本没加载成的 DLL 保持封印:重试无济于事
if PdfLibraryConfigurationSealed then
raise;
Config.Renderer := prpAgg;
ConfigurePdfLibrary(Config);
PDFium.LoadLibrary;
Result := prpAgg;
end;
end;
end;
注意显式的 PDFium.LoadLibrary。在同时 uses Windows 或 Winapi.Windows 的单元里,不带限定的 LoadLibrary 解析到 uses 子句里最后出现的那个单元;轮到 Win32 函数时,无参调用编不过,报一个与 PDFium 毫无关系的参数个数错误
更早一步的校验
ConfigurePdfLibrary 在任何 DLL 登场之前就拒绝某些组合,一律抛 EPdfError。显式 FontBackend(含 pfbpFreeType)要求 Renderer = prpSkia,因为 PDFium 只对 Skia 渲染器过问字体后端。IsolatePerDocument 要求 V8Isolate 为 nil,因为 PDFium 按文档自建 isolate,你再塞一个就挂原生 CHECK。UserFontPaths 里的空字符串被拒。首次加载尝试之后的任何调用都以「PDFium library configuration is already sealed」失败
最后这条规则有个现实后果:你不能先探 DLL 再配它。GetSkiaRenderCapabilities、V8FeaturesAvailable、打开文档和大多数其他入口在内部调 LoadLibrary,当场封印配置。之后再调 UnloadLibrary 也不会重开封印。先配置、再加载、后提问——诊断例程正该按这个顺序来:
uses
SysUtils, PDFium, FPdfView;
function DescribePdfiumState: string;
var
Config: TPdfLibraryConfiguration;
Renderer: string;
begin
Config := GetPdfLibraryConfiguration; // 一份副本,检视无虞
if not PDFium.Loaded then
begin
if PdfLibraryConfigurationSealed then
Exit('PDFium failed to load; configuration is sealed');
Exit('PDFium not loaded; configuration can still change');
end;
// 与 LoadLibrary 构建 FPDF_LIBRARY_CONFIG 时相同的解析
if PdfNativeRendererType(Config.Renderer,
GetSkiaRenderCapabilities.PageRender) = FPDF_RENDERERTYPE_SKIA then
Renderer := 'Skia'
else
Renderer := 'AGG';
Result := Format('Renderer=%s Brotli=%s IsolatePerDocument=%s',
[Renderer, BoolToStr(Config.BrotliEnabled, True),
BoolToStr(Config.IsolatePerDocument, True)]);
end;
启动时记一行这种日志很便宜,而工单里写「服务器上文字看着不一样」时,你第一想要的就是它。PDFium.Loaded 带限定出于与 LoadLibrary 相同的理由:在窗体或组件方法内部,光杆 Loaded 会绑到 TComponent.Loaded
带版本的 C 配置结构出错的两种方式
每个带版本的配置结构——无论是 FPDF_LIBRARY_CONFIG、Win32 的 cbSize 记录还是插件 ABI——都以两种对称的方式出错,包装层必须两头都防。其一:填了字段、版本却太低;其二:抬了版本、字段却留着被库读成刻意选择的零值
- 字段设了、版本太低。往 version 2 结构里写
m_BrotliEnabled= 1,PDFium 看都不会看。调用成功,Brotli 流依旧解不开。防御之道是从实际用到的字段推导版本——LoadLibrary正是这么做的——而不是写死一个 - 版本够了、零值有意义。版本抬到 6,到 version 6 为止的每个字段全部生效。
FillChar把m_RendererType清成FPDF_RENDERERTYPE_AGG——一个真实渲染器,不是「未设置」。防御之道是给所选版本覆盖的每个字段都写上有意图的值,并对照真实构建解析「默认」,而非想当然
对可能让被调方崩溃的值,还有第三条规则:调用之前对照二进制实际能做什么来校验,用拿得到的最强证据,并在代码和文档里如实说明该证据是启发式。导出符号是证明。字符串表里的 crate 名是不错的猜测
速查:PDFium Component 库配置
- 在任何东西加载 DLL 之前调一次
ConfigurePdfLibrary;任何能力查询或文档加载都会封印它 - 设了
BrotliEnabled或IsolatePerDocument、并期待随附运行时产出 Skia 输出的,升级到 v3.123.0 或更新 - 没有特定光栅化器需求就让
Renderer留在prpDefault;现在在任何结构版本下它都解析为构建默认 - 拿
PdfNativeRendererType配GetSkiaRenderCapabilities.PageRender记录实际生效的渲染器 - v3.125.0 起,仅 AGG DLL 上要
prpSkia、非 Fontations DLL 上要pfbpFontations,等来的是EPdfError而非崩溃 - 能力拒绝之后
PdfLibraryConfigurationSealed为 False、可重新配置;DLL 加载失败之后保持 True - 把 Fontations 检测当启发式对待,备好 FreeType 回落
PDFium.LoadLibrary和PDFium.Loaded带单元名写,避开 Win32 与TComponent的同名冲突
要是 DLL 在配置还没出场时就加载失败,从诊断 Delphi 里的 PDFium DLL 加载失败入手;组件在各平台上怎么找到正确的二进制,见在任意目标上加载 PDFium 原生库。渲染器落定之后,渲染缓存与平滑缩放战术讲怎么让查看器的页面渲染保持快
PDFium Component 为 Delphi 和 C++Builder 包装 PDFium 引擎,带着上面这类配置检查,让原生初始化以你能处理的 Pascal 异常失败,而不是进程退出。产品详情与下载见 PDFium Component for Delphi 产品页