技術記事

PDF 墨消し注釈:マーク、適用、そして /RO オーバーレイ

/IC の色なしで描かれた墨消しマークは、適用ステップを抜けると透明な隙間や中空の輪郭として出てくることがあります。レビュワーのコメントはファイルに残ったままです。PDFlibPas は ApplyRedactions で Redact 注釈を適用します。覆われるコンテンツが先に消え、オーバーレイは /RO から、または黒をフォールバックとして /IC から来て、マークの辞書は空にされます

マーキングは可逆、適用は不可逆

ISO 32000-1 §12.5.6.23 は、墨消しをコンテンツの識別とコンテンツの削除に分けます。識別のあいだ、Redact 注釈はユーザーがまだ見て、動かして、再定義できる提案にすぎません。レビューのラウンドが成り立つのはこのためです。削除はアプリケーションに委ねられ、厳しい要求が付きます。注釈が識別するものはすべて消えること、注釈自身が消えること、そして画像の覆われた部分はデータが破壊されることです。ピクセルの上のクリッピングパスや画像マスクは要件を満たしません。キーの一覧は ISO 32000-1 の Table 192(ISO 32000-2 では Table 195)にあり、その大部分は、何を消すかではなく、削除後のページがどう見えるべきかを記述しています

キー仕様の中での役割ApplyRedactions がすること
QuadPoints削除領域を n 個の四角形で指定。あれば Rect に代わって領域になる各四角形のバウンディングボックスを別々に消し込みます。長さが 8 の倍数でない配列は Rect にフォールバックします
RectQuadPoints がないときの削除領域逆転した角を正規化して使用します
IC削除後の DeviceRGB 塗り。なければ透明。RO があれば無視されますあればそのまま保持し、なければ黒で追加します
ROForm XObject のオーバーレイ。IC、OverlayText、DA、Q に優先します通常アピアランスとして取り込み、ページにフラット化します
OverlayText、Repeat領域の上に描かれるラベル。タイル状にもできます生成されたオーバーレイに描画します。Repeat が true のときはタイル状にします
DA、Qラベルのフォント、サイズ、色。行揃えサイズと色は DA から、行揃えは Q から読みます

AddRedactAnnotation でマークを作る

TPDFlib.AddRedactAnnotation(Left, Top, Width, Height, FillR, FillG, FillB, OverlayText, RepeatOverlay) は、選択されたページにマークを追加し、ページの新しい注釈数を返します。それはマークの 1 起点インデックスでもあり、何も追加されなければ 0 です。座標は現在の原点と単位に従うので、例は左上原点に切り替えます。そこでは Top が上端です。この呼び出しは /Rect と、塗りコンポーネントからの /IC と、固定の /DA である /F0 10 Tf 1 1 1 rg、つまり 10 ポイントの白テキストを書きます。空でない OverlayText は、中央揃えラベルのために /OverlayText、/Repeat、/Q 1 を加えます。Redact 注釈は PDF 1.7 の機能なので、古いヘッダーは 1.7 に引き上げられます。アピアランスストリームはまだ書かれず、保留中のマークは、レビュワーのアプリケーションが描くように見えます。ラベルは白に固定されているので、OverlayText は濃い塗りと組み合わせてください。淡い黄色の上ではラベルが消えます。マークはすべての PDFlibPas エディションで作成できます

uses
  System.SysUtils, PDFlibrary;

procedure MarkForReview(const SourceFile, ReviewFile: string);
var
  Pdf: TPDFlib;
  Index: Integer;
begin
  Pdf := TPDFlib.Create;
  try
    if Pdf.LoadFromFile(SourceFile, '') <> 1 then
      raise Exception.CreateFmt('Cannot open %s', [SourceFile]);
    Pdf.SetOrigin(1);            // 左上原点:Top が上端
    Pdf.SetMeasurementUnits(0);  // ポイント
    if Pdf.SelectPage(1) <> 1 then
      raise Exception.Create('Page 1 not found');
    // 証言者名:黒の塗り、中央の白ラベル
    Index := Pdf.AddRedactAnnotation(72, 180, 220, 14, 0, 0, 0, 'REDACTED');
    if Index = 0 then
      raise Exception.Create('Mark was not added');
    // 口座ブロック:免除コードを高いボックスにタイル状に
    Pdf.AddRedactAnnotation(72, 410, 300, 40, 0, 0, 0, '(b)(6)', 1);
    // マークだけ。元のコンテンツのすべてのバイトはまだ存在する
    Pdf.SaveToFile(ReviewFile);
  finally
    Pdf.Free;
  end;
