技术文章

Delphi 中的 NChannel 专色油墨一致性:HotPDF

印前厂把活儿退了回来:同一个专色被分到了两张印版上。HotPDF 在编著期就防住这一点:把 NChannel 写成 ISO 32000-2 的五元素 DeviceN 形式,并为整个文档的每个专色名只保留一个规范的替代空间和调值变换,拒绝第二个相冲突的定义而不是把它发出去

NChannel 不是色彩空间族名

第一个要忘掉的是名字本身。NChannel 不是 SeparationDeviceN 那种意义上的色彩空间族。ISO 32000-2 §8.6.6.5 把它描述为 DeviceN 的一个子类型,于是合规的 NChannel 空间写成五元素数组 [/DeviceN names alternateSpace tintTransform attributes],属性字典携带 /Subtype /NChannel。规范里不存在 [/NChannel ...] 数组。如果你曾手工构造过一个然后看着 RIP 对它耸肩,原因就是这个

HotPDF 把 NChannel 空间写成五元素 DeviceN 数组,其属性字典携带 Subtype NChannel 以及 Process、Colorants、MixingHints 条目,因为规范中根本不存在 NChannel 族名数组
合规的 NChannel 空间是带属性字典的 DeviceN 数组,所以写出端只发这种形式,而读取端仍接受旧式记号

HotPDF 曾在这件事上错过,然后修了,值得明说,因为它塑造了组件今天的行为。旧版 HotPDF 发出族名形式。THotPDF.RegisterNChannelColorSpace 现在只发标准的 DeviceN 加属性形式;因为 NChannel 子类型随 PDF 1.6 到来,生产入口点用 RequirePDFVersion(pdf16, ...) 设闸,对更老目标直接婉拒。渲染侧刻意比写出端宽容:HPDFResolveColorSpace 仍把旧式 /NChannel 记号接受为 DeviceN 族,于是旧写出端产的文件能继续渲染,但 HotPDF 写回任何东西都用标准编码。输入宽容、输出严格在这里是正确的不对称,因为你的读取端必须应付不是它创建的文件,而你的写出端没有这种借口

为什么一个专色名会落到两张印版上?

因为专色着色剂名是文档级印版身份,不是局部参数。两次都叫 Orange 但交出不同替代空间的调用,或替代空间相同但调值变换不同的调用,描述的是两种恰好同名的不同油墨。建分色的 RIP 没有办法调和,于是它做唯一诚实的事:给你两张印版。所以 HotPDF 为文档维护按着色剂名的规范签名。RegisterSpotColorantDefinition 从替代色彩空间和调值函数的形状合成该签名,每个 RegisterSeparationRegisterSeparationFuncRegisterSeparationLUT 和 NChannel 专色定义都经过它。第二个定义不一致时,调用抛异常而不是悄悄登记第二个变体,消息刻意对失败形态说得具体,因为替代方案是三周后在印版打样上发现它

HotPDF 的每个专色登记调用都汇入 RegisterSpotColorantDefinition,它为文档按着色剂名记录一个规范的替代空间与调值签名,当同名的第二个不一致定义到达时抛异常
一个名字一个签名:专色的第二个相冲突定义在编著期就被拒绝,而不是在印版打样上被发现
// Orange 已按 DeviceCMYK 登记,
// 满版调值变换为 0 / 0.55 / 1 / 0
Conflicting := Pdf.RegisterExponentialFunction(
  Domain1, NoInk, OtherOrangeCMYK, 1, []);
try
  Pdf.RegisterSeparationFunc('Orange', 'DeviceCMYK', Conflicting);
except
  on E: Exception do
    // 'Spot colourant "Orange" has inconsistent alternate colour
    //  space or tint definition in this document'
    LogPrepressWarning(E.Message);
end;

把主名字拆成印刷原色与专色

