跳转到内容

以 cmap 感知编码处理 CJK 文本

这则 recipe(范例)会注册一个 CJK TrueType 字体,再通过具备 cmap 感知能力的 FontInfo::encodeText() 门面对繁体中文文本进行编码。这个门面会生成 Identity-H 的双字节 CID 字节流。此范例遵循 examples/35-cjk-cmap-demo.php。在你依赖它之前,请先阅读下方的范围说明。

这套具备 cmap 感知能力的文本编码架构分阶段交付(ADR-013)。第 1 阶段已落地FontInfo::encodeText() 门面与具备 cmap 感知能力的编码策略都已完成接入,且可从用户侧访问。第 2 阶段进行中:它会让 renderer(渲染器)与 writer 都经过这个门面。第 3 与第 4 阶段待完成:每个字体各自的 /ToUnicode/CIDSystemInfo/Encoding/CIDToGIDMap 的输出,以及替代字体的 resolver(解析器),都尚未接入 writer。

请依下列后果规划:

  • 这则范例展示的是编码门面,而不是开箱即用的竖排模式。文档接口现阶段没有公开的书写模式 API,也就是说没有 setWritingMode,也没有 vertical-rl 设置器。
  • 作为基础的范例,根据其自身标头,它是一个集成冒烟测试,而不是符合性测试夹具。在第 3 与第 4 阶段尚未落地之前,通过这条路径生成的输出在 PDF/UA-2 与 PDF/A-4 验证中会出现回归。请勿宣称这条路径的输出符合规范。是否符合规范由检查器决定,而它目前还不会让这份输出通过。
  • 竖排度量基础设施已存在,但属于内部功能。它由 CjkVerticalMetrics 值对象,以及 /W2/DW2 输出器组成。NextPDF 并未将其公开为用户侧「竖排书写」调用,writer 也尚未输出相应字典。
Terminal window
composer require nextpdf/core:^3

此版本约束对应 nextpdf/core 套件。此范例在 PHP 8.4 上运行。随附的 Noto Sans TC 测试夹具让这则范例可以自包含。

ISO 32000-2 以三层建模文本输出:Unicode 码点、字符码,以及字符 ID。对于 CJK TrueType 字体,引擎会使用搭配 Identity-H 编码的复合 Type 0 字体。使用这种编码时,显示字符串由一组组字节对组成,用作 CIDFont 的 Index(索引)(ISO 32000-2)。

FontRegistry::register() 会解析该字体。FontInfo::encodeText($unicodeText) 接着会通过 FontEncodingStrategyResolver 解析出一个编码策略。对于已注册的 TrueType CJK 字体,它会分派到 TrueTypeCmapStrategy。返回的 EncodedGlyphRun 会带有 Identity-H 字节流、PDF 字符串运算元、每个字符的前进宽度、使用到的码点,以及 GID→Unicode 对应表。CJK 子集化会依 ADR-008 使用这些码点。未来的 /ToUnicode 流会使用这份 GID→Unicode 对应表。所选择的模式是 EncodingMode::TwoByteCid

在 PDF 中,两个 CIDFont 结构用于定义竖排书写。第一个是 /W2,表示每个字符的竖排度量数组(ISO 32000-2)。第二个是 /DW2,表示默认竖排度量(ISO 32000-2)。NextPDF 已为二者提供值对象与输出器,分别通过 CjkVerticalMetrics::toW2Array()toW2RangeArray()toDw2Array()。它们属于内部功能,writer 尚未输出这些结构。请参阅范围说明。

  • FontRegistry::register(string $fontFile, string $alias = '', int $fontIndex = 0): FontInfoNextPDF\Typography\FontRegistry
  • FontInfo::encodeText(string $unicodeText): EncodedGlyphRunNextPDF\Typography\FontInfo。第 1 阶段的门面。
  • EncodedGlyphRunNextPDF\Typography\Encoding\EncodedGlyphRunbyteStreampdfStringOperandmodeadvanceWidthstoUnicodeMapusedCodepointsglyphCount())。
  • EncodingModeNextPDF\Typography\Encoding\EncodingModeSingleByteTwoByteCid)。
  • CjkVerticalMetricsNextPDF\Typography\CjkVerticalMetrics。内部的竖排度量值对象。这里列出它是为了保持透明,而不是表示它可作为用户侧的书写路径。

完整的 PHPDoc 表格由源代码生成。

<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Typography\Encoding\EncodingMode;
use NextPDF\Typography\FontRegistry;
$registry = new FontRegistry();
$font = $registry->register('/path/to/NotoSansTC-Regular.ttf', alias: 'NotoSansTC');
$encoded = $font->encodeText('PDF 2.0 引擎');
assert($encoded->mode === EncodingMode::TwoByteCid); // cmap-aware branch fired
echo $encoded->glyphCount() . " glyph run entries\n";

这个范例是自包含的,且能在测试载体中运行。它对应 examples/35-cjk-cmap-demo.php。先注册随附的 Noto Sans TC 夹具。接着确认具备 cmap 感知能力的门面可以访问。然后通过 DocumentFactory 绘制,让你配置好的 registry 生效。

