HotPDF 通过单次调用即可将已加载的 PDF 页面渲染为 Delphi 的 TBitmap:RenderLoadedPageToBitmap(PageIndex, DPI)。该函数解释页面的内容流,并以您选择的分辨率返回一个调用者所有的 24 位 RGB 位图,这正是缩略图条、打印预览或 PDF 到图像导出管道所需要的。本文将逐步介绍该 API,然后介绍区分可用渲染器与玩具的部分:直接从嵌入的字体程序本身绘制文本,而不是从相似的系统字体绘制
为什么渲染 PDF 页面比绘制图像还要难?
PDF 页面不是一幅图画。它是一个程序:由构建路径、选择字体、设置颜色和放置字形的一系列运算符组成的流,针对 ISO 32000-1 §8 中定义的图形模型执行。文件中的任何内容都没有说明像素的外观。要生成位图,您必须运行该程序 — 维持当前变换矩阵、用于 q/Q 的图形状态栈、裁剪路径、填充与描边色彩空间 — 并对结果进行光栅化。这就是为什么“仅将第 3 页显示为图像”是一个内容流解释器,而不是一个文件格式转换
HotPDF 在 v2.253.0 中引入的渲染器由六个解耦的单元构建而成,完美镜像了该模型:一个用于 PDF [a b c d e f] 变换代数的仿射矩阵核心、一个图形状态栈、一个色彩空间解析器(DeviceRGB、DeviceGray、DeviceCMYK、Indexed)、一个将 PDF 路径运算符桥接到 GDI 的路径构建器、一个读取 /Widths 数组以获得正确步进的字体度量层,以及分发运算符并驱动其他五个单元的解释器。图像 XObject 会经过该库用于提取的相同解码栈,因此 HotPDF 可解码用于提取的每个图像过滤器 — 包括经 JPXDecode 压缩的 JPEG 2000 图像 — 也会出现在渲染的输出中
将加载的页面渲染为 TBitmap
RenderLoadedPageToBitmap 接受从零开始的页面索引和 DPI 值,其中 72 DPI 将一个 PDF 用户空间单位映射为一个像素。失败时(索引超出范围、缺少资源)它返回 nil 而不是抛出异常,这样查看器就可以跳过损坏的页面并继续。调用者拥有返回的位图并必须释放它
var
Pdf: THotPDF;
Bmp: TBitmap;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('report.pdf') > 0 then
begin
Bmp := Pdf.RenderLoadedPageToBitmap(0, 144); // 144 DPI 下的第 1 页
if Bmp <> nil then
try
Image1.Picture.Assign(Bmp);
finally
Bmp.Free; // 调用者拥有该位图
end;
end;
finally
Pdf.Free;
end;
end;
DPI 参数为每种常见场景完成了缩放工作。缩略图条在 36 或 48 DPI 下进行渲染,并获得小而快速的位图;屏幕预览在 96 或 144 DPI 下匹配典型的显示密度;300 DPI 的导出路径则生成打印质量的图像。来自 /Rotate 条目的页面旋转和 /MediaBox 原点翻转(PDF 将原点置于左下角,GDI 置于左上角)是在页面到设备矩阵内部处理的,因此 72 DPI 下 of 的 US Letter 页面返回时正好是方向正确的 612×792 像素
为什么渲染的 PDF 缩略图会显示错误的字形?
物理的 PDF 输出中出现错误或近似的字形几乎总是意味着渲染器在替换系统字体,而不是使用文件中嵌入的字体。第一代 HotPDF 渲染器正是这样做的:它从 /BaseFont 中剥离了子集前缀(将 ABCDEF+Arial 变为 Arial),向 GDI 请求该名称的系统字体,并用其绘制文本。对于使用标准编码的 Arial 或 Times New Roman 的文档,其结果看起来很接近。但这只是一种近似,并且会在特定情况下失效
嵌入字形渲染:直接从字体程序本身绘制
HotPDF 通过五个版本(v2.268.0 到 v2.272.0)的迭代,通过解析嵌入的字体程序并将其字形轮廓重放为填充的 GDI 矢量路径,填补了这一空白。渲染页面中的文本现在来自符合标准的查看器所使用的相同轮廓数据,这意味着子集字体、自定义编码和未安装的字体都能以其精确的形状渲染。其支持范围是按字体类型逐步构建的:
对于具有嵌入式 TrueType 程序(FontFile2)的 Type0/CIDFontType2 字体,渲染器直接解析 glyf 和 loca 表:二次贝塞尔轮廓转换为 GDI 理解的三次贝塞尔曲线,重建连续离线点之间的隐含在线点,并递归重放复合字形。同时支持 Identity 和显式流 CIDToGIDMap 布局,并且 CID 步进遵循 /W and /DW 宽度条目,因此双字节 Identity-H 文本能够正确迈步
CFF 程序(FontFile3,无论是 CIDFontType0C、Type1C 还是 OpenType 包装器)获得了完整的 Type 2 字符字串(charstring)解释器:线、曲线、flex 族、提示(hint)掩码,以及带有正确子程序偏置的局部/全局子程序调用。CID 键控的 CFF 程序通过字体的字符集映射字符代码,这对于字形顺序与 CID 顺序不同的子集字体非常重要,并且遵循通过 FDArray/FDSelect 进行的每字形字体-DICT 选择。简单(非 CID)TrueType 字体通过嵌入字体自身的 cmap 表和健壮的子表链解析单字节代码 — Unicode 格式 4 和 12 优先,然后是带有 F000 私有使用镜像的符号子表,接着是遗留的 Macintosh 格式 — 而简单的 Type1 字体通过 CFF 程序内置的编码解析
两项改进使方案更加完善。首先,简单字体的 /Encoding 字典按照 ISO 32000-1 §9.6.6 规定的优先级进行解析:/Differences 数组覆盖基本编码,基本编码覆盖字体程序自身的映射 — 这是 TeX 和派生自 PostScript 的工具链所依赖的路径,字形名称通过 Adobe 字形列表(Adobe Glyph List)、CFF 字符集或 TrueType cmap 解析。其次,Type3 字体(其字形本身就是微型内容流)通过将字体矩阵、字体大小和文本矩阵组合后通过渲染器进行重放;字形空间的 /Widths 按照 ISO 32000-1 §9.6.5 的要求通过 /FontMatrix 解释,声明了 d1 包围盒的字形程序会被裁剪到该盒子中,因此畸形的条形码字形无法在其单元格外部绘制。当代码无法映射时 — 损坏的程序、未映射的字符 — 渲染器会针对该字形回退到系统字体绘制,而不是丢弃整个文本运行段
如何使重复渲染变得快速?
HotPDF 交付的答案是最近最少使用页面缓存:RenderLoadedPageToBitmapCached 保存多达 RenderCacheCapacity 个渲染页面(默认 8 个),以页面索引和 DPI 为键,并且缓存命中将在不触及内容流的情况下返回一份全新的调用者拥有的副本 — 这通常比重新解释页面快数千倍。该模式完美契合查看器:用户在两个页面之间切换,或者重新以相同 DPI 请求相同页面的调整大小事件,每次都会命中缓存
// 缩略图条:第一遍渲染,向回滚动会命中缓存
for I := 0 to ThumbCount - 1 do
begin
Bmp := Pdf.RenderLoadedPageToBitmapCached(I, 48);
if Bmp <> nil then
try
ThumbList.AddThumbnail(I, Bmp);
finally
Bmp.Free;
end;
end;
// 就地编辑加载的页面之后:
Pdf.InvalidateRenderedPageCache; // 下一次渲染将反映该更改
在提高容量之前,请务必清醒地认识到内存开销。300 DPI 下的 US Letter 页面为 2550×3300 像素,作为 24 位位图大约占用 25 MB,因此在导出分辨率下缓存八个页面大约占用 200 MB。在缩略图 DPI 下,同样的八个条目开销远低于一兆字节。请根据您实际缓存的 DPI 大小来设定 RenderCacheCapacity,并在任何就地编辑后调用 InvalidateRenderedPageCache — 缓存仅以页面和 DPI 为键,它无法发现底层内容已发生更改。加载新文档会自动清除缓存
第二级缓存工作在页面缓存下方:解码的图像 XObject 被保留在一个由 ImageCacheMaxBytes(默认 32 MB)限制的字节预算存储中,并采用最近最少使用逐出策略。在每个页面上重复出现的徽标或信头图像在每次文档加载时仅解码一次,而不是每次 Do 运算符都解码一次,这使共享图像页面的渲染时间缩短了大约一半,并以同样的幅度加快了多页 TIFF 导出的速度。InvalidateRenderedPageCache 也会清除此缓存
哪些内容仍然是近似渲染
渲染器针对的是常见的文档 PDF 子集,了解边缘限制是很有必要的。CalRGB、Lab 和基于 ICC 的色彩空间是近似处理的,而不是进行色彩管理 — 设备色彩空间、索引调色板以及采样的 Type 0 函数色彩查找可得到处理,但依赖于 ICC 渲染意图的印刷制作文件在比色上不会完全精确。渐变填充模式(sh)和超出简单 Alpha 的混合模式同样不在范围内,并且 Form XObject 递归受深度限制以作为循环保护。对于发票、报告、合同和表单 — 由文本、路径和图像构成的页面 — 输出是忠实的;对于充满渐变和透明度组的设计打样,请将位图视为预览,而不是打样
实际的结论:如果您的管道使用 HotPDF 生成文档或消费典型的商业 PDF,RenderLoadedPageToBitmap 会以精确的嵌入字形形状、正确的 CID 步进和正确的页面几何图形对其进行双向回转。近似处理存在于商业文档很少涉及的图形模型的角落中
RenderLoadedPageToBitmap、其缓存变体以及此处描述的嵌入字形渲染管道是适用于 Delphi 和 C++Builder 的 HotPDF Component 的一部分发售 — 这是一个没有外部 DLL 依赖的原生 VCL 库,将 PDF 创建、编辑、文本提取和页面渲染涵盖在一个包中