跳到內容

文字:塑形接縫、CJK 與文字段處理

文字模組是塑形接縫:它以小型介面把 UTF-8 文字段轉換成已定位的字形;主機具備真正的 OpenType 後端時就使用該後端,否則退回確定性的備援路徑;同時也提供註冊表,用來登記特定 script 的塑形器。

Terminal window
composer require nextpdf/core:^3

ShaperInterface 是文字排版管線與 OpenType 塑形引擎之間的接縫。它刻意維持最小化:只有一個 shape() 方法,接收一個 ShaperInput 並回傳一個 ShapingResult。回傳類型是唯一對使用端可見的輸出——實作不得洩漏塑形引擎的內部細節,而具型別的回傳值會在結構上強制這一點。ShapingResult 帶有 GlyphRun 記錄清單、回傳的原始文字、script 與方向,以及一個 shaperImpl 標記,用來指出這個結果是由哪個後端產生。

後端選擇是顯式的,且會如實反映能力。ShaperFactory 會執行一次能力探測:若主機具備可運作的 HarfBuzz 繫結,create() 就回傳以 HarfBuzz 為後端的塑形器;否則回傳 NullShaperNullShaper 是一條直通式的備援路徑。它會為每個 Unicode 碼位輸出一個合成字形,並將寬進量與偏移量都設為零。它會標記結果,讓可觀測性能偵測到這次備援。它把寬進量的 resolve(解析)留給字型度量模組處理。這是一條有明文記載的降級路徑,而非完整塑形:替換、連字、標記定位與情境字形都需要真正的後端才能做到。wouldUseRealShaper() 是診斷用的判斷式。正式環境的程式碼應改為根據結果上的 shaperImpl 標記來分支。

特定 script 的塑形是一個 SPI,而非內建實作。ScriptShaperRegistry 是一個 PSR-11 風格的註冊表,會依 ISO 15924 script 標記解析出 MongolianShaperInterfaceTibetanShaperInterface。註冊表以不分大小寫的方式儲存鍵值,並把 script 代碼的可接受性判斷交由單一可信來源決定。註冊表與 script 塑形器介面構成一份凍結的合約,因此擴充功能可以在不更動呼叫端的情況下,註冊一個 Phase-12 的供應者。引擎本身只提供這道接縫,複雜 script 的供應者則由使用端自行提供。

CJK 文字段處理位於排版流程的編碼接縫上。嵌入的 CJK TrueType 字面會以 Type 0 字型輸出,搭配 Identity-H CMap 與一個 CIDFontType2 後代字型——ISO 32000-2 §9.7.4(RAG 摘要因授權上限被截斷;記錄於 _downgraded-claims-o3.md)。當 TrueType 程式被嵌入時,Type 2 CIDFont 會透過 CIDToGIDMap 項目,把字元識別碼對映到字形的 Index(索引)——ISO 32000-2 §9(摘要由 B1 合約頁面釘選)。子集化工具會精確保留原始的字形編號,因此 /CIDToGIDMap /Identity 對子集而言仍然有效。CjkFontValidator 是診斷工具,會在選用某個候選字型之前,檢查它是否涵蓋某個 script 所需的 Unicode 區塊。

類型種類主要成員穩定性起始版本
ShaperInterface介面(interface)shape(ShaperInput): ShapingResult穩定3.2.0
ShaperFactoryfinal classdefault()create()wouldUseRealShaper()穩定3.2.0
NullShaperfinal readonly class直通式的備援塑形器穩定3.2.0
ShapingResultfinal readonly class$glyphRuns$originalText$script$direction$shaperImpl穩定3.2.0
ScriptShaperRegistryfinal classregisterMongolian()getMongolian()hasMongolian(),以及對應的 Tibetan 版本穩定3.1.0
CjkFontValidatorfinal classvalidateCoverage()detectScript()isCjkCodepoint()穩定1.0.0

無論是 register*get*has* 形式,或是 ScriptShaperRegistry 與各 script 塑形器介面,都構成一份凍結的合約。依設計,ShapingResult 是塑形器唯一對使用端可見的輸出。

examples/text/shaper-factory.php
<?php
declare(strict_types=1);
require_once __DIR__ . '/../../vendor/autoload.php';
use NextPDF\Font\Shaper\ShaperFactory;
use NextPDF\Font\Shaper\ShaperImpl;
$factory = ShaperFactory::default();
$shaper = $factory->create();
// Branch on the result tag, not on the concrete class.
$wouldShape = $factory->wouldUseRealShaper()
? 'HarfBuzz backend available'
: 'NullShaper fallback (degraded — no substitution or positioning)';
echo $wouldShape, "\n";

