PDFlibPasは、選択したフォントが描画できない文字を、インストール済みフォントのフォールバックチェーンからクラスタ単位で検索して解決します。シェーピングと双方向ランの順序は保持されます。SetAutomaticFontFallbackで有効にし、AddFontFallbackでチェーンを拡張します。出力に実際に使用されたフォールバックフォントだけがファイルに埋め込まれます
この機能が解決する問題は、テンプレートフォントが想定していなかった文字体系で顧客名が届いたとき、あらゆるドキュメント生成システムが必ず直面するものです。その失敗は静かに起こるため、発覚したときのコストが高くつきます
非対応の文字がエラーではなく消えてしまうのはなぜか?
なぜなら、PDFには「文字を描画できないフォント」という概念自体が存在しないからです。単純フォントはエンコーディングを介してバイトコードをグリフ名に対応させ、複合フォントはCMapを介してコードをグリフインデックスに対応させます。フォントに存在しないグリフを要求すると、グリフインデックス0、つまり.notdefが返され、ほとんどのフォントはこれを「何も描画しない」か「空白の四角形」として描画します。ファイルは構造的に正しく、テキスト演算子も整形式であり、ページは正常にレンダリングされます。ただ、本来名前が表示されるべき場所が空白になっているだけです
ISO 32000-1のどこにも、生成側がこれに気づくべきだという規定はありません。カバレッジを確認せずにテキストを書き出すジェネレーターは、技術的には準拠しているのに内容を静かに失ったPDFを生成してしまい、その欠落は数週間後に顧客の画面上で表面化します。フォールバック機能と欠落グリフレポートがセットで提供されているのはこのためです。解決できるものを解決するのは仕事の半分に過ぎず、解決できなかったものを報告するのがもう半分です
フォールバックはコードポイント単位ではなくクラスタ単位で行われる
粒度こそが、実際に機能する実装と、もっともらしいだけの実装を分ける細部です。テキストは独立した文字の羅列ではありません。デーヴァナーガリー文字の音節、肌の色修飾子付きの絵文字、結合分音記号を伴う基底文字は、いずれも1つのフォントで描画されなければならない1つのクラスタです。なぜなら、その内部のシェーピング判断はそのフォント固有のテーブルに依存しているからです
PDFlibPasはクラスタ単位で解決するため、フォールバックフォントがカバーするクラスタは、そのフォントによって丸ごと描画されます。クラスタの途中で分割し、前半をプライマリフォント、後半をフォールバックフォントで描画してしまうと、技術的には文字は存在していても見た目には崩れた結果になり、これは最初の空白よりもむしろ悪いとさえ言えます。ラン順序も保持されるため、右から左へのランの内部でフォールバックが発生しても周囲のテキストの順序が入れ替わることはありません。同じ仕組みは日本語・中国語の縦書きで説明されている縦書きレイアウトの土台にもなっています
var
Lib: TPDFlib;
begin
Lib := TPDFlib.Create;
try
Lib.SetOrigin(1);
Lib.SetAutomaticFontFallback(1);
// 検索順序:最初に一致したものが採用されるため、広範囲をカバーするフォントは最後に置く
Lib.AddFontFallback('Microsoft YaHei'); // 簡体字中国語
Lib.AddFontFallback('Meiryo'); // 日本語
Lib.AddFontFallback('Segoe UI Symbol');
Lib.AddFontFallback('Segoe UI Emoji');
Lib.SetMissingGlyphPolicy(PDF_MISSING_GLYPH_REPORT);
Lib.AddTrueTypeFont('Arial', 1); // 1 = フォントを埋め込む
Lib.SetTextSize(11);
Lib.DrawText(72, 720, 'Invoice for 北京示例科技有限公司');
Lib.DrawText(72, 700, 'Delivery status: on time');
Lib.SaveToFile('invoice.pdf');
finally
Lib.Free;
end;
end;
チェーンの順序は意図的に決めてください。解決処理はクラスタをカバーする最初のフォントを採用するため、汎用的な汎Unicodeフォントを先頭に置くと、ほぼすべてがそのフォントで処理されてしまい、せっかく用意した文字体系専用のフォントが一切参照されなくなります。専用フォントを先に、汎用フォントを最後に置いてください
レポートか中断か:どちらの失敗を選ぶか
SetMissingGlyphPolicyには、互換性のあるデフォルト値であるPDF_MISSING_GLYPH_REPORTか、PDF_MISSING_GLYPH_ABORTのいずれかを指定します。レポートポリシーでは、テキスト処理はそのまま続行され、解決できないコードポイントは従来どおり破棄されつつ、それぞれが記録されます。中断ポリシーでは、コンテンツが書き込まれる前にテキスト処理そのものが拒否され、LastErrorCodeには521が設定されます
選択の基準はドキュメントの用途です。社内向けの一括レポートであれば、レンダリングを続行して欠落をログに残すべきです。今日の多少不完全なレポートは、レポートが一切ないよりましだからです。法的拘束力のある契約書や請求書、当事者名が記載されるあらゆる文書は中断させるべきです。当事者名の文字が静かに欠落するという欠陥は、係争の場ではなく自社の工程内で発見したいものだからです。中断ポリシーは書き込み前に失敗するため、中途半端なコンテンツストリームが残ることもありません
var
Lib: TPDFlib;
Report: WideString;
begin
Lib := TPDFlib.Create;
try
Lib.SetMissingGlyphPolicy(PDF_MISSING_GLYPH_ABORT);
// ... ドキュメントを構築 ...
if Lib.DrawText(72, 660, CustomerName) <> 1 then
if Lib.LastErrorCode = PDFLIB_ERROR_MISSING_GLYPH then
begin
Report := Lib.GetMissingGlyphReportJSON;
// {"valid":false,"policy":1,"eventCount":1,"events":[
// {"sequence":1,"documentIndex":0,"page":1,"utf16Index":12,
// "codePoint":21271,"unicode":"U+5317","fontName":"Arial",
// "fontType":"TrueType","operation":"DrawText"}]}
EscalateToOperator(Report);
end;
finally
Lib.Free;
end;
end;
このレポートは意図的に機械可読かつ有界に設計されています。各イベントには、ページ番号、文字列内のUTF-16インデックス、数値形式とU+XXXX形式の両方で表したコードポイント、選択されたフォントとその種類、問題が発生した操作が記録されるため、サポートチケットは症状を説明する代わりに正確な文字を名指しできます。トラッカーは直近256件のイベントを保持しており、これはドキュメントを診断するには十分な量でありながら、病的なケースで診断情報がメモリ問題に転じてしまうことがない程度に小さく抑えられています
計測と描画は一致していなければならない
幅の計測は、描画と同じクラスタ単位のフォールバック判断を使用します。当たり前に聞こえるかもしれませんが、自前実装のフォールバック層の多くがここで間違えます。描画パスだけにパッチを当てて計測はプライマリフォントのままにしてしまい、その結果、あらゆるテキストボックス、右揃え、テーブル列が、実際にレンダリングされた幅とは食い違う幅をもとに計算されてしまうのです
両方の経路が同じ解決処理を共有しているため、描画前に計測した文字列は、フォールバックランを含めて、計測時の幅どおりに配置されます。だからこそ、手作業で確認した箇所だけでなく、フォールバックをグローバルに有効化しても安全なのです
使用したものだけが埋め込まれる
フォールバックフォントは遅延埋め込みされます。チェーン内にあってもクラスタを一度も解決しなかったフォントは、出力に何も寄与しません。中国語1文字とラテン文字5,000文字を含むドキュメントに、CJKフォント一式が丸ごと入ることはありません。含まれるのは、そのたった1文字のグリフに対してサブセット化処理が生成した分だけであり、これはファイルサイズ最適化とフォントサブセット化で説明されている挙動です
この遅延性のおかげで、幅広いチェーンを構成しても低コストで済みます。対応するすべてのロケールで必要になりうるフォントを登録しておいても、個々のPDFは実際に使った分のコストしか払いません。自分で生成したのではない、フォントの欠落がすでに既存ファイルの中にあるドキュメントについては、修復の経路が異なり、既存PDFへの欠落フォントの埋め込みで扱っています
運用上の注意点を1つ、はっきり述べておく価値があります。フォールバックは、コードを実行しているマシンにインストールされているフォントに対して解決されます。CJKフォントがインストールされていないサーバーにはフォールバック先が何もなく、そのことは最初の苦情が来る前に、最初のドキュメントの時点でレポートが教えてくれます。依存するフォントは自分で同梱し、それらを埋め込むためのライセンスも確認してください
PDFlibPasはDelphi、C++Builder、LazarusのPDFライブラリで、対応するDLLおよびActiveXインターフェースも備えているため、フォールバックと欠落グリフのAPIはPascal以外の呼び出し元からも利用できます。完全なドキュメントはPDFlibPas Delphi PDFライブラリページにあります