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

107 lines
17 KiB
Markdown
Raw Permalink 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.
# 文書アダプターの実装契約と拡張手順
対象は現在の C++20 / Qt 6 実装である。設計書 01 の R09、02 の処理境界、05 の論理契約との対応を記録する。設計書にある `FormatAdapter` は操作の意図を定義したもので、現在のコードに同名の基底クラスや動的プラグイン機構はない。
## 現在の境界
| 責務 | 実装 | 境界で受け渡すもの |
|---|---|---|
| 開く、取消し、文書切替、履歴、操作の振分け | `src/app/controller.*` | セッショントークン、generation、検証済みの QVariant データ |
| キー・入力欄から有限コマンドへの変換 | `src/core/input.*` | command ID、引数。シェル展開は行わない |
| PDF 解析・描画・構造情報 | `src/pdf/pdf_document.*`、`src/pdf/main.cpp` | CBOR。PDFium ハンドルはワーカーの外へ渡さない |
| PDF 表示・位置解決・タイルキャッシュ | `src/app/pdf_canvas.*` | PDF ポイント座標、画像タイル、renderRevision |
| ZIP/EPUB 解析 | `src/archive/archive.*`、`src/archive/main.cpp` | メタデータ、資源一覧、上限付きの展開ストリーム |
| HTML/EPUB 表示と資源提供 | `src/web/*`、`qml/WebPane.qml`、`resources/reader.js` | 文書別 `doc://` origin、アプリ所有の isolated-world コマンド |
| ワーカー輸送と応答検証 | `src/common/worker_process.*`、`ipc.*`、`contracts.*` | 有限 CBOR、要求 ID、世代、用途別の受信検証 |
| ローカル保存 | `src/core/state.*`、`config.*` | 検証した JSON 状態と TOML 設定 |
PDFium と libzip は GUI 実行ファイルにリンクしない。PDF タイルと WebEngine の DOM を共通画像へ変換する層もない。共有するのは文書操作の意味、位置・世代・能力の扱いである。
## 論理操作と実際の呼出し
| 設計書 05 の操作 | 現在の入口 | 結果・制約 |
|---|---|---|
| probe | Controller の先頭バイト判定、ArchiveReader の内部判定 | 複雑な ZIP/XML の解析はワーカー側。独立した FormatAdapter::probe はない |
| open | WorkerProcess `open`、ローカル HTML の profile 作成 | PDF は初期 metadata と初回描画の成功を確認して切替。HTML は staging view の load 成功を確認 |
| getOutline | PDF `outline(cursor,limit)`、archive `metadata`、Web `index` | PDF は安定 ID と parentId/depth、nextOutlineCursor。Web は読み込んだ資源の索引 |
| getHeadings | PDF `headings(page)`、Web `index` | PDF はページごとに抽出し、exact/page と source を保持。独立した節一覧の cursor はない |
| resolveLocation / navigate | PdfCanvas::goToLocation、Web `location.restore` / `navigate` | PDF のページ情報待ちを含む。後続の明示移動・倍率変更・取消しは未完了の復元を失効させる |
| renderPdfTiles | PDF `render` → PdfCanvas | page、物理scale、devicePixelRatio、rotation、clip、renderRevision。返却は BGRA8888 |
| setViewport | Canvas の geometry/DPR/fit 計算、WebPane の layout | PDF の描画 revision と Web view の documentSerial は別管理 |
| findText | PDF `search`、WebEngineView::findText | PDF は query/page/start/limit、矩形と nextCursor。OCR はない |
| close | WorkerProcess::stop、profile revoke、view dispose | GUI は終了期限を伴うプロセス終了を使う。PDF の close RPC は handle 解放後ワーカーを終了する |
PDF の `pages` は outline を再送しない。初期応答に全ページ・全目次を入れず、ページは最大 256 件かつ 256 KiB、目次は最大 256 件かつ 512 KiB 以下に分割する。目次全体の走査上限は 20,000 件・深さ 64 で、省略を warning と truncated で通知する。GUI はページ範囲・連続性を再検証し、索引は許可したフィールドだけを残してセッション合計 32 MiB に制限する。
## 能力情報
`src/core/types.h` の `DocumentCapabilities` は Qt 非依存で、次の 8 個の `Capability` を持つ。PDF の metadata 生成はこの表現を実際に使用する。通信上の既存表現は `capabilities: {name: state}`、理由コードは `capabilityReasons: {name: reasonCode}` である。
| 項目 | PDF open 成功時 |
|---|---|
| pageNavigation | supported |
| chapterNavigation | unsupported / E_CHAPTER_NAVIGATION_UNSUPPORTED |
| outline | supported または unavailable |
| headings | loading。解析結果と文書内宛先を持つ outline fallback の確定は Controller の責務。外部・禁止アクションだけの目次を supported の根拠にしない |
| textSearch | loading。現在の検索応答で文字を確認した時点で supported、全ページの探索が完了しても文字がなければ unavailable。取消し・旧セッション/旧検索の応答・エラーでは未確定の能力を確定しない |
| textSelection | unsupported / E_TEXT_SELECTION_UNSUPPORTED |
| reflow | unsupported / E_REFLOW_UNSUPPORTED |
| password | unavailable。要求中は open の E_PASSWORD_REQUIRED / E_PASSWORD_INVALID とセッション状態で通知 |
`supported` はアダプターが機能を提供できること、`unavailable` はその文書で対象がないこと、`loading` は未確定、`unsupported` は実装範囲外を表す。未知の能力名・状態を成功扱いしてはいけない。`CommandRegistry::requiredCapability()` がコマンドと能力の対応を返す。
`contracts.cpp::validateCapabilities()` は 8 項目が全て存在すること、状態が上記 4 種の文字列であること、`unsupported` に理由があることを検証する。理由は既知の能力に対応する 128 文字以下の `E_` で始まる英大文字・数字・アンダースコアのコードに限る。PDF の open 応答受信時と全形式の commit 前に検証し、不正な新文書では切替を中止して以前の文書を保持する。Web の能力と理由は Controller が既定の描画ポートから構築する。能力を必要とするコマンドは、検索入力・目次表示を含めて同じ検証と可否判定を通り、未知・欠落を実行許可に変換しない。`loading` は非同期探索を開始できる状態として扱い、`unsupported` の理由は一時通知に表示する。
HTMLのDOM索引がまだ届いていない場合、空の配列と`outline=loading`を目次の欠如へ変換しない。利用者の表示要求を保持し、読み込み中のパネルから確定した索引へ移行する。
PDF metadata の `isTagged` は公開 `FPDFCatalog_IsTagged` の結果であり、タグ付き文書の宣言を示す。構造木の完全性や見出しの存在を保証する値ではない。タグ宣言がある場合でも、個々の構造と位置は `headings` の結果で判定する。参考: [PDFium の catalog API 実装](https://pdfium.googlesource.com/pdfium/%2B/e8b8183f4d7a410b42bc4af6c31dcacb3b365685/fpdfsdk/fpdf_catalog.cpp)。
## 位置・応答の規約
- 内部ページ番号は 0 始まり。画面の物理番号は 1 始まり。ページラベルは文字列として保持し、数値計算に使わない。
- PDF の保存位置は `kind=pdf`、pageIndex、pageLabel、xPt/yPt、viewRotation、fitMode、zoom。xPt/yPt は元 PDF 空間であり、Canvas が CropBox・元回転・利用者回転・DPR を適用する。読書位置は表示上端を基準にする。
- Web の保存位置には token を含む URL を残さず、検証済みの資源相対パス、CFI/fragment、progression等を保存する。EPUBではpackagePath/spineIdrefも照合する。`zoom`は有限数の25–500で、HTML/HTML ZIP/流込EPUBではWebEngineのzoomFactor×100、固定EPUBではページ/幅合わせを基準にするspreadZoom×100を表す。固定EPUBの`fitWidth`はbool(falseはページ合わせ)、`panX`/`panY`は表示可能なパン範囲に対する有限数の0–1。画面サイズから計算する固定ページの最終ピクセル係数やDPRをこの倍率へ混ぜない。
- WebPaneはアプリ側の表示倍率と合わせ方を`location`の結果に加える。Controllerの`logicalWebLocation()`が表示通知・保存位置の復元の両方で型と範囲を検証し、表示済み位置の`zoom`をStateStoreの位置と倍率へ保存する。復元ではレイアウトを確定して表示倍率を適用してからDOM位置を解決する。旧位置に`zoom`がなく別保存の倍率がある場合はそれを同じ境界で検証する。両方になければ既定表示を維持する。後続コマンドは未完了のWeb位置復元を取り消す。
- PDF outline/link の wire target は平坦な page/x/y、HTML/EPUB は href 等を持つ。設計書の再帰的 TocNode と DocumentLocation variant に全面変換する実装ではない。
- 各 IPC メッセージは protocolVersion=1、requestId、sessionId、generation、operation と、payload または result/error のどちらかを持つ。送信元の stdout を命令として扱わない。
- 制御は 1 MiB、画像データは 8 MiB 以下。Linuxはローカルソケット、Windowsは継承した双方向pipeを使う。どちらも同じIpcChannelのバイトストリーム内で、長さヘッダーの上位 bit により画像と制御のフレームを識別する。
- GUI は画像の幅・高さ・stride・形式・バイト数・ページ・clip・renderRevision を再検証する。Canvas の文書番号と revision も照合して、古い画像を採用しない。
- `discardQueued(operation)` は未送信要求だけを除去する。実行中の PDFium 呼出しは中断されず、応答破棄またはワーカー終了で取消しを成立させる。
- `frameReady` は画像の受信であり、画面提示完了ではない。保存・性能測定で表示済み位置が必要な場合は、window の frameSwapped と viewportReady を併用する。
- 保存する位置は全形式でopen時の原本識別情報と結び付ける。保存時にパスを再照合して変更・置換があれば拒否し、復元時は保存済み位置なしと同一性不一致を区別して通知する。完全な内容ハッシュの常時計算は行わない。
## フォーカスと開く失敗の受渡し
一時的な入力欄・操作一覧・ダイアログを開くときは、Main.qmlが元の操作領域を保持する。閉じるときは有効な目次/ページ一覧へ戻し、非表示・空になった領域は本文へ戻す。正常に完了したコマンドが`focus.content`等でフォーカスを指定した場合は、保留していた復帰先を破棄する。形式のcommitや新たなUI操作を追加するときも、後から閉じる入力欄でその指定を上書きしない。
`Controller::openPath()`の直接失敗とstagingセッションの失敗は、`recordOpenFailure()`がbasename・有限の原因分類・利用者対処へ変換する。認識しないコードは一般のopen失敗として扱い、任意のworkerエラー本文・内部例外を利用者説明へ流用しない。新しい失敗コードを追加する場合はこの対応も追加する。既存文書の描画やwarningsは維持し、最後の失敗はController内の`lastOpenFailure_`にだけ保持する。`:info`の別項目で表示し、次のopenPath試行で消去する。StateStoreや表示中セッションのmetadataへ保存しない。最近使った文書の欠落・置換検査など、openPath到達前の拒否は各入口の通知で扱う。
## 形式を追加する手順
1. 新形式の source、位置、能力、必要な読み取り権限、上限を定義する。既存の PDF・Web の描画ポートで扱えるかを判断する。
2. パーサーと依存ライブラリを専用ターゲットに分離し、未信頼解析はワーカーに置く。ワーカーは許可された入力を開き、OS sandbox の成立後に解析する。保護失敗時の通常権限への再試行を作らない。
3. IPC 操作を追加する場合は `ipc.cpp` の whitelist、ワーカーの入力スキーマ、GUI の結果スキーマを同時に更新する。ページング、総量、文字列長、数値範囲、取消し時の破棄条件を決める。
4. Controller の型判定、staging/commit、機能振分け、位置保存を接続する。現在は中央の format 分岐の変更が必要であり、アダプター登録だけでは追加できない。
5. 新コマンドが必要なら CommandRegistry の ID・引数・能力対応、InputRouter の既定割当、設定例、UI ハンドラーを追加する。解析済み文書からコマンド名や実行コードを注入しない。
6. Web 資源を使う形式は文書別 origin と ResourceHandler を利用し、外部通信・文書スクリプト・ダウンロードの既定を維持する。展開先は broker 側の ResourceWriter が所有する。
7. 形式単体、実ワーカー IPC、破損/巨大入力、取消し・文書切替、位置復元、実表示の回帰試験を追加する。依存の固定版と配布告知も更新する。
## R09 / 設計書 05 の評価
R09 の分離基盤は、ワーカー単位の解析、有限コマンド、能力表現、専用描画ポートとして成立している。一方で、形式追加の登録機構、Qt 非依存モデルを端から端まで通す adapter interface、全操作の統一取消しトークンは未実装である。`DocumentIdentity`、`DocumentLocation`、`ResponseStamp` 等は補助モデルと単体試験にとどまり、runtime の保存・通信は QVariant/QCbor と個別検証を使う。`DocumentCapabilities` の採用だけでこの差分を解消したとは扱わない。
UI のモデルは一部が format 分岐に依存する。利用者向け説明はQtの翻訳contextと日本語sourceへ分離したが、IPCは翻訳キーだけを送る統一エラー型ではなく、固定コードと説明文を使用する。Web の再配置には view ごとの serial と JavaScript callback の生存確認があり、設計書の単一 `layoutRevision` モデルとは異なる。これらを拡張時の変更点として扱う。不要な抽象基底を追加して runtime と別の契約を増やすより、次形式の追加時に既存 2 経路をその契約へ移すのが検証可能な手順となる。
証跡は `tests/test_pdf.cpp`、`test_pdf_canvas.cpp`、`test_core.cpp`、`test_archive.cpp`、`test_security.cpp`、`test_app.cpp`、`test_web_qml.cpp` と `tests/PDF-VALIDATION.md` にある。合格した個別試験と設計全体の受入完了は区別する。
## T19 設計レビュー記録(2026-09-19)
**判定: 現在の静的組込み方式による接続手順を採用する。** T19 の受入条件である「UI 固有処理にパーサーを埋め込まず、能力・位置・ナビゲーション契約で接続できる」ことは、上記の責務表・操作表・追加手順から追跡できる。第五の形式の実装や、特定の名前を持つ C++ 抽象基底の存在を合否条件にはしない。
- 設計書 05 は `FormatAdapter` を言語非依存の論理契約と定義し、PDF タイルと Web 可視域には別の描画ポートを認めている。IPC、専用 Canvas、Web コマンドの組合せでこの操作境界を実現することは実装上の選択である。設計書 02 の初期拡張方式も配布元が組み込むモジュールに限定しているため、動的プラグイン機構は必須ではない。
- PDFium と libzip は各ワーカー専用ターゲットへ分離され、QML へ形式パーサーを追加する必要はない。新形式は既存の二つの描画ポートを選ぶか、別の専用ポートを定義し、Controller で正規化済みメタデータ、位置復元、ナビゲーションを接続する。
- 能力は厳密な境界検証と有限コマンドに接続する。位置は PDF 座標または Web の資源・CFI・fragment・progression、ナビゲーションは上限付き索引、非同期応答は要求 ID とセッション世代で区別する。これらの更新が形式追加時の必須作業である。`test_core` の能力スキーマ試験と `test_app` の不正能力での操作拒否試験が、未知の能力を許可扱いしないことを検証する。
- 設計書 02 が目指す「登録だけで追加する」依存方向には差分が残る。現在は Controller の中央分岐と一部 UI モデルを変更する必要があり、Qt 非依存の位置型を実行時全経路で利用しているわけではない。この差分は拡張時の変更箇所として明示する。T19 の接続可能性の採用を、R09 配下の全ての理想的な構造が実装済みという意味にはしない。
以上から、同名基底の不在は単独の未合格理由としない。必須なのは既存文書の挙動、隔離、能力・位置・ナビゲーションの検証を保つ接続経路であり、形式追加時には上記 7 手順と同等の適合試験で再評価する。