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

8.3 KiB
Raw Permalink Blame History

PDF Form構造見出しアダプターの実装契約

状態: 実装済み、Linux回帰確認済み(2026-09-19)。当初のForm-only構造を取得できない制約は、Worker内の共有qpdfメタデータ読取で解消した。本書は実装範囲を記録し、全OS・任意の実文書に対するG-HEADINGS合格を宣言するものではない。試験記録、合成資料の期待値を参照。

構成と所有権

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、Pages::cache

構造の所有元と順序

対象ページと実際に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等の別評価が必要である。