PDF Library for Delphi(PDFlibPas)は、AddWebCaptureCommandを通じて文書カタログにWeb Captureコマンドのメタデータを書き込み、ISO 32000-1 §14.10で定義される/SpiderInfo辞書を埋めます。各コマンドには、取得したURL、取得の深さ、少数のフラグが記録されます。これは意図の記録であって、実際の取得は行いません
この区別こそ、このAPIを読み間違いやすい最大の理由です。サポートエンジニアが、ベンダーのオンラインドキュメントから組み立てたPDFを渡してきて、40ページ目はどこから来たのかと聞いてきます。プロデューサが/SpiderInfoを埋めていれば、答えはカタログの中にあります。URL、クロールの深さ、場合によってはページを生成したPOSTボディまでです。埋めていなければ、コンテンツストリームをいくら読んでも分かりません。こうした来歴の記録は安価で、構造的にうるさい割に、ほとんど常に省略されます
Catalogの/SpiderInfo辞書は実際に何を記録するのか
/SpiderInfoは文書カタログにぶら下がるWeb Capture情報辞書で、PDF Library for Delphiが気にする項目はちょうど2つだけです。/VはWeb Captureデータベースのバージョン番号で、ライブラリは常に1.0と書きます。/Cはコマンド辞書の配列です。このバージョン番号はPDFのバージョンではありません。Web Captureデータモデルそのものを示しており、ISO 32000-1 §14.10が定義する値は1.0だけです。面白いものはすべて/Cに入ります。各コマンド辞書は、/URL(リクエストされたアドレス)、/L(そこから何階層リンクをたどったか)、/F(走査フラグ)を持ちます。オプションのキーが3つあり、プレーンなGETでなかった場合のリクエストそのものを記述します。/PはPOSTデータ、/CTはそのデータのコンテンツタイプ、/Hは追加のリクエストヘッダです。PDF Library for Delphiはこの6つを1つのレコードTPDFlibWebCaptureCommandに写像します。フィールドはURL、Levels、Flags、PostData、ContentType、Headersで、2つのIntegerカウンタ以外はすべてAnsiStringです。/SpiderInfoの存在は文書の最低バージョンも引き上げます。AddWebCaptureCommandはEnsureMinVersion('1.3', 'Catalog /SpiderInfo Web Capture command')を呼び、PDF Library for Delphiのバージョン検出規則も対応するPDF 1.3規則を登録するため、後から読み戻した文書も同じ下限を報告します
Web Captureコマンドが間接オブジェクトでなければならない理由
つまずきやすい構造上の規則が1つあります。コマンド辞書は/Cに入る前に、間接オブジェクトとして登録されなければなりません。ISO 32000-1 §14.10は/Cを、コマンド辞書への間接参照の配列と規定しており、辞書そのものの配列ではないからです。直接辞書をその配列に入れると、有効なPDFとしてパースできてもWeb Captureレコードとしては無意味なものになります。ビューアやアーカイブツールがコマンドリストをたどると、参照があるべき場所にインラインオブジェクトがあり、そのエントリは単にコマンドではありません。PDF Library for DelphiはこれをAddWebCaptureCommandの内部で処理します。辞書を組み立て、OnNewIndObjに渡してオブジェクト番号と世代番号を取得し、そのペアを間接参照に包んで配列へ追加します。OnNewIndObjが正のオブジェクト番号を生成できなければ、関数は0を返して配列には手を付けません。仕様が間接参照を要求する理由は、1つのコマンドを共有できるようにするためです。同じ取得をWeb Capture構造の複数の場所から参照でき、PDFにおける共有オブジェクトは定義上間接オブジェクトだからです
DelphiからWeb Captureコマンドを追加するには
レコードを完全に埋めてからAddWebCaptureCommandを呼びます。この関数は新しいコマンドの1始まりのインデックスか、検証に失敗した場合は0を返します。ここでは普段以上に完全性が重要です。DelphiはローカルレコードのマネージドAnsiStringフィールドを空に初期化しますが、IntegerフィールドのLevelsとFlagsにはそのような処理がないため、埋め忘れたレコードはスタック上のゴミを持ったまま渡ってしまいます。AddWebCaptureCommandはLevelsが1以上、Flagsが非負であることを要求し、スタックのゴミは素直に拒否されるか、あるいはもっと悪いことに、もっともらしい深さとして黙って受け入れられます
var
Lib: TPDFlib;
Command: TPDFlibWebCaptureCommand;
Index: Integer;
begin
Lib := TPDFlib.Create;
try
if Lib.LoadFromFile('captured-site.pdf', '') = 1 then
begin
// 全フィールドを代入する。LevelsとFlagsは素のIntegerなので
// 省略するとスタックのゴミが入る
Command.URL := 'https://example.com/docs/index.html';
Command.Levels := 2;
Command.Flags := PDF_WEB_CAPTURE_SAME_SITE or PDF_WEB_CAPTURE_SAME_PATH;
Command.PostData := '';
Command.ContentType := '';
Command.Headers := '';
// 新しい1始まりのコマンドインデックスを返す。拒否時は0
Index := Lib.AddWebCaptureCommand(Command);
if Index = 0 then
raise Exception.Create('Web Capture command rejected');
Lib.SaveToFile('captured-site.pdf');
end;
finally
Lib.Free;
end;
end;
最初の成功した呼び出しが、その構造を作るきっかけでもあります。AddWebCaptureCommandは内部ヘルパのWebCaptureCommands(True)にコマンド配列を問い合わせ、そのヘルパは/SpiderInfoがなければ/Vエントリと空の/C配列を付けて作成します。既存のものに対しては意図的に慎重です。カタログにすでに/SpiderInfoキーがあり値が辞書でない場合、あるいは/Cキーがあり値が配列でない場合、ヘルパは既存オブジェクトを上書きせずnilを返します。壊れた継承構造は追加を失敗させますが、前のプロデューサが書いたものをPDF Library for Delphiが壊すことはありません
フラグのホワイトリスト:定義済みの3ビットだけ
AddWebCaptureCommandはFlagsをホワイトリストと照合し、PDF Library for Delphiが定義する3つの定数の外にビットが立った値はすべて拒否します。PDF_WEB_CAPTURE_SAME_SITE = 1(同じサイトに留まるリンクだけをたどる)、PDF_WEB_CAPTURE_SAME_PATH = 2(同じパス配下のリンクだけをたどる)、PDF_WEB_CAPTURE_SUBMIT = 4(このリクエストは単純な取得ではなくフォーム送信)です。8や16、うっかりした-1を渡すと、呼び出しは文書に触れずに0を返します
これは通常のPDFパーサより厳しく、意図的です。/Fは自己記述的な構造を持たないビットフィールドなので、未定義ビットは壊れたビットと区別できません。後から値を読み戻すコンシューマには、将来の拡張とタイプミスを見分ける方法がありません。書き込み時に拒否することで、あいまいさはファイルから完全に締め出されます。/P、/CT、/Hは対応するレコードフィールドが空でないときだけ書かれる点にも注意してください。通常のGETは/URL、/L、/Fの3キーのコマンド辞書になり、空文字列のプレースホルダで散らかりません。これはボディを持たないリクエストとして、ファイルをパースする読み手が期待する形です
// フォーム送信:POSTボディとそのコンテンツタイプ
Command.URL := 'https://example.com/search';
Command.Levels := 1;
Command.Flags := PDF_WEB_CAPTURE_SUBMIT; // 4
Command.PostData := 'q=iso32000&scope=all';
Command.ContentType := 'application/x-www-form-urlencoded';
Command.Headers := 'Accept-Language: en-US';
if Lib.AddWebCaptureCommand(Command) = 0 then
raise Exception.Create('submission command rejected');
// same-siteとsame-pathの組み合わせは正当:1 or 2 = 3
Command.Flags := PDF_WEB_CAPTURE_SAME_SITE or PDF_WEB_CAPTURE_SAME_PATH;
// ビット3は仕様で未定義なので、これはファイルに届かない
Command.Flags := 8;
if Lib.AddWebCaptureCommand(Command) <> 0 then
raise Exception.Create('undefined flag bit should have been refused');
コマンドの読み戻し:1つのキーに2つの正当な表現
GetWebCaptureCommandは成功時1、それ以外は0を返します。この関数は他のプロデューサが書いたファイルを読むため、ライタより寛容である必要があります。重要な非対称性は2つです。1つ目は、読み手がコマンドエントリを間接参照でも直接辞書でも受け入れ、必要に応じて参照を解決することです。PDF Library for Delphiは仕様が要求する間接形式だけを書きますが、いい加減なファイルの読み取りを拒否しても誰の得にもなりません。2つ目は、/Pが文字列かストリームのどちらでも許されることです。この1つはWeb Captureの枠を超えて一般化できます。ISO 32000-1は、値が任意に大きくなり得る場合に1つのキーへ文字列かストリームのどちらかを入れることを繰り返し許しています。ここではPOSTボディで、同じパターンはメタデータや埋め込みペイロードの形でフォーマットの別の場所にも現れます。そこでGetWebCaptureCommandの内部ヘルパReadStringはAllowStreamフラグを受け取り、/Pの読み取りだけがTrueを渡します。ストリーム値はフィルタチェーンを通じてデコードされ、PostDataフィールドとして返されます。/URL、/CT、/Hは仕様がストリームの代替を用意していないため、文字列のみのままです。自分でPDF読み取りコードを書くなら、これが盗む価値のある習慣です。文字列と決め打ちせず、どのキーが二重型かを仕様で確認することです
var
Lib: TPDFlib;
Command: TPDFlibWebCaptureCommand;
I, Total, Removed: Integer;
begin
Total := Lib.GetWebCaptureCommandCount;
// パブリックなインデックスは1始まりで、PDF Library for Delphiの他の部分と同じ
for I := 1 to Total do
if Lib.GetWebCaptureCommand(I, Command) = 1 then
Writeln(Format('%s levels=%d flags=%d post=%d bytes',
[Command.URL, Command.Levels, Command.Flags, Length(Command.PostData)]));
// /SpiderInfo辞書全体を削除し、削除されたコマンド数を報告する
// クリア対象がなければ0を返す
Removed := Lib.ClearWebCaptureCommands;
Writeln(Format('cleared %d command(s)', [Removed]));
end;
ClearWebCaptureCommandsはコマンドを数えてから、/Cを空にするだけでなく、カタログから/SpiderInfoキー全体を削除します。空のコマンド配列を持つ/SpiderInfoを残すと、文書にWeb Captureの来歴があり、何も取得されていないと主張することになります。これは来歴が記録されていないこととは別の、偽の主張です。同じ感覚はカタログメタデータ全般に当てはまります。空の殻の辞書は中立的な残骸ではなく、1つの声明です。書き換える前に文書がすでに主張していることを監査するなら、既存のPDFからアウトラインとアノテーションアクションを読み戻すの内省パターンが、ここでの読み取り→クリアの一連の流れと自然に組み合わさります
メタデータを書くことはWeb Captureの実装ではない
範囲については自分に率直でいてください。AddWebCaptureCommandは意図された、または完了した取得の記録を保存するだけで、それ以上は何もしません。PDF Library for DelphiはHTTPリクエストを実行せず、Levelsの値が表すリンクをたどらず、SAME_SITEやSAME_PATHフラグも尊重しません。それらのフラグは何らかのクローラがどう振る舞ったかのドキュメントであって、ライブラリが実行する命令ではありません。独自の取得コードが何であれ、それは完全にあなたの問題です
ISO 32000-1 §14.10のさらに2つの部分も、このAPIの範囲外です。Web Captureはコンテンツセット、つまり個々のページ部品をその出所であるソース素材に結び付ける/Contents構造と、デジタル識別子をそれらのコンテンツセットに対応付ける名前ツリーを定義します。PDF Library for Delphiはどちらも維持しません。手に入るのはコマンドリストだけです。「この文書はどこから取得され、クロールはどの深さまで進んだか」という最上位の答えであり、来歴、監査、アーカイブ取り込みの目的では、通常誰もが実際に尋ねる部分です。URLを監査人のための記録ではなく、読者がクリックできるようにする必要があるなら、それは別の仕事です。アクション側はGoToR、GoToE、Launchアクションの構築、開閉時の発火動作は文書とページのライフサイクルアクショントリガを参照してください。ここで説明したCatalogの/SpiderInfoコマンドメタデータは、DelphiとC++Builder向けのネイティブPDFライブラリPDF Library for Delphiの一部です