使用 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 也尚未輸出其字典。
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 尚未輸出它們。請參閱範圍說明。
API 介面
標題為「API 介面」的區段FontRegistry::register(string $fontFile, string $alias = '', int $fontIndex = 0): FontInfo—NextPDF\Typography\FontRegistry。FontInfo::encodeText(string $unicodeText): EncodedGlyphRun—NextPDF\Typography\FontInfo。第 1 階段的門面。EncodedGlyphRun—NextPDF\Typography\Encoding\EncodedGlyphRun(byteStream、pdfStringOperand、mode、advanceWidths、toUnicodeMap、usedCodepoints、glyphCount())。EncodingMode—NextPDF\Typography\Encoding\EncodingMode(SingleByte、TwoByteCid)。CjkVerticalMetrics—NextPDF\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 firedecho $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-rl與vertical-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-2 | iso32000_2_sec9#x1.x49.p90(第 9 節) | |
| W2 陣列提供每個字元的直書度量,且僅適用於直書使用的 CIDFont。 | ISO 32000-2 | iso32000_2_sec9#x1.x44.p23(第 9 節) | |
| DW2 陣列提供 CIDFont 的預設直書度量。 | ISO 32000-2 | iso32000_2_sec9#x1.x44.p22(第 9 節) |
這則範例顯示具 cmap 感知的 CJK 編碼門面可從使用者端觸及(第 1 階段)。它並未宣稱所產生的檔案具有直書輸出,或符合 PDF/UA-2 / PDF/A-4 規範。writer 端的 /ToUnicode 與直書度量輸出(第 3 與第 4 階段)仍待完成,而檢查器目前還不會讓這份輸出通過。
商業脈絡
標題為「商業脈絡」的區段不適用。