跳转到内容

从语义内容输出带标记的 PDF/UA-2 结构树

本 recipe(示例)会生成一份以 ISO 14289-2(PDF/UA-2)为目标的带标记 PDF。NextPDF 会输出逻辑结构树、标记内容序列、目录语言,以及文件级识别元数据。这套结构支持无障碍内容创作,但符合性由独立检查工具判定。本示例对应 examples/31-pdfua2-tagged.php

Terminal window
composer require nextpdf/core:^3

验证步骤需要在 PATH 中提供 PDF/UA-2 检查工具。本文示例均使用带 ua2 配置文件的 veraPDF。输出带标记的结构不需要 Pro 或 Enterprise 套件。

带标记的 PDF 会在视觉内容流之外,并行携带一棵 逻辑结构树。辅助技术读取的是这棵树,而不是像素布局,因此结构决定了对外暴露的阅读顺序。ISO 14289-2 在这里规定了四项要求。真实(非 artifact)内容必须能通过这棵树访问(§8.2.2)。结构元素必须按已定义的顺序嵌套(§8.2.3)。每个元素都必须 resolve(解析)到已知的结构命名空间,可以直接对应,也可以通过角色映射实现(§8.2.4)。此外,内容的自然语言要在文件级声明,并在语言不同的结构元素上逐一细化(§8.4.4)。

NextPDF 使用带类型的 ConformanceMode 来建模这件事。enableTaggedPdf() 会设置 ConformanceMode::PdfUa2,它会(a)让 HTML 流水线在 parser(解析器)构造时接入 TaggedContentEmitter、(b)设置表示带标记 PDF 的目录 MarkInfoMarked 标志(ISO 32000-2 §14.7),以及(c)为目录 Lang 项目记录 BCP-47 语言。writer 还会输出每页的 Tabs 项目,让 Tab 顺序遵循结构顺序(ISO 32000-2 §14.8)。

严格的 UA-2 不变量只适用于 ConformanceMode::PdfUa2。在任何其他模式下构造严格的 ConformancePolicy,都会按设计抛出 InvalidConfigException

API 接口由 PHPDoc 生成。核心入口点如下:

  • \NextPDF\Core\Document::createStandalone(): Document
  • Document::enableTaggedPdf(string $lang = 'en', ?ConformancePolicy $policy = null): static
  • Document::setLanguage(string $lang): static
  • \NextPDF\Conformance\ConformancePolicy::strictUa2(): self
  • \NextPDF\Conformance\ConformanceMode::PdfUa2(由 enableTaggedPdf() 设定的模式)
  • Document::beginTag(string $type): static / Document::endTag(): static(为非 HTML 内容手动加标记)
examples/31-pdfua2-tagged.php
<?php
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
use NextPDF\Core\Document;
$doc = Document::createStandalone();
// Enable tagged mode BEFORE writeHtml(). The HTML pipeline detects the
// mode at parser construction time and wires the tagged-content emitter.
$doc->enableTaggedPdf(lang: 'en');
$doc->setTitle('Quarterly Accessibility Report');
$doc->setLanguage('en');
$doc->addPage();
$doc->writeHtml(<<<'HTML'
<h1>Quarterly Accessibility Report</h1>
<p>This document opts into tagged PDF so assistive technology can expose
a meaningful reading order.</p>
<ul>
<li>Headings carry semantic roles.</li>
<li>Lists keep their item structure.</li>
</ul>
HTML);
$doc->save(__DIR__ . '/output/31-pdfua2-tagged.pdf');
echo "Created: output/31-pdfua2-tagged.pdf\n";

这是一个自包含、可由测试框架执行的程序。生产环境调用方会在语言标签格式不正确时快速失败,而不是等到外部检查工具运行时才发现。传入 ConformancePolicy::strictUa2(),即可在 API 边界拒绝无效的 BCP-47 标签,再用检查工具的判定为构建把关。

