コンテンツにスキップ
getnextpdf.com

本番環境でフォントをプロビジョニングする

PDF はラップトップ上では正しくレンダリングされるのに、コンテナへ出荷すると空の四角形の列――「豆腐」グリフ――として、あるいはアクセント記号や非ラテン文字が欠落した状態で出てきます。原因はほぼ常に同じです。選択したフォントがデプロイされたイメージに存在しないのです。

ネイティブのインプロセス NextPDF エンジンは、フォントレジストリが読み取れる フォントファイル からフォントを解決します。OS や fontconfig のフォントを自動的には検出しません――OS にインストールされたフォントファイルは、それらのファイルを明示的に登録するか、それらを含むディレクトリを FontRegistry の検索パスに追加した場合にのみ役立ちます。スリムなベースイメージから構築されたコンテナには apt/apk でインストールされたフォントがなく、たとえあったとしても、 レジストリをそれらのファイルに向けない限りネイティブエンジンはそれらを無視します。 修正方法は、実際のフォントファイルをアプリケーションやイメージの中に同梱し、 エンジンに登録することです。レジストリは TrueType(.ttf)、OpenType(.otf)、 TrueType Collection(.ttc)ファイルを読み取ります。レガシーな Type1(.pfb)も受け付けますが、新しい作業ではめったに必要ありません。

始める前に、これらの要素が揃っていることを確認してください。

  • NextPDF core がインストールされている。
  • 使用するつもりの実際のフォントファイルがあり、それらを埋め込むライセンスを持っている。埋め込み権はあなたの責任です――TrueType フォントを埋め込んでサブセット化する を参照してください。
  • ビルドがそれらのファイルをデプロイされる成果物へコピーできる。

これは運用のハウツーです。コードは最小限で、作業はビルドとファイルシステムのレイアウトにあります。単一のフェイスを登録してサブセット化する API レベルの仕組みについては、上にリンクした埋め込み・サブセットレシピを読んでください。このページは、 ファイルを マシンに乗せる ことと、エンジンをそれらに向けることを扱います。

2 つの異なるレンダリングパスがあり、フォントの扱いはそれらの間で異なります。

  • ネイティブのインプロセスエンジン(デフォルト、Document / writeHtml): エンジンは、検出のためにオペレーティングシステムのフォントシステムや fontconfig を呼び出しません。フォントレジストリ を通じてフェイスを解決し、 これはあなたが登録した特定のフォントファイルを読み取るか、検索パスとして構成したディレクトリ内のファイルを見つけます。apt-get install fonts-noto でフォントをインストールしたり fc-cache を実行したりしても、それ自体では何も起こりません ――ネイティブエンジンは、それらのファイルを登録するか、それらのディレクトリをレジストリの検索パスに追加した場合にのみ参照します。
  • Chrome ブリッジ(ヘッドレスブラウザを駆動する HTML-to-PDF レンダラー): このパスは、ブラウザの通常のフォント検出を通じてホストのインストール済みフォントを 使用 します。そのため apt/apk のフォントパッケージと fontconfig がそこでは重要になります。

一般的な「これらのシステムフォントパッケージを Dockerfile にインストールする」 という案内を読んだ場合、それは Chrome ブリッジに当てはまるのであって、このページが扱うネイティブエンジンには当てはまり ません。ネイティブ生成では、ファイルを同梱して登録します。

フォントファイルをアプリケーションツリーの中に置き、バージョン管理されてすべてのビルドとともに出荷されるようにします。慣例的な場所は resources/fonts/ ディレクトリです。

your-app/
├── resources/
│ └── fonts/
│ ├── DejaVuSans.ttf
│ ├── DejaVuSans-B.ttf
│ └── NotoSansCJK-Regular.ttc
└── src/

エンジンのディレクトリ検索がファミリとスタイルでファイルを見つけられるよう、 ファイルに名前を付けます。特定のファイルではなく ディレクトリ を登録し、後で setFont('DejaVuSans', 'B', 12) を呼び出すと、エンジンは構成された各ディレクトリで DejaVuSans-B.ttfDejaVuSansB.ttfDejaVuSans.ttf のようなファイルを探します。 ディレクトリ検索はそれらの候補名を、綴り出された単語ではなく setFont に渡す同じ 1 文字のスタイルコード(太字は B、斜体は I、太字斜体は BI)から構築します――そのため信頼できる形式は Family-<StyleCode>.ttf(例えば DejaVuSans-B.ttfDejaVuSans-BI.ttf)であって、Family-Bold.ttf では ありませんDejaVuSans-Bold.ttf という名前のファイルはディレクトリ検索では決して見つかりません。そのようなファイルを使うには、register() で明示的に登録してください――これはフォントを解析し、ファイル自身のネームテーブルから読み取ったファミリとスタイルでインデックス化するため、綴り出されたファイル名はもはや関係なくなります(ステップ 2 を参照)。

