跳到內容

字型排印:字型登錄表、子集化、CMap、編碼、BiDi

字型排印模組會將字型檔與 Unicode 字串轉換成 PDF 內容串流所需的 bytes。它負責 resolve(解析)字型、維護行程生命週期內的登錄表、執行字元子集化、產生 ToUnicode CMap、套用 cmap 感知的編碼策略,以及提供 Unicode 雙向文字引擎。

Terminal window
composer require nextpdf/core:^3

FontRegistry 是行程生命週期內的字型儲存區,並實作 FontRegistryInterface。它只解析一次 TrueType、OpenType、TTC 或 Type 1(PFB 與 AFM)檔案,並回傳不可變的 FontInfo。這個登錄表專為長時間執行的 worker 設計:開機時預熱一組字型,接著呼叫 lock()。之後登錄表會拒絕所有變動,但查詢仍會持續服務流量。它只持有純 PHP 資料 —— 解析後的中繼資料與原始字型 bytes —— 因此 worker 池可以共用同一個實例。registerFromBinary() 接受原始字型 bytes,HTML 的 @font-face 橋接層會使用它,處理從遠端來源或 data URI 取得的字型。

引擎會嵌入並子集化所使用的每一個字型。嵌入的字型程式會隨 PDF 一併攜帶,因此文件在任何檢視器上都能呈現一致樣貌,不受已安裝的系統字型影響 —— ISO 32000-2 §9。子集只攜帶文件實際參照的字元,這對 CJK 或 Unicode 密集內容至關重要 —— ISO 32000-2 §9。FontSubsetter 會解析原始表目錄、取出 cmap、以遞移閉包解析複合字元的相依關係,並重建 headhheamaxpcmaplocaglyfhmtx 這些表。它會保留原始字元識別碼的編號,並把未使用的槽位補零,因此 CIDToGIDMap/Identity 時仍保持有效。當子集化節省的空間少於百分之十時,它會原封不動回傳原始字型,避免做不划算的工作。CffSubsetter 會對攜帶 Compact Font Format 輪廓表的 OpenType 字型執行等效操作。

文字輸出會經過三階段轉換:先是 Unicode 字碼,接著是內容串流中的字元碼,最後是字型內部的字元識別碼。本模組將這段過程建模為明確的協作者。FontInfo::encodeText() 是門面;FontEncodingStrategyResolver 則依字型分派。帶有 Unicode cmap 的嵌入式 TrueType 或 OpenType 字型會路由到 TrueTypeCmapStrategy,由它輸出雙位元組的 Identity-H 十六進位串流。這正是帶有 Identity-H CMap 與 CIDFontType2 後代的 Type 0 字型所需的形式(ISO 32000-2 §9.7.4;對應的 RAG 區塊摘要因授權上限而被截斷回傳,記錄於 _downgraded-claims-o3.md)。其他每一種字型 —— Base 14 標準字型、Type 1 PFB 與 AFM —— 都會路由到 Base14EncodingStrategy,由它輸出單位元組的 WinAnsi 字面字串。該串流涵蓋完整的 WinAnsiEncoding(Windows code page 1252)字集——含重音符號的拉丁字元、歐元符號,以及常見的排印標點。落在它之外的字碼會被從單位元組串流中捨棄,並在註冊了涵蓋該字碼的字型時,採用逐叢集的字型後備(ISO 32000-2 Annex D.2)。解析器在整個 FontInfo 值空間上皆有定義,沒有可為 null 的路徑。ToUnicodeCMapBuilder 會建立 /ToUnicode 資源,讓閱讀器能從 Identity-H 字型還原出原始的 Unicode。它會套用貪婪的 bfrange 合併,並設有每區塊 100 筆的上限。

