Files
docview/docs/pdf-structure-adapter-plan.md
T
2026-09-21 13:41:40 +09:00

65 lines
8.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# PDF Form構造見出しアダプターの実装契約
状態: **実装済み、Linux回帰確認済み**(2026-09-19)。当初のForm-only構造を取得できない制約は、Worker内の共有qpdfメタデータ読取で解消した。本書は実装範囲を記録し、全OS・任意の実文書に対するG-HEADINGS合格を宣言するものではない。[試験記録](../tests/results/pdf-structure/README.md)、[合成資料の期待値](../tests/fixtures/pdf/headings/manifest.json)を参照。
## 構成と所有権
`PdfDocument`は一つの`PdfMetadataContext`を作り、`LinkBorderReader`と`StructureReader`で共有する。qpdf 12.4.1はPDFWorkerだけのprivate依存であり、MainにはPDF解析オブジェクトを渡さない。元の読み取り専用QFileとパスワードをPDFium/qpdfの双方が使用し、パスからの再openや原本への保存を行わない。qpdf入力の論理オフセットは独立し、各読取でQFileをseekする。終了時は両reader、context、PDFium document、入力の順で破棄する。
構造解析は`headings(page)`時だけ行う。open時に全構造・全ページを走査しない。共有contextは生のページ木を順番に検証し、読んだページ参照を保持する。探索の例外後はそのcontextのページ探索を停止し、次要求で壊れたノードを飛ばしてページ番号を詰めることを禁止する。
qpdf 12.4.1の`setMaxWarnings(nonzero)`には、初期parseでページ木を走査・修復する副作用があるため使用しない。公開`QPDFLogger::setWarn`の破棄Pipelineで、初期解析を含め警告32行・64KiBを制限する。sinkは警告本文を保持・出力せず、超過はstickyにして以降の入力読取でも停止する。qpdfはsinkを呼ぶ直前に警告を内部へ追加するため、境界を越えた最後の1件は内部に存在し得る。xref回復は無効である。この選択はnull Kid保持、32/33警告、初期警告bytes超過の回帰で検証した。[qpdf 12.4.1 Objects::parse](https://github.com/qpdf/qpdf/blob/v12.4.1/libqpdf/QPDF_objects.cc)、[Pages::cache](https://github.com/qpdf/qpdf/blob/v12.4.1/libqpdf/QPDF_pages.cc)
## 構造の所有元と順序
対象ページと実際に`Do`で呼ばれたFormの`StructParents`から、`ParentTree`のNums/Kidsを解決する。リソース辞書にあるだけの未使用Formは見出し候補にしない。候補の`P`をたどってH/H1–H6を得て、各祖先の順序付き`K`内の位置から表示順を決める。RoleMapは循環と深さを検証する。文字の大きさから見出しを推測しない。
`K`の整数MCID、MCR、配列、子StructElemを同じ順序で展開する。`Pg`は最寄りの構造祖先から継承し、局所指定を優先する。`Stm`付きMCRの所有元はそのForm object/generation、なしならページである。**MCID単独をキーにせず、所有元object/generationと組にする。** 各参照をParentTreeへ逆照合し、その所有元のMCIDが同じ見出しの祖先経路に属することを確認する。子要素のP/K不一致も拒否する。
跨ページのHはページ別fragmentとして返す。同一ページ内では構造オブジェクト同一性で重複を除き、同名の別見出しは統合しない。これにより、ページ、別Form、別ページに同じ数値MCIDがあっても文字や位置を借用しない。
## PDFiumオブジェクトとの対応
固定PDFium公開APIはFormの元stream object番号を返さない。このためqpdfの公開tokenizerで、ページと使用Formの`q/Q/cm/Do`、`BDC/BMC/EMC`、名前付きProperties、Form MatrixとResourcesを解析する。画像Doは構造Formに数えず、inline imageは画素を復号しない。未知演算子は対応の信頼性を下げ、壊れたgraphics stackは制約応答にする。
PDFium側は公開Form/PageObject APIで親子関係、局所行列、直接のMCID集合を集める。親ごとに行列とMCID集合が一意に合うFormだけを対応させ、列挙順で決めない。固定版ではFormオブジェクトのGetMatrixは呼出元CTMを示し、そのForm自身のMatrixは子オブジェクトの行列に含まれる。非identity Matrixを持つ入れ子の資料で、この扱いとページ座標を検証している。
`FPDFText_GetTextObject`から文字の所有Formを得て、対応済みの所有元付きMCIDへ文字とbboxを集める。`FPDFText_GetCharBox`はページuser spaceなのでForm行列を再適用しない。exactのx/y/rectは従来のPDF-point契約を保つ。位置の往復変換はCropBox、ページRotate、ユーザー回転0/90/180/270度で0.5pt以内を検証する。
## 応答とfallback
| 条件 | 応答 |
|---|---|
| 構造上のH、ページ、所有元、文字bboxを一意に確認 | `source=pdf-tag, precision=exact`、x/y/rect |
| Hとページを確認したがForm対応が曖昧、同じFormを複数回呼出、文字bboxなし | `precision=page`と具体的reason。確認済みの文字、ActualText/T/Alt、名称なしの順でtitleを選ぶ。推測した座標は付けない |
| ページ/所有元を確認できない、ParentTree不整合、P/K/RoleMap/Form循環 | 空見出しと`structureLimited=true`、固定の`unavailableReason`。未確認を正常な「見出しなし」にしない |
| 解析quota超過 | `structureLimited=true, truncated=true, complete=false` |
| 応答件数/CBOR bytes超過 | 検証済み先頭項目を残し、上と同じ未完了フラグ |
| OBJRまたはannotation-owned StmOwn | 明示的制約。今回の通常Form対応に含めない |
同じFormの複数描画先を大きな矩形にunionしてexactとはしない。同一行列・MCID集合の別Formも列挙順で解決せずpage precisionにする。見出しの存在まで確定できない場合はpage移動先も作らない。
## 上限
| 対象 | 上限 |
|---|---|
| 共有qpdf入力 | 32MiB/操作、256MiB/文書、10秒/操作 |
| 警告 | 32行かつ64KiB/文書。初期解析から適用、超過後継続不可 |
| ページ木 | 深さ64、訪問200,000、未処理ノード100,000 |
| 内容展開 | 16MiB/stream、32MiB/要求ページ。展開中のPipelineで停止 |
| content token | 1,000,000/ページ、通常token64KiB、複合operand1MiB、operand256件、入れ子64 |
| Form呼出 | 10,000/ページ、深さ64 |
| PDFiumオブジェクト/文字 | 100,000オブジェクト、深さ64、1,000,000文字/ページ |
| 構造探索 | 共通step10,000、ParentTree未処理ノード10,000、深さ64 |
| 見出し応答 | 1,000項目、title4,096 UTF-16単位、CBOR512KiB(envelope余裕込み) |
streamの復号は公開`pipeStreamData`からbounded Pipelineへ行い、無制限のgetStreamData後検査をしない。復号済みstream・対応表・ParentTree参照のキャッシュは一要求の寿命だけで、別の文書全体構造キャッシュを設けない。OS側Workerメモリ制限、監視、取消時終了も維持する。Windowsは継承handleを包む同じQFile契約を使用するが、native実行の証拠は本記録に含まない。
## 検証範囲と残る制限
26個の構造資料で、元の6資料、Form-only、RoleMap/名前付きproperty、非identity入れ子、別Form同MCID、構造順、回転/CropBox、跨ページfragment、vector fallbackを検証した。陰性資料は循環、MCR/Pg/Stm不正、ParentTree所有矛盾、graphics stack破損、16MiB直前/超過、CBOR部分応答を含む。共有context専用資料4個と既存null Kid資料で、ページ番号保持と診断上限も確認した。
実Worker/FontBroker/IPCはexact、曖昧・繰返しpage fallback、quota、部分応答、取消を検証した。暗号化Formはqpdf CLIで試験時に作り、誤password拒否・正passwordの同じ借用QFileによるexact抽出・原本不変を確認した。画面上のForm-only heading移動はApp試験が別に担う。全体のrenderer比較、性能、配置、OS別受入記録は総合検証文書を参照する。
OBJR/StmOwnは未対応で、曖昧な描画instanceはpage fallbackに留まる。未検証の制作ソフト由来構造やnative Windowsの結果を、この合成資料の成功から推定しない。より広いexact対応には、元stream IDを公開するPDFium API等の別評価が必要である。