跳转到内容

无障碍:标记原语与 PDF/UA-2 结构模型

NextPDF Core 提供用于无障碍创作的原语:逻辑结构树、标准角色映射、标记内容标记,以及与 ISO 14289-2 (PDF/UA-2) 和 ISO 32000-2 §14.7 所定义结构树模型一致的 BCP-47 语言属性。所生成文件的合规性取决于最终文档、作者的内容选择以及外部检查器。这并不是本库替你作出的保证。

Terminal window
composer require nextpdf/core

带标记的 PDF 包含一棵逻辑结构树,其根节点包含一个 Document 结构元素。辅助技术读取该树,从而推导出不依赖视觉布局的有意义阅读顺序 (ISO 32000-2 §14.7.2;ISO 14289-2 §8.2.5.2)。NextPDF 在 NextPDF\Accessibility 命名空间中通过三个相互协作的类型对此建模。

StructureTree 负责维护这一层级结构。它按页分配标记内容标识符,跟踪父子嵌套关系,并按 ISO 32000-2 §14.7 序列化结构树根、结构元素、父树、角色映射以及 PDF 2.0 标准结构命名空间。createRoot() 创建带有语言属性、必需且唯一的 Document 元素。addElement() 挂接带类型的子元素。hasRoot()rootHasChildren() 报告该树是否存在以及是否有后代。

StructureElement 是单个结构元素字典的值对象。它记录标准结构类型(表 368 中的名称,如 H1H6PLLITableFigureLink)、标记内容标识符条目,以及用于替代文本、替换文本、标题和语言的可选无障碍属性。单个元素可能跨越多个页面,每页累积一个标识符条目,使 kids 数组能够跨页边界引用标记内容。

TaggedContentEmitter 在 HTML 管线与结构树之间充当桥梁。当 Document::enableTaggedPdf() 启用时,HTML renderer(渲染器)会接入发射器,为块级元素生成成对的标记内容运算符以及相应的结构元素节点。HtmlToStructureMap 提供从 HTML 标签到 PDF 结构类型的表驱动映射 (ISO 14289-2 §8)。发射器会将页眉、页脚等装饰性连续内容(如 HTML 页眉和页脚区域)路由到工件 (artifact),从而将其排除在阅读顺序之外。

语言标记由 Bcp47Validator (RFC 5646) 验证。它提供格式良好的语法检查以及基于注册表的有效性检查。严格模式 (ConformancePolicy::strictUa2()) 会在 API 边界拒绝格式错误的标记,而不是在写入时静默丢弃它们。这符合 ISO 14289-2 §8.4.4 中目录语言条目须解析为特定语言的要求。