<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Conformance\ConformancePolicy;
use NextPDF\Core\Document;
use NextPDF\Exception\InvalidConfigException;
$out = getenv('NEXTPDF_COOKBOOK_OUTPUT') ?: (__DIR__ . '/accessible.pdf');
try {
$doc = Document::createStandalone();
// Strict UA-2: a malformed BCP 47 tag throws here, not silently at
// write time. strictUa2() also forces the §8.4.4 Lang validation.
$doc->enableTaggedPdf(lang: 'en-GB', policy: ConformancePolicy::strictUa2());
$doc->setTitle('Accessible Annual Report 2026');
$doc->setLanguage('en-GB');
$doc->addPage();
$doc->writeHtml(<<<'HTML'
<h1>Annual Report 2026</h1>
<p>Audited results for the financial year ending March 2026.</p>
<h2>Segment performance</h2>
<table>
<tr><th>Segment</th><th>Revenue</th></tr>
<tr><td>Cloud</td><td>42.1</td></tr>
<tr><td>Services</td><td>18.7</td></tr>
</table>
HTML);
$doc->save($out);
} catch (InvalidConfigException $e) {
fwrite(STDERR, "Tagged PDF/UA-2 setup rejected: {$e->getMessage()}\n");
exit(1);
}
// The gate is the checker, not the library.
$exitCode = 0;
$report = [];
exec('verapdf --flavour ua2 ' . escapeshellarg($out), $report, $exitCode);
if ($exitCode !== 0) {
fwrite(STDERR, "veraPDF FAILED — output is not PDF/UA-2 conforming\n");
fwrite(STDERR, implode("\n", $report) . "\n");
exit(1);
}
echo "veraPDF PASS — accessible.pdf carries a conforming UA-2 structure\n";

verapdf --flavour ua2 报告文件符合要求的主机上,预期 STDOUT 为:

veraPDF PASS — accessible.pdf carries a conforming UA-2 structure

如果 enableTaggedPdf() 拒绝该语言标签,程序会向 STDERR 输出 Tagged PDF/UA-2 setup rejected: …,然后以非零退出码结束。如果检查工具报告问题,程序会输出 veraPDF FAILED — output is not PDF/UA-2 conforming,然后以非零退出码结束。符合性由检查工具判定:NextPDF 只输出结构,并不声明符合性。

  • 调用顺序。 如果在 writeHtml() 之后才调用 enableTaggedPdf(),它不会追溯为已写入内容加标记。请先启用带标记模式。
  • 严格语言把关。 没有策略时,无法解析的 BCP-47 标签会被静默丢弃,只在检查工具运行时才暴露出来。加上 ConformancePolicy::strictUa2() 后,同一个标签会在 enableTaggedPdf() 边界抛出 InvalidConfigException(ISO 14289-2 §8.4.4 严格路径)。
  • 可重复启用。 调用 enableTaggedPdf() 两次会更新语言,但不会重建已经填充的结构树。
  • 手动加标记。 对于非 HTML 内容,请用 beginTag() / endTag() 包住项目。容器角色(TableTRLLI)会变成没有标记内容的分组元素。叶节点角色(PH1H6TD)则会获得 MCID。
  • 模式互斥。 严格的 ConformancePolicy 只在 ConformanceMode::PdfUa2 下才有效。将严格 UA-2 标志和 PDF/A 模式组合在一起会抛出 InvalidConfigException。如果要生成带标记的 PDF/A 交付物,请分别启用带标记模式与 PDF/A 配置文件。

结构树会额外增加一棵由轻量字典组成的并行树,并为每段文字执行 BDC/EMC 运算符。对于典型报告,这部分额外开销只占输出大小的几个百分点,并且稳定落在 2000 ms / 128 MB 预算之内。语义可重现性配置文件在这里适用,因为面向检查工具的交付物使用结构抽象语法树(AST)和元数据进行比对,而不是比对原始字节。请参阅「符合性」一节。

结构树携带的文字与可见内容相同。如果来源 HTML 含有个人数据,这些数据也能通过这棵树以及 ActualText/Alt 属性访问。请在写入前套用与可见内容相同的遮蔽与最小化处理。加标记不会新增任何泄露通道,但它确实会按设计让文字可被程序提取。

本示例只会向 STDOUT 写入一行固定的进度消息。它会把 PDF 输出到测试框架的旁路通道(NEXTPDF_COOKBOOK_OUTPUT),或某个调用方路径。文件文字绝不会写入日志。请不要将可能返回内容片段的检查工具输出写入共享日志。

