コンテンツにスキップ

タイポグラフィ: フォントレジストリ、サブセット化、CMap、エンコーディング、BiDi

タイポグラフィモジュールは、フォントファイルと Unicode 文字列を、PDF コンテンツストリームが必要とするバイト列に変換します。フォントの解析、プロセス存続期間にわたるレジストリ、グリフのサブセット化、ToUnicode CMap、cmap を考慮したエンコーディング戦略、Unicode 双方向エンジンを担います。

Terminal window
composer require nextpdf/core:^3

FontRegistry は、プロセス存続期間にわたって使われるフォントストアであり、FontRegistryInterface を実装します。TrueType、OpenType、TTC、または Type 1(PFB および AFM)のファイルを一度だけ解析し、不変の FontInfo を返します。このレジストリは長時間稼働するワーカー向けに設計されています。起動時にフォントセットをウォームアップして lock() を呼び出します。ルックアップがトラフィックを処理し続ける一方で、レジストリはそれ以降の変更をすべて拒否します。純粋な PHP データ(解析済みメタデータと生のフォントバイト)だけを保持するため、ワーカープールで 1 つのインスタンスを共有できます。registerFromBinary() は生のフォントバイトを受け取ります。これは、リモートソースまたはデータ URI から取得したフォントに対して、HTML の @font-face ブリッジが使用する入口です。

エンジンは、使用するすべてのフォントを埋め込み、サブセット化します。埋め込みフォントプログラムは PDF 内に格納され、ドキュメントとともに運ばれるため、インストール済みのシステムフォントに依存せず、どのビューアーでもドキュメントを同じように表示できます(ISO 32000-2 §9)。サブセットはドキュメントが参照するグリフだけを含むため、CJK や Unicode を多用するコンテンツでは極めて重要です(ISO 32000-2 §9)。FontSubsetter は元のテーブルディレクトリを解析し、cmap を抽出し、複合グリフの依存関係を推移閉包として解決し、headhheamaxpcmaplocaglyfhmtx の各テーブルを再構築します。元のグリフ識別子の番号付けを保持し、未使用スロットをゼロで埋めるため、CIDToGIDMap/Identity)は有効なまま保たれます。サブセット化で削減できる割合が 10 パーセント未満の場合は、元のフォントをそのまま返します。これにより、効果に見合わない処理を避けます。CffSubsetter は、Compact Font Format アウトラインテーブルを持つ OpenType フォントに対して同等の処理を行います。

テキスト出力は、Unicode コードポイント、コンテンツストリーム内の文字コード、フォント内のグリフ識別子という 3 段階で変換されます。モジュールはこの処理を、明示的な 1 つのコラボレーターとしてモデル化します。FontInfo::encodeText() がファサードとなり、FontEncodingStrategyResolver がフォントごとにディスパッチします。Unicode cmap を持つ埋め込み TrueType または OpenType フォントは TrueTypeCmapStrategy にルーティングされ、2 バイトの Identity-H 16 進ストリームを出力します。これは、Identity-H CMap と CIDFontType2 の子孫を持つ Type 0 フォントが必要とする形式です(ISO 32000-2 §9.7.4。対応する RAG チャンクのダイジェストはライセンス上限により切り詰められて返され、_downgraded-claims-o3.md に記録されています)。それ以外のすべてのフォント(Base 14 標準フォント、Type 1 PFB および AFM)は Base14EncodingStrategy にルーティングされ、1 バイトの WinAnsi リテラル文字列を出力します。このストリームは WinAnsiEncoding(Windows コードページ 1252)のレパートリー全体、すなわちアクセント付きラテン文字、ユーロ記号、一般的な約物をカバーします。その範囲外のコードポイントは 1 バイトストリームから除外され、カバーするフォントが登録されている場合はクラスターごとのフォントフォールバックの対象になります(ISO 32000-2 Annex D.2)。リゾルバーは FontInfo の値空間全体に対して定義されており、null になる経路はありません。ToUnicodeCMapBuilder は、リーダーが Identity-H フォントから元の Unicode を復元できるようにする /ToUnicode リソースを構築します。貪欲な bfrange 結合と、ブロックあたり 100 エントリという上限を適用します。