完整 NChannel 必须让它的每个主着色剂名恰好被交代一次,要么作为印刷原色分量,要么作为专色着色剂。高级 RegisterNChannelColorSpace 重载接收主 ColorantNamesProcessColorantNames、替代空间、整体调值变换、一个 THPDFNChannelSpotColorant 记录数组,以及可选印刷顺序。每个专色记录携带自己的名字、自己的单输入 Separation 调值变换、可选实地度和可选网点增大函数。整体调值变换必须把 N 个输入映射到替代空间分量数;每个专色调值必须把单输入映射到同一个数

const
  Colorants: array[0..4] of AnsiString =
    ('Cyan', 'Magenta', 'Yellow', 'Black', 'Orange');
  ProcessNames: array[0..3] of AnsiString =
    ('Cyan', 'Magenta', 'Yellow', 'Black');
  Order: array[0..4] of AnsiString =
    ('Yellow', 'Magenta', 'Cyan', 'Orange', 'Black');
  Domain5: array[0..9] of Single = (0, 1, 0, 1, 0, 1, 0, 1, 0, 1);
  Range4: array[0..7] of Single = (0, 1, 0, 1, 0, 1, 0, 1);
  Domain1: array[0..1] of Single = (0, 1);
  NoInk: array[0..3] of Single = (0, 0, 0, 0);
  OrangeCMYK: array[0..3] of Single = (0, 0.55, 1, 0);
  GainC0: array[0..0] of Single = (0);
  GainC1: array[0..0] of Single = (1);
var
  Pdf: THotPDF;
  Spots: array[0..0] of THPDFNChannelSpotColorant;
  CSName: AnsiString;
begin
  Pdf.Version := pdf20;
  Pdf.BeginDoc;
  Spots[0].Name := 'Orange';
  Spots[0].TintTransform := Pdf.RegisterExponentialFunction(
    Domain1, NoInk, OrangeCMYK, 1, []);
  Spots[0].HasSolidity := True;
  Spots[0].Solidity := 0.82;
  Spots[0].DotGainFunction := Pdf.RegisterExponentialFunction(
    Domain1, GainC0, GainC1, 1, []);
  CSName := Pdf.RegisterNChannelColorSpace(Colorants, ProcessNames,
    'DeviceCMYK',
    Pdf.RegisterPostScriptFunction(Domain5, Range4,
      '{ pop pop pop pop pop 0 0 0 0 }'),
    Spots, Order);

从这次调用,HotPDF 发出规范要求的属性字典:/Subtype /NChannel,一个 /Process 字典,其 /ColorSpace 是印刷原色空间、其 /Components 按该空间分量顺序列出原色名;一个 /Colorants 字典,为每个专色装一个真正的 [/Separation name alternate tintfn] 数组;以及一个在你提供时携带 /Solidities/PrintingOrder/DotGain/MixingHints 字典。返回的资源名交给 SetFillColorSpaceSetStrokeColorSpace,用法与Separation 和 DeviceN 专色渲染一文中更简单的空间完全相同

HotPDF 把 NChannel 空间的主着色剂名拆成装四个 CMYK 分量的 Process 字典和每专色一个 Separation 数组的 Colorants 字典,每个元数在写出前都被检查
每个主名恰好被认领一次,整体调值变换、每专色调值和印刷顺序都在任何对象发出之前被校验

一致性检查到底拒绝什么?

它拒绝空间内部的结构不自洽,而且在写出任何对象之前就拒绝。着色剂名必须唯一,不得为空,不得是 AllNone。原色与专色定义合起来必须恰好覆盖主名字,没有着色剂身兼两职,也没有悬而未定义的。替代空间是 DeviceCMYK 时,原色分量必须按序是 CyanMagentaYellowBlack,原色名数量必须与替代分量数匹配。每个调值变换和网点增大函数必须是输入输出元数正确的间接函数对象。实地度必须是 0..1 内的有限值。印刷顺序必须为空或主名字的完整排列,绝不能是残缺清单。它不做的是评判颜色:这里没有任何东西检查你的 Orange 调值变换是否真像罐里的油墨、其 CMYK 构成是否是合理代理、或你给的实地度是否匹配承印物上的实测表现。那些是印刷机和测量问题,组件没有资格回答。更简单的纯原色重载设计上更严:它产出纯原色 NChannel 并刻意拒绝接受专色名,因为往名字数组里写一个专色却没有配套 /Colorants 条目会产出结构不合规的文件,而捏造默认定义比失败更糟