end;

QuadPoints が削除領域を決め、返信はテキストを保つ

マークが /QuadPoints を運ぶときは、その四角形たちが ApplyRedactions が消すものを決め、Rect は表示のための外枠にすぎません。テキスト選択ツールが作ったマークは、選択行ごとに 1 つの四角形を持つのが普通です。だからそのようなマークを SetAnnotRect でリサイズすると、画面の枠は動くのに削除領域は元のままです。領域を動かす呼び出しは SetAnnotQuadPoints で、Rect だけに頼るマークは GetAnnotQuadCount が 0 を返します。マークに付くテキストは別の規則に従います。レビューツールはノートを /Contents に、作者を /T に保持します。そして墨消しマークのノートは、消されるまさにそのものを記述しがちです(「顧客の SSN、法務に確認」)。ApplyRedactions はマーク自身の辞書を空にしますが、/IRT でその下にスレッドされた返信は別の注釈で、その矩形が墨消し領域と重なるときだけ削除されます。余白に置かれた返信はテキストを保ちます。下のリストは、各マークを四角形数とともに、その下にスレッドされた返信とともに出力します

Delphi 向け PDFlibPas の墨消しフローにおける QuadPoints 対 Rect の構造。各四角形は自分の削除領域を囲み、余白の返信はテキストを保つ
quadpoints は削除を 1 行ずつ囲みます。マークを動かすことは SetAnnotQuadPoints を意味し、領域の外の余白の返信はテキストを保ちます
procedure ListPendingMarks(Pdf: TPDFlib);
var
  P, I, Parent: Integer;
begin
  for P := 1 to Pdf.PageCount do
  begin
    if Pdf.SelectPage(P) <> 1 then
      Continue;
    for I := 1 to Pdf.AnnotationCount do
      if Pdf.GetAnnotType(I) = 'Redact' then
        Writeln(Format('p%d #%d by %s, quads=%d: %s',
          [P, I, Pdf.GetAnnotTitle(I), Pdf.GetAnnotQuadCount(I),
           Pdf.GetAnnotContentsEx(I)]))
      else
      begin
        Parent := Pdf.GetAnnotReplyTo(I);   // /IRT がないときは 0
        if (Parent > 0) and (Pdf.GetAnnotType(Parent) = 'Redact') then
          Writeln(Format('  reply #%d to #%d: %s',
            [I, Parent, Pdf.GetAnnotContentsEx(I)]));
      end;
  end;
end;

ApplyRedactions のあとにどのオーバーレイが現れるか

/RO エントリは、ある限り常に勝ちます。Table 192 の優先規則がそう要求します。ApplyRedactions はその Form XObject を注釈の通常アピアランス(/AP /N)として取り込み、アピアランス状態を捨ててフラット化します。だから /IC、/OverlayText、/DA、/Q はそのマークに効き目を持ちません。配置が唯一の微妙な部分です。仕様は、オーバーレイの原点が Rect の左下の角に揃うと言います。一方フラット化は、フォームの BBox をその Matrix を通じて Rect へ写像します。フォームフィールドウィジェットにフラット化が適用する BBox と行列の配置と同じです。BBox が [0 0 w h] で矩形に合わせたサイズ、Matrix が単位行列のオーバーレイは、どちらの読み方でも同じに見えます。だから自分のツールが /RO マークを生成するときは、この形を求めてください

