バージョン 3.223.0 から、PDFium Component for Delphi はフォームフィールドのナビゲーション順を、各ページが宣言した /Tabs 方針で決めます。行順の R、列順の C、構造順の S に加え、PDF 2.0 の A と W です。TPdf.FormFieldTabOrder がその順序を返し、ステータス付きのオーバーロードが、宣言された方針が適用されたのか、フォールバックが使われたのかを報告します
これを動かしている苦情はいつも同じです。ユーザーが 2 列の保険フォームを埋め、氏名フィールドの後に Tab を押すと、カーソルは右列の底へ飛びます。インデックスには何も壊れていません。プロデューサは右列を先に /Annots へ書き、視覚順を直すためにページへ /Tabs /R を宣言しただけであり、/Annots を歩いて宣言を無視するビューアは間違った場所に着地します。ウィジェットインデックス対注釈インデックスの記事が、ウィジェット序数と /Annots 配列の関係を扱います。PDFium Component では、確立できるときは常に宣言された方針が勝ち、確立できないときは、コンポーネントが理由を教えます
/Tabs エントリは実際に何を宣言しているのか
/Tabs はページオブジェクト内の省略可能な名前(ISO 32000-2 §7.7.3.3。ISO 32000-1 では表 30)で、そのページの注釈をどの順序で訪問すべきかをプロセッサに伝え、セマンティクスは §12.5.1 の注釈の下で綴られています。PDF 1.5 が 3 つの値を導入し、PDF 2.0 がさらに 2 つを加えました
/R(行順):行は上から下へ、各行の中ではビューアの読む方向/C(列順):列は読む方向へ、各列の中では上から下へ/S(構造順):論理構造ツリーが注釈を参照する順序/A(注釈配列順):/Annots自体の順序(PDF 2.0)/W(ウィジェット順):まずウィジェットを配列順に、その後に他の注釈(PDF 2.0)
手書き実装が引っかかる細部は継承です。/Tabs はページ辞書にのみ属します。ISO 32000-2 §7.7.3.4 は継承可能なページ属性を Resources、MediaBox、CropBox、Rotate に限定するため、/Pages ノードに置かれた /Tabs 鍵は無意味であり、親チェーンを上って 1 つを探すリーダーは、作者が設定していない方針を発明しています。PDFium Component は /Tabs をページ自身から読み、ほかからは読みません
FormFieldTabOrder で宣言された順序を読む
TPdf.FormFieldTabOrder は現在ページのフォームフィールドを、宣言されたタブ順の 0 基点ウィジェット序数で返し、out Status: TPdfFormTabOrderStatus パラメータ付きのオーバーロードが、その順序がどう生まれたのかを説明します。同じペアが TPdfView にもあり、ステータスなしのオーバーロードは同一の方針を使いながらステータスを捨てます
uses
PDFium;
const
ModeNames: array[TPdfFormTabOrderMode] of string =
('unspecified', 'R', 'C', 'S', 'A', 'W', 'unsupported');
procedure DumpTabOrder(const FileName: string; PageNo: Integer);
var
Pdf: TPdf;
Order: TPdfIntegerArray;
Status: TPdfFormTabOrderStatus;
I: Integer;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := FileName;
Pdf.FormFill := True; // すでに既定。フィールドには必須
Pdf.Active := True;
if not Pdf.Active or (Pdf.FormType <> ftAcroForm) then
Exit;
Pdf.PageNumber := PageNo;
Order := Pdf.FormFieldTabOrder(Status);
Writeln('requested ', ModeNames[Status.RequestedMode],
', applied ', ModeNames[Status.EffectiveMode]);
if Status.UsedFallback then
Writeln('fallback: ', Status.ErrorMessage);
if Status.UnstructuredWidgetCount > 0 then
Writeln(Status.UnstructuredWidgetCount, ' widgets not in the structure tree');
for I := 0 to High(Order) do
Writeln(I, ': field ', Order[I], ' ',
string(Pdf.FormFieldInfo[Order[I]].Name));
finally
Pdf.Free;
end;
end;
ステータスレコードこそ、沈黙の順序決定をログに残せるものへ変える部分です。RequestedMode はページが宣言したもの、EffectiveMode は適用されたもの、UsedFallback は宣言された方針が尊重できず注釈配列順が戻ったときに真で、ErrorMessage に短い理由が入ります。SnapshotAvailable は、コンポーネントが文書メタデータをそもそも読めたかを伝えます。欠けた /Tabs は、宣言された方針として通すのではなく、注釈配列順へのフォールバックとして報告されるため、サポートログは「作者がこの順序を求めた」と「ページは何も求めなかった」を区別できます
行順と列順はどうして /Rect 値のソートだけでは済まないのか
行順と列順は、生の注釈座標ではなく表示幾何で計算されなければなりません。ページの /Rotate と文書の読む方向が、どちらも「行」の意味を変えるからです。/Rotate 90 のページの /Rect 配列を y 座標でソートすると、得られるのはビューアが列として表示するものです。そこでコンポーネントは、固有回転込みのページ表示変換を通じてすべてのウィジェット矩形を 1/64 ポイント解像度で写像し、そこからグルーピングします
読む方向はカタログの ViewerPreferences 辞書(ISO 32000-2 §12.2)から来ます。/Direction /R2L は行内の順序を反転し、列の順序も反転します。アラビア語やヘブライ語のフォームが期待するのはこれです。/Direction がないときは L2R が適用されます。L2R でも R2L でもない値が入っているときは、行順と列順が理由付きで注釈配列順へフォールバックします。方向を当て推量するのは、ないと認めるより劣るからです
グルーピングはプロセッサが正当に分かれ得る場所なので、正確な規則を知る価値があります。行順では、未訪問の最上端のウィジェットがアンカーになり、垂直中心がアンカーの上端と下端の間に落ちるすべてのウィジェットがその行へ加わります。列順はこれを水平に映します。同点はウィジェット序数で決定的に切れます。きれいなグリッドでは、これはまともなビューアのどれとも一致します。強く重なったり千鳥になったりしたレイアウトでは、別の許容値を使う別のプロセッサが別々にグルーピングし得て、ISO 32000 のどこも、どちらの答えも間違いとは言っていません
構造順はどう順序を決めるのか
構造順は、構造ツリー内のオブジェクト参照(OBJR)辞書に従います。/K 配列の深さ優先走査が実際に届く順序でです。/ParentTree の鍵順には従わず、StructTreeRoot の列挙がたまたま生む順序にも従いません。これらの順序は単一プロデューサのファイルではしばしば一致し、エディタがツリーを並べ替えた後に乖離します。まさにタグ付きフォームの読み順が最も効いてくる瞬間です。ツリーを自分で保守するなら、/ParentTree と /StructParents の同期を保つガイドが、これと整合を保つべき逆マッピングを扱います
構造ツリーが一度も参照しないウィジェットの処遇は、仕様がプロセッサに委ねています。PDFium Component はそれらを、構造化ウィジェットの後へ注釈配列順で追記し、追記した数を UnstructuredWidgetCount で報告します。構造ツリーがまったくない文書に /S を宣言したページは、空の走査として扱われます。すべてのウィジェットがその末尾へ落ち、カウントがそれを語り、結果は完全なフォールバックとしては印付けされません。循環を持つ構造ツリー、2 度参照される OBJR、/Pg が注釈を所有するページと矛盾する OBJR は、構造順をフォールバックさせます。これらの欠陥は S を使えなくしますが、他のページの R や C は乱しません
タブ順の値はなぜ注釈インデックスではないのか
FormFieldTabOrder が返す配列の値はウィジェットだけを数えます。一方 OBJR は、完全な /Annots 配列に住む注釈オブジェクトを特定します。この 2 つのインデックス空間を混ぜることが、構造順を消費するあらゆるコードで最も起こりやすいバグです。/Annots が [Link, Widget A, Widget B] のページを取ってください。Widget B の OBJR は注釈インデックス 2 を指します。Widget B のフィールド序数は 1 です。2 を FocusFormField へ流し込むと、ページのウィジェット数次第で、間違ったフィールドへフォーカスするか、何もフォーカスしません
コンポーネントはその変換を内部で行うため、その結果は FocusFormField と FormFieldInfo へそのまま入ります。自前のコードが OBJR の宛先や他の注釈インデックスを読むなら、フィールドとして扱う前に明示的に変換してください
// ページの注釈インデックスを、FocusFormField、FormFieldInfo、
// FormFieldTabOrder が使うウィジェット序数へ変換する。ウィジェットでなければ -1
function AnnotationToFieldOrdinal(Pdf: TPdf; AnnotIndex: Integer): Integer;
var
I: Integer;
begin
Result := -1;
if (AnnotIndex < 0) or (AnnotIndex >= Pdf.AnnotationCount) then
Exit;
if Pdf.Annotation[AnnotIndex].Subtype <> anWidget then
Exit; // リンク、ノート、ポップアップはフィールドではない
Result := 0;
for I := 0 to AnnotIndex - 1 do
if Pdf.Annotation[I].Subtype = anWidget then
Inc(Result);
end;
結果は、ページがロードされていて未編集である間だけ有効として扱ってください。ウィジェットの前に任意の注釈を追加・削除すると、両インデックス空間がずれます。編集前のキャッシュ済みマッピングこそ、顧客ファイルでのみ再現する off-by-one の典型です
FocusNextFormField によるページ横断ナビゲーション
FocusNextFormField(AcrossPages: Boolean; WrapDocument: Boolean = True; MaxPagesToScan: Integer = 256) は現在ページのタブ順を歩き、その後、後続ページへ続きます。3.223.0 から、訪問したすべてのページが自前の宣言方針を適用します。ページ横断のオーバーロードは 3.220.0 に届いたため、ラップを検出して自前で PageNumber を変えるようフォームフィールドナビゲーションの記事で勧めていたやり方は、1 ページ内でまだ循環する引数なしのオーバーロードにだけ当てはまります
procedure TFormViewer.HandleTabKey(Shift: TShiftState);
var
Field: Integer;
begin
if ssShift in Shift then
Field := PdfView.FocusPreviousFormField(True) // ページ横断、ラップする
else
Field := PdfView.FocusNextFormField(True);
if Field < 0 then
Beep // 走査内に対象なし。ページとフォーカスは不変
else
UpdateFieldStatus(PdfView.PageNumber, Field);
end;
戻り値は、フォーカスを保持するに至ったページのページローカルなフィールド序数です。PageNumber を並行して読んでください。ページ横断の走査は、ユーザーが意味なく Tab で入れられるウィジェットをスキップします。hidden、invisible、NoView、読み取り専用、signature、未知型、空矩形のウィジェットです。WrapDocument = False を渡すと文書境界で -1 付きで止まります。MaxPagesToScan は 1 から 4096 の間でなければならず、現在ページを含め、検査したすべてのページが計上されます。走査が何も見つけられないときや、ネイティブエンジンが対象へのフォーカスを拒むときは、呼び出しは -1 を返し、前のページとフォーカスをそのままの状態で残します
宣言された順序はどこで効かなくなるのか
宣言された方針は、コンポーネントがページメタデータを信頼して読める場合にだけ尊重され、読めないすべてのケースは、明示的な理由付きで注釈配列ウィジェット順へフォールバックします。境界は列挙できるほど具体的です
- メタデータは元のファイルバイトではなく、現在のネイティブ文書の上限付きの私的シリアライズから来ます。メモリ上のページ移動と編集は、ディスク上のファイルを陳腐化させるからです。スナップショットはコンテンツリビジョンごとにキャッシュされ、通知された編集はそれを無効化し、失敗したスナップショットもキャッシュされます
- 暗号化された文書と、ページオブジェクト編集の最中に要求されたスナップショットにはスナップショットがなく、常にフォールバックします
- スナップショットは 64 MiB、アクティブオブジェクト 65536、ページ 4096、ページあたり注釈 4096、注釈合計 65536、構造深さ 64 で押さえられます。より大きな文書はナビゲーションを滞らせる代わりにフォールバックします
- 対象は AcroForm 文書だけです。XFA フォームはこのプロファイルの外に留まります
- 行順と列順には、有限で空でないウィジェット矩形と健全なページ寸法が要ります。退化した幾何はフォールバックを誘発します
- これらの API は自前のコードからナビゲーションを駆動します。エンジンへ転送された Tab 鍵の扱い自体は PDFium のものが置き換えられないため、宣言順が欲しければ鍵を
FocusNextFormField経由で回してください
問い合わせは現在ページ、ネイティブフォーカス、フォーカス中フィールドの未確定編集テキストを保存し、文書やページのライフサイクルアクションを実行しません。したがってキーストロークごとの FormFieldTabOrder 呼び出しは安全で、所定のコンテンツリビジョンでの最初の呼び出しの後は安上がりです
宣言されたタブ順、ステータス診断、ページ横断フォーカスナビゲーションは、PDFium Component for Delphi and C++Builder の一部です。製品ページは、TPdfFormTabOrderMode と TPdfFormTabOrderStatus のエントリを含む API リファレンス全体へリンクしています