BidiEngine 是 Unicode 雙向演算法(UAX #9,Unicode 16)的邊界服務。隔離支援關閉時,它會委派給舊版解析器,因此現有呼叫端不受影響。隔離支援開啟時,它會執行隔離感知的管線:最大深度為 125 的明確隔離堆疊、弱型別處理階段、含成對括號解析的中性型別處理階段,以及隱含層級與行重排處理階段。候選字型的 CJK 字元涵蓋率是另一項獨立診斷:CjkFontValidator 會依文字系統取樣所需的 Unicode 區塊,並回報涵蓋率百分比。

型別種類主要成員穩定性起始版本
FontRegistryfinal classregister(), registerType1(), registerFromBinary(), registerFromDirectory(), get(), has(), all(), warmup(), lock(), isLocked(), memoryUsage()穩定1.7.0
FontInfofinal readonly class$family, $type, $widths, $unicodeMap, $cmapForward, getKey(), encodeText()穩定1.0.0
FontSubsetterfinal classsubset(string, array<int>, int): string穩定1.0.0
CffSubsetterfinal classOpenType/CFF 輪廓子集化穩定1.0.0
FontEncodingStrategyResolverfinal classresolve(FontInfo): FontEncodingStrategy穩定2.7.0
ToUnicodeCMapBuilderfinal classbuildFromRun(), buildFromMap(), encodeUnicodeUtf16Be()穩定2.7.0
BidiEnginefinal classUAX #9 隔離感知解析穩定3.1.0
CjkFontValidatorfinal classvalidateCoverage(), detectScript(), isCjkCodepoint()穩定1.0.0

FontInfo 是不可變的:它的建構式簽章與公開屬性都已凍結。編碼策略是 (FontInfo, UTF-8 text) 的純函式 —— 每次呼叫,相同輸入都會得到相同的 EncodedGlyphRun

examples/35-cjk-cmap-demo.php
<?php
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
use NextPDF\Typography\Encoding\EncodingMode;
use NextPDF\Typography\FontRegistry;
$registry = new FontRegistry();
$cjkFont = $registry->register('/path/to/NotoSansTC-Regular.ttf', alias: 'NotoSansTC');
$encoded = $cjkFont->encodeText('PDF 2.0 引擎 — 使用 CMap 編碼');
// An embedded CJK TrueType face resolves to the two-byte Identity-H path.
assert($encoded->mode === EncodingMode::TwoByteCid);

register() 只會解析字型一次,並回傳不可變的 FontInfoencodeText() 會經由解析器路由,並回傳一個 EncodedGlyphRun,其中包含 byte 串流、PDF 字串運算元、各字元的前進寬度,以及 /ToUnicode CMap 會用到的 GID 到 Unicode 對映。

examples/typography/registry-warmup.php
<?php
declare(strict_types=1);
require_once __DIR__ . '/../../vendor/autoload.php';
use NextPDF\Exception\NextPdfException;
use NextPDF\Typography\FontRegistry;
use Psr\Log\LoggerInterface;
final readonly class FontBootstrap
{
public function __construct(
private FontRegistry $registry,
private LoggerInterface $logger,
) {}
/**
* Warm a font set at worker boot, then lock the registry for the
* lifetime of the process.
*
* @param list<string> $fontFiles Absolute paths to font files.
*/
public function boot(array $fontFiles): void
{
try {
$this->registry->warmup($fontFiles);
$this->registry->lock();
} catch (NextPdfException $e) {
$this->logger->error('Font warmup failed', ['error' => $e->getMessage()]);
throw $e;
}
$report = $this->registry->memoryUsage();
$this->logger->info('Font cache primed', [
'fonts' => $report->entryCount,
'bytes' => $report->currentBytes,
]);
}
}

warmup() 接著 lock() 就是 worker 的開機程序。在 lock() 之後,任何變動都會擲出例外;查詢則持續服務流量。memoryUsage() 會回傳一個 MemoryReport,讓 worker 能對照預算追蹤字型快取。

  • 已鎖定的登錄表會拒絕 register()registerFromBinary()addFontDirectory()warmup()。請在開機時預熱並鎖定;絕不要在處理請求的過程中註冊字型。
  • FontSubsetter::subset() 會在節省的空間低於百分之十,或缺少某個必要的表時,原封不動回傳原始 bytes。回傳的字型與輸入相同,這是文件中載明的無增益路徑,並非失敗。
  • 子集化器會保留原始字元識別碼的編號,並把未使用的槽位補零。這讓 CIDToGIDMap /Identity 保持有效;不要假設字元識別碼會被重新編號為連續區間。
  • registerFromBinary() 會把這些 bytes 寫入暫存檔以進行解析,並會刪除副檔名檔案與 tempnam() 基底檔案;這個刪除動作放在 finally 區塊中執行。不受信任的字型資料是一個解析攻擊面 —— 在它抵達解析器之前先加以把關(見「安全性注意事項」)。
  • BidiEngine 會在隔離支援關閉時,原樣委派給舊版解析器。此時隔離格式字元會以邊界中性的方式被略過。若要取得完整的 UAX #9 行為,請透過符合性政策開啟隔離支援。
  • CjkFontValidator 會以間隔取樣字碼,而不是逐一測試每個字碼,因此它的涵蓋率數字是統計上足夠可靠的估計值,而非詳盡計數。

字型解析在首次使用時佔最大宗;登錄表會將這項成本攤銷成每個行程只需一次。預熱之後,get()has() 都是 O(1) 的對映查詢。子集化的成本會隨文件實際使用的字元數量而變,而非取決於字型的完整字元表。這正是子集化對 CJK 內容同時帶來體積與速度收益的原因:子集化器會透過二元搜尋、預先配置的緩衝區與大量字串運算,處理擁有 20,000 個以上字元的字型。複合字元解析是有界限的 —— 它在 100 次閉包迭代時設限,以抵禦循環的元件參照。cmap Format 12 解析器會限制群組與項目數量,以針對惡意字型限定記憶體用量。1500 毫秒牆鐘時間與 64 MB 尖峰記憶體的 performance_budget,足以涵蓋一次典型的字型預熱加上文件渲染。

有兩個介面具有安全性影響。第一個是字型輸入。register()registerFromBinary() 會解析任意 bytes。registerFromBinary() 會建立一個暫存檔。路徑中的串流包裝器與 null bytes 會在邊界處被拒絕。不受信任的字型資料必須先通過一道外部資源政策,由它在資料抵達解析器之前限定檔案大小與字元數量。子集化器的二進位讀取器會對每個位移做邊界檢查。cmap 解析器會限制群組、項目與表的數量(Format 12 中為 numGroups > 31000 與項目上限 200,000),讓刻意構造的字型無法驅動無界限的配置。第二個介面是文字還原:ToUnicodeCMapBuilder 會驗證每個字元碼都落在 16 位元碼空間內,且每個 Unicode 值都是有效的純量 —— surrogate(代用對)的半值會被拒絕 —— 因此格式錯誤的對映無法產生損毀的擷取資源。請把任何外部提供的字型或文字都視為不受信任。

宣稱標準條款佐證
文件使用的每個字型都會被嵌入,因此文件無須仰賴系統字型即可呈現。ISO 32000-2§9
嵌入的字型會被子集化為文件參照到的字元。ISO 32000-2§9
嵌入的 CJK TrueType 字型會以 Type 0 字型輸出,搭配 Identity-H CMap 與 CIDFontType2 後代。ISO 32000-2§9.7.4RAG 摘要因授權上限的限制而遭到截斷,此處保留下來可供對照的前綴值為 7a5258772f508e3b,詳見 _downgraded-claims-o3.md

前兩項宣稱是以改寫方式呈現,並以摘要釘選。第三項宣稱的完整 RAG 摘要未被回傳(授權上限截斷);其佐證來自 ADR-013 與 cmap 編碼器開發者總覽,並已記錄為降級。NextPDF 不會重製規範性文字。CJK 內容的 PDF/A-4 與 PDF/UA-2 符合性,取決於寫入端的子集化與 /ToUnicode 接線,並在該處追蹤。

商業 OpenType 功能套件與進階字型後備鏈建構在 Core 的登錄表與編碼接縫之上。Core 字型排印模組無須授權即可嵌入、子集化並編碼每一個字型;付費套件則額外提供精選的後備解析。這裡刻意不放轉換連結 —— 這是文件,不是銷售管道。