ファイルを見えるようにするには、2 つの等価な方法があります。どちらも NextPDF\Contracts\FontRegistryInterface を実装する NextPDF\Typography\FontRegistry を通じて行います。

正確なフェイスを自分で制御する場合は、特定のファイルをエイリアスの下に登録します

use NextPDF\Typography\FontRegistry;
$registry = new FontRegistry();
$registry->register(__DIR__ . '/../resources/fonts/DejaVuSans.ttf', alias: 'DejaVuSans');

register(string $fontFile, string $alias = '', int $fontIndex = 0).ttf.otf.ttc ファイルに加え、レガシーな Type1 .pfb(同じパスから対となる .afm メトリクスを読み込む)を受け付けます。$fontIndex は TrueType Collection (.ttc)内のサブフォントを選択します。register() はファイルを解析し、自身のネームテーブルから読み取ったファミリとスタイルでフェイスをインデックス化するため、 登録後は物理的なファイル名は無関係です。任意の $alias は、フェイスのための追加の検索名にすぎません――スタイルコードではなく、ファイルが提供するスタイルを変更するものでもありません。フォントの埋め込まれたファミリ名以外の名前で setFont() を呼び出したいときに渡します。解析された FontInfo を返します。

エンジンが、自分で制御するフォルダから名前でフェイスを解決するようにしたい場合は、 ディレクトリを登録します

$registry = new FontRegistry('/var/www/app/resources/fonts');
// or, equivalently, after construction:
$registry->addFontDirectory('/var/www/app/resources/fonts');

FontRegistry コンストラクタはそのディレクトリを最初の引数として受け取り、 addFontDirectory() がさらに検索パスを追加します。素の Document も、スタンドアロンの場合のために addFontDirectory() を公開します。

自分で populate したレジストリを使うには、DocumentFactory を通じてドキュメントを構築します。これは、作成するすべてのドキュメントにその正確なレジストリを配線します。

use NextPDF\Core\DocumentFactory;
use NextPDF\Graphics\ImageRegistry;
$factory = new DocumentFactory($registry, new ImageRegistry(maxCacheBytes: 0));
$doc = $factory->create();
$doc->addPage();
$doc->setFont('DejaVuSans', '', 12);
$doc->cell(0, 10, 'Réndéred wîth a bundled face — no tofu.', newLine: true);
$doc->save('/tmp/out.pdf');

Document::createStandalone()独自の 内部レジストリを構築するため、別の FontRegistry に登録したフェイスはそこからは見えません。本番環境では、populate したレジストリが使用されるよう、DocumentFactory(またはフレームワークのファクトリ)を通じてください。

各フレームワーク統合は、同じ 2 つの概念を構成として公開するため、レジストリを直接触ることはめったにありません。Laravel パッケージの nextpdf.php では、fonts_path (デフォルト NEXTPDF_FONTS_PATH、フォールバックは resource_path('fonts'))が検索ディレクトリで、preload_fonts はワーカー起動時に解析される絶対フォントファイルパスのリストです。fonts_path を同梱したディレクトリに向ければ、登録済みのフェイスは自動的に解決されます。

ステップ 3 ―― Docker イメージでフォントをプロビジョニングする

「ステップ 3 ―― Docker イメージでフォントをプロビジョニングする」という見出しのセクション

コンテナでは、フォントファイルはイメージレイヤーの一部でなければならず、ビルド時にコピーされます。resources/fonts/ の下に同梱すると、アプリケーションコードとフォントが一緒に出荷されるため、通常の COPY . . がすでにそれらを運びます。 フォントをビルドコンテキストの外に置く場合は、明示的にコピーし、登録するパスがイメージ内のパスと一致するようにしてください。

# Native engine: NO system font packages are required.
# The native engine does not discover OS-installed fonts automatically; install OS
# font packages (`apt-get install fonts-*`) only if you also register them or point
# the font registry's search directory at their files.
FROM php:8.4-cli
WORKDIR /var/www/app
# Bundle the application, including resources/fonts/, into the image.
COPY . /var/www/app
# Make the bundled directory the engine's font search path.
ENV NEXTPDF_FONTS_PATH=/var/www/app/resources/fonts
CMD ["php", "bin/generate.php"]

