博客

工程师视角的 PDF/A 与 Factur-X:把法律文本撇开,只讲技术

PDF/A 各档到底约束了什么、Factur-X 为什么会在 2026 年成为欧盟强制项,以及从 JSON 渲染器出发到合规 PDF 的最小可用流水线长什么样。

如果有人刚跟你说「下个季度开始,发票必须是带 Factur-X 的 PDF/A-3」,你手上唯一的上下文是法务那边丢过来的几个名词——这篇文章就是为你写的。

我们会避开标准文档那种拗口的腔调,把这几个 profile 实际约束了哪些东西、为什么各国政府开始强制它,以及从一份结构化数据出发产出合规 PDF 的最小流水线,讲清楚。

两段话讲完 PDF/A

PDF 是一种很灵活的格式。过于灵活了——同一份 PDF 规范允许嵌入 JavaScript、链接外部资源(五十年后大概率不存在)、用可逆密码学加密、引用外部字体,以及一百种其他让文档「不自洽」的操作。

PDF/A(A 代表 Archival,归档)就是 PDF 的一份 profile,把那些会导致五十年后渲染不出原貌的部分全部禁掉。高层规则:

  • 所有字体必须嵌入。
  • 不允许 JavaScript、不允许外部链接、不允许音视频。
  • 不允许加密。
  • 透明度必须扁平化,或者由当前 profile 版本明确支持。
  • 颜色必须是设备无关的(强制带 ICC profile)。
  • 所有内容必须在文件内部——任何依赖网络的引用都不行。

它有几个版本,每一代多放开一点对新特性的容忍度:

Profile 年份 新增能力
PDF/A-1b 2005 最初基线——最严
PDF/A-2b 2011 允许 JPEG2000、透明度、图层
PDF/A-3b 2012 允许任意文件附件(Factur-X 的技术基石)
PDF/A-4 2020 基于 ISO 32000-2(PDF 2.0),简化了符合性等级

后缀「b」代表 basic 符合性(视觉保真)。还有「u」(Unicode 映射)和「a」(可访问性打标)两种变体——对绝大多数发票/票据流来说,「b」就够了,因为税务归档关心的是视觉可复现性,不是屏幕阅读器语义。

工程上的要点:如果你用的渲染器声称支持 PDF/A-3b,理想情况下这应该只是一个配置开关({ profile: "PDF/A-3b" } 或等价写法)。如果非要再跑第二个工具(Ghostscript、qpdf、Acrobat)做后处理,那就是一个流水线缺口,要计入运维成本。

为什么偏偏是 PDF/A-3 关键:它是电子发票的载体

PDF/A-3 新加的一项能力,事后看影响极大:PDF 内部可以挂载任意文件附件

听起来无聊,但其实不是。这是欧盟当下正在全面铺开的电子发票强制令的全部技术基础。

架构是这样的:一份 PDF 文件同时

  1. 一份人类可读的发票(视觉版式、金额、品牌)——人读的那一面。
  2. 一份机器可读的 XML 发票——税务机关软件解析的那一面。

两者在同一个文件里,代表同一张发票,而 PDF/A-3 外壳保证这份文件几十年后仍然可解析。

两种主要的 XML 格式:

  • Factur-X(法国)——基于 UN/CEFACT Cross Industry Invoice 的 XML profile
  • ZUGFeRD(德国)——技术上与 Factur-X 一致(两套标准在 2018 年技术合并)
  • EN 16931——两边都实现的欧洲标准

对绝大多数业务流程来说,「Factur-X」和「ZUGFeRD」可以互换使用——它们共享同一份 schema、同一种嵌入机制,符合其中一套的 PDF 通常也直接符合另一套。

强制范围:在哪、什么时候、要什么格式

给 2026 年 Q2/Q3 排期的工程团队一份非穷尽快照:

国家/地区 状态 要求格式
德国 B2B 接收发票自 2025-01-01 起强制;开具自 2027 起强制 EN 16931(Factur-X / ZUGFeRD / XRechnung)
法国 大企业开具自 2026-09 起强制;中小企业 2027-09 通过 Chorus Pro 提交 Factur-X
意大利 B2B 自 2019 起强制 通过 SDI 提交 FatturaPA
波兰 自 2024-07 起强制 KSeF
西班牙 2026 起强制(B2B) 通过 FACe 提交 Facturae
比利时 自 2026-01 起强制 Peppol BIS 3

整体规律:几乎每一个欧盟成员国都在 2024–2027 这个时间窗里推行某种符合 EN 16931 的电子发票方案。只要你的客户在上述任何一个市场运营,你的 PDF 生成器就得在视觉发票之外附带一份对应的 XML。

