跳到內容

契約 / 排版

排版領域包含字型登錄表合約,以及一組文字預處理合約:FontRegistryInterfaceTextPreprocessorInterface,以及不可變的 TextPreprocessResultTextSegment 值物件。全部都是 stable(穩定)。

Terminal window
composer require nextpdf/core:^3

FontRegistryInterface 是行程生命週期內使用的字型儲存區。它會註冊 TrueType、OpenType、TTC 或 PFB 字型,並回傳剖析後的 FontInfo 中繼資料。登錄表的存活期間比個別文件更長,因此一個 worker(工作行程)只需剖析每個字型一次。它可以在啟動時預熱一批字型,再進入鎖定狀態,讓正式環境流量無法變動它。已鎖定的登錄表在呼叫 register()addFontDirectory()warmup() 時會擲出 LogicException,但查詢仍可使用。登錄表也可以透過 registerFromBinary() 從原始二進位資料接收字型。@font-face 橋接會使用這個方法,註冊從遠端來源或 data URI 取得的字型。登錄表只持有純 PHP 資料,沒有任何資源 handle,因此可以安全地在 worker pool 之間共用。

引擎會嵌入並子集化它用到的每一個字型。嵌入的字型程式會隨 PDF 一起攜帶,因此文件能在任何檢視器中一致呈現,不受系統已安裝字型影響——ISO 32000-2 §9。字型子集只攜帶文件實際參照到的字元(glyph),這對 CJK 或大量使用 Unicode 的內容尤其關鍵——ISO 32000-2 §9。登錄表合約會公開剖析後的中繼資料,供子集化與嵌入階段取用。

TextPreprocessorInterface 會在文字進入字元排版、字型子集化、ToUnicode CMap 與結構樹之前先攔截它。這個位置正是其安全性性質的來源:會遮蔽內容的預處理器,會在內容抵達內容串流、字型子集或中繼資料之前就先將它移除。這份合約帶有兩項不變式。預處理器不得引入會影響排版的字元,而且必須保留邏輯閱讀順序;它的職責是內容替換,不是排版。結果是不可變的 TextPreprocessResult,內含一份有序的 TextSegment 值清單。每個 segment 不是直接通過,就是已遮蔽。對於已遮蔽的 segment,顯示文字取決於遮蔽模式:黑框矩形為空字串、與原始長度相符的星號,或一個固定標籤。segment 上的 originalCharCount 是不可逆的測量提示,只用來決定遮蔽矩形的大小。它絕不可用來重建原始內容。

型別種類主要成員穩定度起始版本
FontRegistryInterface介面(interface)register()get()has()all()addFontDirectory()warmup()lock()isLocked()registerBase14()registerFromBinary()memoryUsage()穩定1.7.0
TextPreprocessorInterface介面(interface)process(string): TextPreprocessResult穩定1.9.0
TextPreprocessResultfinal readonly class(最終唯讀類別)$segmentshasRedactions()getDisplayText()穩定1.9.0
TextSegment最終唯讀類別$displayText$isRedacted$originalCharCount$fillColor穩定1.9.0

TextPreprocessResultTextSegment 會固定它們的建構式簽章與公開屬性;可以新增方法,但屬性不得變動。

examples/04-text-and-fonts.php
<?php
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
use NextPDF\Core\Document;
$doc = Document::createStandalone();
$doc->addPage();
$doc->setFont('helvetica', 'B', 18);
$doc->cell(0, 12, 'Bold heading', newLine: true);
$doc->setFont('helvetica', '', 11);
$doc->multiCell(0, 7, 'Body text rendered with a registered font.');
$doc->save(__DIR__ . '/output/04-text-and-fonts.pdf');

setFont() 會透過 FontRegistryInterface resolve(解析)字型家族。獨立文件會使用一個私有的登錄表。worker 則會共用同一個(請見 document 頁面)。