BidiEngine は、Unicode 双方向アルゴリズム(UAX #9、Unicode 16)の境界サービスです。アイソレートサポートをオフにするとレガシーリゾルバーに委譲するため、既存の呼び出し元には影響しません。オンにすると、アイソレートを考慮したパイプラインを実行します。具体的には、最大深度 125 の明示的アイソレートスタック、弱型のパス、ペア括弧の解決を含む中立型のパス、暗黙レベルと行の並べ替えのパスです。候補フォントの CJK グリフカバレッジは別個の診断として扱われます。CjkFontValidator はスクリプトごとに必要な Unicode ブロックをサンプリングし、カバレッジの割合を報告します。

種別主なメンバー安定度導入バージョン
FontRegistryfinal クラスregister(), registerType1(), registerFromBinary(), registerFromDirectory(), get(), has(), all(), warmup(), lock(), isLocked(), memoryUsage()安定(ステーブル)1.7.0
FontInfofinal readonly クラス$family, $type, $widths, $unicodeMap, $cmapForward, getKey(), encodeText()安定(ステーブル)1.0.0
FontSubsetterfinal クラスsubset(string, array<int>, int): string安定(ステーブル)1.0.0
CffSubsetterfinal クラスOpenType/CFF アウトラインのサブセット化安定(ステーブル)1.0.0
FontEncodingStrategyResolverfinal クラスresolve(FontInfo): FontEncodingStrategy安定(ステーブル)2.7.0
ToUnicodeCMapBuilderfinal クラスbuildFromRun(), buildFromMap(), encodeUnicodeUtf16Be()安定(ステーブル)2.7.0
BidiEnginefinal クラスUAX #9 のアイソレートを考慮した解決安定(ステーブル)3.1.0
CjkFontValidatorfinal クラスvalidateCoverage(), detectScript(), isCjkCodepoint()安定(ステーブル)1.0.0

FontInfo は不変です。コンストラクターのシグネチャと public プロパティは凍結されています。エンコーディング戦略は (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() はフォントを一度だけ解析し、不変の FontInfo を返します。encodeText() はリゾルバーを経由してルーティングし、バイトストリーム、PDF 文字列オペランド、グリフごとのアドバンス幅、GID から Unicode へのマップを保持する EncodedGlyphRun を返します。このマップは /ToUnicode CMap によって消費されます。

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() を呼び出します。lock() の後は、あらゆる変更が例外をスローします。ルックアップはトラフィックを処理し続けます。memoryUsage()MemoryReport を返すため、ワーカーはフォントキャッシュを予算と照らし合わせて追跡できます。

  • ロック済みのレジストリは register()registerFromBinary()addFontDirectory()、および warmup() を拒否します。起動時にウォームアップしてからロックしてください。リクエスト処理中に登録してはいけません。
  • FontSubsetter::subset() は、削減量が 10 パーセント未満になる場合、または必須テーブルが欠落している場合に、元のバイト列をそのまま返します。入力と同じフォントが返されるのは、文書化された利得なしの経路であり、失敗ではありません。
  • サブセッターは元のグリフ識別子の番号付けを保持し、未使用グリフをゼロで埋めます。これにより CIDToGIDMap /Identity が有効に保たれます。グリフ識別子が連続範囲に再番号付けされると想定してはいけません。
  • registerFromBinary() は、バイト列を解析するために一時ファイルへ書き込み、拡張子付きファイルと tempnam() のベースファイルの両方を finally ブロックで削除します。信頼できないフォントデータは解析攻撃のサーフェスになります。パーサーに到達する前にゲートしてください(「セキュリティ上の注意」を参照)。
  • BidiEngine は、アイソレートサポートがオフの場合、レガシーリゾルバーへそのまま委譲します。その場合、アイソレート書式設定文字は境界中立として通過します。完全な UAX #9 の動作を得るには、適合性ポリシーでアイソレートサポートをオンにしてください。
  • CjkFontValidator はすべてのコードポイントを検査するのではなく一定間隔でサンプリングするため、そのカバレッジ値は統計的に十分な推定値であり、網羅的なカウントではありません。

フォント解析のコストは初回使用時に支配的です。レジストリはそのコストを、プロセスごとに一度だけ発生する処理として償却します。ウォームアップ後、get()has() は O(1) のマップルックアップです。サブセット化のコストは、フォントの全グリフテーブルではなく、ドキュメントが実際に使用するグリフ数に比例します。これが、サブセット化が CJK コンテンツでサイズと速度の両面に有利な理由です。サブセッターは 20,000 を超えるグリフを持つフォントを、二分探索、事前割り当てバッファ、一括文字列操作によって処理します。複合グリフの解決は有界です。循環的なコンポーネント参照を防ぐため、閉包の反復は 100 回を上限としています。cmap の Format 12 パーサーは、悪意あるフォントに対してメモリ使用量を有界に保つため、グループ数とエントリ数に上限を設けます。ウォール 1500 ms、ピーク 64 MB の performance_budget は、一般的なフォントのウォームアップとドキュメントのレンダリングをカバーします。

セキュリティ上重要なサーフェスは 2 つあります。1 つ目はフォント入力です。register()registerFromBinary() は任意のバイト列を解析します。registerFromBinary() は一時ファイルを生成します。パス内のストリームラッパーと null バイトは境界で拒否されます。信頼できないフォントデータは、パーサーに到達する前に、ファイルサイズとグリフ数を制限する外部リソースポリシーを通過させる必要があります。サブセッターのバイナリリーダーは、すべてのオフセットを境界チェックします。cmap パーサーはグループ数、エントリ数、テーブル数に上限を設けるため(numGroups > 31000 と、Format 12 における 200,000 のエントリ上限)、巧妙に細工されたフォントでも無制限の割り当ては引き起こせません。2 つ目のサーフェスはテキストの復元です。ToUnicodeCMapBuilder は、すべての文字コードが 16 ビットのコードスペース内にあること、すべての Unicode 値が有効なスカラーであることを検証し(サロゲートの片割れは拒否されます)、不正なマップが破損した抽出リソースを生成できないようにします。外部から供給されるフォントやテキストはすべて信頼できないものとして扱ってください。

主張規格箇条根拠
ドキュメントが使用するすべてのフォントの埋め込み、およびシステムフォントに依存しないドキュメントレンダリングISO 32000-2§9
ドキュメントが参照するグリフへの埋め込みフォントのサブセット化ISO 32000-2§9
埋め込み CJK TrueType フェイスを、Identity-H CMap と CIDFontType2 の子孫を持つ Type 0 フォントとして出力ISO 32000-2§9.7.4ライセンス上限により切り詰められた RAG ダイジェスト。プレフィックス 7a5258772f508e3b、参照先 _downgraded-claims-o3.md

最初の 2 つの箇条は言い換えられ、ダイジェストでピン留めされています。3 つ目の箇条の完全な RAG ダイジェストは返されませんでした(ライセンス上限による切り詰め)。これは ADR-013 と cmap エンコーダー開発者概要によって裏付けられ、ダウングレードとして記録されています。NextPDF は規範的テキストを複製しません。CJK コンテンツの PDF/A-4 および PDF/UA-2 適合性は、そこで追跡されているライター側のサブセット化と /ToUnicode の配線によってゲートされています。

商用の OpenType フィーチャーパックとプレミアムのフォントフォールバックチェーンは、Core のレジストリとエンコーディングのシームの上に構築されます。Core タイポグラフィモジュールは、ライセンスなしですべてのフォントを埋め込み、サブセット化し、エンコードします。有料パックは厳選されたフォールバック解決を追加します。コンバージョンリンクの省略は意図的です。これはドキュメントであり、販売経路ではありません。