这篇是 PDF 文档属性应显示自己的品牌,而不是别人家的工具名 的姊妹篇——那一篇讲了为什么要在意 PDF 元数据,这一篇是操作手册:每个字段在 PDF 规范里的角色、谁会读、常见错误,以及如何验证输出里确实写进了你想写的内容。
gPdf 暴露的是 PDF 规范为文档级元数据定义的六个标准字段,放在 DocumentRequest JSON 的 settings.metadata 之下。每个字段都是可选的——没设的话,gPdf 会回退到令牌的 default_metadata(企业策略功能)或系统默认值。
{
"settings": {
"metadata": {
"title": "...",
"language": "...",
"author": "...",
"subject": "...",
"creator": "...",
"producer": "..."
}
}
}
接下来每一节讲一个字段,形式统一:字段是什么、在哪里露面、常见错误、经验法则。顺序就是先填什么、再填什么……最后填什么。
title — 文档是什么
PDF 规范将其描述为“文档标题“。
会在哪里露面:
- PDF 阅读器的标题栏(Adobe Reader、Preview、Foxit、Chromium PDF 阅读器都会显示)。
- 当 PDF 内嵌打开时(
Content-Disposition: inline)的浏览器标签。 - 搜索索引——Spotlight、Outlook、SharePoint、Google Drive 的全文索引器都会读
title并给予较高权重。
常见错误:
- ❌ 把 title 设成文件名。
invoice-20260318.pdf是文件名。title 应该是给人读的,比如Invoice INV-2026-3401。文件名和标题是两件事:文件名给文件系统看,标题给阅读器和搜索看。 - ❌ **title 留空。**阅读器会回退到文件名,呈现出来一看就是机器自动生成的。
- ❌ 把品牌塞进 title。
Acme Logistics — Invoice INV-2026-3401把标题栏挤得乱糟糟的。品牌归author,不归title。
**经验法则:**title 应该和页面渲染出来的 H1 一致。如果发票模板顶部那行写着“Invoice INV-2026-3401“,那就是 title。
language — 为了无障碍、搜索和合规
language 是 BCP-47 语言标签:en、de、zh-Hans、pt-BR、ar-SA。每一份文档都要设。在这六个字段里,它的下游后果最具体、实现成本最低——这就是它排在第 2 位而不是被埋到后面的原因。
会在哪里露面:
- 屏幕阅读器——JAWS、NVDA、VoiceOver 会用它选合适的音素集。一个英文屏幕阅读器在读
language: "de"的 PDF 时会正确发音德语词;不带标签的话,韵律就乱了。 - 搜索引擎和索引器——决定用哪种语言的词干提取和停用词表。
language: "zh-Hans"的发票会用中文分词建索引;缺失标签时通常会默认按英文处理,索引就用不了了。 - PDF/A 合规——PDF/A-2a 和 PDF/A-3a(无障碍配置)要求带语言标签。没有的话,veraPDF 校验直接失败。
常见错误:
- ❌ **不设。**默认应该是“收件方所在的语言“,而不是“平台的默认语言“。大多数会漏的栈干脆就不写这个字段;结果是屏幕阅读器发音不对、搜索索引提取词干错位。
- ❌ 用非 BCP-47 的字符串,比如
"english"或"EN-US"。PDF 规范要求 RFC 5646 标签:en、en-US、de、pt-BR。 - ❌ 不管文档实际内容是什么语言,都硬编码平台默认值(比如永远写
"en")。一份葡萄牙语发票被打上"en"的标签,比不打标签还糟——这是在主动误导索引器。
**经验法则:**标签应该匹配文档实际的内容语言。给巴西客户用葡萄牙语开发票,就设 "language": "pt-BR",不是 "en"。多语言文档则挑出主导语言,其余部分对单个内容元素用 Lang 属性——这是超出文档级 language 字段范畴的、属于带标签 PDF 的无障碍特性。
author — 文档归谁所有
PDF 规范里,author 是“创建文档的个人或组织名称“。对于发给收件方的业务 PDF,答案几乎永远是组织——不过具体形态会因场景而真有差别。
会在哪里露面:
- 每一款 PDF 阅读器的属性对话框,显眼地标着“Author“。
- DMS / 归档索引器,经常用作筛选条件。
- PDF/A 的 XMP 元数据流,会随之进入长期归档。
常见错误:
- ❌
"author": "[email protected]"—— 顺手把操作人员的邮箱漏到每份 PDF 里,进了每个搜索索引,变成长期 PII 隐患。 - ❌
"author": "PDF Generator Service"—— 内部工具名;对收件方毫无意义。 - ❌ 留空 —— Preview 和大多数阅读器会在属性对话框里赤裸裸地显示“(no author)“,读起来就是“没人对这份文档负责”。
有效写法:
- ✅
"author": "Acme Logistics, Inc."—— 直白的组织名。 - ✅
"author": "Acme Logistics — Billing"—— 组织 + 部门,适合送到某个具体接收岗的文档。 - ✅
"author": "Bridge Capital Partners — Fund III"—— 在金融 / 法律领域很有用,这些地方的署名通常是某个具体实体。 - ✅
"author": "Maria López, RICS Surveyor"—— 单一作者出版(报告、估值、法律意见)时,个人本身就是编辑署名。
**经验法则:**author 是收件方应该把文档和谁挂钩的那个实体。在多租户 SaaS 中,平台代客户生成 PDF 时,author 应该是客户的组织名,而不是平台的名称。(平台的名称归 creator——见下文。)对于咨询、出版、法律等场景,个人本身就是品牌,那写个人也无妨。
subject — 这份文档是哪类
subject 是文档的简短描述。阅读器不会把它放在显眼位置——除非打开属性对话框,大多数用户根本看不到。但文档管理系统、归档系统,以及基于规则的邮件 / 文件路由,都会用它。
会在哪里露面:
- 属性对话框的次要位置。
- DMS 路由规则、归档分桶逻辑。
- XMP 元数据流(PDF/A)。
常见错误:
- ❌
"subject": "Invoice for Acme on 2026-03-18 for $4,532.10"—— 这是某一份文档实例的描述,不是分类。这种内容归title。 - ❌ 留空 —— 白白浪费了下游系统的一个免费路由钩子。
- ❌ 类别混用不一致(
"Invoice"vs"Invoice/2026-03"vs"Monthly invoice")—— DMS 过滤器在变动的目标上根本没法分桶。
有效写法:
- ✅
"subject": "Invoice" - ✅
"subject": "Monthly account statement" - ✅
"subject": "Shipping label — 4×6 thermal" - ✅
"subject": "Q3 2026 board pack"
经验法则:合适的粒度是文档类别,而不是文档实例。处理着每天上千份新进 PDF 的 DMS,只要给它一套稳定的词汇表,就能依据 subject 做路由。给平台挑一组有限的类别,一以贯之——平台生成的每一份发票都应严格写 "subject": "Invoice"。
creator 与 producer —— 最容易搞混的一对
到了这里,大多数团队就停下不读 PDF 规范开始猜了。规范说得很清楚:这两个字段含义不同。
creator—— 生成源内容的应用(决定文档应该写什么的上游系统)。producer—— 生成 PDF 字节流的应用(把内容变成 PDF 文件的渲染引擎)。
对于通过 gPdf 这种 JSON 转 PDF API 生成发票的 SaaS 计费平台:
creator= 带版本号的 SaaS 计费平台。正是这个应用决定了这应该是一份给 Acme 的、金额 4,532.10 美元的发票。producer= 渲染器。默认是 “gPdf”。但渲染层是 SaaS 自己选的基础设施,SaaS 完全可以把producer设成自家的平台名——它的平台在某种实质意义上确实通过把活儿派给作为基础设施的 gPdf 而生成了 PDF 字节。
{
"creator": "Acme Billing Platform v7.2",
"producer": "Acme Billing Platform"
}
会在哪里露面:
- 属性对话框,两个字段都有标签。
pdfinfo输出里并排显示。- PDF/A 的 XMP 流(在 PDF/A 中这两个字段都必须非空)。
常见错误:
- ❌ **
creator设成 Chromium / Mozilla 的 User-Agent 字符串。**当 headless 浏览器 PDF 栈自动把 User-Agent 塞进creator时就会出现。那是浏览器版本,不是真理源系统。请覆盖掉。 - ❌ **
producer任其保留为默认渲染器名。**大多数团队从不覆盖,所以每份 PDF 都写着 “Skia/PDF m120” 或 “wkhtmltopdf” —— 关于这件事对 B2B 为什么要紧,参见 白标那篇。 - ❌ **两个字段填一样的值。**可以,但浪费——之所以设两个字段,正是为了让阅读器区分“源应用“和“渲染引擎“。请用上。
经验法则:creator 是你的应用名带版本号(例如 "Acme Billing Platform v7.2");producer 是你应用的品牌或平台名不带版本号(例如 "Acme Billing Platform")。两者都应该是收件方能认得出的值。
空字段、令牌级默认值、下游意外
发上线前,有三个实现细节值得知道:
- **空字符串或仅含空白的字符串等同于未提供。**发
"title": ""和省略title等价——不会把空串写进 PDF,而是走回退链(令牌默认值 → 系统默认值)。这是“我明明设了,却没生效“类报障最常见的原因。 - **令牌策略可以剥离或为元数据字段设默认值。**使用 gPdf 的多租户 SaaS 可以为每个 API 令牌设置
default_metadata,这样该令牌生成的每份 PDF 都会带上客户的author和producer,而无需信任每个开发者在每次请求里都设上。“Acme 的每份 PDF 都必须写 Acme“这种约束,令牌级默认值正是合适的强制点。 - **下游流水线可能重写你的元数据。**gPdf 返回之后再做后处理的工具——没显式开启元数据保留选项的 Ghostscript、某些企业 DRM 工具、某些“PDF 优化器“——会用自己的名字覆盖 Producer,把你刚设好的品牌化元数据一笔抹掉。请对照你真实的生产流水线验证,而不仅仅是看 gPdf 的原始响应。
验证元数据
按上面做完后,有三种快速方式可以检查 PDF 是否真的按你想要的内容交付了:
命令行(macOS / Linux,需要 poppler-utils):
$ pdfinfo your-output.pdf | head -10
Title: Invoice INV-2026-3401
Subject: Monthly invoice — 2026-03
Author: Acme Logistics, Inc.
Creator: Acme Billing Platform v7.2
Producer: Acme Billing Platform
Language: en
Acrobat / Adobe Reader:文件 → 属性 → 说明 标签页。六个字段都在,其中 Title 还会显示在阅读器最上方的标题栏里。
macOS Preview:⌘+I(显示简介)。“PDF” 检查器面板会列出同样的字段。
如果任何字段显示为空、空白、或者出现一个你没设过的工具名,回头排查请求体——最常见的原因是发了 ""(空字符串),API 把它当成“未提供“,走回退链取到了某个默认值。次常见的原因是下游流水线(Ghostscript、DRM、优化器)在 gPdf 返回之后覆盖了字段;请对生产环境测试,而不只是看原始渲染响应。
PDF/A 归档中的元数据
如果你为长期归档而渲染、用 settings.profile: "pdfa-2b"(或 -2a、-3a、-3b),元数据就不再是可选项,而是承重结构:
- 在符合 PDF/A 的文件里,
producer字段不能为空——至少会落到系统默认值。 - 无障碍配置(PDF/A-2a、PDF/A-3a)要求
language。没有的话,veraPDF 校验直接不通过。 - PDF/A 要求的 XMP 元数据流会从上述六个字段自动生成,你无需自己构造。
title、author、subject、creator、producer和language全部会进入 XMP 流,这样下游归档的元数据索引器(Preservica、Archivematica)就可以基于这些字段建目录,无需重新解析文档正文。
对一份归档文档而言,品牌化的元数据不只是品牌打磨,而是这件制品耐久性的一部分。十年后,德国海关、巴西税务局,或任何打开你这份 PDF 的长期归档系统,看到的都将是你渲染那天写进这些字段的内容。在渲染时主动设好,是你唯一一次机会。
gPdf 暂未提供的能力
为了对今天的能力面诚实:PDF 规范还定义了 Keywords(自由形式的搜索关键词),以及一条支持任意自定义键值对的 XMP 元数据流。当前 API 里 gPdf 都没暴露。
如果今天就要往 PDF 内塞任意业务数据(订单 UUID、仓库编码、模板版本),变通办法是:
- 把
subject设成下游系统可解析的结构化短字符串。 - 把业务数据放在自家数据库里,以文件名或内容哈希为键。
- 等一等——XMP 自定义字段已经在路线图上,上线后才是面向隐藏机器可读流程上下文的正解。
把“品牌化元数据“(今天就有的六个标准字段)与“自定义业务元数据“(未来的 XMP 自定义字段)混为一谈,是最容易超额承诺当下能力的方式。在你自己的规划里,值得把这两件事分开。
一个完整示例
一家 SaaS 计费平台(Acme Billing Platform)给一位德国客户(Müller Versand GmbH)开发票,准备以 PDF/A 形式归档:
{
"settings": {
"profile": "pdfa-3b",
"metadata": {
"title": "Rechnung RE-2026-0412",
"language": "de",
"author": "Müller Versand GmbH",
"subject": "Monatsrechnung — März 2026",
"creator": "Acme Billing Platform v7.2",
"producer": "Acme Billing Platform"
}
}
}
对生成的 PDF 跑 pdfinfo:
$ pdfinfo invoice-2026-0412.pdf | head -10
Title: Rechnung RE-2026-0412
Subject: Monatsrechnung — März 2026
Author: Müller Versand GmbH
Creator: Acme Billing Platform v7.2
Producer: Acme Billing Platform
Language: de
title 是德语,author 是 Müller Versand(客户的 GmbH 实体,也是文档收件方),creator 是 Acme Billing Platform(决定页面上写什么的编辑系统),producer 是 Acme Billing Platform 的品牌,language 标得正确——德语屏幕阅读器和稍后在 Müller 的 DMS 里抓到这份文档的德语全文索引器都能识别。PDF/A-3b 配置意味着这套元数据也会被序列化到 XMP 流里以备长期归档。
文件属性里没有任何地方点名 gPdf、Chromium 或任何客户没选的工具——这正是要点所在。
最小可行的升级
如果你已经在 POST 到 /api/v1/pdf/render,而当前调用没有 settings.metadata,最小的改进就是在已经在发的 JSON 里加三行:
{
"pages": [...],
"settings": {
+ "metadata": {
+ "author": "Your customer's organisation",
+ "producer": "Your platform"
+ }
}
}
两个字段,一个新键。几秒钟用 pdfinfo 就能验证。这两个落地后,等有时间再补 title、language、subject 和 creator。
这件事的落点
- §4.14.2 Metadata——这些字段的 API 参考。
- PDF 白标(姊妹篇)——动机和 B2B SaaS 的语境。
- 面向工程师的 PDF/A 和 Factur-X 详解——如果你的元数据故事里包含长期归档,值得一读。