跳转到内容
getnextpdf.com

PDF 对自己知道些什么:元数据与 XMP 封包

Spec: ISO 16684-1:2019Spec: ISO 32000-2

打开一份现代 PDF,它或许会把自己描述两遍。有一份小巧、老旧的键/值清单,叫做 Document Information 字典,还可能有一块 XML——XMP 封包——以一种更丰富、更结构化的形式说着大致相同的内容。本页讲的就是那两套并行的系统,为何两者都存续了下来,以及为何封存与搜索流水线坚持要它们彼此一致。

对标题的简短回答:一份 PDF 知道它的标题、作者、主题、关键词、制作它的工具,以及时间。它把那份知识同时储存在两处,而有意思的工程问题就在于让它们保持诚实。

元数据是文档中人很少看见、而机器几乎总在读取的部分。你操作系统的文件预览、一个数字资产管理器、一份图书馆目录、一个搜索索引、一个法律电子取证工具——它们之中有许多会在文档正文之前、或与之并行地读取元数据,并信任它所说的内容。

因此当这两套系统彼此不一致时——Info 字典说是一个作者,而 XMP 封包说是另一个——下游某处会挑一个,而你无从选择是哪一个。一个搜索索引可能呈现出错误的标题。一个封存验证器可能直接拒绝该文件。这种漂移在一条流水线被它绊倒之前都是隐形的,而那正是发现它的最糟时机。

  • 一份 PDF 可以用两套并行系统携带元数据:老旧的 Document Information 字典,以及现代的 RDF/XML XMP 封包
  • XMP 封包被包进一个 xpacket 处理指令——一个 begin 标头与一个 end 尾段——好让工具能找到它,甚至能就地重写它,而不必重新解析整份文件。
  • 属性住在命名空间里:dc(Dublin Core)放标题与创建者,xmp 放创建与修改日期,pdf 放制作器,xmpMM 放文档身份与历史。
  • PDF/A 要求 XMP 元数据,而那些有 XMP 对应项的 DocInfo 条目必须与之相符——一处不一致便是一次验证失败。
  • NextPDF 把这两者当作一项工作来处理:它两者都写入、并把 XMP 读回,且守护封包的大小,宁可失败封闭,也不输出一份格式损坏的文件。

诚实的框定方式是:这是一个伪装成格式化问题的同步问题。Document Information 字典(Spec: ISO 32000-2, §14.3.3),由尾段透过 /Info 条目引用,是一份扁平的字符串清单:Title、Author、Subject、Keywords、Creator、Producer,以及一对日期。它简单、早于 XML,且因为太多工具仍会先读它,它至今仍随附在几乎每一份文件里。

XMP 封包(Spec: ISO 16684-1:2019, §7)是现代的那一半。它是一份 XML 文档——确切地说是 RDF/XML——把文件建模为一组分组进模式(schema)的具名属性(property),每个模式绑定到一个命名空间(Spec: ISO 16684-1:2019, §6)。那种结构正是扁平字典做不到的:一个值可以是一份有序清单、一个带语言标签的替代项(“英文标题、法文标题”),或一笔结构化记录。在一份 PDF 里,那份 XML 住在一个附着于文档目录的元数据流中(Spec: ISO 32000-2, §14.3.2),那正是当前读取器查看的典型位置。

有三个细节值得剖析,因为它们正是工具把封包弄错之处。

那个包装。 一个 XMP 封包被一个处理指令所括住,好让在封包以可直接搜索之字节储存的那些格式中,程序能定位到元数据而无需完整解析;而具体到 PDF 中,目录元数据流才是典型的定位器。begin 标头携带一个声明文本编码的字节顺序标记;end 尾段携带一个陈述封包是只读、还是可就地编辑的标志位。当它可写时,序列化器会在 XML 之后留下一段空白填充,好让编辑器能略微增长内容,而不必移动其后的每一个字节。那段填充并非装饰——它正是让就地元数据编辑得以成立的东西。

