initial commit

This commit is contained in:
2026-09-21 13:41:40 +09:00
commit 855c7328df
411 changed files with 85352 additions and 0 deletions
+64
View File
@@ -0,0 +1,64 @@
# 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等の別評価が必要である。