三个光栅化器可以读同一份 PDF,却对它说了什么各执一词。PDF Library for Delphi 里的内置引擎不需要任何额外文件,样样都渲染得称职,这也是它坐上默认位置的理由。Cairo 带来的是另一套透明度与抗锯齿流水线,当软蒙版或混合模式在别处出错时,人们往往会转而找它。PDFium 承载的是 Chrome 的渲染代码,因此一个在浏览器里看着对的页面,在 PDFium 下通常也对,代价是一个体积不小的 DLL,以及它坚持要匹配的位数。抽象地讲,三者没有哪一个是正确的。正确性是按文档而言的,而要弄清哪个引擎更适合某一批语料,唯一诚实的办法就是把这批语料在每个引擎上都跑一遍
这就是把引擎当作运行期选择而不是构建期选择的理由。losLab 出品的 Delphi 与 C++Builder PDF 库 PDF Library for Delphi 把三者都放在同一个渲染界面之后,于是这个决定的代价是一个整数,而不是一处代码分支。剩下的问题归结为:如何在它们之间安全地选择、如何确认某个已部署的二进制实际带了哪些引擎,以及如何不让渲染状态悄悄污染下一个作业
一个调用界面背后的三个光栅化器
库给自己的引擎编了号。引擎 1 是内置渲染器,也是默认项,在 Windows 上带 GDI+ 平滑选项。引擎 2 是 Cairo,引擎 3 是 PDFium,两者都在运行期经 SelectRenderer 选定。这两个外部引擎从 DLL 加载,其路径要在选择它们之前用 SetCairoFileName 和 SetPDFiumFileName 提供。不管哪个引擎处于活动状态,实际工作都走同一批调用:RenderPageToFile、RenderPageToStream、RenderDocumentToFile。切换引擎只是挪动一个数字;你其余的渲染代码根本察觉不到
目标模型的覆盖面远不止位图。渲染器类还能输出到元文件(WMF、EMF、EMF+)、EPS、设备上下文、打印机和 HTML5,其中 Cairo 与 PDFium 只有在编译进来之后才会作为额外目标出现。三个引擎分歧最明显的地方是光栅输出,因此本文的示例用的都是它
永远不要假定某个引擎存在:启动时先探测
Cairo 与 PDFium 属于条件编译特性,这意味着一个二进制完全可以在不含它们的情况下构建出来。真出现这种情况时,索要引擎 2 或 3 并不会引发任何异常。SelectRenderer 只是返回一个不同于你所请求 ID 的值,而忽略返回值的代码会继续用原先活动的那个引擎渲染下去。防御手段是一次启动探测:让每个引擎自报家门,并把答案记录下来:
function ProbeEngines(PDF: TPDFlib): string;
begin
Result := 'built-in'; // 引擎 1 始终存在
if (PDF.SetCairoFileName('cairo.dll') = 1) and (PDF.SelectRenderer(2) = 2) then
Result := Result + ', cairo';
if (PDF.SetPDFiumFileName('pdfium.dll') = 1) and (PDF.SelectRenderer(3) = 3) then
Result := Result + ', pdfium';
PDF.SelectRenderer(1); // 干正事之前先恢复默认引擎
end;
在启动时跑一次这段探测,并把它的结果连同每个渲染作业一起写进日志。当客户报告渲染差异时,最常见的第一个问题就是他们的安装里到底有哪些引擎,而日志里躺着的那一行答案,不用远程桌面就能了结这件事。还有个有用的副作用:如果 SetPDFiumFileName 本身返回 0,你立刻就知道问题出在 DLL 上(路径不对、位数不对、依赖缺失),而不是二进制没编进 PDFium 支持,因为在 SelectRenderer 有机会运行之前,路径调用就什么也没解析到
一个 Options 整数背后的十种输出格式
渲染调用上的 Options 参数选定输出编码:0 是 BMP,1 是 JPEG,2 是 WMF,3 是 EMF,4 是 EPS,5 是 PNG,6 是 GIF,7 是 TIFF,8 是 EMF+,9 是 HTML5。对预览和归档页面图像来说,PNG(5)是明智的默认值。而对文件体积比锐利边缘更要紧的照片类扫描件,JPEG(1)搭配 SetJPEGQuality 是更好的选择
有一种格式对目标流藏着一条要求。BMP 路径先写图像数据,再回退到偏移 0x26 去修补文件头里的分辨率字段。把它指向一个只能向前的流,比如一个压缩包装器或一个网络套接字,调用就会以一种看起来像引擎故障、实则不是的方式失败。当无法避开不可寻址的目标时,请改渲染 PNG,或者先把 BMP 暂存到一个内存流里,等它完整之后再整体转发出去
你传进去的 DPI 不是你拿到的 DPI
每个渲染调用都接受一个 DPI 参数,但你实际得到的分辨率是这个值乘以全局渲染缩放。SetRenderScale 的起始值是 1.0,而你一旦改了它,新的系数就会悄悄作用于该实例上此后的每一次渲染:
PDF.SetRenderScale(2.0); // 此后每一次渲染都会加倍
PDF.RenderPageToFile(150, 1, 5, 'p1.png'); // 实际相当于 300 DPI
PDF.SetRenderScale(1.0); // 记得复位,否则缩略图会大得吓人
同样的粘性也适用于 SetRenderCropType 和 JPEG 质量设置。在一个用同一个共享实例同时产出缩略图、预览图和印刷分辨率图像的服务里,这些残留设置才是那张偶尔冒出来的「缩略图怎么突然有 40 MB」工单背后的真正原因。有两条干净的出路:在每个操作开头把相关状态复位,或者给每种输出档位专门配一个实例,让状态无从跨界泄漏
在动别的引擎之前,先把默认引擎调好
相当一部分「我们需要换个引擎」的诉求,最后被证明只是乔装打扮过的设置问题。内置渲染器通过 SetGDIPlusOptions 以及范围更广的 SetRenderOptions 一族暴露它的平滑行为,而 SetGDIPlusFileName 让你在部署环境自带了某个不寻常运行时的时候把它指向特定的 GDI+ 运行时。低 DPI 下锯齿状的线条图、缩略图里发虚的文字、渐变上的色带:所有这些都对这些旋钮有反应,而拧它们对安装包毫无成本。相比之下,加进 Cairo 或 PDFium 意味着多发若干 DLL、多跟踪第二或第三个位数变体,还要背上更新它们的义务
因此,面对一条质量投诉有其自然的操作顺序。先用客户完全相同的 DPI 与缩放去复现,因为有一半的情况在这两者对齐之后差异就蒸发了。接着试试内置引擎的平滑选项。只有到这时,才在其他所有变量都保持不变的前提下,把同一页跨引擎并排比较:以相同 DPI 分别经引擎 1、2、3 渲染成 PNG,把三份都附上。通常三者中有两者一致,而这个多数派会告诉你,异常的那一个究竟是文档被做了不同的解读,还是你自己的基线预期出了偏差。三张具体的图像了结一场「渲染错了」的争论,远比一段形容词快得多
一条会自己交代原因的回退链
探测和状态纪律到位之后,回退链本身就很短。检测失败依靠的是 LastRenderError,它保存着最近一次渲染中引擎自己的消息文本,渲染成功时则为空:
procedure RenderPageWithFallback(PDF: TPDFlib; Page: Integer; const OutFile: string);
begin
PDF.SelectRenderer(1); // 先用内置引擎
PDF.RenderPageToFile(200, Page, 5, OutFile); // 5 = PNG
if PDF.LastRenderError = '' then Exit;
LogEngineFailure('built-in', Page, PDF.LastRenderError);
if PDF.SelectRenderer(3) = 3 then // PDFium 作为重量级后备
begin
PDF.RenderPageToFile(200, Page, 5, OutFile);
if PDF.LastRenderError = '' then Exit;
LogEngineFailure('pdfium', Page, PDF.LastRenderError);
end;
raise Exception.CreateFmt('Page %d failed on all available engines', [Page]);
end;
这里有两个设计要点分量不轻。链条会记录每次切换发生的原因,因为一行写着「自 3.7 版起这一页开始回退到 PDFium」的日志,是你希望在监控里看到趋势、而不是任其丢失的回归信号。回退顺序本身则是一条值得按工作负载来选的策略。内置引擎部署时不需要额外 DLL,这让它在多数安装里都是正确的首选;而透明组或异常着色密集的文档,通常正是一个团队会去接入替补引擎的全部理由。没有哪个引擎在普遍意义上最快,这恰恰是按调用来选择的意义所在:拿你真实文档的样本、在你真实的 DPI 上对每个引擎做基准测试,并在引擎 DLL 或文档构成发生变化时重做这次测量。每一次赢下争论的都是语料
越过单页:TIFF 批处理与实时设备上下文
按页调用旁边还有两位邻居把工具箱补齐。RenderAsMultipageTIFFToFile 把一个页码区间表达式直接渲染成多页 TIFF,这是交接给那些比 PDF 还要古老的文档管理系统时的天然形态。RenderPageToDC 为预览控件直接绘制到 Windows 设备上下文上,它由自己的三个粘性设置管辖(SetRenderDCOffset、SetRenderDCErasePage,再加上裁切类型),这些设置需要与缩放系数同样的复位纪律。屏幕预览和打印路径渲染自身的陷阱之多,足以单开一篇文章,链接见下
接下来去哪儿
有个习惯值得带走:由于 SelectRenderer 对该实例上此后的每一次调用都生效,你可以让某一页顽固的内容改用另一个引擎重试,而文档其余部分继续留在默认引擎上。关于预览绘制、打印机选择和 DevMode 处理,请接着看打印预览与设备上下文一文。当渲染要为超大文件的高吞吐流水线供料时,直接访问指南里那套基于句柄的做法,与通过 DARenderPageToFile 的按页渲染天然配套
引擎打包方式、支持的格式和试用构建详见 PDF Library for Delphi 产品页