マーク付きコンテンツは、ISO 32000-1 §14.6がページコンテンツのタグ付けのために定義するメカニズムであり、タグ付きPDFとPDF/UAはどちらもその上に構築される。PDFium Componentはこれを直接公開する。PageObjectMarksはページオブジェクトからすべてのBDCタグとそのプロパティリストを読み、AddPageObjectMarkは1つを書き、RemovePageObjectMarkは1つを削除し、PageObjectMarkedContentIDはコンテンツを構造ツリーにつなぐMCIDを返す
構造ツリーが、それが記述するコンテンツにつなぎ戻せるようになるまで、アクセシビリティツールは推測でしかない。構造ツリーは「これは見出し」と言う。MCIDは、その見出しが実際にどのページのどのマークであるかを言う。アプリケーションがタグ付けをチェックし、修復し、報告するには、両方が読めなければならない
バイト列におけるマークとは何か
タグ名とオプションのプロパティリストを持つBDCオペレータで、EMCで閉じられる。コンテンツストリームの中では/P <</MCID 3>> BDC ... EMCのように見える。タグ/Pがロールを名指し、辞書がプロパティを運び、オペレータの間にあるすべてがマーク付きコンテンツだ。そのスパンの中のページオブジェクトがマークを持ち、それがPDFiumが返すものであり、PDFium Componentがレコードに変えるものだ
TPdfContentMarkはハンドル、タグName、TPdfContentMarkParamの配列を保持する。各パラメータはKey、Kind、その種別によって選ばれる1つの意味のある値フィールドを持つ。pmpInt、pmpFloat、pmpString、pmpBlobだ。種別は、たまたま成功したゲッターからではなく、PDFium自身の型報告から来る。これが、プロパティリストを読むことと、プロパティリストを推測することの違いだ
var
Marks: TPdfContentMarks;
M: TPdfContentMark;
P: TPdfContentMarkParam;
I: Integer;
begin
Pdf.PageNumber := 1; // PageNumber is 1-based
for I := 0 to Pdf.ObjectCount - 1 do // page object indexes are 0-based
begin
Marks := Pdf.PageObjectMarks(I);
for M in Marks do
begin
Memo1.Lines.Add('mark ' + M.Name +
' (MCID ' + IntToStr(Pdf.PageObjectMarkedContentID(I)) + ')');
for P in M.Params do
case P.Kind of
pmpInt: Memo1.Lines.Add(' ' + P.Key + ' = ' + IntToStr(P.IntValue));
pmpString: Memo1.Lines.Add(' ' + P.Key + ' = ' + P.StringValue);
pmpFloat: Memo1.Lines.Add(' ' + P.Key + ' = ' + FloatToStr(P.FloatValue));
pmpBlob: Memo1.Lines.Add(' ' + P.Key + ' = ' +
IntToStr(Length(P.BlobValue)) + ' bytes');
end;
end;
end;
end;
なぜpmpUnknownが2つの異なることを意味するのか
pmpUnknownは、PDFiumがFPDF_OBJECT_UNKNOWNを報告したときに返される。そしてPDFiumは、存在しないキーに対しても同じものを返す。この層では2つのケースを区別できず、そうでないふりをするのは、そう述べるよりも悪い
あなたのコードに対する実地の帰結はこうだ。pmpUnknownを「ともかくデコードを試みるかもしれない型」としてではなく、「ここに使える値はない」として扱うこと。プロパティがワークフローにとって重要なら、認識できる種別で存在することを検証し、未知から不在を推測しないこと。プロパティリストを読めないマークは、黙って受け入れるべきマークではなく、報告すべきマークだ
マークレコードはあなたが所有するハンドルではなくスナップショットだ
Handleフィールドはライブラリに属する。マークが削除され、ページオブジェクトが破壊され、ページがアンロードされた瞬間に古くなる。だからレコードは短い命の読み取り専用スナップショットだ。ページをまたいでキャッシュすれば、エンジンが回収したメモリを指すポインタを握ることになる
これはPDFiumにおけるページオブジェクトハンドル全般に適用される規律であり、同じ場所で人を捕らえる。マークレコードで生成されたリストコントロール、ユーザーが別のページへ移動し、ナビゲーションと無関係に見えるクラッシュ。必要な値——名前、キー、数値——を書き出し、ハンドルは手放すこと。変換後に古くなるページオブジェクトハンドルのノートが、一般則とそれがほかのどこで噛むかを扱っている
マークの追加と、見逃しやすい保存ステップ
AddPageObjectMarkはページオブジェクトのインデックス、タグ名、完全なパラメータセットを受け取る。パラメータは1回でセットとして書かれ、キーを1つずつパッチするわけではない。だからTPdfContentMarkParamにHas*センチネルがない。それらが守るはずの「既存レコードの1フィールドを更新する」というケースは生じない
明示的に述べる価値のある部分がある。マークを追加するとページコンテンツストリームをリビルドし、タグが保存を生き残るようにする。SaveAsは単独ではコンテンツを再生成しないため、これを明示的にしなければならなかった。オブジェクトモデルの中にだけ存在する変更は捨てられ、保存されたファイルは元のものとまったく同じに見える。PDFiumのページに何かを追加し、出力にないことに気づいたことがあるなら、通常はこれが理由だ
var
Params: TPdfContentMarkParams;
begin
SetLength(Params, 1);
Params[0].Key := 'MCID';
Params[0].Kind := pmpInt;
Params[0].IntValue := NextMcid;
Pdf.AddPageObjectMark(ObjectIndex, 'P', Params); // rebuilds the content stream
Pdf.UpdatePage;
Pdf.SaveAs('tagged-out.pdf');
end;
これがドキュメントを何にし、何にしないか
マーク単独ではタグ付きPDFを作らない。適合するタグ付きドキュメントは、これらのMCIDを参照する構造要素を持つ構造ツリーと、ドキュメントがマーク済みであると宣言する/MarkInfoエントリと、標準の言う通りの意味を持つロール名を必要とする。どの構造要素も指さないMCIDを持つ/Pマークを書けば、タグ付きだと主張するコンテンツと、それに一度も言及しない構造ツリーを得る
このレベルでマーク付きコンテンツが真に価値を発揮するのは、点検と修復だ。どのページオブジェクトがタグ付きかを監査し、アーティファクトとしてマークされるべきだったものを見つけ出し、構造ツリーに対してMCIDを突き合わせて孤立を見つける。その作業の構造ツリー側については、PDF/UA構造ツリー検証の解説を参照のこと。そしてタグが最終的に向けられる読書体験については、DelphiでアクセシブルなPDFリーダーを作るノートを参照のこと
PDFium ComponentはDelphi、C++Builder、Lazarusのアプリケーションに、PDFiumエンジン上の高レベルVCL APIを与え、マーク付きコンテンツ、構造ツリー、アクセシビリティ検証を通常のPascalコードから扱えるようにする。完全なAPI面についてはPDFium Component製品ページを参照のこと