PDFium Componentを使うと、Delphiアプリケーションは、PDFが埋め込んでいないフォントを参照している場合に、どのフォントバイト列を使うかを自分で決められます。ConfigureSystemFontProviderはIPdfSystemFontProviderの実装をインストールし、これが、フェイス名、ウェイト、イタリックフラグ、文字セット、ピッチファミリーを備えた、PDFiumが発行するすべてのフォントマッピング要求を受け取り、使用すべきTrueType、TrueType Collection、またはOpenTypeのバイト列で応答します
これが存在する理由は、埋め込まれていないフォントがレンダリングのくじ引きになってしまうからです。Arialという名前を持ちながら何も埋め込んでいないPDFは、ワークステーションではArialでレンダリングされ、Linuxサーバーではメトリクス互換の代替フォントでレンダリングされ、ロックダウンされたコンテナイメージではホストのマッパーが見つけた何かでレンダリングされます。同じ請求書がそれぞれで異なって見え、改行位置がずれ、顧客はアーカイブされたコピーと一致しないドキュメントを受け取ることになります
なぜサーバーにフォントをインストールするだけではだめなのか?
それが答えになる場合もあり、そうであればそれを選んでください。しかし、よくある3つの状況ではうまくいきません。ライセンスによって、自動レンダリング用にサーバーへフォントをインストールすることが禁じられている場合があります。コンテナイメージは頻繁に再構築されるため、手作業でインストールしたフォントは次のデプロイで消えてしまいます。そして、規制対象のワークフローでは、レンダリングスタックがバージョン管理下の成果物から再現可能でなければなりませんが、マシン全体へのフォントインストールはそうではありません
プロバイダーは、この決定を自分のアプリケーション側に移すことで、この3つすべてに対応します。フォントは自分が管理するリソースとして配布され、マッピングポリシーはレビュー可能なコードとなり、同じバイナリはどこでも同一にレンダリングされます。何がたまたまインストールされているかに依存する部分が一切ないからです
プロバイダーをインストールする
設定は、ライブラリが読み込まれる前に行わなければなりません。PDFiumは初期化時にシステムフォント情報の構造体を受け取り、その後に配布したハンドルを保持し続けます。そのため、ドキュメントが開いている状態でプロバイダーを差し替えると、PDFiumがまだ保持しているフォントハンドルが無効になってしまいます。コンポーネントは、レンダリングを壊れたままにするのではなく、そのような差し替えをきっぱりと拒否します:
uses
PDFium;
type
TAppFontProvider = class(TInterfacedObject, IPdfSystemFontProvider)
public
function ResolveFont(const Request: TPdfSystemFontRequest;
out Font: TPdfSystemFontData): Boolean;
end;
function TAppFontProvider.ResolveFont(const Request: TPdfSystemFontRequest;
out Font: TPdfSystemFontData): Boolean;
var
Path: string;
begin
// 決定論的マッピング: フェイス名にウェイトとイタリックを加えたもので
// このリクエストに対して配布するファイルを決める
Path := MapFaceToBundledFile(Request.FaceName, Request.Weight,
Request.Italic, Request.Charset);
Result := Path <> '';
if not Result then
Exit;
Font.FaceName := Request.FaceName;
Font.FontData := LoadFileBytes(Path); // 完全なsfntまたはTTCのバイト列
Font.Charset := Request.Charset;
Font.TTCIndex := 0; // コレクション内のインデックス
end;
var
Policy: TPdfSystemFontPolicy;
begin
Policy := TPdfSystemFontPolicy.Default;
Policy.AllowDefaultFallback := False; // ホストがすべてを決定する
Policy.AllowFaceSubstitution := False; // 異なるフェイス名は拒否する
Policy.MaxFontBytes := 32 * 1024 * 1024;
Policy.MaxCacheEntries := 64;
ConfigureSystemFontProvider(TAppFontProvider.Create, Policy);
// ここで初めてライブラリを読み込みドキュメントを開く
end;
終了処理は逆の順序で行われます。まずプロバイダーをPDFiumから切り離し、その後にライブラリをアンロードします。この切り離しを省略すると、ネイティブなフォントハンドルが、まもなく解放されようとしているPascalオブジェクトを指したまま残ってしまいます。これは、参照カウント式のインターフェースとC言語のライブラリを混在させるコードでよくある、典型的なシャットダウン時のアクセス違反です
ポリシーフラグが実際に決めていること
AllowDefaultFallbackは、2つの動作モードを切り替えるスイッチです。これをオフにすると、プロバイダーが拒否した要求は単純に失敗します。これは、コーパス内のすべてのフォントが説明できることを証明したい場合に望ましい挙動です。取りこぼしがあれば、覆い隠されることなく即座に見えるようになります。これをオンにすると、解決できなかった要求はFPDF_GetDefaultSystemFontInfoが返すマッパーに委譲されます。その間も外部からは、フェイス名、文字セット、テーブルデータ、フォント削除処理が発生元に応じて正しく振り分けられる、1つの統一されたハンドルラッパーとして見え続けます
AllowFaceSubstitutionは、プロバイダーが要求されたものとは異なるフェイス名で応答してよいかどうかを制御します。これをオフにすると、フォントの置換は偶然ではなく明示的な決定になります。これは、ドキュメントが指定するフォントのメトリクスが、ページ割りを変えてしまうほど異なる場合に重要になります
コンポーネントは、プロバイダーの応答がPDFiumに届く前にすべて検証します。空のデータは拒否され、規定より大きいフォントはMaxFontBytesに照らして拒否され、TTCのインデックスはチェックされ、PDFiumがファイル全体ではなく個々のsfntテーブルを求めてきた場合には、フォントディレクトリからそのテーブルだけが提供されます。この最後の機能があるおかげで、プロバイダーは完全なフォントファイルを渡すだけでよく、生のPascalオブジェクトをC ABI越しに公開する代わりに、テーブルレベルの問い合わせへの応答はコンポーネント側に任せられます
宙ぶらりんのフォントデータを作らないキャッシュ
フォントマッピングの要求はレンダリング中に絶えず繰り返されるため、応答はフォント選択のあらゆるパラメータを網羅するキーでキャッシュされ、有界のLRU(最近最も使われていない順)で追い出されます。ここでの微妙な点は生存期間です。キャッシュエントリがちょうど追い出されたフォントのバイト列を、PDFiumがまだ読み込んでいる最中かもしれないのです
キャッシュは参照カウント式の動的配列を保持し、各ネイティブハンドルは独自のスナップショットを保持しています。そのため、追い出しは使用中のメモリを解放するのではなく、参照カウントを1つ減らすだけです。削除コールバックはハンドルを解放し、有効数のカウントを維持します。実務上これは、進行中のレンダリングの足元からデータを引き抜いてしまうリスクなしに、MaxCacheEntriesをメモリの都合に合わせて調整できることを意味します
プロバイダーは自分のスレッドで呼び出されるのか?
いいえ、必ずしもそうとは限りません。PDFiumは自身のワーカースレッドからマッパーを呼び出すことがあるため、実装はスレッドセーフでなければなりません。共有カウンター、キャッシュ、設定の監視は、それぞれコンポーネント内部で独自のクリティカルセクションによって保護されていますが、ResolveFontの内部のコードを安全にするのは利用側の責任です
最も安全な形は、変更可能な共有状態に一切触れないプロバイダーです。起動時に構築したテーブルから読み取り、ファイルやリソースからバイト列を読み込み、返す、というだけです。独自の共有キャッシュを使うルックアップが必要であれば、それを保護してください。また、例外は自分の実装の内部にとどめてください。Pascalの例外がPDFiumのスタックを巻き戻して伝播することは決してあってはならないからです。コンポーネントはC ABIの境界で例外を捕捉し、失敗または任意のデフォルトフォールバックに変換しますが、それを通常の制御フローとしてあてにすると、パフォーマンスを損ない、バグを覆い隠すことになります。コンポーネントの他の部分におけるスレッド処理の規則は、レンダリングロックの規律で説明したのと同じ原則に従っています
本番環境でマッピングを証明する
統計情報は、フォント置換を当てずっぽうから、検証可能なものへと変えてくれます。GetSystemFontProviderStatisticsは、プロバイダーが設定・インストールされているかどうか、何件のマッピング要求があったか、それらがどのように満たされたか(キャッシュヒット、プロバイダーヒット、デフォルトフォールバックヒットに分けて)を、拒否された応答、失敗した要求、有効なハンドル数、キャッシュ済みフォント数とともに報告します:
var
Stats: TPdfSystemFontStatistics;
begin
Stats := GetSystemFontProviderStatistics;
Writeln(Format('requests=%d cache=%d provider=%d fallback=%d',
[Stats.MapRequests, Stats.CacheHits, Stats.ProviderHits,
Stats.DefaultFallbackHits]));
Writeln(Format('rejected=%d failed=%d handles=%d cached=%d',
[Stats.RejectedProviderResponses, Stats.FailedRequests,
Stats.ActiveHandles, Stats.CachedFonts]));
// フォールバックを無効にした適合性検証の実行では、フォールバックヒットや
// 失敗した要求が1件でもあれば、配布していないフォントをドキュメントが参照したことを意味する
if (Stats.DefaultFallbackHits > 0) or (Stats.FailedRequests > 0) then
raise Exception.Create('unmapped font encountered - update the font set');
end;
RejectedProviderResponsesのカウントが増加していくのは、プロバイダーがポリシーの拒否するデータで応答しているという兆候であり、たいていはサイズ超過のファイルか置換されたフェイスが原因です。これらの要求は静かにフォールバックまたは失敗へと劣化するため、アラートを設定する価値があります。マッピングテーブルを構築する前に、あるドキュメントが実際にどのフォントを必要としているかを診断するには、PDFフォントプロパティの分析で説明した検査ルートが、ドキュメントごとに埋め込みフォントと非埋め込みフォントを一覧表示します
フォントの供給、レンダリング、テキスト抽出は、Delphi、C++Builder、Lazarusにおいて同じライブラリインスタンスを共有します。デプロイの詳細についてはPDFium Component for Delphiページで説明しています