イミュータブルまたは読み取り専用のファイルシステムreadOnlyRootFilesystem コンテナ、サーバーレスイメージ、または堅牢化されたホスト)では、フォントファイルは生成時に読み取られて決して書き込まれないため、読み取り専用のマウントで問題ありません。 エンジンが書き込みたいかもしれない唯一のものは、その解析済みフォントキャッシュです。 そのディレクトリに小さな書き込み可能なボリュームを与えるか、起動時にレジストリをウォームアップしてロックし(次のセクション)、ランタイムでの書き込みや登録が試みられないようにしてください。

長時間稼働するワーカーでは、起動時にすべてのフェイスを一度解析し、その後レジストリを ロック して、リクエストごとの登録が発生せず、誤構成が静かにフォールバックする代わりに大きく失敗するようにします。

$registry = new FontRegistry('/var/www/app/resources/fonts');
$registry->warmup([
'/var/www/app/resources/fonts/DejaVuSans.ttf',
'/var/www/app/resources/fonts/DejaVuSans-B.ttf',
]);
$registry->lock();

lock() の後は、register()addFontDirectory()warmup() が送出するため、 「イメージ内のパスが間違っている」というミスを、本番環境での豆腐ページではなくハードな起動時の失敗に変えます。

各必須フェイスで 1 ページをレンダリングするデプロイメントスモークチェックを追加してください。下のヘッダーチェックは、ドキュメントが 出力を生成した ことだけを検証します――フォントが解析、埋め込み、あるいは解決すらされたことを証明しません。 エンジンが見つけられないフェイスは、標準のベースフォントにフォールバックするかもしれず(そして現在の非ストリクトな挙動の下では、適合性プロファイルが代わりに同梱の代替を供給するかもしれません)、その間も有効で空でない PDF を出力します ――そのため、たとえそのフォールバックが起きても、このチェック単独では静かな劣化を捕捉できません。フォールバックがすべてのパスで保証される、あるいは静かであることに頼らないでください。下に示すように、埋め込まれたプログラムを直接検証してください。

$doc = $factory->create();
$doc->addPage();
$doc->setFont('DejaVuSans', '', 12);
$doc->cell(0, 10, 'warmup check', newLine: true);
$pdf = $doc->getPdfData();
// `getPdfData()` would normally throw on a real failure; this header check only
// confirms serialization returned PDF bytes, not that any specific font resolved.
if (!str_starts_with($pdf, '%PDF')) {
throw new RuntimeException('Font warmup smoke check produced no PDF output.');
}

フェイスが欠落しているときに実際にデプロイを失敗させるには、出力された PDF に埋め込まれたフォントプログラムがあるか確認します。解決される登録済みフェイスは、 埋め込まれたプログラムを伴う独自のフォント辞書を持つため、その存在をアサートすると、 (エンジンが何にフォールバックしたかにかかわらず)要求したフェイスが決して解決されなかったという、ヘッダーチェックが見逃すケースを捕捉できます。どのキーがプログラムを保持するかはアウトラインフォーマットに依存します。TrueType アウトライン(.ttf.ttc)は /FontFile2 を、CFF/OpenType アウトライン (PostScript アウトラインを持つ .otf)は /FontFile3 を、レガシーな Type1 (.pfb)は /FontFile を使用します。

フォーマットに依存しない「何らかのフォントプログラムが埋め込まれている」という信号だけが必要なら、/FontFile 単独 をテストします――/FontFile/FontFile2/FontFile3 の両方の部分文字列であるため、素の部分文字列チェックがすでにあらゆるアウトラインタイプにマッチし、/FontFile2//FontFile3 を追加の || 分岐として加えるのは冗長です。

if (!str_contains($pdf, '/FontFile')) {
throw new RuntimeException('No embedded font program found — face fell back.');
}

ただし、素の /FontFile 部分文字列はアウトラインタイプを区別できません。それらを区別するには、/FontFile/FontFile2/FontFile3 にも反応しないよう、 単語境界付きの正確なトークンでマッチします。

$isTrueType = preg_match('~/FontFile2\b~', $pdf) === 1; // TrueType (.ttf/.ttc)
$isCffOtf = preg_match('~/FontFile3\b~', $pdf) === 1; // CFF/OpenType (.otf)
$isType1 = preg_match('~/FontFile(?![23])\b~', $pdf) === 1; // Type1 (.pfb)
if (!$isTrueType && !$isCffOtf && !$isType1) {
throw new RuntimeException('No embedded font program found — face fell back.');
}