最小可用流水线

把标准文档里的繁文缛节先放一边。工程视角的全景是:

   ┌─────────────────────┐
   │  Your invoice data  │  (already a JSON object somewhere)
   └─────────┬───────────┘


   ┌─────────────────────┐
   │ Build EN 16931 XML  │  (deterministic mapping; well-tested libs exist)
   └─────────┬───────────┘


   ┌─────────────────────┐
   │ Render PDF/A-3b +   │
   │ attach the XML      │  (single API call to gPdf — or two-step elsewhere)
   └─────────┬───────────┘


   ┌─────────────────────┐
   │  Hand off to        │
   │  Chorus Pro / SDI / │
   │  Peppol / etc       │
   └─────────────────────┘

里面两步不算 trivial:

第一步:构造 XML

这步烦,但纯机械。把发票数据(行项、税、总金额、买卖双方)映射到 EN 16931 的 XML 字段名上。Java/Node/Python 都有现成库可用——直接搜「factur-x library」加你的语言。除非真的享受啃 XML schema 规范,不要从头写。

第二步:渲染 PDF/A-3 并挂载 XML

这一步,渲染器的选择就关键了。

没有内建支持的情况:先渲一份普通 PDF,然后用工具后处理转成 PDF/A-3,并且把 XML 作为嵌入文件挂上去。常见组合:Ghostscript + qpdf,或者付费工具如 Aspose。多两步,多两个失败点,还要保证后处理不会让视觉版式漂移。

有内建支持(gPdf 走的路线):一次调用搞定。

curl -X POST https://api.gpdf.com/api/v1/e-invoice/render \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  --data '{
    "settings": {
      "profile": "pdfa-3b",
      "e_invoice": {
        "standard": "factur_x",
        "profile": "en16931",
        "document_type": "invoice",
        "xml": {
          "format": "cii",
          "encoding": "utf8",
          "content": "<?xml version=\"1.0\"?><rsm:CrossIndustryInvoice>...</rsm:CrossIndustryInvoice>"
        }
      }
    },
    "pages": [{ "size": "a4", "elements": [/* invoice layout */] }]
  }' \
  --output invoice-with-einvoice.pdf

整条流水线就是这么短。渲染器产出 PDF/A-3b,把 XML 以 factur-x.xml(或 zugferd-invoice.xml,两种命名所有消费方都认)的形式挂载上去,然后把字节流返回。

常见踩坑点

几件事大家通常都是踩过才学会:

「PDF/A」和「字段用了 PDF/A 兼容字体」不是一回事

PDF/A-3 文件要求所有字体都嵌入,而且必须覆盖到所用字形的完整范围。如果发票里出现一个日本客户名、渲染器降级到一份没法完整嵌入的字体,校验工具会直接拒收。检查渲染器在 PDF/A 模式下是不是真的嵌入了 CJK 字体——很多默认不嵌。

视觉和 XML 必须一致

XML 发票和视觉发票本来就该代表同一张发票。税务稽核会逐项比对。如果代码产出的 XML 是 total: 119.00,而视觉 PDF 上写着 Total: 120.00(因为四舍五入 bug 或者一份过期模板),你的档案里就留了一笔税务数据不一致。两边都从同一份事实来源生成,最好走同一段代码路径。

EN 16931 里的「Profile」分级

Factur-X 内部还分 profile:MINIMUM、BASIC、EN 16931、EXTENDED,区别在 XML 里携带多少数据。默认选 BASIC,除非客户明确要求更高——它涵盖税码、行项、买卖双方、总金额,足以覆盖大约 95% 的 B2B 发票场景。EN 16931 这档主要是给边角情形多补点字段。

提交前先校验

正式发给税务系统之前,务必先用 PDF/A 校验器(veraPDF 是开源标准)验一遍 PDF,并且用 EN 16931 schema 验一遍 XML。Chorus Pro / SDI 这些系统拒收次数会算进监管那边的可靠性指标。

TL;DR

PDF/A 是一种「文档自洽」的 profile;PDF/A-3 加了挂载文件的能力;Factur-X / ZUGFeRD 就是「PDF/A-3 里挂一份 EN 16931 XML」。欧盟电子发票强制令把这套组合在 2025–2027 之间推成了事实上的 B2B 发票格式。

如果你用的渲染器把 PDF/A-3 + Factur-X 当成一个配置开关,迁移就是机械活;如果不是,你就是在搭一条多步运维流水线。gPdf 的 /api/v1/e-invoice/render 就是「一个开关」的那种方案——完整 schema 见 API 参考,或者直接在 Playground 跑一份样例渲染。