技术文章

在 Delphi 中渲染 PDF 渐变:轴向与径向

如果 PDF 的表头带有 Word 或 Excel 风格的渐变背景,以前将其渲染为图像时,在色彩混合处通常会返回一个空白的白色矩形。HotPDF 的页面渲染器现在支持针对类型 2 轴向和类型 3 径向渐变的 PDF sh(阴影)操作符:它沿着渐变轴计算阴影函数并绘制混合色彩,而早期版本会默默丢弃该操作符,导致该区域留白。这里包含了几乎涵盖所有办公文档渐变的这两种阴影类型;网格类型则未被包含,本文坦率地指出了这条界线所在的位置

为什么 PDF 渐变会渲染为白盒子?

PDF 渐变是一个矢量结构,而不是渲染器可以直接传送的位图。阴影字典 (ISO 32000-1 §8.7.4.5) 描述了几何形状(Coords 数组中的轴或一对圆)以及颜色函数,而 sh 操作符在当前裁剪区域绘制该阴影,这与您 在页面画布上绘制路径和填充 时 HotPDF 暴露的图形模型相同。HotPDF 的页面渲染器逐个遍历内容流操作符,任何没有代码路径的操作符都会被跳过,以便页面的其余部分仍能渲染。在 2.335.0 版本之前,sh 处于该跳过的集合中:表头或全页渐变对输出没有任何贡献,因此背景显示为白色。文件从未损坏,渲染器只是没有该操作符的处理程序,而白盒子正是缺失处理程序时的表现

阴影背后的函数求值链

在评估其函数之前无法绘制阴影,因此该机制必须首先就位。类型 2 轴向和类型 3 径向阴影都携带 /Function,它将沿着渐变的一个参数化位置映射为颜色 (ISO 32000-1 §7.10),并且 HotPDF 的函数求值器解析 FunctionType 并对其进行分发。类型 2 指数插值 (§7.10.3) 计算 C0 + t^N * (C1 - C0) 且带有 DomainRange 夹紧;类型 0 采样函数 (§7.10.2) 读取 8 位、16 位或 32 位采样的网格,并通过 Encode 映射选择最近的,这是在 我们对类型 0 采样颜色查找的演练 中深入涵盖的案例;类型 3 拼接函数 (§7.10.4) 通过 Bounds 数组选择子函数并递归进入。类型 4 PostScript 计算器函数 (§7.10.5) 则不进行评估——它们优雅地降级为评估失败,而不是输出错误的颜色,这符合 HotPDF 在渲染路径上采取的保守策略。该求值器在 2.334.0 版本中发布,比操作符本身早了一个版本,因为阴影绘制器的效果取决于其底层的函数

HotPDF 如何将连续渐变转换为像素?

HotPDF 使用有限的纯色带流来逼近连续的混合色彩。阴影绘制器遍历渐变轴,将其拆分为最多 64 个色带(当轴在设备像素中较短时则更少),计算每个色带参数化位置的函数,并用实心刷子填充垂直于轴的矩形。这里的权衡是色带数量与平滑度:较多的色带能更紧密地跟踪混合,但填充开销也更大,64 是刻意设定的上限。因为该数量还与设备像素中的轴长度成比例,所以分辨率至关重要。在更高的 DPI 下渲染同一页面会拉长轴,因此较短的渐变会获得更多的色带,其条纹会变柔和,直到达到 64 个色带的上限

// 阴影离散化器沿着渐变轴使用最多 64 个色带
// 但绝不会超过该轴在设备像素中的长度。渲染
// 同一页面在更高 DPI 下会拉长该轴,因此较短的渐变
// 会获得更多色带,色带化会变柔和,直到达到 64 个色带的上限
Thumb := Pdf.RenderLoadedPageToBitmap(PageIndex, 96);   // 较粗糙的色带
Proof := Pdf.RenderLoadedPageToBitmap(PageIndex, 300);  // 最多 64 个色带

将阴影页面渲染到位图

公共入口点保持不变:RenderLoadedPageToBitmap 接受基于 0 的页面索引和 DPI 值,返回一个调用者拥有的 24 位位图,并在失败时返回 nil 而不是引发异常,因此阴影支持对于您已有的代码是透明的。以前将其渐变表头渲染为空白的页面,现在会返回绘制了混合色彩的相同位图,且无需更改 API 即可获得它。唯一值得记住的是所有权:调用者负责释放返回的位图。完整的渲染管道(内容流解释、嵌入字形和页面缓存)在 在 Delphi 中将 PDF 页面渲染到位图的指南 中有涵盖

var
  Pdf: THotPDF;
  Bmp: TBitmap;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('quarterly-report.pdf') > 0 then
    begin
      // 150 DPI 下的第 1 页(基于 0)。曾经返回空白的
      // 渐变填充表头现在携带了其色彩混合
      Bmp := Pdf.RenderLoadedPageToBitmap(0, 150);
      if Assigned(Bmp) then
      try
        Bmp.SaveToFile('page1.bmp');
      finally
        Bmp.Free;
      end;
    end;
  finally
    Pdf.Free;
  end;
end;

一个简单的回归测试可以通过相同的调用实现。渲染页面,在渐变色带所在位置对一个像素进行采样,并确认它不再是渲染器以前退回的纸张白——这是一个单行检查,可以捕捉到是否默默返回旧的空操作行为

Bmp := Pdf.RenderLoadedPageToBitmap(0, 150);
if Assigned(Bmp) then
try
  // sh 操作符绘制时为 True;如果区域退回到白色则为 False
  HeaderPainted := Bmp.Canvas.Pixels[Bmp.Width div 2, 8] <> clWhite;
finally
  Bmp.Free;
end;

HotPDF 不绘制的内容:网格阴影与裁剪边缘

四种网格阴影类型超出了范围,文档在开始前应当了解这一点。在 ISO 32000-1 §8.7.4.5.5 到 §8.7.4.5.8 中定义的自由格式 Gouraud 三角形网格 (类型 4)、网格格式 Gouraud 网格 (类型 5)、Coons 补片网格 (类型 6) 和张量积补片网格 (类型 7) 都不会被渲染;使用其中一种的页面仍会渲染,只是该区域保持原样。还有两处边界值得明确说明:色带矩形覆盖的是阴影的条纹,而不是被精确裁剪到当前的裁剪路径,因此被遮罩为圆形或不规则形状的渐变会绘制为其边界条纹,直到后期的版本中引入精确的区域裁剪。HotPDF 还假设函数输出为 RGB 或灰度,因此以 CMYK 或特殊色彩空间编写的阴影是近似处理的,而不是进行色彩管理。对于大多数 Delphi 和 C++Builder 团队实际处理的文档(报表、发票、账单和导出为 PDF 的幻灯片),轴向和径向基本上涵盖了您会遇到的每种渐变,而网格繁多的设计样张则应当作为预览而不是打样来对待

HotPDF 的页面渲染器、此处描述的 sh 操作符支持以及底层的类型 0/2/3 函数求值器作为适用于 Delphi 和 C++Builder 的 HotPDF Component 的一部分提供——这是一个没有外部 DLL 依赖的原生 VCL 库,在单个包中涵盖了 PDF 的创建、编辑、文本提取和页面渲染