DelphiとLazarus向けPDFium Componentでは、TPdf.Active := Trueの代入は、PDFのロードが失敗しても決してraiseしません。TPdf.SetActiveがすべての例外をcatchして、コンポーネントを非アクティブのままにするからです。本当のエラーを見るには、代わりにTPdf.LoadDocument(Options, Report)を呼びます。このオーバーロードは元の例外を再raiseし、ロードステータス、ネイティブのPDFiumエラーコード、クロスリファレンステーブルの再構築が要ったかどうかを、TPdfLoadReportに詰めます
問題はたいていバッチコードで顔を出します。テーブル抽出ジョブが、現実世界のPDF 13個が入ったフォルダを、共有のTPdf1個で巡回し、7個が失敗として戻ってくる。7個のどれも実際には壊れていません。ロードを囲むexceptブロックは決して発火せず、ログは間違ったファイル名を名指し、最初に見えるエラーは非アクティブなコンポーネントについての素のEPdfErrorで、本当に失敗したロードの数行後のプロパティ読み取りからraiseされています。2つの別個の挙動が積み重なってこの絵を作っており、どちらも設計どおりに動いています
PDFのロードが失敗してもTPdf.Active := Trueがraiseしない理由
TPdf.SetActiveはLoadDocumentを、あらゆる例外クラスを飲み込んでコンポーネントを非アクティブのままにするだけのtry..exceptで包みます。飲み込みは意図的です。同じsetterは、フォームデザイナがIDEでActiveを切り替えるときにも走りますし、悪いパスでIDEを落としてはいけません。実行時、TPdf.Activeはネイティブの文書ハンドルが存在するかを報告するだけで、失敗したロードの後はFalseを読み、他には何も起こりません。raiseされたものは何であれ消えます。パーサーからのEPdfErrorでも、ストリームエラーでも、半分だけバインドされたpdfium.dllからのEAccessViolationでも。Delphiでのpdfium.dllロード失敗の診断で述べた詳細なDLLメッセージは、それらを飲み込まない呼び出しを通じてだけ、あなたのハンドラへ届きます
Pdf.FileName := FileName;
try
Pdf.Active := True; // SetActiveはロード例外をすべて飲み込む
except
on E: Exception do
Log.Add(FileName + ': ' + E.Message); // 決して実行されない
end;
// 失敗は代わりにここで、一般的なEPdfErrorとして表面化する:
// 'Cannot perform this operation on an inactive Pdf1 component'
Log.Add(Format('%s: %d pages', [FileName, Pdf.PageCount]));
// 既存コードへの最小修正:代入の直後にActiveをテストする
// v3.122.1以降、LastLoadReportが飲み込まれたエラーのテキストを保持する
Pdf.Active := True;
if not Pdf.Active then
Log.Add(FileName + ': load failed: ' + Pdf.LastLoadReport.ErrorMessage);
失敗はついに、最初のガード付き呼び出しで姿を見せます。TPdf.PageCountは、ほとんどの文書プロパティと同じくCheckActiveから始まり、コンポーネントは名指すものの、ファイルも原因も名指さないEPdfErrorをraiseします。代入の直後にPdf.Activeをテストすれば、誤帰属するクラッシュが、正直な「failed」エントリに変わります。PDFiumPas v3.122.1より前は、原因はその時点で消えていました。v3.122.1以降、失敗した代入はLastLoadReportを、エラーテキストを運ぶplsFailedレポートで置き換えるので、原因は生き残ります。例外オブジェクトそのものとバイトレベルの監査には、それでも別の入り口が要ります
1つのTPdfの使い回しが2つ目のファイルから失敗する理由
TPdf.FileNameはコンポーネントが非アクティブの間にしか代入できません。だから共有インスタンスは、2つ目のファイルをロードしようとするより前に拒否します。TPdf.SetFileNameはCheckInactiveから始まり、同じガードがPasswordとFormFillも守ります。最初のロードが成功した後もインスタンスはアクティブのまま、次の代入はraiseし、バッチループがその例外をcatchして先へ進むと、エラーは新しいファイル名の下に着地します。一方、旧文書はまだ開いたままです。飲み込まれたロード失敗と混ざると、ログは現実と一致しなくなります。13ファイルの再現では、共有インスタンスは7失敗を報告し、文書ごとの新しいTPdf.Create(nil)は全部の13を開きました。ファイル間でActive := Falseを設定するのも有効ですが、文書ごとに1インスタンスなら、構造的にすべてのファイルが隔離されます
TPdfLoadReport付きのTPdf.LoadDocumentが得られるもの
TPdf.LoadDocument(const Options: TPdfLoadOptions; out Report: TPdfLoadReport)は本当の例外をraiseし、さらに何が起きたかを構造化された形で教えます。ファイルのオーバーロードはFileNameをロードし、兄弟のオーバーロードはTBytesやポインタ+サイズを取り、ストリームはLoadCustomDocument(AStream, AOwnsStream, Options, Report)が担当します。どれもオプションを検証し、インスタンスが非アクティブかを確認し、ヘッダ、startxref、xrefセクション、%%EOFマーカーのバイトレベル監査を走らせ、それからネイティブロードを実行します。監査は、信頼できないPDFのためのパーサーリソース予算で述べたのと同じ種類の上限に括られます。TPdfLoadOptions.DefaultはAuditByteLimitを256 MiB、MaxIssuesを256、MaxXrefSectionsを1024、MaxXrefEntriesを4,000,000に設定します。失敗時、メソッドはReport.Status := plsFailedを設定して再raiseします。Reportはその場に書かれるので、中身は例外を生き延び、コピーがTPdf.LastLoadReportへ格納されます
レポートのフィールドは、バッチログが実際に必要とする問いに答えます。StatusはplsNotAttempted、plsLoaded、plsLoadedWithRecovery、plsRejected、plsFailedのいずれかです。NativeErrorCodeはFPDF_GetLastErrorを保持します。FPDF_ERR_PASSWORD(4)が、FPDF_ERR_FORMAT(3)として報告された破損ファイルから、パスワードの欠落や誤りを切り分けます。UsedRecovery、CrossReferenceTableValid、RecoveryRouteは、PDFiumがxrefテーブルの再構築を要したかを語り、Issuesは監査の発見をCode、Severity、Offset、ObjectNumber、MessageText付きで列挙します。MaxIssuesがリストを打ち切ったときはIssuesTruncatedが立ちます
uses
SysUtils, Classes, TypInfo, FPdfView, PDFium;
procedure ProcessBatch(Files, Log: TStrings);
var
I: Integer;
Pdf: TPdf;
Options: TPdfLoadOptions;
Report: TPdfLoadReport;
begin
Options := TPdfLoadOptions.Default(plmCompatible);
for I := 0 to Files.Count - 1 do
begin
Pdf := TPdf.Create(nil); // 文書ごとに1インスタンス
try
Pdf.FileName := Files[I];
try
Pdf.LoadDocument(Options, Report);
except
on E: Exception do
begin
// LoadDocumentがraiseしてもReportは埋まっている
if Report.NativeErrorCode = FPDF_ERR_PASSWORD then
Log.Add(Files[I] + ': password required')
else
Log.Add(Format('%s: %s (%s)', [Files[I],
GetEnumName(TypeInfo(TPdfLoadStatus), Ord(Report.Status)),
E.Message]));
Continue;
end;
end;
if Report.UsedRecovery then
Log.Add(Files[I] + ': opened after PDFium rebuilt the xref table');
ExtractTables(Pdf, Log);
finally
Pdf.Free;
end;
end;
end;
plmStrictでロードすべきとき
黙って修復されたファイルが、拒否されたファイルより困る場面ではplmStrictを使います。アーカイブの受け入れ、証拠の扱い、署名パイプラインなどです。PDFiumは壊れたクロスリファレンステーブル(ISO 32000-1 §7.5.4)を、ファイルからオブジェクトを探して黙々と再構築します。ビューアには有り難い挙動ですが、与えられたバイトをそのまま処理せねばならないものにとっては問題です。ネイティブロードの後、コンポーネントはFPDF_DocumentHasValidCrossReferenceTableを尋ねます。plmCompatibleモードでは、再構築はplsLoadedWithRecoveryとplicNativeCrossReferenceRebuild警告になります。plmStrictモードでは、コンポーネントは文書をアンロードし、plsRejectedを設定し、plicStrictModeRejectedを加え、「Strict PDF load rejected the document」付きのEPdfErrorをraiseします。厳格モードは監査エラーも拒否し、TPdfLoadOptions.Default(plmStrict)はRequireFinalEndOfFileMarkerを有効にします。最後の%%EOFの欠落や、その後ろのデータ(§7.5.5)が、警告からエラーへ昇格するわけです。xref監査は、PDFium VCLでオブジェクトストリームとxrefストリームを検証するのオブジェクトレベル検査を補完します
function AcceptForArchive(const FileName: string; out Reason: string): Boolean;
var
Pdf: TPdf;
Report: TPdfLoadReport;
I: Integer;
begin
Result := False;
Reason := '';
Pdf := TPdf.Create(nil);
try
Pdf.FileName := FileName;
try
Pdf.LoadDocument(TPdfLoadOptions.Default(plmStrict), Report);
Result := True; // xref有効、監査エラーなし
except
on E: EPdfError do
begin
Reason := E.Message;
for I := 0 to High(Report.Issues) do
if Report.Issues[I].Severity = plisError then
Reason := Reason + sLineBreak + Format(' at offset %d: %s',
[Report.Issues[I].Offset, Report.Issues[I].MessageText]);
end;
end;
finally
Pdf.Free;
end;
end;
TPdf.LastLoadReportが真実を語らなくなる場所
TPdf.LastLoadReportが完全になるのは、オプションを取るLoadDocumentオーバーロードの後だけです。バイト監査を走らせるのはそれらのオーバーロードだけだからです。成功したActive := Trueはバイト監査なしのcompatibleモードレポートを書くので、AuditAttemptedはFalseのままです。PDFiumPas v3.122.1より前、失敗したものは何も書かず、共有インスタンスではLastLoadReportが前のファイルを述べ続けました。しばしば安心感のあるplsLoaded付きで。v3.122.1以降、失敗したロードはすべてレポートを置き換えます。raiseせずにコンポーネントを非アクティブのままにする失敗したActive := Trueも、失敗した素のLoadDocumentやLoadCustomDocumentの呼び出しも、エラーテキスト付きのplsFailedを記録します。これも監査なしです。実務で効いてくる隙間がもう2つあります。オプション検証とCheckInactiveはレポートの初期化より前に走るので、負のAuditByteLimitや、すでにアクティブなインスタンスは、レポートを出さずにraiseします。そしてNativeErrorCodeに意味があるのは、PDFiumが実際にパースを試みたときだけです。存在しないファイルでは、ラッパーはPDFiumが走る前にraiseするので、代わりにErrorMessageと例外テキストを記録してください
実務の規則は短いです。Active := Trueは、非アクティブなコンポーネントが許容できる結果である、デザイナに束縛されたビューアのために取っておく。それ以外の場所、とりわけバッチとサーバーのコードでは、文書ごとにTPdfを1個作り、LoadDocument(Options, Report)を呼び、raiseされる例外をcatchして、Report.Status、NativeErrorCode、エラーレベルのIssuesをファイル名と一緒に記録します。費用は呼び出し箇所あたり数行で、すべての失敗が本当の原因付きで、正しいファイルに帰着します
ロードレポートAPI、厳格モード、バイトレベル監査は、Delphi、C++Builder、Lazarus向けPDFium Componentに搭載されています。レンダリング、テキスト抽出、フォーム入力、PDF/A検証とともにです