ShaperFactory::default() 會接上正式環境的能力探測。create() 會在工廠的生命週期內記憶化所選的後端。能力狀態的真實答案,來自 wouldUseRealShaper() 以及每個結果上的 shaperImpl 標記。

examples/text/script-shaper-registry.php
<?php
declare(strict_types=1);
require_once __DIR__ . '/../../vendor/autoload.php';
use NextPDF\Text\Shaping\MongolianShaperInterface;
use NextPDF\Text\Shaping\ScriptShaperRegistry;
final readonly class ComplexScriptBootstrap
{
public function __construct(private ScriptShaperRegistry $registry) {}
/**
* Register a consumer-supplied Mongolian shaper provider at boot so
* the layout pipeline can resolve it by ISO 15924 script tag.
*/
public function register(MongolianShaperInterface $mongolian): void
{
$this->registry->registerMongolian($mongolian);
}
public function hasMongolian(): bool
{
return $this->registry->hasMongolian();
}
}

註冊表是複雜 script 供應者的整合點。引擎提供這道接縫以及凍結的存取器形式。Mongolian 與 Tibetan 的實作則由使用端提供。

  • NullShaper 的結果,其寬進量為零、偏移量為零。不要直接把這些位置餵給文字排版——請改從字型度量模組解析寬進量,並透過 shaperImpl 標記偵測這次備援。
  • 空輸入會產生一個空的 glyphRuns 清單,而非一個空的文字段。使用端的迭代程式碼不需要特別處理長度為零的文字段。
  • ScriptShaperRegistry 並未直接實作 Psr\Container\ContainerInterface,因此具型別的存取器才能在靜態分析下保有其收窄後的回傳類型。請使用 getMongolian()getTibetan(),而非泛用的 get()
  • script 標記會以正規的 ISO 15924 alpha-4 值來比對,且以不分大小寫的方式儲存。傳入 MongTibt。查找時大小寫無關緊要。
  • CJK 擴充 B 區字元位於 Unicode 第 2 平面,會迫使子集中產生一個 cmap Format 12 子表。編碼路徑會處理這種情況。不要假設 CJK 的全部內容都落在基本多文種平面內。

能力探測在每個 ShaperFactory 實例上只執行一次,且後端會被記憶化,因此重複呼叫 create() 幾乎不耗成本。NullShaper 的成本與輸入文字段的碼位數呈線性關係,且不涉及 I/O。ScriptShaperRegistry 的解析是一次常數時間的鍵值查找。CjkFontValidator 會以固定間隔抽樣碼位,而非逐一測試每一個,因此即使面對 20,000 個字形的 CJK 字型,涵蓋率檢查仍然便宜。1500 毫秒掛鐘時間與 64 MB 尖峰用量的 performance_budget 足以涵蓋一次典型的執行。真正塑形時的主要成本來自 OpenType 後端本身;當備援啟用時,這部分不屬於本流程的成本範圍。

塑形接縫接收一個 UTF-8 字串。NullShaper 會以盡力切分的方式容忍格式錯誤的 UTF-8,而不會拋出例外,因為這個備援路徑既有的合約本來就是「不做真正的塑形」。呼叫端已預期會得到品質較低的輸出。位元組偏移的群集合約採用以位元組為導向的長度,這對多位元組輸入而言是正確的,也避免了偏差一個碼位的群集對映缺陷。真正的後端(若存在)是第三方原生函式庫。請把它的輸入視為不可信,並在上游限制文字段的長度。script 塑形器註冊表儲存的是使用端提供的供應者——這些實作的信任邊界屬於使用端,而非引擎。

宣稱標準條款證據
嵌入的 CJK TrueType 字面會以 Type 0 字型輸出,搭配 Identity-H CMap 與一個 CIDFontType2 後代字型。ISO 32000-2§9.7.4摘要因授權上限而被截斷,此處僅保留前綴 7a5258772f508e3b,詳見 _downgraded-claims-o3.md
嵌入的 Type 2 CIDFont 會透過 CIDToGIDMap 把字元識別碼對映到字形索引。ISO 32000-2§9

兩項條款皆已改寫。第二項以摘要釘選(沿用自 B1 合約頁面),第一項則由 ADR-013 與 cmap 編碼器的開發者總覽佐證。NextPDF 不重製規範性文字。塑形後端與 PDF 符合性無關。此處的符合性宣稱針對的是編碼接縫所產生的 CJK 字型字典輸出,在 ADR-013 與 cmap 編碼器的開發者總覽中有進一步說明。

一套進階的文字前處理管線與擷取服務,建立在 Core 的塑形接縫與文字段處理值類型之上。Core 文字模組會提供這道接縫、備援路徑與 script 塑形器註冊表,且不需要授權。省略轉換連結是刻意的。