带标记的 PDF 并不是信任边界。依赖结构树进行自动化处理的消费方仍必须验证该文件,因为恶意生成方可以输出一棵结构上格式正确、却会误导人的树。请把这套结构视为一种无障碍辅助手段,而不是完整性或真实性信号。

本示例不执行任何密码学运算。FIPS 模式不会改变它的行为。不涉及任何签名或加密。

PDF/UA-2 要求NextPDF 输出的内容条款
真实内容位于结构树中StructTreeRoot,搭配各区块的 StructElem 以及通过 MCID 关联的标记内容ISO 14289-2 §8.2.2
已定义的嵌套与阅读顺序区块元素按文件顺序映射到 grouping/leaf 角色ISO 14289-2 §8.2.3
已知的结构命名空间角色位于 PDF 2.0 命名空间;HTML 标签在需要时会做角色映射ISO 14289-2 §8.2.4
文件与元素语言目录 Lang 来自 BCP-47 标签;每个元素的 Lang 会在语言不同时分别带上ISO 14289-2 §8.4.4
非文本内容带有文本替代Alt/ActualText 带在 figure/non-text 结构元素上ISO 14289-2 §8.5.1
表格关系Table/TR/TH/TD 角色,并带表头关联ISO 14289-2 §8.2.5.26
分卷识别元数据文件级识别信息安排在保存时写入ISO 14289-2 §引言(pdfua2#p17

PDF/UA-2 将无障碍要求叠加在 ISO 32000-2 的带标记 PDF 机制之上。NextPDF 依赖的映射如下:

NextPDF 输出ISO 32000-2 §14 机制条款
逻辑结构树(StructTreeRoot带标记 PDF 的逻辑结构§14.7 (iso32000_2_sec14#x1.x38.p13)
目录 MarkInfo << /Marked true >>带标记 PDF 的标记标志§14.7 (iso32000_2_sec14#x1.x40.p3)
每页的 Tabs 项目,遵循结构顺序结构导航/Tab 顺序§14.8 (iso32000_2_sec14#x1.x50)

PDF/UA-2 是 WCAG 2.2 以格式无关方式陈述的结构要求在 PDF 格式中的具体表达。相关对应如下:

WCAG 2.2 成功准则这则范例产生的 PDF/UA-2 机制
1.3.1 信息与关系(Level A)结构树让标题、列表与表格关系可通过程序判定(wcag_2_2#x2.x3.x3.x1.p3)。
1.3.2 有意义的顺序(Level A)结构顺序定义了不受视觉布局影响的阅读顺序。
3.1.1 页面语言(Level A)取自 BCP-47 标签的目录 Lang 项目。
1.1.1 非文本内容(Level A)Alt/ActualText 带在非文本结构元素上(ISO 14289-2 §8.5.1)。

这份映射说明了输出结构在哪些位置支持相应的 WCAG 2.2 准则。它不是一项 WCAG 符合性声明。WCAG 符合性涵盖整体用户体验,由无障碍评估判定,而不是由生成方决定。

陈述规范条款参考 ID
真实内容需要逻辑结构。ISO 14289-2§8.2.2
结构元素遵循已定义的嵌套与阅读顺序。ISO 14289-2§8.2.3
每个结构元素都会解析到一个已知的命名空间,可直接对应,也可通过角色映射。ISO 14289-2§8.2.4
自然语言会在文件级与结构元素级声明。ISO 14289-2§8.4.4
非文本内容带有文本替代。ISO 14289-2§8.5.1
表格单元格带有 row/header/数据关系。ISO 14289-2§8.2.5.26
带标记 PDF 的标记是目录 MarkInfoMarked 标志。ISO 32000-2§14.7
符合性依据该部分标准判定,而非由生成方主张。ISO 14289-2§8.14.2

NextPDF 输出支持无障碍内容创作的带标记结构。支持不等于符合。 本示例并不声明 PDF/UA-2 符合性。这项判定由独立检查工具(例如 veraPDF)完成。在宣称某个文件符合之前,请先执行检查工具。