PDFlibPasはPDF/VCR-1をタグ付き構造プロファイルとして実装しています。置換定義はStructTreeRootの直下にある子分岐として存在し、テンプレートルートは/O /GTS_Templateを持ってフィールドセットを宣言し、各プレースホルダは/O /GTS_Replacementを持って1つのデータフィールドにバインドします。ページレベルの辞書という要素はどこにもありません
この一文こそ、ほとんどの実装が間違える部分で、間違えると検証失敗以上のコストがかかります。置換プランを読み間違えたバリアブルデータジョブはクラッシュしません。大量に印刷され、間違ったフィールドが間違ったボックスに入り、最初に気づくのは5万通のダイレクトメールを手にした顧客です
置換定義が実際にある場所
PDF/VCR-1は置換モデルを、プライベートなページ辞書ではなく、ISO 32000-1 §14.7で説明される論理構造階層に置きます。テンプレートルートはStructTreeRootから直接ぶら下がる構造要素で、属性オーナーGTS_Templateによって識別され、GTS_Fieldsによってフィールド語彙全体を1か所で宣言し、オプションのGTS_Pagesでページ選択を駆動するフィールドを指定します
プレースホルダはそのルートの中にネストされた構造要素で、属性オーナーGTS_Replacementを持ちます。それぞれがGTS_Generator(PDFlibPasはPassThroughを要求)、宣言されたフィールドをちょうど1つ指定するGTS_Data値、オプションのGTS_BBoxを持ちます。ページへのバインドもページアノテーションではありません。要素は/Pgで自分のページを指し、/Kでマークコンテンツシーケンスを指します。ページ辞書より構造を選ぶことには、述べるのは簡単でも過小評価しやすい結果が伴います。構造ツリーはカタログから到達できる一般的なグラフなので、プロファイルを消費するものは辞書リーダではなく、それが意味するすべての危険を抱えたツリーウォーカーでなければなりません。構造ツリーを扱ったことがなければ、その仕組みはタグ付きPDFの構造とアクセシビリティで説明されています
最初のトップレベル分岐だけを検査すると、なぜ悪いファイルを通してしまうのか
StructTreeRootは多数の直接の子を持てるため、テンプレートルートを見つけた時点で止まるバリデータは残りを見ることができません。これはPDFlibPasが避けるように作られた具体的な失敗で、検査しないより悪い偽の合格をまさに生み出します。適合と報告されながら2つ目のテンプレートルートを含むファイル、あるいはどの消費者も尊重しないテンプレート分岐の外に置かれたプレースホルダです
そこでPDFlibPasのValidatePDFVCRStructureは同じツリーを2パスで走査します。最初のパスはStructTreeRootのすべての直接の子を列挙し、GTS_Templateを持つものを数えます。ちょうど1つ以外は、プレースホルダを1つも読む前に拒否されます。2つ目のパスはテンプレートの分岐だけでなくすべてのトップレベル分岐に降りていき、2つのものを同時に拒否します。ルートではないのにGTS_Templateを持つネストされた要素と、テンプレート分岐の外で見つかったGTS_Replacement要素です
走査は深さ64レベルと1,000,000ノードに制限され、訪れたすべての要素のオブジェクト番号と世代番号を記録して、サイクルや繰り返し参照を追跡するのではなく報告します。これらの数値はパフォーマンス調整ではありません。構造ツリーは攻撃者が制御するオブジェクトグラフで、上限なしに再帰するバリデータは、ファイル拡張子が付いたスタックオーバーフローです
テンプレートの作成と、APIが曲げようとしない制約
PDFlibPasは、構造規則を保存時に発見するのではなく書き込み時に強制するブラケット型APIで作成を公開しています。SetPDFVCRModeは文書をこのプロファイルに切り替え、内部的にISO 15930-7のPDF/X-4ベースモードを要求し、mark-infoフラグをオンにします。マークコンテンツのないPDF/VCR-1ファイルには、プレースホルダが指すものが何もないからです。そこからBeginPDFVCRTemplateはフィールドリストとページ選択フィールドを受け取りますが、データスキーマを設計する前に知っておく価値のある厳しさがあります。フィールド名はPDFVCRValidFieldNameを通り、ASCIIの英字、数字、アンダースコア、ハイフン、ピリオドだけを受け入れます。それらはエスケープのあいまいさなしにPDF名へシリアライズできる文字だからです。PDFVCRNormalizeFieldListはリストを大文字小文字区別のまま保持し、重複を即座に拒否します。ページ選択フィールド自体も宣言済みリストに含まれなければなりません
var
Lib: TPDFlib;
Diag: WideString;
begin
Lib := TPDFlib.Create;
try
Lib.NewDocument;
if Lib.SetPDFVCRMode(1) <> 1 then
raise Exception.Create('PDF/VCR mode was refused');
Lib.NewPage;
if Lib.BeginPDFVCRTemplate('Salutation,FullName,AccountNo',
'AccountNo') <> 1 then
raise Exception.Create('Field list rejected');
// 1つのプレースホルダは宣言されたちょうど1つのフィールドにバインドする
Lib.BeginPDFVCRPlaceholder('FullName', 72, 690, 320, 712);
Lib.DrawText(72, 694, 'SAMPLE NAME');
Lib.EndPDFVCRPlaceholder;
Lib.EndPDFVCRTemplate;
Diag := Lib.GetPDFVCRDiagnostics;
if Diag <> '' then
begin
Writeln('PDF/VCR blockers: ', Diag);
Exit;
end;
Lib.SaveToFile('campaign-template.pdf');
finally
Lib.Free;
end;
end;
ブラケット型APIが何をもたらすかに注目してください。BeginPDFVCRPlaceholderは、テンプレートがアクティブでタグスタックがちょうど1レベル深いとき以外は開くことを拒否します。これが、プレースホルダがテンプレートルートの直接の子であり、互いにネストしないことをPDFlibPasが保証する仕組みです。座標も検証され、NaN、無限大、反転した矩形を拒否するため、悪いバウンディングボックスは1週間後のプリフライトレポートではなく呼び出し時点で失敗します
プレースホルダが独自のMCIDシリアライズ経路を必要とするのはなぜか
PDF/VCR-1は/Kが裸の整数であることを求めるからです。これは一般的なタグ付きPDFライタが構造要素とコンテンツのリンクをシリアライズする方法ではありません。ISO 32000-1 §14.6では、構造要素は通常、要素が/Pgで自分のページをすでに識別している場合は直接のMCID整数で、そうでない場合はページを明示的に指名するマークコンテンツ参照辞書を通じて、マークコンテンツを参照します。多くの目的でタグ付き出力を生成するライブラリは、ページをまたいだ合成に優れる辞書形式に標準化しがちです
PDFlibPasはここで一般的な選択肢ではなく狭い選択肢を取りました。SetCurrentStructElemDirectMCIDKidは現在の構造要素にフラグを立て、そのフラグを持つ要素だけが/Kを直接MCIDとしてシリアライズし、ページは/Pgで運ばれます。他のすべては既存のMCR辞書経路をそのまま保ちます。これは意図的なトレードオフで、声に出して言う価値のある種類のものです。プロファイルを追加しても、他のタグ付き文書がすでに生成している出力のバイトを変えるべきではありません。だからプロファイルはグローバルスイッチではなく独自の経路を得ます。既存のタグ付きファイルはすべて以前のシリアライズを維持し、リグレッションの表面積は新機能の大きさのままです
1つの走査に2つの消費者
PDFlibPasは置換マニフェストと適合判定を同じ2パス走査から導出します。だからマニフェストをジョブプランとして信頼できます。走査は検証しながらTPDFVCRManifestDataレコードを埋めます。Fieldsはテンプレートルートから、PageSelectionFieldは宣言されていればそこから、PlaceholdersはFieldName、PageNumber、MCID、HasBBox、BBoxを持つTPDFVCRPlaceholderInfoエントリの配列です。適合のエントリポイントはCheckCompliancePDFVCR(InputStream, Password, Options, ResultList)で、すべてを一度に実行するのではなく依存順でチェックを実行します。最初に完全なPDF/Xチェック、次にGTS_PDFXVersionをPDF/X-4と照合、次にhttp://www.npes.org/pdfvcr/ns/id/名前空間から読むGTS_PDFVCRVersion、そして最後に構造の走査です。Optionsに1を渡すと最初の問題で停止します。これはバッチゲートで望む挙動です。0を渡すとすべてを収集します。これはレポートで望む挙動です
// 自分で作ったのではなく受け取ったテンプレートを検証する
var
Issues: TStringList;
Stream: TFileStream;
I: Integer;
begin
Issues := TStringList.Create;
Stream := TFileStream.Create('supplier-template.pdf',
fmOpenRead or fmShareDenyWrite);
try
if CheckCompliancePDFVCR(Stream, '', 0, Issues) <> 100 then
for I := 0 to Issues.Count - 1 do
Writeln('PDF/VCR: ', Issues[I]);
finally
Stream.Free;
Issues.Free;
end;
end;
マニフェストは契約なので、適合ファイルにしか存在しない
BuildPDFVCRManifestは問題リストが空のときだけJSONを返し、それ以外は空文字列を返します。これは自己目的の防御的コーディングではありません。マニフェストはそこから実際の出力を駆動するコンポジションエンジンに消費され、2つのテンプレートルートを持つファイルから抽出した部分的なフィールドリストを渡すと、エンジンが非適合の構造を信頼できる置換プランとして扱ってしまいます。ドキュメントAPIを通じて、GetPDFVCRManifest(InputFileName, Password)は同じ呼び出しをラップし、ファイルが資格を満たさないときに最後のエラーコードをPDFLIB_ERROR_PDFVCR_COMPLIANCE(608)に設定するため、抽出の失敗と空のテンプレートを区別できます。JSONはfields、文字列またはnullのpageSelectionField、そして各エントリがfield、page、mcidと4つの数値のbboxまたはnullを与えるplaceholders配列を報告します
var
Lib: TPDFlib;
Manifest: WideString;
begin
Lib := TPDFlib.Create;
try
Manifest := Lib.GetPDFVCRManifest('campaign-template.pdf', '');
if Manifest = '' then
// 608はファイルがPDF/VCR-1適合でないことを意味し
// 401はファイルを開けなかったことを意味する
Writeln('No manifest, error ', Lib.LastErrorCode)
else
Writeln(Manifest);
finally
Lib.Free;
end;
end;
コンシューマに持ち込むべき細部が1つあります。pageの値は、ビューアで人間が見る1始まりの番号ではなく、ライブラリ全体の0始まりのページ規則に従います。これを混同すると、コンテンツが間違ったシートに載る静かなオフバイワンになり、両方の数値が構造的に有効なためどのバリデータも捕捉しません
境界がどこにあるのか
PDFlibPasのPDF/VCR-1サポートは、テンプレート作成、保存ゲート、スタンドアロンのプリフライト、マニフェスト抽出をカバーします。置換自体は実行しません。プレースホルダへのデータ代入とパーソナライズされたインスタンスの生成はコンポジションステップで、意図的にライブラリの外にあります。マニフェストこそ、コンポジションエンジンを独立に書けるようにするインターフェースだからです。保存ゲートも修復ではなく拒否を選びます。間違ったアノテーションフラグを黙って正規化できるプロファイルと違い、PDFVCRReadyForSaveが強制するすべてのPDF/VCR-1制約は、推測が文書の意味を変えてしまうものです。テンプレートルートをでっち上げる、あるいはバインドされていないプレースホルダがどのフィールドに属するかを選ぶと、検証は通るが嘘をつくファイルができます。同じ修復対拒否の線引きが別のプロファイルについてPDF/E-1オーサーモードと制限付きプリフライトで論じられ、1つの文書に対して複数の適合チェックを実行するワークフローは適合性と署名のワークベンチでカバーされています
作成、制限付き構造走査、保存ゲート、マニフェスト抽出はすべてPDFlibPas Delphi PDF libraryに同梱されます。つまりテンプレートをプロファイルの下で生成し、ライタの状態を見たことのない別のコード経路で検証できます。その分離こそ、適合の主張に価値を持たせる唯一の理由です