PDF/X-6n 输出意图必须覆盖每个已登记专色

N 着色剂的 PDF/X-6n 文件声明它的着色剂两次,两次声明必须一致。AddPDFX6ExternalOutputIntent 写出带 ColorantTable 的外部 ICC 配置引用,而在此之前 ValidateRegisteredSpotOutputColorants 走遍文档已登记的每个专色,表里缺一个就抛异常。检查双向运行:一旦输出意图发布了它的着色剂清单,之后对清单外名字的专色登记同样被拒。AddPDFX6ExternalOutputIntentSpotData 在其上加每油墨元数据,并执行自己的规则,尤其是一个着色剂要么带实地度值、要么带 CxF/X-4 光谱数据,绝不两个都带。这与PDF/A、PDF/X 和 PDF/UA 验证一文讨论的是同一个合规面

Spectral := TMemoryStream.Create;
try
  LoadCxFForInk('Orange', Spectral);   // ISO 17972-4 载荷
  Pdf.AddPDFX6ExternalOutputIntentSpotData(
    'ECG-5', 'Five-colour output condition',
    'https://profiles.example.com/ecg-5.icc', '5CLR',
    Colorants,
    '00112233445566778899AABBCCDDEEFF', #4#3#0#0,
    ['Cyan'], [0.70],                  // 一个着色剂的实地度
    Order,
    ['Orange'], [Spectral]);           // 另一个的光谱数据
finally
  Spectral.Free;
end;

两个操作细节容易漏看。支撑这一切的注册表是每文档的,在文档边界清空,于是把新文件加载进同一个 THotPDF 实例不会继承上一文档的专色身份;这种隔离正是重点,因为泄漏的签名会让下一份活儿里完全有效的工作被拒。配置侧也有自己的硬限制:外部输出意图要求 PDF 2.0 目标、绝对的 HTTP 或 HTTPS 配置 URL、四字节 ICC 色彩空间签名,PDF/X-6n 则要求 2 到 15 个着色剂并配匹配的 2CLR 到 FCLR 签名

检查在哪里止步

CxF/X-4 处理是要诚实说清的部分。HotPDF 对光谱流施加有界结构安全检查,并确认里面的油墨身份与你点名的着色剂匹配。它给载荷大小封顶,拒绝内嵌空字节,拒绝任何含 DOCTYPEENTITY 声明的流,要求可辨认的 CxF 根,并要求恰好一个 SpotInkCharacterisation 元素、携带恰好一个等于你着色剂名的 SpotInkName。那是对付畸形和敌意输入的闸门,不是模式验证器。它不是完整的 ISO 17972-4 实现,不验证你的光谱测量,也不反向审计任意的第三方既有对象图或内嵌 ICC 配置内部的着色剂表。如果你的工作流依赖完整 CxF 合规,把文件交给组件之前先用专用工具验证

有一个相邻约束会咬到从没想过会遇见它的人。明度软蒙版不能把 SeparationDeviceN 或 NChannel 用作其透明组 /CS;组混合空间必须是设备空间或基于 CIE 的空间,于是组内的专色颜料在计算明度之前先经其调值变换解析进替代空间。RegisterLuminositySoftMaskState 正是为了让它不可能意外出错而构建 DeviceGray 组。实际后果是:以这种方式蒙版的重专色设计是经由其 CMYK 代理而不是其油墨被评估的,当你按叠印打样与渲染设备札记把它和分色打样对比时,这一点很要紧

这些并不能免去印版打样的需要,但它把整一类印前退件从印刷车间搬回了构建。本文描述的 NChannel、Separation 和 PDF/X-6n 输出意图 API 随 Delphi 和 C++Builder 标准版 HotPDF Delphi 组件交付,其参考文档记录了完整记录布局和每个调用失败关闭的确切条件