Delphi向けPDFium Componentでは、TPdfLibraryConfigurationのBrotliEnabledやIsolatePerDocumentをオンにすると、エラーも出さずバンドルされたSkiaビルドがAGGレンダラーへ切り替わっていました。どちらのオプションもFPDF_LIBRARY_CONFIGを、PDFiumがm_RendererTypeを文字どおり読むバージョンまで引き上げるからです。v3.123.0からはデフォルトのレンダラーがDLL自身のデフォルトのままであり、v3.125.0からは、DLLが応えられないSkiaやFontationsの要求はプロセスを殺す代わりに、キャッチ可能なEPdfErrorを上げます
どちらのバグも自己申告しませんでした。1つ目は、見た目は問題ないページを作りました。ただ、出荷してテストしたビルドと違うラスタライザーで描かれていて、アンチエイリアスとテキストの縁がわずかに違うだけです。2つ目は自己申告しました。大音量で。ネイティブの初期化の内側からホストプロセスを道連れにすることで。両方とも同じ場所から来ています。バージョン番号が言うまでは数えられないフィールドを持つ、バージョン付きのC構造体であり、そのゼロ値は「未設定」ではなく本物の選択だという場所です
FPDF_LIBRARY_CONFIGはどのレンダラーを使うかをどう決めるのか
FPDF_InitLibraryWithConfigがm_RendererTypeを参照するのは、構造体のVersionフィールドが4以上のときだけです。そしてそのバージョンからは、値を書かれた通りに使います。バージョン4未満ではPDFiumはこのフィールドを無視してビルドデフォルトを選びます。PDF_USE_SKIA付きでコンパイルされたビルドではSkia、それ以外ではAGGです
それ以降の各フィールドも同じパターンに従います。構造体は1能力ずつ成長し、各能力は新しいバージョン番号と一緒にやって来ました。PDFium ComponentはLoadLibrary内で、あなたのTPdfLibraryConfigurationからネイティブ構造体を組み立て、設定したオプションが要求する分だけバージョンを上げます
| 構造体バージョン | 追加するフィールド | 設定するもの |
|---|---|---|
| 2 | m_pIsolate、m_v8EmbedderSlot | 常に書き込む。V8Isolate、V8EmbedderSlot |
| 3 | m_pPlatform | V8Platformがnilでない |
| 4 | m_RendererType | prpDefault以外のRenderer |
| 5 | m_FontLibraryType | pfbpDefault以外のFontBackend |
| 6 | m_BrotliEnabled | BrotliEnabled = True |
| 7 | m_IsolatePerDocument | IsolatePerDocument = True |
罠は最後の2行にあります。バージョンは累積です。バージョン6の構造体は、バージョン4でもありバージョン5でもあります。だからBrotliだけを頼んだのに、PDFiumはm_RendererTypeとm_FontLibraryTypeを読みます。その瞬間にこの2つのフィールドに座っているものが何であれ、それがレンダラーとフォントバックエンドになります。選ぶつもりがあったかなかったに関係なく
Brotliを有効にすると、なぜレンダラーがAGGへ切り替わったのか
v3.123.0より前、PDFium ComponentはprpDefaultに対しFPDF_RENDERERTYPE_AGGをm_RendererTypeへ書き込んでいました。だから構造体をバージョン6か7へ押し上げる設定はすべて、SkiaビルドにAGGを強制しました。コンポーネントと一緒に出荷されるpdfium.dllとpdfium.v8.dllランタイムはSkiaビルドなので、これは例外的な環境ではなく、デフォルトの配備を直撃しました
このマッピングは、書かれたときは無害に見えました。バージョン2か3ではフィールドは決して読まれないので、prpDefaultは本当に「DLLが何をするかでも」を意味していました。BrotliEnabled(バージョン6)かIsolatePerDocument(バージョン7)が現れた瞬間、同じコードが「好みなし」を明示的なAGGの要求へ変えたのです。何も失敗しませんでした。PDFiumは普通に初期化し、全ページを描画し、エラーコードを返しませんでした。PDFiumの視点では、呼び出し側がAGGを頼んでAGGを受け取ったのだから
ピクセルハッシュなら、スクリーンショットでは見えない場所でスワップが見えます。同じサンプルドキュメントの最初のページを3つの設定で描画すると:
- デフォルト設定:ハッシュ
502D77C3711B4ACF BrotliEnabled= TrueでRendererはprpDefaultのまま:ハッシュF75B5EB4728ADE87- 明示的な
prpAgg:ハッシュF75B5EB4728ADE87。Brotli実行と同一
v3.123.0の修正は公開関数PdfNativeRendererTypeで、TPdfRendererPreferenceを、m_RendererTypeへ書き込まれる値へ解決します。prpAggとprpSkiaは1対1に対応します。prpDefaultは今、ロードされたDLLがFPDF_RenderPageSkiaをエクスポートしていればSkiaへ、そうでなければAGGへ写像します。このエクスポートはSkiaデフォルトそのものと同じPDF_USE_SKIA条件でコンパイルされるので、DLLの外から観測できる唯一のビルド特性です。修正後、Brotli設定はデフォルト設定と同じハッシュを生みます
フォントバックエンドに同じ問題はありませんでした。m_FontLibraryTypeはバージョン5から読まれ、そのゼロ値FPDF_FONTBACKENDTYPE_FREETYPEは、フィールドがまったく読まれないときのPDFiumのデフォルトでもあります。だからpfbpDefaultにFreeTypeを書き込むことは、ネイティブのデフォルトを正確に再現します。ゼロ値は常に間違いなのではなく、自動的に正しいことは決してない、というだけです
v3.123.0以降なら、自然に書く起動コードが書かれた通りに動きます:
uses
PDFium;
procedure ConfigurePdfiumAtStartup;
var
Config: TPdfLibraryConfiguration;
begin
// ネイティブライブラリをロードするものより先に走らせること
Config := TPdfLibraryConfiguration.Default;
Config.BrotliEnabled := True; // FPDF_LIBRARY_CONFIGをバージョン6へ上げる
// RendererはprpDefaultのまま:FPDF_RenderPageSkiaをエクスポートする
// ビルドではSkiaへ、AGG専用ビルドではAGGへ解決される
SetLength(Config.UserFontPaths, 1);
Config.UserFontPaths[0] := 'C:\ProgramData\MyApp\Fonts';
ConfigurePdfLibrary(Config);
end;
BrotliEnabledがPDF 2.0の/BrotliDecodeストリームを復号可能にするのは、DLL自体がPDF_ENABLE_BROTLI付きでビルドされているときだけだという点も覚えておいてください。フラグは要求であり、Brotli対応のないビルドでは効果がありません。TPdfLibraryConfiguration.Hardenedは、AllowMachineTimeがFalseになる点を除けばDefaultと同じです。ドキュメントのJavaScriptが実際の時計を読むのを阻みます。信頼できないファイルのサーバー側処理の出発点として理にかなっています
DLLが含まないバックエンドを要求すると何が起きるのか
PDFiumは、ビルドに存在しないレンダラーやフォントバックエンドに対してエラーを返しません。FPDF_InitLibraryWithConfigがネイティブのCHECKに落ちます。Windowsではブレークポイント例外として表面化し、呼び出しの周りに構造化例外ハンドラーがなければプロセスを終了させます。ヘッダーはまさにそのことを、未対応の値は「同様に即座にクラッシュで失敗する」と警告して言っています
具体的な2ケースは、FPDF_RENDERERTYPE_SKIAを受け取るAGG専用ビルドと、Fontationsを持たないビルドが受け取るFPDF_FONTBACKENDTYPE_FONTATIONSです。バンドルされたSkiaランタイムは2番目のグループです。Skiaで描画しつつ、フォントはFreeTypeを使います。これに対しprpSkiaとpfbpFontationsを一緒に要求すると、Delphi側ではExternal exception 80000003が出ました。デバッガーや例外ハンドラーがたまたま捕まえたとしても、状況はまだ復元不可能です:
- PDFiumは半分初期化されたまま置き去りにされます
- プロセス全体の設定はすでに封印済みなので、
ConfigurePdfLibraryは修正された設定を拒みます - 同じプロセスで別の設定で再試行することはもうできません
これはBrotliのバグの正反対の失敗です。あちらでは、フィールドが誰も選んでいない値を持ち、PDFiumが黙って受け入れました。こちらでは、フィールドが呼び出し側が意図的に選んだ値を持ち、PDFiumはそれについての議論を一切受け付けません。どちらも、ラッパーがネイティブ呼び出しの前に解決せねばならない問題です。呼び出しの後には、捕まえるものが何も残らないからです
PDFium ComponentはSkiaとFontationsをどう事前チェックするのか
v3.125.0から、LoadLibraryはDLLのエクスポートを結合した後、FPDF_InitLibraryWithConfigを呼ぶ前に設定を検証し、未対応のレンダラーやフォントバックエンドを、問題の設定と代替を名指すメッセージ付きのEPdfErrorへ変えます。DLLはアンロードされ、設定は封印解除されるので、呼び出し側は別の設定を選んで再ロードできます
判定そのものは純粋関数PdfLibraryConfigurationSupportErrorにあります。設定と、ビルドを記述する2つのブール値を取り、組み合わせが安全なら空文字列を返します。ネイティブの状態に触れないので、任意の能力の組み合わせで自分のテストから呼べます。LoadLibraryの中では、2つのブール値は異なる種類の証拠から来て、異なる信頼度に値します:
- Skiaは
FPDF_RenderPageSkiaエクスポートの存在から検出します。PdfNativeRendererTypeが使うのと同じ合図です。エクスポートとSkiaレンダラーは1つの条件でコンパイルされるので、チェックは正確です - Fontationsには自分のエクスポートがありません。残る痕跡は、バイナリへ引き込まれたRustのフォントクレートだけです。だからPDFium Componentは、ロードされたライブラリファイルをクレート名
skrifaとread-fonts(read_fontsも)で走査します。走査はpfbpFontationsが要求されたときだけ走り、読めないファイルは「Fontationsなし」と数えます
Fontationsのチェックはヒューリスティックで、1方向に間違い得ます。それらの文字列をすべて剥ぎ取られたFontationsビルドは、動いたはずなのに拒否されます。このトレードは意図的に選ばれました。偽の拒否のコストは、キャッチできる例外とFreeTypeへのフォールバックです。偽の受け入れのコストはプロセスです
封印解除はチェックと同じくらい重要です。LoadLibraryはロードのごく最初に設定を封印するので、リセットがなければ、能力の拒否の後もConfigurePdfLibraryはあらゆる再試行にEPdfErrorの「PDFium library configuration is already sealed」で答え続けます。拒否の経路は最初にUnloadLibraryを呼びます。この時点でのFPDF_DestroyLibrary呼び出しは安全です。PDFiumはまだ初期化されておらず、すぐ戻るからです。DLLの欠落やアーキテクチャーの不一致のような他のロード失敗は封印を保つので、再試行ループは両者を区別せねばなりません:
uses
SysUtils, PDFium;
function StartPdfiumPreferringSkia: TPdfRendererPreference;
var
Config: TPdfLibraryConfiguration;
begin
Config := TPdfLibraryConfiguration.Default;
Config.Renderer := prpSkia;
ConfigurePdfLibrary(Config);
try
PDFium.LoadLibrary; // ユニット修飾:Windows.LoadLibraryと同名のため
Result := prpSkia;
except
on E: EPdfError do
begin
// 機能の拒否はDLLをアンロードし、設定を封印解除する。
// まったくロードできなかったDLLは封印されたまま:再試行しても無駄
if PdfLibraryConfigurationSealed then
raise;
Config.Renderer := prpAgg;
ConfigurePdfLibrary(Config);
PDFium.LoadLibrary;
Result := prpAgg;
end;
end;
end;
明示的なPDFium.LoadLibraryに注目してください。WindowsやWinapi.Windowsも使うユニットでは、修飾なしのLoadLibraryはuses節で最後に現れるユニットへ解決します。それがWin32関数なら、引数の数のエラーで、パラメータなしの呼び出しはコンパイルできません。エラーメッセージはPDFiumについて何も語りません
さらに早く行われる検証
ConfigurePdfLibraryは、DLLがまだ登場しないうちから一部の組み合わせを拒みます。すべてEPdfErrorです。明示的なFontBackend(pfbpFreeTypeを含む)にはRenderer = prpSkiaが要ります。PDFiumがフォントバックエンドを参照するのはSkiaレンダラーのためだけだからです。IsolatePerDocumentにはV8Isolateがnilであることが要ります。PDFiumはドキュメントごとに自分のisolateを作り、あなたが1つ渡すとネイティブのCHECKに落ちるからです。UserFontPathsの空文字列は拒否されます。そして最初のロード試行より後の呼び出しは、すべて「PDFium library configuration is already sealed」で失敗します
この最後のルールには実務上の帰結があります。DLLを先に調べて後から設定することはできません。GetSkiaRenderCapabilities、V8FeaturesAvailable、ドキュメントを開く操作、その他ほとんどのエントリーポイントは内部的にLoadLibraryを呼び、その場で設定を封印します。後からUnloadLibraryを呼んでも再オープンしません。設定し、それからロードし、それから質問する。診断ルーチンが従うべき順序はまさにこれです:
uses
SysUtils, PDFium, FPdfView;
function DescribePdfiumState: string;
var
Config: TPdfLibraryConfiguration;
Renderer: string;
begin
Config := GetPdfLibraryConfiguration; // コピーなので、検査して安全
if not PDFium.Loaded then
begin
if PdfLibraryConfigurationSealed then
Exit('PDFium failed to load; configuration is sealed');
Exit('PDFium not loaded; configuration can still change');
end;
// LoadLibraryがFPDF_LIBRARY_CONFIGを組み立てたときと同じ解決
if PdfNativeRendererType(Config.Renderer,
GetSkiaRenderCapabilities.PageRender) = FPDF_RENDERERTYPE_SKIA then
Renderer := 'Skia'
else
Renderer := 'AGG';
Result := Format('Renderer=%s Brotli=%s IsolatePerDocument=%s',
[Renderer, BoolToStr(Config.BrotliEnabled, True),
BoolToStr(Config.IsolatePerDocument, True)]);
end;
起動時にこの1行をログするのは安く、そして「サーバーでテキストの見た目が違う」というサポートチケットで最初に欲しいものです。PDFium.Loadedが修飾されているのはLoadLibraryと同じ理由です。フォームやコンポーネントのメソッドの中では、裸のLoadedはTComponent.Loadedへ結合します
バージョン付きC設定構造体が壊れる2つの道
バージョン付きの設定構造体は、FPDF_LIBRARY_CONFIGであれ、Win32のcbSizeレコードであれ、プラグインのABIであれ、対称な2つの道で失敗します。ラッパーは両方に備えねばなりません。1つ目は、バージョンを低いままにしたままフィールドを埋めること。2つ目は、ライブラリが意図的な選択として読むゼロ値のフィールドを残したままバージョンを上げることです
- フィールド設定済み、バージョン低すぎ。バージョン2の構造体へ
m_BrotliEnabled= 1を書いても、PDFiumは一度も見ません。呼び出しは成功し、Brotliストリームは復号不能のままです。防御は、ハードコードする代わりに、実際に使うフィールドからバージョンを導くこと。LoadLibraryがやっているのはそれです - バージョンは十分、ゼロフィールドに意味がある。バージョンを6へ上げると、バージョン6までのすべてのフィールドが生きます。
FillCharはm_RendererTypeをFPDF_RENDERERTYPE_AGGへゼロ化します。これは本物のレンダラーであって「未設定」ではありません。防御は、選んだバージョンがカバーするすべてのフィールドを意図的な値で書くこと、そして「デフォルト」を想定する代わりに実際のビルドに対して解決することです
呼び出し先をクラッシュさせ得る値には3つ目のルールが続きます。呼び出しの前に、そのバイナリが何をできるかに対して検証する。利用できる最強の証拠を使い、その証拠がヒューリスティックであるときは、コードでもドキュメントでも正直に言う。エクスポートされたシンボルは証明です。文字列テーブルの中のクレート名は、かなり良い推測です
クイックリファレンス:PDFium Componentのライブラリ設定
ConfigurePdfLibraryは1回、何かがDLLをロードするより前に呼ぶ。どんな能力の問い合わせやドキュメントのロードも封印しますBrotliEnabledやIsolatePerDocumentを設定し、バンドルランタイムからSkia出力を期待しているなら、v3.123.0以降へ- 特定のラスタライザーが必要でないなら
RendererはprpDefaultのままに。今はどの構造体バージョンでもビルドデフォルトへ解決します - 実際にどのレンダラーが動いているかログするには、
PdfNativeRendererTypeをGetSkiaRenderCapabilities.PageRenderと一緒に使う - v3.125.0以降なら、AGG専用DLLでの
prpSkiaや非Fontations DLLでのpfbpFontationsは、クラッシュではなくEPdfErrorを期待する - 能力の拒否の後は
PdfLibraryConfigurationSealedがFalseで再設定できます。DLLのロード失敗の後はTrueのままです - Fontationsの検出はヒューリスティックとして扱い、FreeTypeへのフォールバックを保つ
- Win32と
TComponentの名前衝突を避けるため、PDFium.LoadLibraryとPDFium.Loadedはユニット名付きで書く
設定以前にDLLが失敗するなら、まずDelphiでのPDFium DLLロード失敗の診断から、そしてコンポーネントが各プラットフォームで正しいバイナリをどう見つけるかは任意ターゲットでのPDFiumネイティブライブラリのロードを。レンダラーが決まったら、レンダーキャッシュと滑らかなズームの戦術がビューアーでページ描画を速く保つ方法を扱います
PDFium Componentは、このような設定チェック付きでDelphiとC++BuilderのためにPDFiumエンジンをラップします。ネイティブの初期化は、プロセス終了ではなく処理可能なPascal例外として失敗します。製品の詳細とダウンロードは、PDFium Component for Delphi product pageにあります