PDF Function 是规范里比较安静的一角。大多数开发者只会遇到它一次,就是 Type 2 轴向着色为了在两种颜色之间渐变而需要的那个函数,之后便不再回头看它。这很可惜,因为函数机制本质上是一个小型通用求值器,PDF 会把它复用于着色、传递函数、半色调专色函数、分色 tint 变换以及软蒙版传递曲线。在四种函数类型里,Type 0 最强大,也最少被真正理解。它是一种 sampled function,也就是由查看器在其间做插值的多维输出网格。因为网格里可以装下你想放进去的任意数字,所以 Type 0 function 可以表达任意非线性映射,而这正是颜色查找表,也就是 LUT,最典型的形状
本文会先按 ISO 32000-1 §7.10.2 把 Type 0 字典拆开讲清楚,再展示文档流水线里最值得关心的两个案例:一个三输入、三输出的 RGB 到 RGB 颜色校正 LUT,以及一个单输入的专色色调变换。两者使用的是同一个 sampled-function builder,区别完全只在于网格有几个输入维度
采样函数是阅读器用于插值的网格
Type 0 function 通过在规则网格上存储样本并在样本之间插值,把一个 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 位,与 Table 38 允许的取值一致
样本流按固定顺序读取。第一个输入维度变化最快,然后是第二个,以此类推,而在每个网格点上,n 个输出分量按顺序存储。对一个 8 位 RGB 到 RGB 表来说,每个网格点就是三个字节,顺序是红输出、绿输出、蓝输出,并且红输入维度扫描得最快。还有两个键负责把连续空间映射到整数网格上。/Encode 会把每个输入从它的 /Domain 区间映射到样本索引范围 0 到 Size[i] - 1,/Decode 则会把原始存储整数映射回 /Range 区间。如果你把它们保持为默认值,那么定义在 [0 1] 上的输入就会完整落在整个网格范围内,而存储值 255 会解码成输出范围的上限,这恰好就是一个归一化到 [0,1] 的颜色 LUT 所需要的行为
阶数 1 对比阶数 3
在网格点之间,阅读器必须进行插值,而 /Order 选择如何插值。/Order 1 是多线性插值:沿一个轴是线性,跨两个轴是双线性,跨三个轴是三线性。它速度快,这正是大多数查看器中的硬件所做的,并且对于平滑的颜色转换,它通常与更花哨的方法无法区分。/Order 3 请求三次样条插值,这通过在样本中拟合更平滑的曲线,以更多的工作量和每个评估点周围更宽的支撑区域为代价
这里真正的权衡是网格密度和曲线平滑度。当网格很粗而映射本身又明显带弯曲时,三次阶插值会更有价值,因为两个相距较远样本之间的直线容易把色调曲线压平,而这种压平在渐变上是肉眼看得见的。一旦网格足够密,段长就已经很短,线性插值也能很好地贴住曲线,此时 cubic 带来的收益就很有限。实践里比较稳妥的规则是,只在小网格或变换很陡的时候才考虑 /Order 3,其他情况下直接保留线性默认值。还要注意,/Order 只属于 Type 0 function,HotPDF 也会明确拒绝 1 和 3 之外的任何值
3D LUT:三输入,三输出
RGB 到 RGB 的颜色纠正是三输入网格的教科书案例,即用于颜色分级和设备匹配的经典 3D LUT。立方体的每个轴是一个输入通道,每个网格点存储该输入坐标的纠正后 RGB 三元组,并且阅读器对任何传入颜色周围的角样本进行三线性插值。这里三个输入是不可避免的,因为纠正后的红可能取决于输入的绿和蓝,而不只是取决于输入的红;单通道曲线无法表达通道串扰,但立方体可以
HotPDF 通过 RegisterSampledFunction 构建 Type 0 流。这个 API 会直接接收 /Domain、/Range、/Size、/BitsPerSample 和样本字节,然后返回函数对象。对一个标准的归一化立方体来说,你会在三个输入轴和三个输出通道上都使用 [0,1] 边界,给出一个 N x N x N 的尺寸,再传入压平后的样本表。builder 会校验字节数是否和网格匹配:对于字节对齐的深度,它期待的长度是 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 function 本身。比较诚实的做法,是让每个网格点都经过 ICC 管理的颜色转换计算,也就是和软打样使用同一套引擎,这样网格里的数字才会在明确的源配置文件和目标配置文件之间具有可解释意义。用 RegisterICCProfile 注册界定转换的配置文件,它会记录一个 ICCBased 色彩空间,可以是 1、3 或 4 个分量,并返回一个资源名,供 LUT 所作用的内容引用。Type 0 function 负责携带插值表,而 ICC profile 则负责说明这些端点到底代表什么
一维情况:专色色调变换
专色颜色空间依靠相同的机制来完成完全不同的工作。专色空间(在 ISO 32000-1 第 8.6.6.4 节中定义)通过将名称与色调变换配对来表示单一着色剂(例如 Pantone 专色墨水或光油):该函数将一维色调值(0 表示无墨水,1 表示满墨)映射到设备实际可以渲染的替代颜色空间(通常为 CMYK)。该色调变换通常是一个类型 0 函数,现在网格正好有一个输入轴
这和 3D LUT 恰好形成鲜明对照。专色墨水只有一个自由度,因此它的 tint transform 只需要一个输入,网格也只是一条样本线,而每个样本点都保存该 tint 水平下的 CMYK,或者其他替代色彩空间值。RGB 立方体之所以必须有三个输入,是因为它的定义域本来就是三维,而且三个通道彼此耦合。相同的函数类型,相同的插值规则,不同的只是维度;规范重用了一个求值器,让 /Size 决定你是在遍历一条线、一个平面,还是一个立方体。HotPDF 用 RegisterSeparationLUT 把整个专色流程封起来,它会在内部从扁平字节数组构建一个单输入的 Type 0 tint transform,并返回色彩空间资源名
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 builder 采用的是同一种防御式校验,而这正是你希望从一个否则可能只会在打印时静默失败的函数身上得到的行为
相同机制再次出现的地方
一旦你把 Type 0 看成通用插值表,另外两类设备控制功能也就不再像特例了。传递函数会在分量值送往输出设备时对它们做调整,本质上就是每个通道一个函数;HotPDF 通过 RegisterTransferFunctionState 把它注册成 ExtGState,这个调用可以接收一个合并函数,也可以接收每通道一个函数的数组。因为这些都是普通函数对象,所以你完全可以把 RegisterSampledFunction 返回的 THPDFStreamObject 直接交给它,让传递曲线由 sampled table 驱动,而不是由解析公式驱动
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;
黑版生成和 undercolor removal 也属于同一家族。当设备把 RGB 转成 CMYK 时,它必须决定到底把多少灰色分量交给黑墨承载,规范用函数来表达这件事,也就是图形状态字典中的 /BG2 和 /UCR2 条目,它们都是从计算出的灰度值到黑墨用量的单输入曲线。当你需要的是一条实测曲线,而不是一条解析曲线时,它们同样可以用 Type 0 function 表达,通过 RegisterSampledFunction 按同样方式构建,再放进图形状态里。真正值得记住的结论是,PDF function 从来都不是颜色管理本身发生的地方;它只是承载你借助真实色彩引擎做出的那个决策的查找表,而 Type 0 是唯一灵活到足以装下任意这类决策的函数类型
如果你想把字体、图像和颜色资源如何一起输出到最终文档这件事看得更完整一些,可以继续看使用字体和图像处理报表输出的文章。如果输出还必须通过归档或印刷预检,那么PDF/A、PDF/X 和 PDF/UA 验证文章里涉及的色彩空间与 output intent 规则,就会进一步决定这些函数哪些能用、设备颜色又必须如何标记。这些能力都包含在面向 Delphi 和 C++Builder 的HotPDF Component中,与建立在同一 Type 0 核心之上的着色、ICC 和分色 API 一起提供