PDFiumPas 是围绕 Google PDFium 引擎构建的 Delphi 和 C++Builder 封装,可通过 TPdf.SaveAs 方法的 PdfVersion 参数,将文档保存为 1.3 至 1.7 的精确 PDF 版本。PDFium 自带的 FPDF_SaveWithVersion 调用只会改写 %PDF-M.m 标头,不会检查文档实际内容是否符合该版本。PDFiumPas 通过保存后的合规检查弥补这一缺口:在文件离开该方法前遍历活动交叉引用修订链,并检查 Adobe Extension Level 声明
这种区别在印刷生产中最为重要,因为 PDF/X 配置文件会指定精确的 PDF 版本,而预检工具或 RIP 会拒绝那些实际内容悄悄偏离自身标头的文件;从输出角度看,这正是使用 PDFiumPas 验证适合印刷的 PDF/X 文档所讨论的场景。SaveAs 通过 TPdfVersion 枚举公开目标版本,包括 pv13 至 pv17,以及较早的 pv10 至 pv12 值,同时提供独立的 TSaveOption 以选择增量重写或完整重写。传入 PdfVersion 后,PDFiumPas 会在一次调用中完成两项工作:请求 PDFium 写入目标标头,然后重新读取刚写入的字节;如果活动内容无法合法存在于该版本中,就拒绝返回这个文件
var
Pdf: TPdf;
begin
Pdf:= TPdf.Create(nil);
try
Pdf.FileName:= 'source.pdf';
Pdf.Active:= True;
try
Pdf.SaveAs('press-ready.pdf', saNoIncremental, pv17);
except
on E: Exception do
// E.Message names the offending feature and the version or
// extension level it actually needs, for example:
// "RichMedia annotations and RichMediaExecute actions require
// /Extensions /ADBE with /BaseVersion /1.7 and /ExtensionLevel 3
// or newer."
raise;
end;
finally
Pdf.Free;
end;
end;
为什么不能盲目信任文件中的最后一个对象定义
PDF 文件中某个编号最后出现的实体对象,不一定是符合规范的阅读器今天解析该编号时得到的对象。经历过多次增量更新的 PDF 并不存在一张唯一的对象图,而是把多次历史状态层叠在同一个文件中;每次追加都可能释放某个对象、用新的代数重新定义它,或者让旧的实体内容留在两个 endobj 标记之间,却不再有交叉引用条目指向它
在显式跟踪 xref 修订之前,PDFiumPas 正好遇到过这种故障模式:后续页面对象重写可能使 Redact 注释成为孤立对象,或者某个 /MarkInfo 字典虽然仍以实体字节存在,却没有 xref 条目指向它。字节扫描仍可能找到这些内容,并触发已经不适用于阅读器实际打开文档的版本特性检查。这里的失败方向是误拒绝,而不是误接受:文件当前修订确实已经不再包含某项特性,却仍可能因为无人可达的旧内容而无法保存为更低版本
PDFiumPas 如何确定哪些对象定义真正处于活动状态
PDFiumPas 采用与符合规范的阅读器相同的方式解析活动对象集:遍历交叉引用链,而不是扫描字节寻找对象头。解析器从文件中最后一个 startxref 偏移开始,沿每个 /Prev 链向后穿过旧修订,同时解析传统交叉引用表、由 /XRefStm 连接的混合流,以及纯交叉引用流。遍历顺序从新到旧,并在第一次看到某个对象编号时确定它的定义,因此后续修订中的空闲条目会正确覆盖早期修订中写入的对象实体,而新的偏移或代数下的重新定义也总是优先于它所替代的内容
对象流成员还需要额外检查,普通偏移查找本身无法提供这一点;更深入的机制见使用 PDFiumPas 验证对象流和交叉引用流。从 /ObjStm 恢复的压缩对象必须确认其父流在同一次遍历中处于活动状态,并且其索引必须与该成员在流头部中的实际位置一致,PDFiumPas 才会把它视为活动内容。ISO 32000-1 第 7.5.8.4 节还描述了一种混合引用情形:传统兼容表将某对象标为空闲,而尾部的 /XRefStm 条目同时在其他位置把同一个对象定义为压缩成员。PDFiumPas 会先把补充 xref 流合并到同一修订中,再应用传统条目,因此压缩定义会按照规范预期取得优先权
Adobe Extension Level:版本号之上的闸门
%PDF-1.7 标头只承诺 ISO 32000-1 在 2008 年标准化的特性集,而 PDF 生产工具如今依赖的若干能力是在之后作为 Adobe 专属补充发布的,并叠加在同一个版本号之上。Adobe 将每个补充注册为一对 BaseVersion 和 ExtensionLevel,并把它记录在文档目录的 /Extensions 字典中,同时使用开发者前缀标识来源;Adobe 自有扩展使用 ADBE。这样,阅读器就能区分普通的 PDF 1.7 文件和实现了编号扩展级别的 PDF 1.7 文件。没有该声明时保存为 pv17 本身并不构成错误,只有当活动内容确实依赖该声明应覆盖的特性时,问题才会出现
哪些高版本特性会触发显式版本闸门
PDFiumPas 检查的是一份由规范驱动的明确清单,而不是仅凭版本号猜测。图像字典只要明确包含 /SMaskInData 条目,或 /BitsPerComponent 值为 16,就需要 PDF 1.5;十六位情形直接遵循 PDF Reference 1.5 第 4.8 节的图像组件规则。RichMedia 注释和 RichMediaExecute 动作需要 /BaseVersion /1.7 以及不低于 /ExtensionLevel 3。PRC 3D 流通过同时包含 /Type /3D 和 /Subtype /PRC 的字典识别,需要相同的基础版本,但只需要 /ExtensionLevel 1。地理空间 Measure 字典和 Projection 注释需要 /BaseVersion /1.7 以及 /ExtensionLevel 3,也就是 RichMedia 所依赖的同一 Adobe 补充
如果你要在 PDFiumPas 之上自行构建版本闸门逻辑,地理空间检查中有一个值得了解的规范细节。ISO 32000-1 表 254 将 Measure 字典的 /Type 条目标为可选,只说明“如果存在,则应为 Measure”;而表 311 要求 3D 流字典必须包含 /Type,PRC 内容就位于其中。现实中的地图工具经常生成省略 Measure 字典 /Type、只写入 /Subtype /GEO 的 GeoPDF,因此 PDFiumPas 的地理空间检测只匹配 /Subtype,不会像 PRC 3D 检测那样要求两个键都存在。如果两个字典都强制要求 /Type,符合规范的 GeoPDF 内容就可能在未被检测的情况下绕过闸门,最终进入没有扩展级别声明支撑的普通 PDF 1.7 文件
PDFiumPas 会自动降级不受支持的特性吗
它并不提供这种通用能力,认为它会自动降级正是这里需要避免的误解。SaveAs 会通过内部例程 ValidatePdfVersionCompliance 传递目标版本;当该例程发现目标版本或其扩展级别声明无法支持某项特性时,SaveAs 会携带该例程的错误文本抛出异常,而不会写入文件。调用方得到的是精确且指出特性名称的原因,绝不会得到一个被悄悄改写的文档。PDFiumPas 唯一会自动重写内容的地方是目标版本为 PDF 1.3 时:它会移除 PDFium 无论目标版本如何都会写入 ExtGState 字典的透明度默认值 /BM /Normal、/CA 1 和 /ca 1,因为这些特定值不带来视觉意义,而 PDF 1.3 早于这些键的出现
// PDF 1.3 targets rewrite the saved bytes to strip transparency
// defaults PDFium always emits, so incremental mode cannot apply
Pdf.SaveAs('legacy-archive.pdf', saIncremental, pv13);
// raises: PDF 1.3 normalization is incompatible with incremental
// save mode
真正的非默认透明度和图像软掩码在目标为 PDF 1.3 时仍会直接导致失败,因为移除它们会改变页面的实际外观,PDFiumPas 不会替你做出这个决定。在把精确版本投入批处理流程前,还应提前规划两个相关限制。显式版本输出永远不会携带 /Encrypt 字典;如果源文件受保护,保存会立即失败。这与禁止加密的 PDF/X 和 PDF/A 配置文件恰好一致,但也意味着解密是工作流中的独立步骤,不是 SaveAs 会替你完成的操作。PDFiumPas 也没有公开方法把 /Extensions /ADBE 声明写入目录,因此源文件如果包含 RichMedia、PRC 3D 或地理空间内容却缺少该声明,无论请求哪个 PdfVersion 都无法通过闸门。该声明必须已经存在于源文件中,通常由生成工具写入,否则就必须在保存前移除相关特性。精确版本保存前值得先检查只读属性 TPdf.PdfVersion,因为它会解析与保存时验证器相同的、考虑目录信息的有效版本:当前的标头或 /Version 覆盖值,以实际生效者为准
Pdf.FileName:= 'incoming.pdf';
Pdf.Active:= True;
// PdfVersion resolves the same catalog-aware effective version the
// save-time validator uses, so a mismatch here is worth investigating
// before spending a full SaveAs attempt on it
LogSourceVersion('incoming.pdf', Pdf.PdfVersion);
将精确版本目标上的 SaveAs 异常视为预检报告,而不是程序错误:消息会指出源文档违反的确切条款,这正是印刷厂或归档流程在文件继续流转前需要的信息。本文介绍的显式版本保存路径、活动 xref 修订解析器和 Adobe Extension Level 检查,都是 Delphi 和 C++Builder 标准PDFiumPas 组件的一部分;产品页还提供完整的 TPdf.SaveAs 参考,以及其他合规和表单 API