文件能打开,档案系统却提示“不符合 PDF/A”;设计稿在电脑上看着正常,送到印厂又被退回来,说不是 PDF/X。这种问题我一般不先翻 Python 库。
PDF/A、PDF/X 都不是改个扩展名,也不是往元数据里塞一行版本号。字体、颜色空间、ICC Profile、透明度、加密、输出意图,只要有一处不符合,文件照样能打开,但验证就是不过。
Python 在这里更适合做调度,真正重写 PDF 的活交给 Ghostscript。
PDF/A 偏长期归档,要求文件尽量自包含;PDF/X 偏印刷交付,更关心 CMYK、字体嵌入、页面裁切框和输出条件。Ghostscript 的 pdfwrite 可以生成 PDF/A 1~3 的 B 级文件,也能处理常用 PDF/X 输出,但必须提供对应的定义文件和 ICC Profile。
先准备三个东西:
pip install pikepdf
系统安装 Ghostscript,然后从它的 lib 目录复制:
PDFA_def.psPDFX_def.ps
这两个文件不能原样扔进去跑。里面的 ICCProfile 要改成机器上的绝对路径。做 PDF/A-2b,我一般放一个可靠的 RGB ICC;做 PDF/X-3,则按印厂要求放 CMYK ICC。ICC 用错了,程序可能不报错,颜色却已经不是原来的颜色,这种问题比转换失败更麻烦。
Python 包装层不用写得太花:
from pathlib import Pathimport shutilimport subprocessclassPdfStandardizer:def__init__(self) -> None: self.gs = self._find_ghostscript() @staticmethoddef_find_ghostscript() -> str:for name in ("gs", "gswin64c", "gswin32c"): executable = shutil.which(name)if executable:return executableraise RuntimeError("没有找到 Ghostscript 命令行程序")def_convert( self, source: str, target: str, definition: str, standard_args: list[str], ) -> None: src = Path(source).resolve() dst = Path(target).resolve() rule = Path(definition).resolve()ifnot src.is_file():raise FileNotFoundError(f"输入文件不存在:{src}")ifnot rule.is_file():raise FileNotFoundError(f"定义文件不存在:{rule}") dst.parent.mkdir(parents=True, exist_ok=True) command = [ self.gs,"-dBATCH","-dNOPAUSE","-dSAFER","-sDEVICE=pdfwrite","-dPDFACompatibilityPolicy=2",f"-sOutputFile={dst}", *standard_args, str(rule), str(src), ] result = subprocess.run( command, capture_output=True, text=True, encoding="utf-8", errors="replace", )if result.returncode != 0:raise RuntimeError("PDF 转换失败:\n" + result.stderr[-2000:] )defto_pdfa2b(self, source: str, target: str, definition: str) -> None: self._convert( source, target, definition, ["-dPDFA=2","-sColorConversionStrategy=RGB", ], )defto_pdfx3(self, source: str, target: str, definition: str) -> None: self._convert( source, target, definition, ["-dPDFX=3","-sColorConversionStrategy=CMYK", ], )
调用时把归档和印刷分开,别共用一套 ICC:
worker = PdfStandardizer()worker.to_pdfa2b("incoming/contract.pdf","out/contract-a2b.pdf","profiles/PDFA_def.ps",)worker.to_pdfx3("incoming/brochure.pdf","out/brochure-x3.pdf","profiles/PDFX_def.ps",)
这里我把 PDFACompatibilityPolicy 设成了 2。遇到不兼容内容就直接失败,不让它生成一个带着 PDF/A 声明、实际却不合规的文件。Ghostscript 默认策略可能保留不兼容特性,只打印警告,这种“看起来成功”的结果我不太信。
转换完成也不能只看返回码。至少再跑一次 veraPDF:
verapdf -f 2b out/contract-a2b.pdf
看到 isCompliant="true" 才算过。pikepdf 能读出文件声称的 PDF/A 级别,但它检查的是“文件怎么声明自己”,不是完整合规性;真正验收还得用验证器。
再说 PDF/A 转回普通 PDF。
这句话容易把人带偏。PDF/A 本身就是 PDF 的受限版本,不存在把页面内容“解码回去”。多数业务所谓转回普通 PDF,只是取消 PDF/A 合规声明,后面允许继续编辑、合并或者重新加密。
我一般只移除 XMP 里的 PDF/A 身份,不随手删 OutputIntent。输出意图不是垃圾元数据,颜色管理后面可能还要用。
from pathlib import Pathimport pikepdfdefremove_pdfa_claim(source: str, target: str) -> None: src = Path(source) dst = Path(target) dst.parent.mkdir(parents=True, exist_ok=True)with pikepdf.open(src) as pdf:with pdf.open_metadata() as metadata:for key in ("pdfaid:part", "pdfaid:conformance"):if key in metadata:del metadata[key] pdf.save(dst, fix_metadata_version=False)
调用:
remove_pdfa_claim("out/contract-a2b.pdf","out/contract-normal.pdf",)
还有一个容易漏的点:Ghostscript 会重新组织 PDF 内部对象,目标是尽量保持视觉效果一致,不保证原始结构逐项不变。带数字签名、交互表单、图层或者特殊书签的文件,转换后必须单独回归,尤其是数字签名,重写文件后基本不能再按原签名验。
所以这套流程不是“执行命令—文件生成—结束”,而是转换、验证、抽样打开、检查字体和颜色。少一步,问题通常不会在测试环境暴露,往往等档案系统或者印厂替你发现。