符号类别摘要
Document::enableTaggedPdf(string $lang = 'en', ?ConformancePolicy $policy = null): static方法启用结构树和 HTML 桥接;设置 mark-info 和目录语言条目。
Document::setLanguage(string $lang): static方法设置文档级自然语言 (BCP-47)。
Document::isTaggedPdfEnabled(): bool方法检查当前活动的合规模式是否强制要求结构标记。
StructureTree::createRoot(string $lang = 'en'): int方法创建强制性的单个 Document 根元素。
StructureTree::addElement(int $parentIndex, string $type, int $pageIndex, ...): int方法挂接一个带类型的子结构元素。
StructureTree::hasRoot(): boolrootHasChildren(): bool方法检查该树是否存在以及是否有后代。
StructureElementfinal 类单个结构元素的值对象(替代文本、替换文本、标题、语言、标识符)。
RoleMap::standard(): array<string,string>静态标准结构类型词汇表 (ISO 32000-2 表 368 加上 PDF 2.0 类型)。
Bcp47Validator::isWellFormed/isValid/validate/normalise方法RFC 5646 语法及基于注册表的语言标记验证。
AccessibilityAutoFixerRegistryfinal 类面向启发式结构修复器、可选用的 PSR-11 风格注册表。
<?php
declare(strict_types=1);
use NextPDF\Core\Document;
$doc = Document::createStandalone();
// The BCP 47 tag drives the catalog language entry and the
// structure-tree root language attribute.
$doc->enableTaggedPdf(lang: 'en');
$doc->setTitle('Tagged accessibility demo');
$doc->addPage();
// Semantic HTML maps to structure elements: h1 to /H1, p to /P,
// ul and li to /L plus /LI. Text runs are wrapped in
// marked-content operators with stable identifiers.
$doc->writeHtml('<h1>Document title</h1><p>Body paragraph.</p>');
$doc->save(__DIR__ . '/output/tagged.pdf');
<?php
declare(strict_types=1);
use NextPDF\Conformance\ConformancePolicy;
use NextPDF\Core\Document;
use NextPDF\Exception\InvalidConfigException;
use Psr\Log\LoggerInterface;
final class AccessibleReportWriter
{
public function __construct(private readonly LoggerInterface $logger)
{
}
public function render(string $html, string $bcp47Lang, string $outPath): void
{
$doc = Document::createStandalone();
try {
// strictUa2() rejects malformed BCP 47 tags at the API
// boundary (ISO 14289-2 §8.4.4) instead of dropping silently.
$doc->enableTaggedPdf($bcp47Lang, ConformancePolicy::strictUa2());
} catch (InvalidConfigException $e) {
$this->logger->error('Rejected language tag for tagged PDF', [
'lang' => $bcp47Lang,
'reason' => $e->getMessage(),
]);
throw $e;
}
$doc->setTitle('Quarterly accessibility report')
->setLanguage($bcp47Lang)
->addPage();
$doc->writeHtml($html);
// The engine emits a Degraded / ComplianceRisk advisory directing
// the caller to validate externally; surface it to operators
// rather than treating tagged output as certified.
foreach ($doc->getWarnings() as $warning) {
$this->logger->warning('Tagged-PDF advisory', [
'code' => $warning->code->value,
'message' => $warning->message,
]);
}
$doc->save($outPath);
}
}
  • 调用顺序。 先调用 enableTaggedPdf(),再调用 writeHtml()。HTML 管线在解析器构造时检查合规模式,不会对已渲染的内容回溯接入发射器。
  • 空结构树。 调用了 enableTaggedPdf() 但未挂接任何结构后代的文档,不会在其元数据中声明 PDF/UA-2。发布门控条件是 rootHasChildren(),而不是 hasRoot(),因为以空结构树声称符合 PDF/UA-2 的文件会被验证器拒绝 (ISO 14289-2 §5;由 EmptyTaggedPdfDoesNotAdvertisePdfUa2Test 验证)。
  • 合规模式坍缩。 在同一文档上同时调用 enablePdfA()enableTaggedPdf() 会使单值合规判别符坍缩为以后一次调用为准。副作用(结构树、mark-info)仍会累加,并会发出 CONFORMANCE_MODE_CLOBBERED 警告,使该坍缩可被观察到。
  • 自动修复器并非自动启用。 内置修复器 (EmptyTagStripperLegacyLangNormaliserRootLangFallback) 随 NextPDF\Accessibility\AutoFixer\* 一同提供,但绝不会自动注册。使用方必须在 AccessibilityAutoFixerRegistry 上显式注册它们。

NextPDF 会发出与 PDF/UA-2 结构树模型一致的结构,但不会自动创建它无法推断的语义。以下内容需要作者提供标记或属性,NextPDF 不会为你自动生成:

  • 图像及其他非文本内容的替代文本;
  • 超出 HTML 标记所表达范围的表头作用域,以及表头到单元格的关联;
  • 当可见链接文本不能自描述时的链接用途文本;
  • 在视觉上以列表形式排布但未使用列表标记的内容的列表语义;
  • 当源顺序与预期阅读顺序不一致时,经过校正的阅读顺序;
  • 对含义不明的内容判断其属于装饰性内容还是有意义内容。

本库不执行端到端的 PDF/UA-2 验证。运行时会发出 Degraded / ComplianceRisk 通告 (PDFUA2_FOUNDATIONAL),提示调用方使用外部检查器验证输出,以用于生产环境签核。请使用 PDF/UA 检查器(例如 veraPDF)进行验证。NextPDF 不会代你声明合规性。最终文档的合规性取决于创作选择以及验证器,而非取决于是否调用该 API。

结构树构建的复杂度与结构元素数量呈线性关系。每个标记内容序列的标识符分配为均摊常数时间。序列化是对元素集合的单次线性遍历。HTML 驱动标记的主要开销在于 HTML 管线本身,而非标记发射。performance_budget 中声明的每个配方上限(1500 ms 实际耗时、64 MB 峰值)适用于典型的多页语义文档。大型文档的扩展与元素数量呈线性关系,而不是与页数呈线性关系。

语言标记和无障碍属性会流入 PDF 名称对象和字符串对象。NextPDF 通过 PdfStringEscaper 对它们进行转义,防止格式错误或恶意的语言、替代文本、替换文本和标题值突破其 PDF 对象上下文。严格模式还会在 API 边界拒绝未注册的 BCP-47 标记,在其到达写入器之前收窄输入面。无障碍属性可能携带作者提供的自由文本。请将它们视为不可信输出,并像审查其他文档内容一样进行审查。有关配置文件检查器的行为,请参阅合规模块

本页将库行为映射到条款标识符。它并不声明你的输出符合规范。所引用的条款均为转述,绝不直接引用原文。有关条款级表格和明确列出的未覆盖范围,请参阅PDF/UA-2 规范映射。引用块哈希记录在 docs/public/modules/core/_normative-evidence-a11y.md 中。