Technical Article

Delphi 中的 PDF 类型 0 采样函数实现 3D 颜色 LUT

PDF 函数是规范中较为低调的部分。大多数开发人员只接触过它们一次,即类型 2 轴向着色为了在两种颜色之间淡化而需要的东西,之后便不再关注。这很遗憾,因为函数机制是一个小型的通用评估器,该格式将其重用于着色、转换函数、半色调网点函数、专色色调和软掩码转换曲线。在四种函数类型中,类型 0 是最强大也是最不被理解的。它是一个采样函数:一个可供阅读器在其间进行插值的多维输出值网格。因为网格可以容纳您放入其中的任何数字,所以类型 0 函数可以表达任意非线性映射,这正是颜色查找表的精确形状。

本文将介绍 ISO 32000-1 第 7.10.2 节中定义的类型 0 字典,然后展示在文档流水线中最重要的两个案例:三输入 RGB 到 RGB 颜色纠正 LUT,以及单输入专色色调变换。相同的采样函数构建器同时服务于两者,它们之间的区别完全在于网格拥有多少个输入。

采样函数是阅读器用于插值的网格

类型 0 函数通过在常规网格上存储样本并进行插值,将 m 输入矢量映射到 n 输出矢量。ISO 32000-1 第 7.10.2 节列出了描述该网格的键。/Domain 对应每个输入保存两个数字,即每个输入轴的下限和上限。/Range 对应每个输出组件保存两个数字。/Size 是一个由 m 个整数组成的数组,给出沿每个输入轴的样本数,因此在三维空间中边长为 12 个样本的网格具有 /Size [12 12 12] 并存储 1,728 个网格点。/BitsPerSample 设置每个存储值的精度;HotPDF 接受 1、2、4、8、12、16、24 和 32 位,与表 38 允许的值相匹配。

样本流按固定顺序读取。第一个输入维度变化最快,然后是第二个,依此类推,并且在每个网格点处,n 个输出组件按顺序存储。对于八位的 RGB 到 RGB 表,每个网格点是三个字节,布局为红输出、绿输出、蓝输出,首先扫描红输入。还有两个键将连续世界映射到整数网格。/Encode 将每个输入从其 /Domain 区间映射到样本索引范围 0Size[i] - 1,而 /Decode 将原始存储的整数映射回 /Range 区间。当您将它们保留为默认值时,跨越 [0 1] 的输入将干净地落在完整网格上,并且存储的 255 字节解码为输出范围的最大值,这正是 [0,1] 归一化颜色 LUT 所需要的。

阶数 1 对比阶数 3

在网格点之间,阅读器必须进行插值,而 /Order 选择如何插值。/Order 1 是多线性插值:沿一个轴是线性,跨两个轴是双线性,跨三个轴是三线性。它速度快,这正是大多数查看器中的硬件所做的,并且对于平滑的颜色转换,它通常与更花哨的方法无法区分。/Order 3 请求三次样条插值,这通过在样本中拟合更平滑的曲线,以更多的工作量和每个评估点周围更宽的支撑区域为代价。

权衡的是网格密度与曲线平滑度。当网格粗糙且映射具有可见曲率时,三次阶数就发挥了作用,因为两个遥远样本之间的直线会使色调曲线变平,以至于眼睛会在渐变上察觉到。一旦网格密集,段就足够短,线性插值可以紧密跟踪曲线,而三次阶数带来的收益微乎其微。一个实用的规则是,仅在小网格或陡峭变换时才使用 /Order 3,否则保留线性默认值。请注意,/Order 仅适用于类型 0 函数,并且 HotPDF 拒绝除 1 或 3 之外的任何值。

3D LUT:三输入,三输出

RGB 到 RGB 的颜色纠正是三输入网格的教科书案例,即用于颜色分级和设备匹配的经典 3D LUT。立方体的每个轴是一个输入通道,每个网格点存储该输入坐标的纠正后 RGB 三元组,并且阅读器对任何传入颜色周围的角样本进行三线性插值。这里三个输入是不可避免的,因为纠正后的红可能取决于输入的绿和蓝,而不只是取决于输入的红;单通道曲线无法表达通道串扰,但立方体可以。

HotPDF 通过 RegisterSampledFunction 构建类型 0 流,该函数直接接受 /Domain/Range/Size/BitsPerSample 和样本字节,并返回函数对象。对于标准的归一化立方体,您在所有三个输入轴和所有三个输出上传入 [0,1] 边界、N x N x N 大小以及扁平的样本表。构建器验证字节数是否与网格匹配:对于字节对齐的深度,它期望 OutputCount x (BitsPerSample div 8) x 大小的乘积,如果数组长度错误则抛出异常,因此计算错误的步幅会在注册时响亮地失败,而不是在稍后渲染为垃圾数据。

const
  N = 17;  // 17 x 17 x 17 cube, the common ICC LUT resolution
var
  LutFn: THPDFStreamObject;
  Samples: TBytes;
begin
  // Fill Samples with N*N*N grid points, 3 bytes each (R,G,B output),
  // red input varying fastest. Build the corrected triple for each
  // grid coordinate with your ICC-managed conversion, then store it.
  SetLength(Samples, N * N * N * 3);
  BuildCorrectedCube(Samples, N);   // your color-managed fill

  LutFn := Pdf.RegisterSampledFunction(
    [0,1, 0,1, 0,1],   // /Domain: three input axes on [0,1]
    [0,1, 0,1, 0,1],   // /Range:  three output channels on [0,1]
    [N, N, N],         // /Size:   the cube resolution per axis
    8,                 // /BitsPerSample
    Samples,
    1);                // /Order 1 = trilinear