那些命名空间。 一个属性唯有对照它的命名空间才有意义,而其中有少数几个近乎通用。Dublin Core(dc)持有标题与创建者。XMP basic 模式(xmp)持有创建与修改日期以及创作工具。PDF 模式(pdf)持有制作器字符串与关键词。媒体管理模式(xmpMM)持有文档的身份及其衍生历史——那条说着“这份文件来自那一份”的轨迹。就这些命名空间 URI 与属性名称达成一致——通常以约定俗成的前缀展示——正是核心模式的全部要点(Spec: ISO 16684-1:2019, Annex B);两个都说着 Dublin Core 标题属性的工具,无需事先约定便能互操作。

那条一致性规则。 因为两套系统都能命名同一个属性,它们便可能彼此不一致。封存设定档拒绝允许那种情况。一份 PDF/A 文件必须携带一份 XMP 元数据流,而任何有 XMP 对应项的 Document Information 条目都必须与之相符;一处不一致便是一次验证失败。实务上的要点很直接:在一条封存流水线里,这两者不是各自独立的字段,而是被写了两次的同一个事实,且它们必须保持步调一致。

NextPDF 把这三件事当作单一关注点来处理。元数据流是一条路径,而非两条相互竞争的路径。

  1. Collect the values onceTitle, author, subject, keywords, creator, producer and dates are set on the document a single time, so there is one source of truth, not two.
  2. Write the Info dictionaryThe DocInfo writer emits the legacy Title, Author, Subject, Keywords, Creator, Producer and date entries that older tools read first.
  3. Build the XMP packetThe XMP builder serializes the same values as RDF/XML under dc, xmp, pdf and xmpMM, wrapped in the xpacket header and trailer with writable padding.
  4. Guard the packet sizeBefore serialization is accepted, the engine checks the packet against a size bound and fails closed with a typed exception rather than emit a malformed or oversized stream.
  5. Read XMP back to verifyThe XMP reader parses the packet so a pipeline can confirm the engine wrote the metadata it was given — the kind of check an archival validator builds on.
How NextPDF keeps a document's two metadata systems consistent: one set of values is written to both the Info dictionary and the XMP packet, the packet is size-guarded before it is serialized, and the same packet can be read back so a pipeline can confirm the metadata serialized as intended.

下面的写法把元数据设定一次,然后让引擎把它扇出到两套系统,接着把 XMP 标题读回,好让一条流水线能检查它。那个大小守护,正是把“大概没问题”变成“不行就可被证明地拒绝”的那一行。

<?php
declare(strict_types=1);
use NextPDF\Core\Document;
use NextPDF\Metadata\Exception\XmpPacketTooLargeException;
$document = Document::createStandalone();
// Set the values ONCE. The engine writes them to both the Info dictionary
// and the XMP packet, so the two systems cannot drift apart at the source.
$document->setTitle('Annual Report 2026');
$document->setAuthor('Records Office');
$document->setSubject('Statutory annual filing');
$document->setKeywords('annual report, statutory, 2026');
try {
// Building the document serializes the XMP packet. The engine guards the
// packet size and fails closed: a packet that exceeds the bound is a
// typed exception, never a silently truncated or malformed stream.
$bytes = $document->getPdfData();
} catch (XmpPacketTooLargeException $e) {
// Decide deliberately: trim the metadata, not the guarantees.
error_log('XMP packet exceeded the size bound: ' . $e->getMessage());
throw $e;
}
// Read the packet back. This confirms the XMP the engine serialized carries the
// value you set — the kind of integrity check an archival pipeline runs before
// it trusts the file. (Both systems are written from one source, so they agree
// by construction; reading the XMP back proves it serialized as intended.)
$xmp = $document->readXmpMetadata($bytes);
$xmpTitle = $xmp->get('dc', 'title'); // 'Annual Report 2026'
if ($xmpTitle !== $document->getTitle()) {
throw new \RuntimeException('XMP title does not match the value that was set.');
}

