PDFium Componentは、2つのTPdfメソッドでDelphiからPDFの任意コンテンツレイヤー(OCG)を制御します。InspectOptionalContentはすべてのレイヤーを、PDFiumが実際に描画する可視性と合わせて一覧し、SaveAsOptionalContentConfiguredは選んだレイヤーのオン・オフを反映した検証済みコピーを書き出します。2つ目のメソッドはさらに、放置すればあなたの編集を静かに元に戻してしまうUsageと/ASのルールも無効化します。どちらもTPdfに既に開かれている文書に対して動くため、ビューアの表示と同期を取るべき2つ目のパーサーはありません
この要望が届くのは、たいていCADやGISの現場からです。図面セットには寸法、注釈、タイトルブロックが別々のレイヤーで入っており、サプライヤーへ渡す前に寸法を隠したコピーが欲しい、というものです。PDFiumは任意コンテンツを正しく描画しますが、公開ABIにはOCGを列挙し、構成を選び、レイヤー状態を切り替える関数がありません。そこでオブジェクトレベルに降りて/OCPropertiesを編集し、保存し、再読み込みしても、レイヤーはまだそこにあります。理由はPDFiumの可視性ロジックにあり、バイトを触る前に理解しておく価値があります
/ONと/OFFを編集してもPDFiumの描画が変わらない理由
構成ディクショナリの/ONと/OFF配列を編集するだけでは足りません。PDFiumはOCG自身の/Usageディクショナリ内の明示的な状態にこれらの配列より勝たせるからであり、さらに/ASの自動状態ルールがその両方を上書きし得ます。ISO 32000-1 §8.11.4は構成と使用ディクショナリを別々の機構として記述していますが、PDFiumのレンダラーはそれを1つの判定へ折りたたみ、InspectOptionalContentはこの順序でそれを再現します
- 構成の
/BaseStateから出発する。ここでは/ONと/Unchangedはどちらも可視と数え、隠すのは/OFFだけ - 構成の
/ON配列を適用し、続いて/OFF配列を適用する。両方に挙がったグループは最終的に隠れる - 要求された用途についてグループの明示的なUsage状態を適用する。たとえば
/Usage << /View << /ViewState /OFF >> >>のようなもので、ここまでのすべてを上書きする /Intentに/Viewも/Allも含まないグループは可視として扱う。ビューintentの可視性に加担しないためだ- 最後に選択された構成の
/AS配列を実行する。一致するイベントに対するそのエントリは、列挙されたグループの状態を設定する
人を焼くのは3つ目のステップです。レイアウトツールが保存したファイルは、全OCGに/ViewState /ONを載せていることがよくあります。そうなるとPDFiumは丹念に編集した/OFF配列を無視します。保存は成功し、ファイルは綺麗に再オープンし、それでもレイヤーは描かれます。PrintとExportについては、OcExplicitUsageStateはまずPrintStateかExportStateを読み、該当エントリがなければViewStateへフォールバックします。単独のViewState /ONが印刷でもレイヤーを釘付けにするわけです。OCMD(§8.11.2.2)を参照するマーク付きコンテンツは、続いてこれらのグループごとの結果に照らして解決されます。/Pポリシー経由で、あるいはあれば/VE可視性式で解決します
PDFiumが実際に表示するレイヤーの一覧を出す方法
TPdf.InspectOptionalContentはTPdfOptionalContentInventoryを返します。そのGroups配列は各OCGのオブジェクト番号、名前、intents、3つのUsage状態、言語、ズーム範囲、Lockedフラグ、ラジオグループインデックス、そして計算済みのEffectiveVisibleを運びます。メソッドはまずPDFiumに現在のメモリ内文書を保存させ、オブジェクトストリームを展開し、結果を走査するため、セッション内の先行する編集が反映されます。構成インデックス0は常にデフォルトの/Dディクショナリであり、/Configsのエントリはインデックス1から続きます。デフォルト引数の-1はインデックス0を選びます。/OCPropertiesを持たない文書では、メソッドは例外を投げる代わりにFalseを返し、理由をErrorMessageに入れます
procedure TFormMain.ListLayers;
var
Inv: TPdfOptionalContentInventory;
G: TPdfOptionalContentGroup;
begin
// UsageのデフォルトはocuView。-1は構成0、すなわち/Dディクショナリを選ぶ
if not Pdf.InspectOptionalContent(Inv) then
begin
Memo1.Lines.Add('No usable layers: ' + Inv.ErrorMessage);
Exit;
end;
Memo1.Lines.Add(Format('Configuration %d: %s',
[Inv.SelectedConfigurationIndex,
string(Inv.Configurations[Inv.SelectedConfigurationIndex].Name)]));
for G in Inv.Groups do
Memo1.Lines.Add(Format('obj %d %s visible=%s locked=%s radio=%d',
[G.ObjectNumber, string(G.Name),
BoolToStr(G.EffectiveVisible, True),
BoolToStr(G.Locked, True), G.RadioGroupIndex]));
end;
Memberships配列はすべてのOCMDを、そのPolicy(ocmpAnyOn、ocmpAllOn、ocmpAnyOff、ocmpAllOff)、生のVisibilityExpressionテキスト、そして自身のEffectiveVisibleと合わせて報告します。いくつかの境界ルールは意図的です。/Pのデフォルトは/AnyOnであり、グループを持たないOCMDは可視と数えます。既知のOCGではないオブジェクト番号への参照は、式全体を失敗させるのではなく可視として扱います。/VEの評価はネスト深さ32で止まり、それより深いものは隠として扱います。敵対的または自己参照する式が、インスペクションをスタックオーバーフローに変えないためです
SaveAsOptionalContentConfiguredで新しいレイヤー状態を書く
TPdf.SaveAsOptionalContentConfiguredはTPdfOptionalContentStateChangeレコード(グループオブジェクト番号とVisible)の配列を受け取り、選択された構成がちょうどその状態を生む文書を書き出します。選択された構成は/BaseState /ONと、全グループを網羅する完全な/ON・/OFF配列を受け取り、既にUsageディクショナリを持つ各OCGは、新しい状態に一致する明示的なViewState(Options.Usageに従ってPrintState・ExportStateの場合もあり)を受け取ります。TPdfOptionalContentConfigureOptions.Defaultでは、選択された構成の/ASキーが取り除かれるため、open、print、exportのイベントがレイヤーを元に戻すことはできません
procedure TFormMain.SaveWithoutDimensions(DimensionsObj, NotesObj: Integer);
var
Changes: TPdfOptionalContentStateChanges;
Options: TPdfOptionalContentConfigureOptions;
Report: TPdfOptionalContentConfigureReport;
begin
SetLength(Changes, 2);
Changes[0].GroupObjectNumber := DimensionsObj;
Changes[0].Visible := False;
Changes[1].GroupObjectNumber := NotesObj;
Changes[1].Visible := True;
// 構成0、ocuView、DisableAutomaticStateとEnforceRadioGroupsはTrue
Options := TPdfOptionalContentConfigureOptions.Default;
if not Pdf.SaveAsOptionalContentConfigured('C:\Out\Drawing-NoDims.pdf',
Changes, Options, Report) then
raise Exception.Create('Layer update rejected: ' + Report.ErrorMessage);
Log(Format('%d of %d groups changed, %d Usage states rewritten, /AS removed: %s',
[Report.ChangedGroupCount, Report.GroupCount,
Report.UpdatedUsageStateCount,
BoolToStr(Report.RemovedAutomaticState, True)]));
end;
書き込み経路は、PDFium自身の保存出力をバイト単位の接頭辞として保ち、書き直された構成オーナーとUsageディクショナリを運ぶOCGオブジェクトだけを追加し、その後に新しいxrefセクションとtrailerを続けます。1バイト目が出力先へ届く前に、結果は別のTPdfで厳格なロードポリシーのもと再オープンされ、相互参照テーブルが検証を通らなければメソッドは失敗します。ファイルオーバーロードはさらに一歩進んでいます。ターゲットの脇に一時ファイルへ書き出し、検証が成功した後にだけターゲットを置き換えるため、拒否された更新が中途半端な図面を残すことはありません。PDFium ComponentのPDF名前ツリー・番号ツリーエディタが使うのと同じ、検証付き増分リビジョンの手法です
構成付き保存が拒否すること
構成付き保存は、文書自身が禁じている変更や安全に表現できない変更を拒否し、その拒否はすべて出力先に触れる前に起きます。/OCGsにないオブジェクト番号は問答無用で失敗します。構成の/Locked配列に挙がっているグループの変更は失敗しますが、現在値の再宣言は許されます。EnforceRadioGroupsが有効なとき、可視メンバーが2つ以上残ることになる/RBGroupsのセットは、他を黙ってオフにする代わりに拒否されます。暗号化文書は、平文の増分オブジェクトがアクティブなセキュリティハンドラを運べないため拒否されます。署名付き文書はAllowSignedDocument = Trueを渡さない限りEPdfErrorを投げます。ページの見た目を変えることは、署名カバレッジや認証ポリシーを壊し得るからです
function TFormMain.SavePrintPreset(Target: TStream;
const Changes: TPdfOptionalContentStateChanges): Boolean;
var
Options: TPdfOptionalContentConfigureOptions;
Report: TPdfOptionalContentConfigureReport;
begin
Options := TPdfOptionalContentConfigureOptions.Default;
Options.Usage := ocuPrint; // /Print << /PrintState ... >> を書き出す
Options.ConfigurationIndex := 1; // /Configsの最初のエントリ。/Dではない
try
Result := Pdf.SaveAsOptionalContentConfigured(Target, Changes, Options,
Report); // AllowSignedDocumentはFalseのまま
if not Result then
ShowMessage(Report.ErrorMessage);
except
on E: EPdfError do
begin
ShowMessage(E.Message); // 署名付きファイル:Targetには何も書き込まれない
Result := False;
end;
end;
end;
バッチジョブに組み込む前に、トレードオフを知っておきましょう。追加されるリビジョンは、元のファイルバイトではなくPDFiumのフル再保存の上に載ります。署名付き入力が明示的な同意を必要とするのはまさにそのためです。書き直しは選択された構成を/BaseState /ONへ正規化するため、作者の/Unchangedや/OFFのベースラインは、同じ結果の可視性を持つ明示的な配列に置き換わります。/ASの削除は、紙のときだけ現れる透かしレイヤーのような印刷専用の仕掛けを取り除きます。これらのルールを残したいならDisableAutomaticStateをFalseにします。ただしそのイベントでは要求した状態が上書きされ得るを受け入れましょう。プラス面では、PDF/A-2(ISO 19005-2 clause 6.9)とPDF/UA(ISO 14289-1 clause 7.10)はどちらも構成ディクショナリ内の/ASを禁じているため、デフォルト出力はPDFium ComponentによるPDF/Aプレフライト検証が報告していたはずの問題を1つ取り除きます
Delphi PDFビューアにおけるレイヤー制御の位置
ビューアでは、レイヤー制御はインベントリと保存結果の再ロードに基づくチェックリストです。チェックリストをGroupsから埋め、Lockedのエントリは無効化し、RadioGroupIndexを共有するメンバーは排他として扱い、適用時にはTMemoryStreamへ書き出してそのストリームをTPdfへ読み込み直せば、ビューが新しい状態を描きます。TPdfとTPdfViewの配線は、DelphiでPDFium VCLを使って機能豊富なPDFビューアを構築するで扱っています。ライセンス、トライアルダウンロード、その他の機能一式は、PDFium Component for Delphiの製品ページにあります