PDFのインクリメンタルアップデート(増分更新)を使用すると、Delphiアプリケーションは変更されたオブジェクトのみを追加することでドキュメントを変更し、元のバイトデータを一切書き換えることなく維持できます。losLab PDF LibraryはこれをAppendToStreamを介して実装しており、ISO 32000-1 §7.5.6で定義されているインクリメンタルセクションのみを書き込むため、2 GBのファイルに対する1つのしおり(ブックマーク)の編集は、完全な再書き込みの代わりに数キロバイトの出力で済みます。署名されたドキュメントの署名を無効にすることなく更新できるのも、これと同じ仕組みによるものです
これにより解決される課題は具体的です。完全保存を実行するとファイル全体が再書き込みされます。すべてのオブジェクトが再シリアル化され、すべてのクロスリファレンス(相互参照)オフセットが再計算され、出力ファイルは元のファイルとバイトレベルでの関連性を失います。40 KBの請求書であれば問題ありませんが、ドキュメントタイトルのタイプミスを修正しただけの2 GBのスキャン済みアーカイブで、20バイトを変更するために2ギガバイトを書き直すのは非効率的です。また、ファイルにデジタル署名が含まれていた場合、その再書き込みによって署名が破壊されてしまいます
なぜPDFを保存するとデジタル署名が壊れるのか?
PDFのデジタル署名は、ドキュメントの論理的なコンテンツに署名するのではなく、物理ファイルのバイト範囲に署名します。署名辞書内の/ByteRangeエントリは、暗号化ダイジェストがカバーするファイル内の正確なスパンを記録します。これらのバイトを再シリアル化する保存操作は、意味的に同一のドキュメントを生成するものであってもダイジェストを変更するため、すべての検証ツールは署名が破損していると報告します。これは設計によるものであり、署名は抽象的なドキュメントモデルではなく、署名者が目にした実際のバイトデータを証明するからです
インクリメンタルアップデートは、PDF仕様が提供する回避策です。インクリメンタル保存は、元の%%EOFの後に新しいデータを追加し、署名されたバイト範囲には一切触れないため、既存の署名は自身がカバーするバイトに対して検証され続けます。検証ツールは、追加された変更(2番目の署名、フォームへの入力、注釈など)を個別に分類し、それらが許可された変更であるかどうかを判断します。すべてのマルチ署名ワークフローはこれに依存しており、各署名者が最後の署名の上にインクリメンタルセクションを追加します。署名パイプラインを構築している場合、姉妹編の記事であるDelphiでのPAdES署名と検証において、署名バイト範囲とインクリメンタルセクションがどのように相互作用するかが詳しく説明されています
ISO 32000-1 §7.5.6におけるインクリメンタルアップデートの仕組み
ISO 32000-1 §7.5.6では、このモデルを3つのルールで定義しています。第1に、元のファイル内容は完全にそのまま残され、1バイトも移動しません。第2に、変更されたオブジェクトや新しく作成されたオブジェクトは最後の%%EOFの後に追加され、それぞれ以前と同じオブジェクト番号を持ちます(変更されたオブジェクトは、単に古い定義をシャドウイングする新しい定義を取得します)。第3に、新しいクロスリファレンスセクションとトレーラが追加され、トレーラの/Prevエントリは以前のクロスリファレンスセクションのバイトオフセットを指し示し、リーダーが最新から最古へと順に辿って各オブジェクトの最新定義を解決するためのチェーンを形成します
この構造からは、2つの有用な特性が得られます。更新処理のコストは、ドキュメントのサイズではなく変更された内容に比例します(追加にかかるコストは、変更されたオブジェクトのサイズにわずかなxref/トレーラのオーバーヘッドを加えたものです)。また、ファイル自体がバージョン履歴になります。以前のすべてのリビジョンが物理的に残っているため、監査人は任意の以前の%%EOFの位置でファイルを切り詰め、その時点で存在していた正確なドキュメントを復元できます。各修正の前にドキュメントがどのような外観であったかを証明する必要があるコンプライアンスワークフローにおいて、この組み込みの監査証跡はインクリメンタル保存を採用する決定的な理由になることがよくあります
AppendToStreamを使用したインクリメンタルアップデートの書き込み
losLab PDF Libraryは、AppendToStream(AppendMode: Integer; OutStream: TStream): Integerを介してインクリメンタル出力を公開しており、成功した場合は1を、失敗した場合は0を返します。AppendModeパラメータは、ターゲットストリームに出力する内容を選択します。モード0は完全なファイルを書き込みます。最初に元のソースバイトがストリームにコピーされ、その後にインクリメンタルセクションが追加されます。モード1はインクリメンタルセクション自体(差分)のみを書き込み、ソースバイトを完全にスキップします。モード2は、まずSetAppendInputFromStringを介して登録された呼び出し側提供のプレフィックスを書き込み、その上に更新セクションを追加します
モード1は、システム設計において興味深い選択肢です。差分が自己完結しているため、オリジナルとは独立して配信できます。リビジョンをオブジェクトストレージに個別のブロブとして保存したり、差分のみをリモートサイトにレプリケートしたり、ベースファイルと増分チェーンを結合して任意のリビジョンを再構築したりできます。再構築のルールは、単純なバイト結合(元のファイルが最初で、その後に各差分が順番に続く)です。これは、§7.5.6がインクリメンタルに更新されたファイルに対して規定しているレイアウトそのものだからです
元のファイルをコピーせずに、ライブラリはどのようにxrefオフセットを計算するのか?
インクリメンタルセクション内の相互参照エントリには、絶対バイトオフセット(差分の開始位置からではなく、完全なファイルの開始位置から測定した位置)を含める必要があります。これにより、モード1において、ライブラリは元のバイトを出力しないにもかかわらず、記録するすべてのオフセットは元のバイトが存在するかのように装う必要があるという問題が生じます。losLab PDF Libraryは、シリアライザに対して仮想的な座標空間を提供する内部ストリームアダプタTPDFAppendSectionStreamを使用してこの問題を解決します。このアダプタは元のファイルのバイト長をベースオフセットとして作成され、その位置とサイズをベースオフセットにこれまでに書き込まれた分を加算した値として報告し、新しく書き込まれたバイトのみを呼び出し側のターゲットストリームに転送します
その結果、モード1では、ディスク上でもメモリ内でも、ソースドキュメントのコピーが実体化されることはありません。単純な実装(ファイル全体を一時バッファに書き込み、末尾を切り取る)では、元のPDF全体のコピーが一時的に保持されることになりますが、ギガバイト規模の入力に対しては、これこそがインクリメンタルアップデートによって回避すべきコストです。このオフセット仮想化技術は、ライブラリの他の部分で使用されているバイト参照シフトと非常によく似ています。バイト参照シフトによる高速なPDF結合に関する記事では、ドキュメントの結合に適用された同じアイデアが示されており、直接ファイルアクセスによる大容量PDFの結合と分割に関するガイドでは、RAMに収まりきらないファイル用のI/Oアーキテクチャについてカバーしています
SaveToStreamによるフル保存のストリーミング
インクリメンタル出力はストリーミング機能の半分に過ぎず、もう半分は完全保存時の処理です。losLab PDF LibraryのSaveToStreamは、ドキュメント全体を一度中間的なAnsiStringにレンダリングしてからそのバッファを一括で書き出すのではなく、ドキュメントシリアライザをターゲットストリームに対して直接駆動します。古いアプローチでも動作はしましたが、すべての完全保存時に出力の2つ目の完全なコピーが一時的にメモリ内に保持されることを意味していました。これは10 MBであれば無害ですが、500 MBでは大きな負担となり、32ビットプロセスで数ギガバイトのデータを出力する場合には超えられない壁となっていました。直接シリアル化を行うことで、ピークメモリ使用量はシリアル化された長さではなく、ドキュメントのオブジェクト構造に追従するようになります
var
Doc: TPDFlib;
Delta: TMemoryStream;
begin
Doc := TPDFlib.Create;
try
if Doc.LoadFromFile('contract.pdf', '') <= 0 then
Exit;
// Small edit: the kind of change that should not
// trigger a rewrite of the whole file
Doc.SetInformation(3, 'Amended 2026-07-04'); // key 3 = /Subject
Delta := TMemoryStream.Create;
try
// AppendMode = 1: write only the incremental section.
// Original bytes + Delta = a complete, valid PDF.
if Doc.AppendToStream(1, Delta) = 1 then
Delta.SaveToFile('contract.delta.bin');
finally
Delta.Free;
end;
finally
Doc.Free;
end;
end;
共有モードの教訓:AppendToFileが0を返したケース
この領域における1つのデグレード(先祖返り)は、その失敗パターンが一般化できるため、振り返る価値があります。AppendToFile(FileName)は、インクリメンタルアップデートをディスク上の既存のPDFに直接追加します。これは、ファイルを読み込み、変更を加え、同じパスに追加するという、その場での監査証跡ワークフローにおいて自然な呼び出しです。v3.71.2において、このシーケンスが0を返すようになりました。根本的な原因は書き込み側ではなく読み込み側にありました。大容量ドキュメントのオンデマンド読み込みをサポートするため、LoadFromFileはドキュメントオブジェクトの有効期間中、ソースファイルのハンドルを開いたままにしますが、そのハンドルはfmShareDenyWriteで開かれていました。その後、AppendToFileが書き込み用に同じファイルを再度開こうとした際、読み込み側自体の共有モードによって拒否され、APIは1バイトも書き込む前に失敗しました
この修正では、読み込み側の共有モードをfmShareDenyNoneに緩和しました。インクリメンタル追加はファイルの末尾以降にのみバイトを追加し、リーダーの長期ハンドルが提供している領域を書き換えることは決してないため、これは安全です。このライブラリをラップする開発者、あるいは同様のストリーミング読み込み機能を構築する開発者にとっての一般的な教訓は、遅延ロードを行うハンドル保持型のリーダーと同一ファイルへのライターは競合関係にあり、オープン時に選択する共有モードは実装の詳細ではなくAPIの契約であるということです。コード内でAppendToFileが0を返すことがあれば、プロセス内の他の処理が制限的な共有モードでターゲットファイルをまだ保持していないかまず確認してください
var
Doc: TPDFlib;
Output: TFileStream;
begin
Doc := TPDFlib.Create;
try
if Doc.LoadFromFile('archive.pdf', '') <= 0 then
Exit;
// ... edits that justify a full rewrite ...
Output := TFileStream.Create('archive-rewritten.pdf', fmCreate);
try
if Doc.SaveToStream(Output) = 0 then
Writeln('Save failed, error ', Doc.LastErrorCode);
finally
Output.Free;
end;
finally
Doc.Free;
end;
end;
実質的なコスト:インクリメンタルアップデートが適していないケース
インクリメンタルアップデートは、書き込み効率と引き換えにファイルサイズを犠牲にするため、このトレードオフは必ずしも有利に働くとは限りません。各リビジョンは変更されたオブジェクトを追加する一方で、置き換えられた古い定義もファイル内に残るため、何百回も編集されたドキュメントにはデッドオブジェクトが蓄積され、すべてのリーダーが走査しなければならない長い/Prevチェーンが形成されます。さらに悪いことに、「削除された」コンテンツは失われません。リビジョン5で削除されたテキストは、依然としてリビジョン4 of バイトデータ内に物理的に存在しており、ファイルを切り詰めることで誰でも復元可能です。したがって、黒塗り(墨消し)、サニタイズ、または機密コンテンツの削除を行う場合は、完全な再書き込みが必要です。黒塗りした箇所をインクリメンタル保存することは、情報漏洩を招く行為に他なりません
蓄積された増分や未使用のオブジェクトを整理して圧縮することが目的である場合、あるいは暗号化などのドキュメント全体のプロパティを変更する場合(再暗号化はすべての文字列とストリームに影響するため、「インクリメンタル」な要素は残りません)、または編集履歴をファイルに残さずにクリーンな成果物を生成する場合には、完全保存が適切な選択です。合理的なルールとして、ドキュメントが編集途中で変更されており、特に署名が含まれている場合はAppendToStreamまたはAppendToFileを使用し、ドキュメントがシステムから離れるライフサイクルの境界や、履歴を統合する必要がある場合には、完全なSaveToStreamによる再書き込みを使用します
インクリメンタルアップデート、仮想オフセット差分出力、およびストリームへの直接シリアル化は、すべてDelphi、C#、およびVB.NET向けの標準的なlosLab PDF Libraryの一部です。製品ページには、上述した署名機能や大容量ファイル対応機能と並んで、完全保存および追加のAPI構成が記載されています