这里没有任何一条路径会让两套系统悄悄地各走各的:值进入一次,两个写入器消费同一个来源,而读取器让一条流水线检查结果。

常见的信念是,Info 字典已经过时,你可以无视它。你不能——还不行。大量已安装的软件,包括某些操作系统文件预览与较旧的封存工具,仍然先读、或只读 Info 字典。丢弃它并不会让一份文件现代化;它会让该文件在任何尚未采用 XMP 的东西看来都是无标题的。

镜像反面的错误,是把这两者当作你各自填写的独立字段。那正是它们漂移的方式。它们是同一个事实的两种序列化。从一个来源写它们,否则就接受下游某处会挑你不打算要的那个版本。

  • NextPDF 写入两套系统,并把 XMP 读回。 它的范畴是 XMP 元数据构建器、DocInfo 写入器,以及一个 XMP 读取器。它并不承诺成为一个通用的 RDF/XML 查询引擎。
  • XMP 封包受大小守护,并失败封闭。 一个超出引擎边界的封包会引发一个类型化异常。引擎不会为了让一个超量请求“塞得进去”而输出一份被截断、填充超限,或以其他方式格式损坏的封包。
  • 一致性是在写入时从一个来源强制实现的;它不是对一份并非你产生之文件的神奇协调。 如果你导入一份其两套系统本就彼此不一致的文档,解决那一点是你的决定,而非一次隐式的重写。
  • PDF/A 的元数据要求是封存设定档的一部分。 本页解释那条规则;而标准符合性的裁决属于一个验证器,对封存工作而言一向如此。关于那条边界,参见封存页面。

元数据构建器、DocInfo 写入器,以及 XMP 读取器都是 Core 能力。诚实的限定语,连同那个大小守护,置于下方。

Metadata: Info dictionary and XMP packet — edition availability
EditionAvailability
Core

The XMP metadata builder, the Document Information dictionary writer, and the XMP reader ship in Core, with the size guard that fails closed on an oversized packet — including the PDF/A conformance output and validation that make the XMP metadata stream mandatory and require DocInfo to match it.

Pro

Adds batch and templated metadata workflows over the same Core writer and reader.

Enterprise

Adds the broader conformance policy and reporting surface across a document estate.

  • 封存与 PDF/A — XMP 封包从此不再可选、且 DocInfo 到 XMP 的一致性规则成为一项过或不过之要求的地方。
  • 可交给审计员的合规 — 为何能往返一致、并与自身相符的元数据,是一份可审计产物的一部分。
  • PDF 文件剖析 — 尾段、目录,以及元数据流在文件结构中所处的位置。
  • 流与过滤器 — 元数据流是一个流;这是 PDF 流如何被编码并保持确定性的方式。
  • Document Information 字典 — 那份由尾段透过 /Info 条目引用的老旧、扁平的键/值元数据:Title、Author、Subject、Keywords、Creator、Producer,以及日期。早于 XMP;仍被广泛读取。
  • XMP — Extensible Metadata Platform,一种以具名属性分组进模式来描述资源的 XML(RDF/XML)格式。标准化为 ISO 16684-1。
  • xpacket — 那个包裹一个 XMP 封包的处理指令,带一个 begin 标头(编码标记)与一个 end 尾段(只读或可写标志位),好让工具能定位并编辑封包而无需完整解析。
  • 命名空间 / 模式 — 一套绑定到一个 URI 的具名属性词汇。常见前缀:dc(Dublin Core)、xmp(XMP basic)、pdf(PDF 专属)、xmpMM(媒体管理 / 文档身份)。
  • 元数据流 — 那个附着于文档目录、携带 XMP 封包的 PDF 对象。现代读取器首先查看的典型位置。
  • PDF/A — 封存 PDF 设定档族(ISO 19005)。它要求一份 XMP 元数据流,并要求任何有 XMP 对应项的 Document Information 条目都与之相符,否则验证失败。