PDF 是一个容器:内嵌文件与关联数据
Spec: ISO 32000-2, §7.11.4ISO 32000-2 §7.11.4Spec: ISO 32000-2, §14.13ISO 32000-2 §14.13Spec: ISO 19005-3, PDF/A-3ISO 19005-3 PDF/A-3
多数人把 PDF 想象成一叠页面。那是你看得见的部分。但 PDF 同时也是一个容器,它能在内部携带整份其他文件——一份电子表格、一段 XML 载荷、原始的源文档——全部捆进同一份你交给别人的单一文件里。
本页解释那是如何运作的:储存字节的内嵌文件流、列出它们的名称树,以及那个决定一份附件究竟只是搁在那里、还是真的具有某种意义的关键键。
为何这很重要
标题为“为何这很重要”的章节一份无类型的附件与一份有类型的附件,在人眼看来完全相同。两者都是骑在一份 PDF 内部的文件,而且——在这个引擎里——两者都与文档相关联。差别在于,其中一份会告诉机器它有何用途,而另一份则把关系留白,让机器去猜。
对一份混合电子发票而言,那个差别就是整盘棋的胜负。一个税务平台不会读你那一页发票;它读的是你内嵌的 XML。如果那份 XML 是作为一团未加区分的 blob 附上,而非作为可见文档的发票数据,一个符合标准的读取器就没有可靠的办法知道它是该处理的载荷。页面看起来完美无缺。发票却被拒收。失败在数日之后才抵达,背后还压着一笔被冻结的款项。
在产生文件的那一层把关系弄对,远比一份份被拒发票地慢慢发现它要便宜得多。
简短版本
标题为“简短版本”的章节- 一份 PDF 可以把任何文件的字节内嵌为一份内嵌文件流(Spec: ISO 32000-2, §7.11.4ISO 32000-2 §7.11.4)。该流携带数据,外加一份小巧的参数字典:原始大小、日期,以及一个校验和。
- 内嵌文件登记在
EmbeddedFiles名称树中,因此读取器可以按名称枚举它们,而不必扫描整份文档。 - 一份关联文件则更进一步:它声明一个
AFRelationship(Spec: ISO 32000-2, §7.11.3ISO 32000-2 §7.11.3)——八个标准值之一(Source、Data、Alternative、Supplement、EncryptedPayload、FormData、Schema、Unspecified),或一个自定义值——说明该文件如何与它所附着的内容相关。 - 那种有类型的关系,正是混合电子发票(ZUGFeRD / Factur-X)与 PDF/A-3 附件背后的机制(Spec: ISO 19005-3, PDF/A-3ISO 19005-3 PDF/A-3)。
- NextPDF 在核心中支持原始的容器原语:
embedFile()与embedFileFromString(),并带一个明确的关系。Advanced 版本则在这些原语之上添加专用的 EN 16931 / ZUGFeRD / Factur-X 电子发票内嵌器。
NextPDF 如何处理
标题为“NextPDF 如何处理”的章节把它想成两层堆叠在一起。
下层是储存。一份内嵌文件流(Spec: ISO 32000-2, §7.11.4ISO 32000-2 §7.11.4)是把原始文件的字节包进一个 PDF 流对象,外加一份记录原始大小、修改日期,以及未压缩数据校验和的参数字典。该流是透过一份文件规格字典来抵达的,其 /EF 字典指向内嵌文件流——流本身并不携带 /EF。读取器可以把文件逐字节地原样取回。为了让这些文件可被找到,文档目录持有一份 EmbeddedFiles 名称树——一张从名称到每份文件规格的排序映射——因此查看器可以列出“这份 PDF 内部有这 3 个文件”,而不必走遍每一页。
上层是意义。单凭其本身,一份内嵌文件只是存在着。关联文件机制(Spec: ISO 32000-2, §14.13ISO 32000-2 §14.13)把一份文件附着到某样东西上——整份文档、一页、一个图形对象——并替它盖上一个 AFRelationship 的印记。ISO 32000-2 定义了一套由八个标准值组成的小型词汇(Spec: ISO 32000-2, §7.11.3ISO 32000-2 §7.11.3),并也允许自定义值;每个标准值都回答一个精确的问题:
AFRelationship | 它对该文件断言了什么 |
|---|---|
Source | 这是可见内容据以生成的源材料(例如,原始的文字处理文档)。 |
Data | 这是与可见内容相系的结构化数据——典型案例是一页已渲染发票背后的发票 XML。 |
Alternative | 这是同一内容的一种替代表现形式(例如,一个音频或视频版本)。 |
Supplement | 这是扩充该内容、但并非其一部分的补充材料。 |
EncryptedPayload | 该内嵌文件是一份加密载荷,PDF 将其包裹为一个不透明的 blob。 |
FormData | 该文件是表单数据(FDF、XFDF,或一段 XML 表单载荷)。 |
Schema | 该文件是一份描述某个 Data 文件结构的 schema(例如,XML 数据的 XSD,或一份 JSON Schema)。 |
Unspecified | 关系被刻意不予陈述。诚实,但它对机器什么也没说。 |
在这八个之外,标准也允许应用程序专属的自定义关系值,因此这套词汇是可扩展的,而非固定不变的。
一份关联文件是由两样东西协同界定的,而非单凭一个键。/AF 关联把文件规格绑定到文档的某个部分;文件规格中的 AFRelationship 键接着陈述其语义关系。关联点(文档目录、一页,或一个对象)上的 /AF 条目是一个数组——该数组包含一份或多份文件规格字典,通常以间接引用形式出现;/AF 并非单一引用。一份文档层级的关联文件,就是列在文档目录 /AF 数组中、携带其 AFRelationship 的那份文件规格。把那份电子表格标为 Unspecified,你便把它与文档关联了起来,却对机器只字未提其缘由。把同一份电子表格标为 Data,你便告诉了每一个符合标准的读取器它是什么、有何用途。字节是相同的。语义则不然。
这正是为何电子发票这个案例并不是“附上一个 XML 文件”。它是“把这份 XML 内嵌为这份文档的 Data 关联文件,置于一个符合标准的 PDF/A-3 载体之中”——而发票有效性与法律承认仍是该载体并不执行的独立检查。整个流程有四个阶段,而顺序正是让它保持正确的关键。
- Store the bytesThe file is wrapped in an embedded file stream with its size, dates, and a checksum (ISO 32000-2 §7.11.4).
- Register it by nameThe file specification is added to the EmbeddedFiles name tree so a reader can enumerate attachments without scanning the document.
- Declare the relationshipAn AFRelationship value (one of the eight standard values such as Source or Data) marks how the file relates to the content, associated at the document level (ISO 32000-2 §14.13.3).
- Make it archivalA PDF/A-3 carrier permits the embedded payload to ride inside one conforming archival PDF/A document; invoice validity and legal acceptance remain separate checks (ISO 19005-3).
那第四个阶段,正是 PDF/A-3 之所以作为一个独立设定档存在的原因。早先的封存设定档限制了能内嵌什么;PDF/A-3(Spec: ISO 19005-3, PDF/A-3ISO 19005-3 PDF/A-3)正是那允许任何格式的文件骑在一份符合标准的封存文档内部的部分。它允许内嵌的载荷——它并不验证那份载荷,也不授予法律地位。少了它,那份混合发票——同时既是一个人所读的那一页、又是税务系统所解析的数据的单一文件——便根本无法成为一份符合标准的封存 PDF/A 文档;而发票是否有效、是否在法律上被接受,仍是一个独立的问题。Advanced 版本所添加的专用电子发票内嵌器,正是恰恰盖在这之上的便利接缝:它内嵌载荷、把关系设为 Data、并正确地登记它,让你不必亲手组装容器管路。更深层的发票与封存机制住在下方链接的两个相邻页面上;本页讲的是它们两者都立足其上的那个容器。
实际示例
标题为“实际示例”的章节一段小而完整的程序。真正要紧的那两个调用,正是一份无类型关联文件与一份有类型关联文件之间的差别——而关系是一个你应当设定的明确参数。在这个引擎里,两个调用都会产生一份关联文件:embedFile() 与 embedFileFromString() 总是把文件规格登记进文档目录的 /AF 数组,因此关系唯一改变的就是该关联意味着什么。它默认为 Unspecified,那会关联该文件、却对机器只字未提其缘由;对一份电子发票载荷,你把它设为 Data,好让读取器能找到它。
<?php
declare(strict_types=1);
use NextPDF\Core\Document;use NextPDF\Navigation\AFRelationship;
$document = Document::createStandalone();$document->addPage();$document->setFont('helvetica', 'B', 16);$document->cell(0, 12, 'Invoice INV-2026-0042', newLine: true);
// An UNTYPED associated file: the bytes are embedded AND the file spec is// added to the document catalog's /AF array, but the relationship says// nothing about why. A reader can open it; a machine cannot tell its role.// The relationship is left Unspecified (its default); the second argument is// the human-readable description. embedFile accepts the AFRelationship enum.$document->embedFile( '/srv/invoices/INV-2026-0042-source.docx', 'Original source document', AFRelationship::Unspecified,);
// A TYPED associated file: the invoice XML is declared as the DATA behind// the visible page. This is the relationship a hybrid e-invoice reader// looks for — the same intent the dedicated e-invoice embedder sets.// embedFileFromString takes the data, a filename, a description, and a// relationship as a PDF-name string ('/Data').$invoiceXml = $generateCiiXml(); // your ERP authors this; the engine never does$document->embedFileFromString( $invoiceXml, 'factur-x.xml', 'Factur-X invoice data', '/Data',);
$bytes = $document->getPdfData();'/Data' 这个关系不会被认错。第一份附件——留作 Unspecified——同样被关联了起来,只是没有一个陈述出来的含义。对于两个调用,引擎都会写出内嵌文件流、把文件加入 EmbeddedFiles 名称树、把它的文件规格列进文档目录的 /AF 数组,并记录你所陈述的关系——它不会替你挑一个。这个引擎没有仅名称树的模式:每一个你以这种方式内嵌的文件都是一份文档关联文件,因此关系是你唯一掌控的杠杆。
常见的误解
标题为“常见的误解”的章节常见的假设是,“内嵌”与“关联”是同一件事的两个说法。它们不是。内嵌讲的是储存——字节在 PDF 内部。关联讲的是绑定——文件规格被列在文档某个部分的 /AF 数组中,并携带一个 AFRelationship。在抽象的 PDF 模型里,一份文件可以被内嵌进名称树而从未被关联;NextPDF 的 embedFile() 路径并不会把它留在那里——它总是写出 /AF 关联——因此对这个引擎而言,悬而未决的问题从来不是一份文件是否被关联,而是关系陈述了什么。
第二个陷阱:假定查看器会“想明白”哪个附件才是发票。一个符合标准的读取器不应当去猜。它会去寻找那个关系陈述为 Data 的文件。把关系留作 Unspecified,你便关联了载荷,却没有对机器陈述任何关于其角色的有用信息。
限制与边界
标题为“限制与边界”的章节这套容器机制强大到一个值得诚实以对的地步:embedFile() 会读取 PHP 进程所能读取的任何路径。那是它的功能——同时也是它的边界。引擎附上它所被给予的字节;它并不会、也无法替你判断一个路径是否是你有意暴露的那一个。
| Edition | Availability |
|---|---|
| Core |
|
| Pro | Not in this edition |
| Enterprise | Not in this edition |
还有两条限制值得直白地陈述:
- 内嵌并非验证。 引擎携带你给它的字节。内嵌的 XML 是否是一份符合标准的发票载荷,是一个独立的问题,由一个验证器来回答——参见发票页面。
- 一份有类型的附件,单凭其本身并不是一份符合标准的封存文件。 要让那份混合文件成为一份合法的 PDF/A-3 文档,需要封存模式以及一次独立的标准符合性检查——参见封存页面。
相关文档
标题为“相关文档”的章节- 发票与电子发票开立 — 这套机制所成就的使用案例:一份携带机器可读发票、作为其
Data关联文件的混合 PDF。 - 封存与 PDF/A — 为何载体是一份 PDF/A-3 文件,以及标准符合性承诺了什么、又没承诺什么。
- PDF 文件剖析 — 名称树与文档目录在文件结构中所处的位置。
- 流与过滤器 — 一份内嵌文件的字节如何被储存并压缩在一个流对象内部。
词汇表
标题为“词汇表”的章节- 内嵌文件流 — 一个持有外部文件字节的 PDF 流对象,外加一份记录其原始大小、日期,以及校验和的参数字典(ISO 32000-2 §7.11.4)。
- EmbeddedFiles 名称树 — 文档目录中那张按名称列出内嵌文件的排序映射,让读取器可以枚举附件,而不必扫描整份文档。
- 关联文件 — 一份透过
/AF关联(在文档目录、一页,或一个对象上)绑定到文档某部分、并携带一个陈述它如何与该内容相关之AFRelationship的内嵌文件;本页所聚焦的,正是文档层级的那个案例——目录/AF数组中的那份文件规格(ISO 32000-2 §14.13.3)。 - AFRelationship — 那个其值命名了关系的文件规格键(ISO 32000-2 §7.11.3)。它取八个标准值之一(
Source、Data、Alternative、Supplement、EncryptedPayload、FormData、Schema、Unspecified),或一个自定义值;Data是一份混合电子发票载荷所用的值。 - PDF/A-3 — ISO 19005-3 那个允许任意格式文件被内嵌、从而成就一份符合标准之混合文档的封存设定档。
- 混合发票 — 一份既是人类可读页面、又是机器可读内嵌发票载荷的 PDF 文件。