いずれにせよ、これを信頼できるデプロイゲートではなく、粗いヒューリスティックにすぎない ものとして扱ってください。シリアライズされた PDF に対する生のバイト検索は、 いくつかの理由で不正確です。フォントプログラムは 圧縮されたオブジェクトストリーム の中に存在しうる(そこでは /FontFile* が平文バイトとして決して現れない)、 インクリメンタル更新 がオブジェクトを追加または置き換えうる、埋め込まれていない、または standard-14 フォント は正当にフォントプログラムをまったく持たない、 そして シリアライズの違い(オブジェクトの順序、空白、名前のエンコーディング)がトークンを移動または隠蔽しうる、からです。せいぜい 何らかの フェイスがプログラムを埋め込んだことを確認するだけで――あなたが望んだ 特定の フェイスが解決されたことは決して確認できません。

本物のデプロイゲートには、バイト検索に頼らないでください。適切な PDF パーサーまたはオブジェクトインスペクターで出力された PDF を解析し、対象フェイスのフォントオブジェクトが埋め込まれた /FontFile//FontFile2//FontFile3 プログラムを持つことをアサートするか、統合で利用できるなら製品が提供するフォント解決アサーションを使用してください。上のトークンを意識した正規表現は、手早いローカルの健全性チェックには有用ですが、デプロイを失敗させるべきなのは構造的な検査です。埋め込みとフォント辞書の構造は、TrueType フォントを埋め込んでサブセット化する に記述されています。

  • createStandalone() は独自のレジストリを持ちます。 別の FontRegistry に登録したフェイスは、スタンドアロンドキュメントからは見えません。レジストリがアクティブなものになるよう、DocumentFactory(またはフレームワークのファクトリ)を使ってください。
  • スタイルファイルはファイルとして存在しなければなりません。 エンジンは、 レギュラーのフェイスから太字や斜体を合成しません。setFont('DejaVuSans', 'B') を呼び出すと、ディレクトリ検索は DejaVuSans-B.ttfDejaVuSansB.ttf、または DejaVuSans.ttf(小文字と .otf のバリアントも)を探します――候補をリテラルの B スタイルコードから形成するため、DejaVuSans-Bold.ttf を探すことは決してありません。DejaVuSans-Bold.ttf のような綴り出された名前のファイルは、register() で明示的に登録した場合にのみ解決されます。これはファイル名に関係なく、ファイル自身のネームテーブルから読み取ったファミリとスタイルでインデックス化します。それを見つけるためにディレクトリ検索に頼ると失敗し、その後エンジンはベースフォントにフォールバックするかもしれません(保証された、または常に静かなパスではありません) ――このページが警告する劣化です。
  • ストリームラッパーとリモートパスは拒否されます。 レジストリは、URI スキームやヌルバイトを含むパスを拒否します。ローカルファイルのみを登録してください。 ランタイムで取得するフォントには、生のバイトを伴う registerFromBinary() を使用します。
  • ロックされたレジストリはイミュータブルです。 いったん lock() を呼び出すと、 以降の register()addFontDirectory()warmup() は送出します。検索メソッドは引き続き利用できます。ロックする前にすべてを登録してウォームアップしてください。
  • CJK コレクションは大きいです。 $fontIndex.ttc の正しいサブフォントを登録し、より大きな埋め込みサブセットに備えてください。埋め込み・サブセットレシピの CJK に関する注記を参照してください。
  • フォントファイルは信頼できないバイナリ入力です。信頼できるソースからのフォントのみを同梱し、エンドユーザーから受け付けるあらゆるフェイスの出所を検証してください。
  • ウォームアップ後にレジストリをロックすると、ランタイムの変更面が取り除かれ、パスのミスが静かに出力を劣化させる代わりに起動時に失敗するようになります。
  • ユーザー入力を登録ファイルパスへ補間しないでください。同梱されたフェイスの固定セットを登録し、リクエストが任意のファイルシステムパスを選べるようにしないでください。

このガイドは規範的な標準への適合を主張しません。示されているすべてのシンボルは検証済みの公開面です。NextPDF\Typography\FontRegistryregister()addFontDirectory()warmup()lock()、ディレクトリのコンストラクタ引数)、 その NextPDF\Contracts\FontRegistryInterface 契約、 NextPDF\Core\DocumentFactory::create()、そして NextPDF\Core\Document::setFont() / addFontDirectory() です。Laravel の fonts_pathpreload_fonts キーは、 nextpdf/laravel パッケージの文書化された構成です。埋め込みとサブセットタグの挙動は、その ISO 32000-2 引用とともに、関連項目の下にリンクした埋め込み・サブセットレシピに文書化されています。