/RO がないとき、ApplyRedactions は辞書を正規化し、アピアランスを自分で組み立てます。/C と /BS を削除し、/Border を [0 0 0] にします。これらのエントリは保留中のマーク、たいてい色のついた輪郭を記述していて、それを最終ページに焼き付けると、完成した墨消しが未完成に見えます。境界が消えれば、塗りが矩形全体を覆います。ラベルは生成されたフォームの中に描かれます。サイズと色は /DA から、行揃えは /Q から、タイル状は /Repeat から。そしてフォント名 F0 は、そのフォーム自身のリソースの中で標準 Helvetica に束縛されます。この束縛がものを言うのは、ファイルがツールの間を旅するときです。ISO 32000-1 §12.7.3.3 は DA のフォント名を、インタラクティブフォームのデフォルトリソース(/DR)のエントリと定義します。フォームフィールドのない文書にはそれがなく、同じマークを適用する別のアプリケーションはフォールバックフォントに置き換えるかもしれません。PDFlibPas はその検索を飛ばします。そして Helvetica なら、ラベルは REDACTED や免除コードのようなラテン文字のテキストに向きます

Delphi 向け PDFlibPas の ApplyRedactions 後のオーバーレイ判定フロー。RO があれば勝ってフラット化され、IC がなければ黒にフォールバック
RO の Form XObject は通常アピアランスとして取り込まれ、フラット化されます。ないマークは再構築されたアピアランスを得て、IC がなければ黒の塗りになります

欠けた /IC は黒い箱になる

仕様の、/IC がないときの既定は透明な内部です。そして PDFlibPas は意図的にそこから外れます。削除のあと、透明な内部は紙を見せます。かつて名前のあった場所の白い隙間です。その隙間は、最初から何もなかったレイアウトと同じに見えます。リーダーは墨消しを空白と区別できず、レビュワーは抽出テキストを比べない限り、墨消しが起きたことを確認できません。中空の枠、つまり輪郭だけのマークの保留アピアランスをフラット化する適用ステップが出すものは、別の仕方で失敗します。誰も行動しなかった提案のように読めるのです。私たちは既定を文字どおりに踏むことを検討し、黒い箱を選びました。何かが伏せられたという目に見える合図は、墨消しされた文書が読者に語るものの一部であり、そしてこの選択は、作者が塗りを一度も明言しなかったマークにしか効かないからです。辞書に既にある /IC は、その値が何であれ使われます。白を求めたプロデューサーは白を得ます

オーバーレイが描かれる前に何が消えているか

ApplyRedactions はページごとに働き、どのオーバーレイをフラット化するより先にすべての削除を終えます。だからオーバーレイが自分の領域に捕まることはありません。ページごとに、四角形ごとの矩形を 1 つずつ、あるいは四角形を持たないマークの Rect を集め、それぞれに対してすべてのコンテンツ層を消し込みます:

  • テキストはグリフごとに切り取られます。ページフォントと完全なテキスト状態から測り、部分的に覆われた文字列の残りは元の位置に留まります
  • 領域の中に塗るベクターパス、インライン画像、シェーディングはコンテンツストリームから削除されます
  • Form XObject は再帰的に入り込みます。他のページや配置も描くフォームは、プライベートコピーを通じて墨消しされるので、それらのページは元のものを保ちます
  • 画像 XObject は覆われたピクセルだけを失い、黒(CMYK では K インク全量)で上書きされます。JPEG、JPEG 2000、JBIG2、CCITT の画像はデコードされ、ロスレスの Flate ストリームとして格納し直されます
  • タグ付き PDF では、削除されたマーク済みコンテンツに付いていた /Alt と /ActualText が、ページコンテンツと構造ツリーでクリアされます
  • 矩形が領域に触れる他の注釈は削除され、ウィジェットがそこに座るフォームフィールドは値を失います

ピクセルのステップは、仕様の画像条項を実践に移したものです。スキャンしたパスポートの上に塗られた黒い矩形は、目を満足させます。それでも §12.5.6.23 は落ちます。誰かが画像を抽出した瞬間、元のサンプルがデコードされるからです。同じエンジンが、渡された矩形からコンテンツを直接取り除き、SanitizeDocument と組む RedactRegion の背後に座っています。注釈は一切読みません。コンテンツが消えたら、各マークはフラット化され、辞書は /Type と /Subtype まで切り詰められます。フラット化は注釈をページの /Annots 配列から取り除きます。それでも辞書は文書の中のオブジェクトで、それを書き出す保存は、/Contents、/RC、/T とレビューの足跡の残りを受取人に渡してしまいます