end;

立方体的比色法正确性存在于您如何填充它,而不是存在于 PDF 函数中。可靠的途径是通过 ICC 管理的转换计算每个网格点(即驱动软校样的同一引擎),这样网格中的数字相对于定义的数据源和目标配置文件就有了意义。使用 RegisterICCProfile 注册界定转换的配置文件,该函数记录 ICCBased 颜色空间(1、3 或 4 个组件)并返回可附加到 LUT 所填充内容资源名称。类型 0 函数携带插值表;ICC 配置文件携带终点的含义。

一维情况:专色色调变换

专色颜色空间依靠相同的机制来完成完全不同的工作。专色空间(在 ISO 32000-1 第 8.6.6.4 节中定义)通过将名称与色调变换配对来表示单一着色剂(例如 Pantone 专色墨水或光油):该函数将一维色调值(0 表示无墨水,1 表示满墨)映射到设备实际可以渲染的替代颜色空间(通常为 CMYK)。该色调变换通常是一个类型 0 函数,现在网格正好有一个输入轴。

这与 3D LUT 形成了鲜明的对比。专色墨水是单一自由度,因此其色调变换需要一个输入,而网格是一线样本,每个样本保存该色调水平下的 CMYK(或其他替代)值。RGB 立方体需要三个输入,因为其定义域是三维的且通道相互作用。相同的函数类型,相同的插值规则,不同的维度;规范重用了一个评估器,并让 /Size 决定您是在遍历一条线、一个平面还是一个立方体。HotPDF 将整个专色封装在 RegisterSeparationLUT 中,该函数在内部从扁平字节数组构建单输入类型 0 色调变换,并返回颜色空间资源名称。

var
  SpotCS: AnsiString;
begin
  // Four CMYK output bytes per tint grid point, tint domain [0..1].
  // Here 0% ink -> all zero, 100% ink -> a rich spot build,
  // with two interior steps; the tint transform interpolates between.
  SpotCS := Pdf.RegisterSeparationLUT(
    'PANTONE 286 C',         // colorant name
    'DeviceCMYK',            // alternate color space
    [  0,   0,   0,   0,     // tint 0.00 -> 0,0,0,0
      90,  60,   0,   0,     // tint 0.33
     100,  80,   0,  10,     // tint 0.66
     100,  72,   0,  18]);   // tint 1.00 -> full ink build
  // Use SpotCS with SetFillColorSpace / SetFillColor on a page.
end;

样本数必须是网格点的整数:替代空间组件数的正倍数,且至少有两个点,以便存在可以插值的网段。如果对 CMYK 替代空间传入每个点三个字节,调用将拒绝它,这与 3D 构建器应用的防御性验证相同,这正是您对于在打印时可能会默默失败的函数所期望的。

相同机制再次出现的地方

一旦您将类型 0 视为通用的插值表,另外两个设备控制功能就不再看起来像特例了。转换函数在组件值发送到输出设备的过程中对其进行调整,它只是每个通道的函数;HotPDF 通过 RegisterTransferFunctionState 将其注册为 ExtGState,该函数接受一个组合函数或每个通道的函数数组。由于这些函数是普通的函数对象,您可以将 RegisterSampledFunction 返回的 THPDFStreamObject 传给它,并从采样表驱动转换曲线,而不是从公式驱动。

var
  ToneFn: THPDFStreamObject;
  GsName: AnsiString;
begin
  // A single-input, single-output sampled tone curve on [0,1].
  ToneFn := Pdf.RegisterSampledFunction(
    [0,1], [0,1], [256], 8, ToneCurveBytes, 1);

  // Apply it to all channels as a combined /TR2 transfer function.
  GsName := Pdf.RegisterTransferFunctionState(ToneFn, []);
  // Select GsName on the page before drawing the affected content.
end;

黑版生成和底色去除属于同一家族。当设备将 RGB 转换为 CMYK 时,它会决定将多少灰色成分作为黑色墨水承载,规范将该决定表达为一个函数,即图形状态字典的 /BG2/UCR2 条目,每个条目都是从计算出的灰色到黑色数量的单输入曲线。当您想要测量的曲线而不是解析曲线时,这些也是类型 0 函数,以同样的方式通过 RegisterSampledFunction 构建并放置在图形状态中。值得记住的教训是,PDF 函数绝不是颜色管理发生的地方;它是承载您使用真实颜色引擎所做决定的查找表,而类型 0 是唯一灵活到足以承载任何决定的函数类型。

有关字体、图像和颜色资源如何输出到完成文档的更广泛图景,请参见我们使用字体和图像进行报告输出的演练。当输出必须通过存档或打印预检检查时,PDF/A、PDF/X 和 PDF/UA 验证指南中涵盖的颜色空间和输出意图规则将管理允许使用这些函数中的哪些函数以及如何标记设备颜色。所有这些都随适用于 Delphi 和 C++Builder 的 HotPDF 组件一起交付,与构建在相同类型 0 核心上的着色、ICC 和专色 API 一起提供。