PDFium Component は、TPdf.SaveAsLinearized を通じて、Delphi から Fast Web View(リニアライズド)PDF を生成します。このメソッドは現在の文書を PDFium で保存した後、その結果を ISO 32000-1 附属書 F に従って書き直します。1 ページ目のオブジェクトを先頭へ移動し、2 つのクロスリファレンスセクションを書き出し、ページオフセットと共有オブジェクトのヒントテーブルが、任意のページのためにどのバイト範囲を取得すべきかをビューアに告げます
誰もが気にする理由はネットワークです。オブジェクトストレージや文書ポータルから配られる 40 MB のカタログは、最初の数キロバイトで 1 ページ目を示すべきであり、380 ページへのジャンプは数回の HTTP Range リクエストで済むべきであって、全量ダウンロードではないはずです。ビューアがそれをできるのは、ファイルがリニアライズドされているときだけです。編集パイプラインから出てくるほとんどの PDF はそうなっていません。PDF を書く自然なやり方は、クロスリファレンステーブルを末尾に置き、ページオブジェクトをライターがたまたま出力した場所にばら撒くからです
PDFium がリニアライズドファイルを読めて書けないのはなぜか
PDFium にはリニアライゼーションの完全なリーダーがあり、ライターはありません。FPDFAvail_* availability API はリニアライゼーション辞書を検出し、ヒントテーブルをデコードし、必要なバイト範囲をちょうどホストに要求します。PDFium Component のプログレッシブダウンロードとキャンセルのワークフローが築かれているのはこの上です。一方、公開の保存 ABI が提供するのは、素のコピー保存とターゲット PDF バージョン付きの保存だけです。オブジェクトを並べ替えたりヒントストリームを出したりするものは何もありません
PDFium Component は、PDFium が既にうまくやっていることを再実装せずにこの隙間を塞ぎます。TPdf.SaveAsLinearized はまず既存の SaveAs を呼ぶため、セマンティックな保存は依然 PDFium の所有物です。編集されたページ、フォームフィールドの状態、バージョン処理はすべて、何年も鍛えられてきたコードパスを通ります。コンポーネントはその後、その安定した中間 PDF に対して附属書 F の再構築パスを実行し、リニアライズド結果をターゲットへ書き出します。仕事をこう分割すれば、リニアライゼーションが注釈やフォームを理解する必要は生まれず、対象はオブジェクト、参照、バイトオフセットだけです
Delphi から TPdf.SaveAsLinearized を呼ぶ
TPdf.SaveAsLinearized には 2 つのオーバーロードがあり、1 つは TStream 用、もう 1 つはファイル名用で、どちらも SaveAs と同じオプションの Option: TSaveOption と PdfVersion: TPdfVersion パラメータを取ります。どちらも成功時に True を返します。ファイルのオーバーロードはターゲットディレクトリの一時ファイルへ書き込み、出力先をアトミックに置き換えるため、失敗した保存が、良い PDF があった場所に書きかけの PDF を残すことはありません
var
Pdf: TPdf;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'catalog-2026.pdf';
Pdf.Active := True;
// ... ページ編集、フォーム入力、スタンプなど、パイプラインの作業 ...
if not Pdf.SaveAsLinearized('catalog-2026-web.pdf', saNoIncremental, pv17) then
raise Exception.Create('Linearized save failed');
finally
Pdf.Free;
end;
end;
パラメータの規則は意図的に厳格です。saIncremental はエラーを上げます。インクリメンタル更新はファイル末尾に新しいリビジョンを追記し、先頭積み込みのレイアウトを即座に壊すからです。PDF 1.2 未満のターゲットバージョンも拒否されます。pvUnknown はソースのバージョンを保ちます。暗号化された入力には saRemoveSecurity が要ります。ライターはオブジェクト番号を振り直し、PDF のオブジェクトレベル暗号鍵はオブジェクト番号から導出されるからです。そしてリニアライゼーションは全体書き直しであるため、既存の電子署名はすべて無効化されます。署名付きかつリニアライズドで届けなければならないなら、先にリニアライズし、後から署名します
ライターは Fast Web View のためにオブジェクトをどうグループ化するのか
リニアライゼーションは、1 ページ目をファイルの先頭へ動かすことだけではありません。ライターは現在のリビジョンをパースします。クロスリファレンスデータがクラシックなテーブルか、ハイブリッドファイルか、クロスリファレンスストリームかにかかわらず、アクティブなオブジェクトストリームのすべてのメンバーをスタンドアロンのオブジェクトへ展開します。それから参照グラフを組み立て、オブジェクトを 5 つのグループへ振り分けます。ファイルを開くのに必要な文書レベルのオブジェクト、1 ページ目のオブジェクト、後続の 1 ページに私的なオブジェクト、後続の複数ページで共有されるオブジェクト、そしてその他すべてです
このグラフ歩行の罠は /Parent です。すべてのページ辞書はページツリーを上へ指し戻しており、素朴な走査はそのポインタを親ノードへたどり、/Kids を通じてすべての兄弟ページとそのコンテンツストリームへ下ります。1 歩後には、1 ページ目のグループが文書全体を含み、ファイルは技術的にはリニアライズドでも、レンジ優先アクセスには役に立ちません。PDFium Component はページの /Parent リンクと、収集中のページの外側のページツリーノードを無視します。1 ページ目のセクションを小さく保っているのはこれです。深いページツリーを持つ 51 ページのリグレッションサンプルでは、1 ページ目のセクションは 5289 バイト、1105354 バイトの出力の 0.48% です
入力のオブジェクトストリームとクロスリファレンスストリームは、普通のジェネレーション 0 の間接オブジェクトへフラット化され、すべての参照は新しい番号へ書き直され、出力はクラシックなクロスリファレンスセクションを使います。圧縮オブジェクトストリームと比べればファイルサイズはいくらか余計にかかりますが、ヒントテーブルのカバレッジが単純になり、リニアライゼーションを理解するすべてのビューアが扱うレイアウトになります。ある出力では Fast Web View よりコンパクトさが重要なら、その仕事のための別のオブジェクトストリームライターを PDFium Component は持っています
リニアライゼーション辞書と 2 つの xref セクションには何が入るのか
PDFium Component が書くリニアライズドファイルは、2 つのクラシックなクロスリファレンスセクションと、最初のオブジェクトの中のリニアライゼーション辞書を運びます。最初の xref セクションはリニアライゼーション辞書に続き、文書レベルと 1 ページ目のオブジェクトをカバーするため、ビューアはファイルの先頭だけで 1 ページ目をレンダリングできます。メインの xref セクションは末尾に座り、その他すべてをカバーします
リニアライゼーション辞書自身が、ビューアが他の何を読むよりも先に必要とする数値を保持します。ファイル長の /L、ヒントストリームのオフセットと長さの /H、1 ページ目のオブジェクト番号の /O、1 ページ目セクションの終わりの /E、ページ数の /N、そしてメイン xref テーブルの最初のエントリのオフセットの /T です。これらはすべて最終レイアウトに依存するのに、辞書はレイアウトより先に座っています。自然な幅の数値で書くとループができます。999999 から 1000000 へ伸びた長さが 1 バイトを加え、以降のすべてのオフセットをずらし、それがまた別の数値の幅を変え得て、と続きます
PDFium Component は、リニアライゼーション辞書に固定の 320 バイトを予約してこのループを断ちます。すべてのオブジェクト、ヒントストリーム、両 xref セクション、トレーラーをまず書きます。すべてのオフセットが確定した後、辞書をその場で埋め、予約幅までパディングします。微妙な細部が 1 つ、/T です。これはメイン xref テーブルのオブジェクト 0 エントリを指さねばならず、その前にある xref キーワードを指してはなりません。寛容なビューアはどのみちファイルを開けますが、厳格なバリデータはキーワードのオフセットにフラグを立てます
ヒントテーブルのオフセットがヒントストリームを無視するのはなぜか
リニアライズド PDF のヒントテーブルのオフセットは、ヒントストリームがファイルに存在しないかのように解釈されます。附属書 F がこう定めるのは、ヒントストリームが 1 ページ目セクションと残りの間に座りつつ、自分のサイズが自分の含む数値へ還流しないようにするためです。ライターにとっての実務上の帰結は間違えやすいです。ヒントストリームより後にある実際のオフセットは、ページオフセットか共有オブジェクトのヒントテーブルに入る前に縮められねばなりません
問いは、いくら縮めるかです。魅力的な答えはストリームデータの長さです。ヒントコンテンツのように感じられるのはこの部分だからです。正しい答えは、ヒントの間接オブジェクト全体の長さです。N 0 obj、ストリーム辞書、stream と endstream キーワード、endobj を含めてです。データ長だけ引けば、以降のすべてのオフセットが数十バイトずれます。1 ページ目はヒントストリームより前に住んでいるため、依然レンダリングされます。だからこのバグは手早い目視チェックを生き延び、ビューアが後のページへジャンプし、間違ったオブジェクトの途中から始まるレンジを要求したときにだけ現れます
両方のヒントテーブルは、附属書 F に従い、最上位ビットからビットパックされます。コンポーネントは、本物のクライアントがやるやり方で結果を検証します。スパースなレンジストアが 51 ページのサンプルの最初の 1 KiB だけを供給し、PDFium のネイティブなヒント処理が残りを要求します。フルファイルが一度も存在しないまま、最後のページが開きます
Delphi でリニアライズド PDF を検証する
最も安いチェックは、PDFium 自身に尋ねることです。TPdf.InspectAvailability は文書を読み込まずにストリームに対して availability API を実行し、その Linearization フィールドが plnLinearized、plnNotLinearized、plnUnknown を報告します
function IsFastWebView(const FileName: string): Boolean;
var
Stream: TFileStream;
Info: TPdfAvailability;
begin
Stream := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
try
Info := TPdf.InspectAvailability(Stream);
Result := Info.Linearization = plnLinearized;
finally
Stream.Free;
end;
end;
これは PDFium がレイアウトを認識することの確認であって、ヒントが正しいことの証明ではありません。本物のエンドツーエンドのテストには、ファイルをレンジ制限付きのトランスポートで配り、TPdfProgressiveDocument で後のページを開きます。PDFium VCL で大きな PDF をオンデマンドにストリーミングするガイドがやるのと同じやり方です。ファイルのほんの一部を取得しただけでページがレンダリングされるなら、ヒントテーブルは仕事を果たしています。リリースパイプラインに 2 つ目の独立したリニアライゼーションチェッカーを足すのも価値があります。位置のずれた /T やわずかに間違ったオフセットへの寛容さは、ビューアごとに違うからです
心に留める限界が 2 つあります。オブジェクトの実体化器は今も 2 GiB 以上の配列を拒否するため、極端に大きな文書は現時点では別の戦略を要します。そしてリニアライゼーションが報われるのは、サーバーが HTTP Range リクエストを尊重するときだけです。Accept-Ranges を剥ぎ取るプロキシの後ろでは、リニアライズドファイルは他とまったく同じようにダウンロードされます
FAQ
TPdf.SaveAsLinearized は電子署名を有効のまま保つのか
いいえ。TPdf.SaveAsLinearized はすべてのオブジェクトを振り直し、書き直します。既存の署名は無効化されます。先にリニアライズし、その後に署名してください
PDFium Component は暗号化 PDF をリニアライズできるのか
saRemoveSecurity を渡したときだけです。オブジェクトレベルの暗号鍵はオブジェクト番号に依存し、ライターは番号を変えるため、出力は暗号化なしで書かれます
リニアライズド出力はどの PDF バージョンに対応するのか
TPdf.SaveAsLinearized は、ソースバージョンを保つ pvUnknown と、PDF 1.2 以上の任意のターゲットを受け付け、追記されるリビジョンがレイアウトを壊すため saIncremental を拒否します
Fast Web View 出力、プログレッシブローディング、availability API は、Delphi と C++Builder 向け PDFium Component の一部です。Delphi、C++Builder、Lazarus プロジェクトのために PDFium をラップします