适用于 Delphi 的 PDFium Component 通过 TPdf.ValidatePdfX 验证适合印刷的 PDF/X 文档,该函数在两个层面上实现了 ISO 15930 检查:八项字节级内容检查(禁用 LZW 压缩、JavaScript、表单字段、OPI 引用、缺失 TrimBox、未设置 Trapped 键等)加上使用 FPDFFont_GetIsEmbedded 在每页的每个文本对象上验证字体嵌入的 PDFium 对象模型处理。其结果是一个 TPdfXValidationResult 记录,指明检测到的一致性级别,并将每项违规列为类型化枚举,这样您的 Delphi 应用程序就能在实际制作版面之前,准确告诉客户为什么文件会在印刷厂被退回
如果您曾将一项工作快递给商业印刷厂,却收到一行字的退单 — “无 TrimBox”、“字体未嵌入”、“未设置 Trapped” — 您就会明白太晚发现问题的代价。PDF/X 是 PDF/A 在印前的对应标准:归档 PDF/A 保证文档自现在起数十年后的渲染完全一致,而 PDF/X 则保证文档在明早其他人的 RIP(光栅图像处理器)上的分色、图像和裁剪完全一致。这两个标准共享相似的机制(XMP 标识、OutputIntents、嵌入式 ICC 配置文件),但回答的是不同的问题,这就是为什么组件为两者分别提供了独立的验证器 — PDF/A 部分在使用 PDFium Component 进行 PDF/A 预检验证中进行了介绍
ISO 15930 实际上对适合印刷的 PDF 有什么要求?
ISO 15930 的存在是为了使物盲交换(blind exchange)成为可能:设计师将文件交给一位从未沟通过的印刷商,而印刷商可以在没有电话联系、没有缺失字体的电子邮件、也没有链接图像遗留在设计师笔记本电脑上的情况下,制作出正确的输出。标准中的每一条规则都为这一目标服务。字体必须嵌入,因为不能假定接收方的 RIP 拥有这些字体。外部引用被禁止,因为文件本身必须是完整的。交互式功能被禁止,因为墨水没有 onclick 处理器
PDFium Component 识别三种一致性系列,并通过验证结果中的 TPdfXConformance 枚举进行报告:pxc1a 代表 PDF/X-1a:2001(ISO 15930-1,PDF 1.3/1.4 上严格的 CMYK 加专色基线),pxc3 代表 PDF/X-3:2002(ISO 15930-3,允许 RGB、Lab 和 ICC 管理的颜色),以及 pxc4 代表 PDF/X-4:2010(ISO 15930-7,最终在 PDF 1.6 基础上允许实时透明和图层)。完全没有携带 PDF/X 标识的文件返回为 pxcNone,这本身也是一个有用的答案:文档从未声称自己是适合印刷的,而验证器报告的其余所有内容都解释了要达到这一标准需要怎么做
一旦您像 RIP 厂商那样思考,这些禁令就合情合理了。/LZWDecode 在每个 PDF/X 变体中都被禁用,因此符合标准的消费者绝不依赖于具有兼容性和许可历史的滤镜;Flate 在没有历史包袱的情况下完成相同的工作。JavaScript、AcroForm 字段和 /AA 附加动作(additional-actions)字典被禁用,因为印刷文件必须是纸上标记的固定描述 — 任何能在打开时改变外观的内容都会破坏“所校即所印”的保证。OPI(开放印前接口)占位符被禁止,因为在设计上,它们是对存储在其他地方的高分辨率图像的引用,而“其他地方”正是盲交换所禁止的
为什么印刷厂会拒绝没有 TrimBox 的 PDF?
TrimBox(裁切边框)是成品的页面 — 切纸机切下后剩下的矩形。每个 PDF 页面都有的 MediaBox(介质边框)仅仅是纸张:它包括出血位、裁切标记、套准标记和色条。拼版软件通过 TrimBox 在印张上对页面进行定位;如果没有 TrimBox,操作员就必须猜测您的名片实际上在哪里结束,而错误的猜测会切掉您的出血位,或者在一侧边缘留下白边。这就是为什么 ISO 15930要求在每个页面上都有一个 TrimBox(或 ArtBox),以及为什么在文档的任何页面上都找不到 /TrimBox 键时,ValidatePdfX 会抛出 pvxiMissingTrimBox
/Trapped 键回答了另一个不同的生产问题。陷印(Trapping)是一种印前技术,即稍微重叠相邻的颜色,以便微小的印刷套印不准不会在它们之间产生白缝。印刷商需要知道这项工作是否已经完成:对已经陷印的文件再次进行陷印会使重叠加倍,而在未陷印的文件上跳过陷印则会带来产生可见缝隙的风险。因此,PDF/X 要求 Info 字典显式声明 /Trapped /True 或 /Trapped /False — 缺失键或 /Unknown 会迫使人工检查文件,这正是盲交换旨在消除的沟通。组件将其标记为 pvxiTrappedNotSet
使用 TPdf.ValidatePdfX 运行双层验证
TPdf.ValidatePdfX 不需要参数,并返回一个带有三个成员的 TPdfXValidationResult 记录:Conformance(检测到的 PDF/X 风味)、Issues(TPdfXValidationIssue 值的 Pascal 集合)以及一个 IsCompliant 辅助属性。在内部,它将加载的文档序列化为内存流,在其上运行字节级检查器,然后为每个字体的嵌入检查遍历 PDFium 对象模型。一个最小的预检门槛看起来像这样:
uses PDFium, FPdfPdfx;
procedure CheckPrintReadiness(const FileName: string);
var
Pdf: TPdf;
Res: TPdfXValidationResult;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := FileName;
Pdf.Active := True;
Res := Pdf.ValidatePdfX;
Writeln('Detected conformance: ',
Ord(Res.Conformance)); // pxc1a, pxc3, pxc4, pxcNone ...
if Res.IsCompliant then
Writeln('PDF/X checks passed')
else
begin
if pvxiMissingTrimBox in Res.Issues then
Writeln('REJECT:页面上没有 /TrimBox');
if pvxiTrappedNotSet in Res.Issues then
Writeln('REJECT:/Trapped 缺失或为 /Unknown');
if pvxiPdfiumFontNotEmbedded in Res.Issues then
Writeln('REJECT:页面使用了未嵌入的字体');
if pvxiLzwForbidden in Res.Issues then
Writeln('REJECT:存在 LZWDecode 滤镜');
end;
finally
Pdf.Free;
end;
end;
因为 Issues 是一个普通的 Pascal 集合,所以您可以根据工作流的需要对其进行划分 — 将结构性问题视为硬性拒绝、将 pvxiMissingTitle(在标准中是 SHOULD,而不是 MUST)视为警告,并记录其余内容。相同的记录类型也提供给组件的报告生成器,因此,如果您宁愿生成一份人类可读的文档,而不是对枚举进行分支处理,那么在使用 PDFium Component 构建批量预检报告 CLI 中的模式也完全适用于 PDF/X
字节级层能抓住什么 — 以及它会遗漏什么
字节级层是对文档结构字节进行的记号(token)扫描(流主体被置空),因此即使 JPEG 碰巧包含 /JavaScript 字节模式,也不会触发误报。除了标记检查(XMP pdfxid:GTS_PDFXVersion、带有嵌入式 ICC 配置文件的 OutputIntent、尾部 /ID、禁用加密)之外,内容检查还增加了八项检查,每项检查都有其自身的枚举值:
pvxiLzwForbidden— 文件中任何地方出现/LZWDecode滤镜(在所有 PDF/X 变体中均被禁用)pvxiJavaScriptForbidden— 存在/JavaScript动作或名称树pvxiFormFieldsForbidden— 存在/AcroForm字典或/XFA项pvxiAdditionalActions— 存在/AA附加动作(additional-actions)字典pvxiEmbeddedFilesForbidden— 存在/EmbeddedFiles或/FileAttachment注释pvxiOpiForbidden—/OPI或/Alternates项引用了可替换的图像内容pvxiMissingTrimBox— 任何页面上都找不到/TrimBoxpvxiTrappedNotSet—/Trapped缺失或设置为/Unknown
字节扫描速度极快且不需要渲染引擎,但它在字体上存在固有的盲区:在那个层面上,检查器只能应用粗糙的启发式算法 — 当它完全找不到嵌入的字体程序时才标记该文档。对于字节扫描来说,一个嵌入了九种字体、溜进了一种系统字体的文件看起来是完全没问题的。这唯一的空白正是第二层存在的原因
通过 PDFium 对象模型检查每个字体的嵌入
PDFium Component 的对象模型层精确地回答了字体问题。在字节级检查之后,TPdf.ValidatePdfX 会遍历每个页面、要求 FPDFPage_CountObjects 获取对象列表,并针对每个文本对象通过 FPDFTextObj_GetFont 解析字体句柄并查询 FPDFFont_GetIsEmbedded。在文档任何地方只要有一种未嵌入的字体,就会将 pvxiPdfiumFontNotEmbedded 到问题集合中。该遍历在两个层面上进行了短路 — 一旦确认了问题,它就会停止扫描页面上的对象,并停止加载后续页面 — 因此在一个存在违规的 300 页产品目录上,结论往往在第一页之后就得出了
两点值得了解的限制注意。第一,该层需要加载 PDFium 库,并需要导出 FPDFFont_GetIsEmbedded 的构建版本;当缺失该导出时,检查将被跳过而不是失败,因此较老的 DLL 绝不会产生幻像拒绝。第二,该检查仅回答“是否嵌入”,没有其他内容 — 它不区分完全嵌入与子集化,也不检查字形覆盖范围。当文件失败且您需要知道在哪一页上的哪种字体时,在 Delphi 中使用 PDFium 分析 PDF 字体属性中的枚举技术正好可以接替验证器的布尔值留下的工作
在不加载文档的情况下验证流 — 或不加载 DLL
字节级检查器也公开为独立的函数,即 FPdfPdfx 单元中的 ValidatePdfXCompliance(Source: TStream),它是纯 Object Pascal,不依赖于 PDFium DLL。这使得它可以在不欢迎渲染引擎的地方部署:Web 服务器上的轻量级上传网关、审查所生成艺术作品的 CI 任务,或者在您不希望发布原生二进制文件的平台上的 Lazarus 服务。可以喂给它任何可寻址的流:
uses Classes, FPdfPdfx;
function QuickPdfXGate(const FileName: string): Boolean;
var
Fs: TFileStream;
Res: TPdfXValidationResult;
begin
Fs := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
try
Res := ValidatePdfXCompliance(Fs);
Result := Res.IsCompliant and (Res.Conformance <> pxcNone);
finally
Fs.Free;
end;
end;
权衡很明确:独立路径运行标记检查和所有八项内容检查,但不运行针对每个字体的 PDFium 层,因此其字体判定会退回到粗糙的启发式算法。明智的架构会将 ValidatePdfXCompliance 用作廉价的第一道门槛,而将完整的 TPdf.ValidatePdfX 留给通过了第一道门槛的文件
此验证器的终点与完整预检的起点
对印前边界保持诚实很重要:ValidatePdfX 验证标识标记、结构性禁令、页面几何键、Trapped 声明以及一直到单个文本对象的字体嵌入。它并不测量总墨水覆盖率、不验证每个颜色空间对于所声称的变体是否合法(例如 X-1a 的仅 CMYK 规则)、不比对图像分辨率与加网线数、也不评估叠印和透明度扁平化行为 — 这些需要一个色彩管理型的预检引擎,且单元自身的文档也说明了要与之一并使用进行最终认证。双层检查带给您的是 80% 的属于结构性且可提早检测出的拒绝,在您的 Delphi 代码内花费数毫秒即可捕获,而不是在明天来自印刷厂的退单邮件中
两种验证层、用于生成合规输出的 PDF/X 标记注入 API、以及共享相同架构的 PDF/A、PDF/UA、PDF/E 和 PDF/VT 验证器,均在适用于 Delphi 和 C++Builder 的 PDFium Component 中发售 — 一个组件,从渲染到印前把关