1つのTHotPDFインスタンスは、いくつでもドキュメントを続けて書き出せます。EndDocの後のBeginDocは、新しい独立したドキュメントを開始します。1インスタンスの複数ドキュメント再利用はHotPDF v2.764.0から利用できます。メッセージPlease load the document before using BeginDocは、隣のガードに属するものです。BeginDocで開始したドキュメントがまだEndDocで閉じられていないとき、LoadFromFileがこのメッセージを上げます。どちらの挙動もコンポーネントのライフサイクルから導かれるもので、ライフサイクルが分かればどちらも謎ではなくなります
THotPDFインスタンスは1つのドキュメントか、ドキュメントファクトリか
THotPDFインスタンスはドキュメントファクトリです。1度だけ作成して、たとえばフォームに置いたコンポーネントとして、データベース接続を開いたままクエリを次々と実行するのと同じように、ドキュメントを次々と供給できます。BeginDocはドキュメントを開き、インスタンスに進行中のドキュメントがあることをマークします。EndDocはすべてをFileNameへシリアライズし、そのファイル用に登録されたすべてを破棄します。画像、フォント、フォームフィールド、アウトライン、ドキュメントJavaScript、名前付きデスティネーション、ページラベル、ICCプロファイル、出力インテントを含みます。書き出されたドキュメントのページとオブジェクトは、次のBeginDocが解放するまで読み取り可能なままなので、ValidatePDFUA2Structure、CurrentPageNumber、PagesCountはEndDocの後も機能します。ReproducibleOutputをオンにしていれば、再利用したインスタンスから出した2つ目のドキュメントは、新しいインスタンスから出したそのドキュメントとバイト単位で同一になります
ライフサイクル、それが起こるべき順序で
HotPDFが最初から書き込むすべてのドキュメントは同じ4つのビートに従い、順序は交渉の余地がありません。Createはコンポーネントを割り当てます。BeginDocはドキュメントを開き、構造的な選択を固定するため、ファイル全体に影響を与えるもの(ページサイズ、圧縮、暗号化、出力ファイル名)はCreateとBeginDocの間に設定する必要があります。次に描画します。次にEndDocがディスクにバイトを書き込みます。Freeはインスタンスを解放します。BeginDocの前に配置された描画呼び出しは着地するページがなく、その後に割り当てられたドキュメント全体のプロパティは、文句なしに無視されます。再利用したインスタンスでは、FileName、Compression、PDFACompliance、Author、Titleのようなプロパティ設定は次のドキュメントに引き継がれるので、異なる場合は各BeginDocの前に再設定してください
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.FileName := 'invoice.pdf';
Pdf.BeginDoc; // ドキュメントを開きます
Pdf.CurrentPage.SetFont('Arial', [], 11);
Pdf.CurrentPage.TextOut(50, 760, 0, 'Invoice 2026-042');
Pdf.EndDoc; // invoice.pdf を書き込み、閉じます
finally
Pdf.Free; // 1 つのインスタンス、1 つのドキュメント
end;
end;
これを作業の単位として読んでください。1つのBeginDoc、1つのEndDoc、ディスク上の1つのファイル。2つ目のファイルは2つ目のBeginDocとEndDocのペアです。同じインスタンスでも、新しいインスタンスでも
「再利用」が意味するもの:1つのインスタンスか、ファイルごとの新しいインスタンスか
倹約版も機能します。コンポーネントを1回構築し、バッチをループし、ループ内でBeginDocとEndDocを呼び出します。各反復は独立したファイルを生成し、あるドキュメント用に登録されたものが次に流れることはありません。保護付き出力には1つの罠が残ります。EndDocはUserPasswordとOwnerPasswordをクリアするので、ActivateProtectionがまだオンなら、次のBeginDocの前にパスワードを再設定しなければなりません。そうしないと、空のパスワードでファイルを保護する代わりに例外が上がります。以下のバージョンは別の道を取り、各出力を独自の短命なオブジェクトとして扱います。コンポーネントの作成コストは、PDFをレイアウトしてシリアライズする作業に比べて微々たるものなので、どちらの道でもインスタンスを溜め込んで節約できるものはほとんどありません
procedure WriteBatch(const Names: TArray<string>);
var
I: Integer;
Pdf: THotPDF;
begin
for I := 0 to High(Names) do
begin
Pdf := THotPDF.Create(nil); // 各パスで新しいインスタンス
try
Pdf.FileName := Names[I] + '.pdf';
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('Arial', [], 12);
Pdf.CurrentPage.TextOut(50, 760, 0, 'Statement for ' + Names[I]);
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
end;
ループ内にあるtry/finallyは、レビューで擁護する価値のある部分です。BeginDocや描画の呼び出しが1つのドキュメントの途中で発生しても、その反復のインスタンスは次が始まる前に解放されるため、1つの不良レコードが半分構築のコンポーネントを立ち往生させ、残りの実行を台無しにすることはありません。Createをループの上に引き出す方法も機能しますが、例外で途中放棄されたドキュメントがあったら、次のレコードには新しいインスタンスを与える良い理由になります
既存のファイルの変更は別のエントリポイントです
完全に正当な「再利用」の2つ目の読み方があります。空白のドキュメントではなく、すでに存在するPDFを開いて変更したい場合です。そのパスはBeginDocをまったく通過しません。それがエラーメッセージがロードを名前付けするまさにその理由です。ファイルをロードし、編集し、選択した名前で保存します
var
Pdf: THotPDF;
PageCount: Integer;
begin
Pdf := THotPDF.Create(nil);
try
PageCount := Pdf.LoadFromFile('contract.pdf');
if PageCount > 0 then
begin
Pdf.CurrentPage.SetFont('Arial', [fsBold], 10);
Pdf.CurrentPage.TextOut(40, 30, 0, 'REVIEWED');
Pdf.SaveLoadedDocument('contract-reviewed.pdf');
end;
finally
Pdf.Free;
end;
end;
LoadFromFileはページ数を返し、ゼロ以下の値はロードが失敗したことを意味するため、CurrentPageに触れる前に確認する価値があります。ペアリングは重要です。LoadFromFileで開いたドキュメントは、BeginDoc/EndDocペアではなく、SaveLoadedDocumentで保存されます。このペアは、何もないところから作成するドキュメントに属しています。この2つを混在させることは、この記事の冒頭で述べたライフサイクルガードを引っかける最も一般的な方法です。2つのフローを頭の中で分離してください。BeginDoc ... EndDocは作成し、LoadFromFile ... SaveLoadedDocumentは編集します
ファイルロックの問題は現実であり、答えはビューアウィンドウを強制終了することではありません
ライフサイクルエラーはしばしば2つ目の不満と一緒に現れ、この2つは同じファイル再生成ワークフローで表面化するため、絡み合います。ユーザーが作成したばかりのPDFを開き、AcrobatまたはFoxitで開いたままにし、再構築をトリガーします。EndDocは同じパスへ書き込もうとしますが、ビューアがライターをブロックする読み取り共有を保持しているため、OSは拒否し、アクセス拒否エラーになります。これはコンポーネント状態の問題ではなく、純粋にWindowsのファイルロックの問題であり、回避策ではなく実際の答えが必要です
出回っている回避策、つまりトップレベルのウィンドウを列挙し、タイトルがPDFビューアのように見えるものにWM_CLOSEを投稿することは、間違った本能です。それはプロセスの境界を越えてプログラムが所有していないウィンドウを閉じ、タイトルテキストによってビューアを推測し、ユーザーの保存されていない注釈を尋ねることなく破棄する可能性があります。そのアプローチ全体を匂いとして扱ってください。信頼できる修正は、別のプロセスが保持している可能性のあるパスに決して書き込まないことです。同じディレクトリ内のテンポラリファイルにシリアライズし、EndDocが成功したらアトミックな名前変更で所定の場所にスワップします。ビューアがまだ古いファイルを開いている場合、名前変更はきれいに成功するか、大きな音を立てて失敗し、ロックと戦うのではなく明確なメッセージを表面化させます
uses
System.SysUtils, System.IOUtils;
procedure WritePdfAtomically(const FinalPath: string);
var
Pdf: THotPDF;
TempPath: string;
begin
// 一時ファイルは対象と同じディレクトリに置きます。同じ NTFS ボリューム内の
// rename は名前を原子的に交換しますが、ボリュームをまたぐ移動は
// コピー+削除に劣化し、その保証を失います
TempPath := TPath.Combine(TPath.GetDirectoryName(FinalPath),
TGUID.NewGuid.ToString + '.pdf.tmp');
try
Pdf := THotPDF.Create(nil);
try
Pdf.FileName := TempPath;
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('Arial', [], 11);
Pdf.CurrentPage.TextOut(50, 760, 0, 'Invoice 2026-042');
Pdf.EndDoc; // ここで一時ファイルがディスクに完成します
finally
Pdf.Free;
end;
// 所定の位置にスワップします。TFile.Move は上書きを拒否するため、古い
// ターゲットを先に削除します。ビューアが旧ファイルを保持していれば、
// 正常なバイトに触れる前に、削除が大きな音と共に失敗します
if TFile.Exists(FinalPath) then
TFile.Delete(FinalPath);
TFile.Move(TempPath, FinalPath); // or: RenameFile(TempPath, FinalPath)
except
if TFile.Exists(TempPath) then
TFile.Delete(TempPath); // 途中書きの一時ファイルを放置しません
raise;
end;
end;
そのコードに関する2つの正直な脚注。TFile.Moveと古典的なRenameFileは両方とも同じWindowsのrenameにマップされ、ソースと宛先が同じボリューム上にある場合にのみアトミックです。これがまさに一時ファイルがTPath.GetTempPathではなく宛先ディレクトリに移動する理由です。また、delete-then-moveのペア自体は1つのアトミックステップではありません:どちらのファイルも存在しない短いウィンドウがあります。レポートを再生成するデスクトップアプリの場合、そのウィンドウは無関係です。同じボリューム上でより強力な契約を必要とする読者は、Win32のReplaceFileまたはMoveFileExをMOVEFILE_REPLACE_EXISTINGで直接呼び出すことができ、これによりスワップが1回の呼び出しに折りたたまれます
ドキュメントを常に再生成する大量のサーバーの場合、よりクリーンな規律は、各出力を一意の名前(タイムスタンプまたはジョブID)で書き込み、2つの実行が同じパスを競合しないようにし、個別の保持ポリシーによって古いファイルをクリーンアップすることです。パターンは、リクエストごとの命名規律の1行です
// リクエストごとに 1 つずつ出力パスを使います。2 つの並行ジョブが同じ名前を
// 競合することはなく、rename の取り回しも失われるロックもありません
OutName := Format('statement-%s-%s.pdf',
[CustomerId, TGUID.NewGuid.ToString.Trim(['{', '}'])]);
Pdf.FileName := TPath.Combine(OutputDir, OutName);
リクエストIDまたはジョブIDは、周囲のフレームワークが既にそれを手渡している場合、GUIDと同じように機能し、ファイル名をログ行に無料でトレースバックできるようにします。いずれにせよ、原則は同じです。書き込んでいるファイルが、書き込む瞬間にあなただけのものであるように設計します。ウィンドウを無理やり閉じたからではなく、他の何かがバイトに触れていないため、ロックは消えます
修正の形
2つの問題を根本まで剥くと、どちらも境界を尊重することに関するものです。ステートマシンのエラーは、ドキュメントの境界を尊重することを求めています。次を開始またはロードする前に、1つのドキュメントをEndDocで終わらせることです。ファイルロックのエラーは、ファイルの境界を尊重することを求めています。他の何も読んでいない場所に書き込み、その結果を所定の場所へ移動することです。どちらも、コンポーネントの回避工作やデスクトップのスクリプト化を求めていません。どちらも、各ドキュメントを自己完結型の作業単位として扱うことから自然に導かれます。新しく作り、きれいに書き、解放する。これは、コンポーネントの残りの部分を予測可能にするのと同じパターンです
ここに示すBeginDoc、EndDoc、LoadFromFile、およびSaveLoadedDocumentの呼び出しは、DelphiおよびC++Builder用のHotPDF Delphi Componentの一部です