HotPDFはページのコンテンツストリーム内のすべての演算子を巡回し、それぞれをVisitLoadedPageContentOperatorsを通じて自分のコードへ渡す。演算子ごとに1つのアクションを選ぶ:保持する、削除する、独自のバイト列で置換する、巡回を停止する、または編集全体を却下する、のいずれかである。すべての演算子が保持された場合、書き換え後のストリームは元のものとバイト単位で完全に同一になる。この性質こそが、自分が作成していないドキュメントに対してもこの仕組みを安全に実行できる理由である
コンテンツストリームの編集は、PDFツールが往々にして誤りを犯す領域である。ストリームはスタックマシン上の演算子の列であり、ドキュメントツリーではない。デコード済みバイト列に対して正規表現を素朴に走らせるアプローチは、文字列リテラルの中に演算子に見えるものが含まれた瞬間、あるいはインライン画像に任意のバイナリが含まれた瞬間に破綻する。すでにストリームをトークン化済みのビジターにはその問題がない
演算子レベルのアクセスで実際に何ができるか
タスクには3つの系統がある。監査:ページが既知の集合外の色を何回設定しているかを数える、あるいは埋め込まれていないフォントで描画されたテキストを見つける。除去:マークコンテンツとして描かれた透かしを削除する、あるいは特定の画像呼び出しを取り除く。そして正規化:ある演算子の形式を、下流の消費者がより良く扱える同等のものに置き換える
これらはいずれも1つの要件を共有する:ストリームの残り99パーセントには一切手を触れないでいられなければならない。HotPDFの構文木はソースのコピーを1つとコンパクトなスパンを保持し、編集はソート済みの重複しないオーバーレイとして扱われる。結果を書き出す際、変更していない範囲は直接コピーされ、置換用バイトは実際に変更した場所にのみ出力される。そのため、何も変更しない往復編集がドリフトを生み出すことはない
ビジター契約
インターフェースを実装するかイベントを割り当て、その上で監査にはApplyChangesをfalseに、書き換えにはtrueに設定して訪問メソッドを呼び出す。イベントデータはフェーズ、演算子名、そのオペランド数、バイトスパン、現在のネスト状態、そしてオペランドを読むための借用ビューを保持する
uses
HPDFDoc, HPDFContentStream;
procedure TAuditor.HandleOperator(Sender: TObject;
var EventData: THPDFContentOperatorEventData);
var
Bytes: TBytes;
begin
if EventData.Phase <> covpOperator then
Exit;
// marked content内に描画されたtext-showing operatorを数える
if (EventData.OperatorName = 'Tj') or
(EventData.OperatorName = 'TJ') then
begin
Inc(FTextOperators);
if EventData.State.MarkedContentDepth > 0 then
Inc(FTextInsideMarkedContent);
end;
// 必要なときだけoperandを読む
if EventData.OperatorName = 'Do' then
if EventData.View.GetOperandBytes(0, Bytes) then
NoteXObjectUse(TEncoding.ANSI.GetString(Bytes));
EventData.Action := coaKeep;
end;
procedure TAuditor.Run(const FileName: string);
var
Pdf: THotPDF;
Stats: THPDFContentOperatorVisitStatistics;
I: Integer;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile(FileName) <= 0 then
Exit;
Pdf.OnContentOperator := HandleOperator;
for I := 0 to Pdf.LoadedPageCount - 1 do
Pdf.VisitLoadedPageContentOperators(I, False, Stats); // audit only
Writeln(Format('%d operators visited, max q/Q depth %d',
[Stats.VisitedOperatorCount, Stats.MaximumGraphicsStateDepth]));
finally
Pdf.Free;
end;
end;
ビューが可変ではなく借用である理由
コールバックが変更可能な構文木を受け取ることは決してない。受け取るのは、現在の操作のバイト列や特定のオペランドを要求に応じて取得できるビューであり、それがすべてである。すべての変更はアクションフィールドと置換フィールドを経由する
この間接化は単なる儀式ではない。ツリーの直接変更を許せば、ビジターは置換バイト数の予算や重複編集のチェックを回避できてしまい、バイトスパンのオーバーレイモデルにおける重複編集は、大きく破綻するのではなく静かに誤った出力を生み出す。変更をアクションフィールド経由に絞ることで、ビジターの意図にかかわらず、すべての編集が同じ方法で検証される
必要時にのみ読み取ることは、よくあるケースを軽量に保つことにもつながる。演算子名だけを数える監査は、オペランドのバイト列を一切実体化しない。これは、数十万個の操作を持つコンテンツストリームでは重要になる
状態フィールドは常に入力を記述する
各イベントは、その演算子が実行される直前の元の入力における、グラフィックステートの深さ、テキストオブジェクトの深さ、マークコンテンツの深さ、互換性セクションの深さを保持する。これはビジターが以前の演算子を削除または置換していた場合でも変わらない
状態を編集後の出力ではなく元の入力に固定することは、実用上の利点を持つ意図的な選択である:同じドキュメントに対して同じ監査を2回実行すれば同一の結果が得られ、編集と並行して実行される監査も自身の基準フレームをずらすことがない。出力における深さを知る必要があれば、何を削除したかは自分で分かっているので計算できる。逆に、移動し続ける出力から安定した入力位置を再構築することは不可能である
削除と置換、そしてそれを守るルール
置換内容は生のコンテンツストリームバイト列であり、その構文についてはあなた自身が責任を負う。それを取り巻く保護策は、置換バイトの合計に対する予算、重複禁止の要件、そして結果が受理される前に再パースする任意の出力検証である
procedure TWatermarkStripper.HandleOperator(Sender: TObject;
var EventData: THPDFContentOperatorEventData);
begin
EventData.Action := coaKeep;
if EventData.Phase <> covpOperator then
Exit;
// watermark formをpaintするoperatorをdrop
if (EventData.OperatorName = 'Do') and OperandIsWatermark(EventData) then
EventData.Action := coaDelete
// 迷い込んだcolourをhouse greyにnormalise
else if EventData.OperatorName = 'rg' then
begin
EventData.Replacement := TEncoding.ANSI.GetBytes('0.2 0.2 0.2 rg');
EventData.Action := coaReplace;
end
// 理解できないものに遭遇したらedit全体を拒否
else if EventData.OperatorName = 'BI' then
EventData.Action := coaReject;
end;
var
Options: THPDFContentOperatorVisitOptions;
begin
Options := THPDFContentOperatorVisitOptions.Default;
Options.ValidateOutput := True; // 受け入れる前にreparse
Options.MaxReplacementBytes := 64 * 1024;
Pdf.OnContentOperator := HandleOperator;
if Pdf.VisitLoadedPageContentOperators(PageIndex, True, Options, nil, Stats) then
if Stats.Modified then
Pdf.SaveLoadedDocument('cleaned.pdf');
end;
アクションの集合が2つの退出手段を持つのには理由がある。coaStopは巡回を終了し、それまでに決定した内容をそのまま保持する。これは探していたものが見つかった時に適している。coaRejectは訪問全体を放棄し、ページには一切手を触れない。これは書き換えルールがカバーしていない構文に出会った時に適している。インライン画像は典型的な例である:そのバイナリペイロードは、色を正規化するビジターが編集していくべきものではない
統計の読み方、そして巡回が止まる場所
ステータスフィールドは完了、停止、却下、不正な入力、上限超過、不正な出力、そしてビジターエラーを区別する。これらは異なる問題である:不正な出力は置換したバイト列が再パースできなかったことを意味し、不正な入力は元のストリームがすでに不正な形式であり編集が試みられなかったことを意味する
各カウンターは監査を自己検証可能にする。VisitedOperatorCountとKeptOperatorCount、DeletedOperatorCount、ReplacedOperatorCountは合計が一致しなければならず、最大深さの数値は、そのページが詳しく調べる価値があるほど深くネストしているかどうかを示す。SourceBytesとOutputBytesを照らし合わせれば、編集によるサイズへの影響が1つの数値で分かる
よくあるタスクに対しては、ビジターを一切書かなくて済む、より高レベルな経路が存在する。本当のリダクションに必要なリソースのクリーンアップとともにコンテンツを永続的に削除する方法はロード済みドキュメントに対するリダクションとページの継ぎ合わせで扱われている。表示されているテキストの置換は既存PDFにおける検索と置換で扱われている。ルールがテキストではなく構造に関するものである場合、あるいは既存のレポートでは答えられない監査が必要な場合は、演算子ビジターに手を伸ばすとよい
コンテンツストリームの巡回、抽出、書き換えはいずれも、DelphiおよびC++Builderにおいて同じロード済みドキュメントモデルに対して動作する。機能一覧の全体はHotPDF Delphi PDFコンポーネントページにある