examples/contracts/typography-production.php
<?php
declare(strict_types=1);
require_once __DIR__ . '/../../vendor/autoload.php';
use NextPDF\Contracts\FontRegistryInterface;
use NextPDF\Contracts\TextPreprocessorInterface;
use NextPDF\Exception\NextPdfException;
use Psr\Log\LoggerInterface;
final readonly class FontWarmupService
{
public function __construct(
private FontRegistryInterface $fonts,
private TextPreprocessorInterface $preprocessor,
private LoggerInterface $logger,
) {}
/**
* Warm a font set at boot, then lock the registry.
*
* @param list<string> $fontFiles Absolute paths to font files.
*/
public function boot(array $fontFiles): void
{
try {
$this->fonts->warmup($fontFiles);
$this->fonts->lock();
} catch (NextPdfException $e) {
$this->logger->error('Font warmup failed', ['error' => $e->getMessage()]);
throw $e;
}
}
public function redact(string $text): string
{
$result = $this->preprocessor->process($text);
return $result->hasRedactions()
? $result->getDisplayText()
: $text;
}
}

worker 的啟動順序是先呼叫 warmup(),再呼叫 lock()。在 lock() 之後,變動操作會擲出例外;查詢則會繼續服務流量。

  • 已鎖定的登錄表會拒絕每一個變動方法。請在啟動時預熱並鎖定;切勿在處理請求期間呼叫 register()
  • registerFromBinary() 會把字型位元組寫到暫存檔以便剖析。未受信任的字型資料是一個剖析攻擊面——請透過 ExternalResourcePolicyInterface 加以把關(請見 security-policy 頁面)。
  • 任何 TextPreprocessor 都不得加入換行、歸位字元或 tab。這會改變排版,並破壞合約的第一項不變式。
  • TextSegment::$originalCharCount 只是一個寬度提示。用它來推斷原始內容會破壞遮蔽,並違反合約的第三項不變式。
  • TextPreprocessResult::getDisplayText() 對黑框 segment 依設計會回傳空字串。不要把空的 segment 當成預處理失敗。

首次使用的成本主要來自字型剖析;登錄表會把這個成本攤提為每個行程只支付一次。預熱之後,get()has() 都是 O(1) 的 map 查詢。memoryUsage() 會回傳一個 MemoryReport,讓 worker 能依預算追蹤字型快取。文字預處理的成本與輸入長度成線性關係。segment 清單會增加有上界的額外負擔,與遮蔽匹配的數量成比例。1500 ms 牆鐘時間與 64 MB 尖峰的 performance_budget,足以涵蓋一組典型字型的預熱加上文件繪製。子集化成本會隨實際用到的字元數量縮放,而不是隨字型的完整字元表縮放。因此,子集化會降低 CJK 內容的輸出大小與繪製成本。

排版領域有兩個與安全性相關的面向。第一個是字型輸入:registerFromBinary() 會剖析任意位元組。未受信任的字型資料必須先通過一個 ExternalResourcePolicyInterface,在抵達剖析器之前先限制檔案大小與字元數量。第二個是遮蔽:TextPreprocessorInterface 被刻意安排在字元排版、字型子集化、ToUnicode CMap 與結構樹之前,正是為了讓已遮蔽的內容永不進入繪製後的成品。若遮蔽是以繪製時的覆蓋層方式實作,原始文字會洩漏到內容串流與子集中。合約的擺放位置可防止這一類缺陷。segment 上的測量提示刻意設計為不可逆。請把任何由外部提供的字型或文字都視為未受信任。

主張標準條款佐證
文件用到的每一個字型都會嵌入,因此文件無需仰賴系統字型即可呈現。ISO 32000-2§9
嵌入的字型會子集化為文件參照到的那些字元。ISO 32000-2§9

兩項條款皆為改寫。NextPDF 不會重製規範性條文。PDF/A-4 對每一個字型強制要求嵌入。該符合性記載於擷取與無障礙頁面。