墨消し前のリビジョンを残さない保存

インクリメンタル保存は、上のすべてを無効にします。§7.5.6(ISO 32000-1 と ISO 32000-2 で同じ番号)の下では、インクリメンタル更新は変更されたオブジェクトをファイルの末尾に追記し、元のバイトをそのまま残します。だから元のリビジョンは、ページコンテンツも、画像も、注釈付きのマークも、ファイルを前の %%EOF で切り取る人には読め続けます。PDFlibPas Small Business エディションの ExtractDocumentRevision は、それを 1 回の呼び出しでやります。ApplyRedactions のあとは、SaveToFile で新しいファイルを書きます。追記する SaveChanges は決して使わない。保存の前に、GarbageCollectObjects がオブジェクトテーブルを掃き、もう何も参照しないオブジェクトを落とします。適用ステップの前にマークが運んでいたアピアランスストリームなどです。完全保存はその掃除を飛ばします。SetGarbageCollect が有効にするか、SaveToFileOptions に渡す TPDFlibSaveOptions レコードの GarbageCollect フィールドがセットされていない限り。メタデータが残る隙間です。SanitizeDocument(SANITIZE_METADATA or SANITIZE_XMP_PACKET) は Author、Subject、Keywords、Creator、Producer を空にし、XMP ストリームを取り除きます。Title は自分で確認する分として残ります。ここでは SANITIZE_ALL を避けてください。SANITIZE_ANNOTATIONS を含むので、文書に残っているすべての注釈を削除します。残すつもりのノートもそこへ含まれます

Delphi 向け PDFlibPas の ApplyRedactions 後の保存タイムライン。インクリメンタル追記は元を復元可能なまま残すので、リリースではサニタイズ、ガベージコレクト、完全な書き直しを行う
適用後のリリースパスは、メタデータのサニタイズ、未参照オブジェクトの一掃、新しいファイルへの保存です。インクリメンタル保存は墨消し前のリビジョンを読めるまま残すのですから

ApplyRedactions は RedactRegion と同じく、Enterprise エディションで利用できます。素の TPDFlib では 0 を返し、LastErrorCode を PDFLIB_ERROR_ENTERPRISE_REQUIRED(900)にセットします。だからリリースステップは、PDFlibEnterprise ユニットの TPDFlibEnterprise を使います

uses
  System.SysUtils, PDFlibrary, PDFlibEnterprise;

procedure ApplyAndRelease(const ReviewFile, ReleaseFile: string);
var
  Pdf: TPDFlibEnterprise;
  Applied, Orphans: Integer;
begin
  Pdf := TPDFlibEnterprise.Create;
  try
    if Pdf.LoadFromFile(ReviewFile, '') <> 1 then
      raise Exception.CreateFmt('Cannot open %s', [ReviewFile]);
    Applied := Pdf.ApplyRedactions;   // すべてのページのすべての Redact マーク
    if Applied = 0 then
      raise Exception.CreateFmt('No marks applied (error %d)',
        [Pdf.LastErrorCode]);
    // Info と XMP は、ページがもう見せないものをまだ名指せる
    Pdf.SanitizeDocument(SANITIZE_METADATA or SANITIZE_XMP_PACKET);
    Orphans := Pdf.GarbageCollectObjects;
    // 新しいファイルへの完全な書き直し。墨消しのあとに SaveChanges はしない
    if Pdf.SaveToFile(ReleaseFile) <> 1 then
      raise Exception.Create('Save failed');
    Writeln(Format('%d marks applied, %d unreferenced objects dropped',
      [Applied, Orphans]));
  finally
    Pdf.Free;
  end;
end;

期待結果:ApplyRedactions は全ページにわたってフラット化した Redact マークの数を返します。MarkForReview が書いたレビューコピーなら 2 です。そして GarbageCollectObjects は、掃除が落とした未参照オブジェクトの数を返します

どのツールがマークを描いたかにかかわらず、その /RO と /IC のエントリが、受取人がページで見るものを決めます。そして PDFlibPas Enterprise for Delphi の適用ステップが、その下で消えたものを決めます