<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\DocumentFactory;
use NextPDF\Graphics\ImageRegistry;
use NextPDF\Typography\Encoding\EncodingMode;
use NextPDF\Typography\FontRegistry;
$cjkFontPath = dirname(__DIR__, 2)
. '/fonts/test-fixtures/Noto Sans TC/NotoSansTC-Regular.ttf';
if (!is_file($cjkFontPath)) {
fwrite(STDERR, "Missing CJK font fixture: {$cjkFontPath}\n");
exit(1);
}
$fontRegistry = new FontRegistry();
$cjkFont = $fontRegistry->register($cjkFontPath, alias: 'NotoSansTC');
// Phase 1 facade: prove the cmap-aware path is reachable from userland.
$cjkSample = 'PDF 2.0 引擎 — 使用 CMap 编码';
$encoded = $cjkFont->encodeText($cjkSample);
if ($encoded->mode !== EncodingMode::TwoByteCid) {
fwrite(STDERR, "Expected TwoByteCid (TrueTypeCmapStrategy branch)\n");
exit(2);
}
$imageRegistry = new ImageRegistry(maxCacheBytes: 0);
$documentFactory = new DocumentFactory($fontRegistry, $imageRegistry);
$doc = $documentFactory->create();
$doc->setTitle('NextPDF CJK CMap-Aware Encoding Demo');
$doc->setLanguage('zh-Hant');
$doc->addPage();
$doc->setFont('helvetica', 'B', 16);
$doc->cell(0, 12, 'CJK cmap-aware encoding (Phase 1 facade)', newLine: true);
$doc->setFont('helvetica', '', 10);
$doc->cell(0, 6, 'Mode: ' . $encoded->mode->name . ' (Identity-H, 2-byte CIDs)', newLine: true);
$doc->cell(0, 6, 'Glyphs: ' . $encoded->glyphCount() . ' run entries', newLine: true);
$doc->cell(0, 6, 'Bytes: ' . strlen($encoded->byteStream) . ' encoded bytes', newLine: true);
$doc->ln(4);
$doc->setFont('NotoSansTC', '', 18);
$doc->cell(0, 12, $cjkSample, newLine: true);
$out = getenv('NEXTPDF_COOKBOOK_OUTPUT');
$doc->save($out !== false ? $out : __DIR__ . '/cjk-vertical-writing.pdf');
echo "Wrote cjk-vertical-writing.pdf (Phase 1+2 dry-run; not a conformance fixture)\n";

预期的 STDOUT:

Wrote cjk-vertical-writing.pdf (Phase 1+2 dry-run; not a conformance fixture)
  • 不是符合性测试夹具。 根据基础范例自身标头,这份输出是一个集成冒烟测试。在第 3 与第 4 阶段尚未落地之前,PDF/UA-2 与 PDF/A-4 检查对它会出现回归。请勿把它登记为符合性黄金样本。
  • 没有书写模式 API。 没有任何公开调用可以切换到竖排书写,因此也无法覆盖 vertical-rlvertical-lr/W2/DW2 输出器已在内部存在。它们尚未对外公开,也尚未写入字体字典。
  • registry 的归属。 Document::createStandalone() 会创建自己的 registry。请改用 DocumentFactory,让文档读取你填入 CJK 字体的那个 registry。
  • 最终字节流路径。 在第 2 阶段完成之前,可见内容流仍会沿用旧的文本路径。现阶段已经确认、可访问的部分是上游编码步骤,也就是 cmap 正向查找加上 Identity-H 字节流。
  • CJK 子集化成本。 大型 CJK 字体会通过一个隔离的子进程进行子集化。该子进程备有 PHP 原生的回退机制与两秒的超时(ADR-008)。

encodeText() 会对输入执行一次 cmap 正向查找扫描。它相对于码点数量呈线性,即 O(n)。预算为 wall_ms: 2000, peak_mb: 128。这个预算是这组里最高的,因为 CJK 字体很大,对它们进行子集化是主要成本。ADR-008 会隔离这项工作,避免阻塞调用端。

CJK 字体文件是不受信任的二进制输入。解析器会拒绝流包装器路径与 null 字节。CJK 子集化会在一个不继承状态的隔离子进程中执行(ADR-008)。对于终端用户提供的字体,请验证字体来源。CJK 文本内容会被绘制,而不是被解读。

陈述规范条款参考 ID
对于 Identity-H/Identity-V 的 Type 0 字体,显示字符串是用于索引 CIDFont 的字节对。ISO 32000-2iso32000_2_sec9#x1.x49.p90(第 9 节)
W2 数组提供每个字符的竖排度量,且仅适用于竖排书写所使用的 CIDFont。ISO 32000-2iso32000_2_sec9#x1.x44.p23(第 9 节)
DW2 数组提供 CIDFont 的默认竖排度量。ISO 32000-2iso32000_2_sec9#x1.x44.p22(第 9 节)

这则范例表明具备 cmap 感知能力的 CJK 编码门面可从用户侧访问(第 1 阶段)。它并未宣称所生成的文档具备竖排输出,或符合 PDF/UA-2 / PDF/A-4 规范。writer 端的 /ToUnicode 与竖排度量输出(第 3 与第 4 阶段)仍待完成,目前检查器也不会让这份输出通过。

不适用。