packages feed

hpdft-0.4.7.0: dev/0.5-roadmap.md

# 0.5 ロードマップ — layout モードとコードベース改善

2026-07-06 時点の方針検討メモ。0.4.6.4 のコードベースレビュー結果(A 節)と、0.5 の新機能(B〜D 節)の実装方針をまとめる。**A 節と C 節は 0.4.7.0 として実装済み**(下記マイルストーン表参照)。B 節と D 節の新機能は 0.5.0 から着手する。

## 位置づけ

0.4 系は「抽出パイプラインの品質と性能」(geom 高速化、TUI、CMap 対応、パーサ堅牢化)に加え、**0.4.7 でコードベース整備(A 節)と TUI 高さ指定(C 節)を完了**した。0.5 は次を主眼とする。

1. **見た目再現の layout モード**(pdftotext -layout 相当だが、東アジア文字幅と罫線に対応した実用版)
2. **テキスト抽出 UX の拡張**(`-o` ファイル出力、ページ範囲、grep 強化、JSON 等 — D 節)

`dev/0.5-text-ux.md` の持ち越し分(UX-3: `-o` ファイル出力)も 0.5 に含める。

---

## A. 既存コードの改善(0.4.7.0 で実装済み)

0.4.6.4 時点のレビュー結果。以下は **0.4.7.0** で対応済み(CHANGELOG 参照)。

### A1. 字句解析の共通化(P0) — **実装済み**

`PDF.StreamLex` に数値正規化・hex 分解・UTF-16BE / SJIS / JIS コード分割を集約。`ContentStream` / `Interpret` は import のみ。ユニットテストは `StreamLex` 1 系統。

### A2. ページツリー走査の一本化(P0) — **実装済み**

`Page.pageRefsFromRoot` を export。`Text.walkdown` と CLI(`app/Cli/*`)はこれを利用。`pageorder` / `PageTree` は削除。

### A3. サイレント失敗の可視化(P0) — **実装済み**

`PageContentFailed` warning を追加。legacy の `pageContent` と grep の geom 失敗が stderr に出る。

### A4. エラー処理の統一(P1) — **実装済み**

`parseObjStmHeader` / `parseObjStmValue` を `PdfResult` 化。`Encrypt.hexToBytes` を total 化。

### A5. テストの穴(P1) — **実装済み**

golden に geom モード(`expected-geom/`)を追加。`test/EncryptSpec.hs` で RC4 鍵ストリームをテスト。

### A6. CLI の整理(P2) — **実装済み**

`app/Cli/*` に分割。`diff --legacy` 削除、`-p` ヘルプ typo 修正、`passwordOpt` 共通化。

### A7. パッケージ構成(P2) — **実装済み**

TUI / CLI を `app/` へ移動。`PDF.ContentStream` を other-modules 化。library に `-O2` を追加(executable からは除去)。

### A8. 性能(P2) — **実装済み**

`StreamLex.parsePdfNumberFromByteString` を `Interpret.readNumber` から利用。

---

## B. 新機能: layout モード(見た目再現テキスト)

### 目標

`hpdft text --layout FILE` で、ページの視覚配置を等幅テキストとして再現する。pdftotext -layout 相当だが、次の点で「より実用的」を狙う。

1. **東アジア文字幅**: CJK 文字を 2 セル幅として桁位置を計算する(pdftotext は日本語 PDF で桁がずれる)。幅計算は `TuiScroll.charDisplayWidth` を共有モジュールへ昇格して流用
2. **罫線の再現**(Phase 2): geom パイプラインは既に `ItemGraphic`(矩形)を持っている。表の罫線を `─ │ ┌ ┼` 等の罫線素片で描画できれば、表組みの再現性で pdftotext を明確に超えられる
3. **縦書きの扱いを明示**: 混在ページで壊れず、少なくとも読み順テキストに劣化する

### 現状の材料と足りないもの

geom パイプラインの `Glyph`(Interpret.hs 56–65)に座標・advance 幅・フォントサイズ・wmode が揃っており、入力データは十分。ただし:

- **`Line`(Layout.hs 855–865)は入力に使えない**。`buildLines` はストリーム順 fold で同一 baseline のグリフを 1 本に連結するため、行内の大きな空隙(多段組の列間、表のセル間)が失われている。layout モードは Glyph 列から直接「セグメント」(baseline が同じで inline 方向の隙間が閾値以下の連続 run)を作る必要がある
- **MediaBox / `/Rotate` が未実装**。現状 `src/` のどこもページ属性を読んでいない(ページ幅は glyph bbox からの近似のみ)。安定した紙面座標系のため、ページ辞書から MediaBox と Rotate を取得し、Rotate は CTM 前置で正規化する。A2 の走査一本化に載せる

### 設計

新モジュール `PDF.Render`(仮)に実装。tagged パスは MCID 結合で座標を捨てるため、**layout は geom 専用**とする。

```haskell
data RenderOptions = RenderOptions
  { renderWidth  :: Maybe Int   -- 出力桁数。Nothing なら本文フォントから自動
  , renderRules  :: Bool        -- 罫線描画(Phase 2)
  }

pageTextLayout :: Document -> PageRef -> RenderOptions -> PdfResult T.Text
```

手順:

1. `pageItems` から Glyph を取り、`filterPageGlyphs` 相当のノイズ除去後、セグメント化(baseline 差 0.4×size 以内 & inline gap が `0.6 × size` 超で分割。閾値は fixture で調整)
2. **スケール決定**: 横 1 セル = `本文サイズの中央値 × 0.5` pt、縦 1 行 = `行送りの中央値`。`--width N` 指定時は `MediaBox 幅 / N` をセル幅にする
3. **行配置**: セグメントを baseline でクラスタし、行クラスタ間の gap から空行数を決める(`max 0 (round (gap / 行送り) - 1)`)
4. **桁配置**: 各セグメントを `round ((x - 左端) / セル幅)` 桁目に置く。East Asian width で埋めながら、先行セグメントと重なる場合は右へ 1 セル以上ずらす(重ね書きより順序保持を優先)
5. 行末の空白は落とす。全ページを `\f`(form feed、pdftotext 互換)区切りで連結

縦書き(wmode 1)ページ: Phase 1 では格子配置をせず、既存の読み順ソート(右列→左列)による段落テキストへフォールバックし、stderr に注記を出す。転置レンダリング(縦列→横行)は需要を見て Phase 3。

### CLI / API

- `hpdft text --layout [-w N] FILE`(`--geom` らと排他)。`-p` 併用可
- `PDF.Page` に `pageLayoutText` の薄いラッパを追加
- 既存 `LayoutOptions`(ルビ・脚注)とは概念が別なので混ぜない。`--layout` 時は `--ruby` / `--footnotes` を無効とする(リフローしないのでルビは位置のまま出る)

### テスト

- fixture: 2 段組、表(罫線つき)、インデント付きコード、日本語混在の 4 種を最小 PDF で作成し golden に `expected-layout/` を追加(A5 の geom golden を兼ねる)
- ユニット: スケール決定・桁配置・重なり回避を純関数として切り出してテスト

### フェーズ

| Phase | 内容 |
|-------|------|
| 1 | MediaBox/Rotate 取得、セグメント化、格子レンダラ、`--width`、fixture |
| 2 | 罫線描画(`ItemGraphic` → 罫線素片)、表の桁揃え改善 |
| 3 | 縦書き転置、TUI からの layout 表示 |

---

## C. TUI の表示高さ指定 — **実装済み(0.4.7.0)**

`--height N`(行数)または `N%`(端末高さの割合)。`app/TuiGeometry.hs` でパース・クランプ。未指定時は従来どおり下半分。SIGWINCH は見送り。

---

## D. そのほかの機能候補

「PDF をテキストとして扱う」用途で需要がありそうなものを優先度順に。

### D1. `-o` ファイル出力(UX-3 持ち越し、P1)

`dev/0.5-text-ux.md` で設計済み・未実装。`text -o OUT FILE` で tagged→geom の高品質全文をファイルへ、`OUT.tmp` → rename の原子的書き込み、進捗は stderr。設計変更なしで実装のみ。

### D2. ページ範囲指定(P1)

現状 `-p N` は単一ページのみ。`-p 3-10`、`-p 5-`、`-p 1,3,7-9` を受け付ける。`text` / `image` / `grep` で共通に使えるようパーサを 1 箇所に置く。実装が軽く、pdftotext の `-f`/`-l` 相当として要望されやすい。

### D3. JSON 構造化出力(P2)

`pageRegions`(ページ番号・段落番号・bbox・テキスト)が既に公開 API にあるので、`text --json` でそのまま JSON 化するだけで下流ツール(RAG の前処理、検索インデクサ)に使える。aeson 依存を足すか、手書きシリアライザで依存ゼロを保つかは要判断(現状の依存ポリシーは軽量指向なので手書き寄り)。

### D4. grep の強化(P2)

既存 `grep` サブコマンドはマッチ行を出すのみ。`-n`(ページ番号:行番号)、`-C N`(前後行)、`-c`(件数)を追加。A3 の失敗可視化とセットで「検索漏れが起きない」ことを保証する。デフォルトを legacy パス(高速)にし、`--geom` で高品質に切り替え。

### D5. リンク・注釈の抽出(P2)

`hpdft links FILE`: `/Annots` の URI / GoTo リンクをページ番号・矩形・アンカーテキスト(矩形内の Glyph から復元)付きで列挙。目次(`toc`)と補完的で、文書の外部参照一覧を作る用途に需要がある。geom パイプラインの座標がそのまま使える。

### D6. 幅精度の改善(P3、cmap-support.md との連動)

`SJISmap` / `JISmap` はコード→CID 変換を持たないため `/W` を引けず `/DW` 代用(cmap-support.md P3 に記載)。layout モードでは幅誤差が桁ズレとして直接見えるため、B 節 Phase 2 の結果を見て優先度を再評価する。

### 見送り

- **RTL / BiDi**: 対象コーパスにないため引き続きスコープ外
- **pdftotext 互換フラグ群**(-htmlmeta 等): 需要が出てから
- **SIGWINCH 対応**: C 節に記載のとおり見送り

---

## マイルストーン案

| 順 | 版 | 内容 |
|----|-----|------|
| 0 | **0.4.7.x** | **A1–A8 + C**(字句共通化、走査一本化、失敗可視化、エラー統一、テスト補強、CLI/パッケージ整理、性能、TUI 高さ指定)— **完了** |
| 1 | 0.5.0 | B Phase 1: `--layout` MVP(MediaBox/Rotate、格子レンダラ、fixture)+ D 節新機能の実装開始 |
| 2 | 0.5.1 | B Phase 2: 罫線描画 |
| 3 | 0.5.2 | D1 `-o` + D2 ページ範囲 |
| 4 | 0.5.3 | D4 grep 強化 + B Phase 3 要素 |
| 5 | 0.5.x | D3 JSON / D5 links / 縦書き転置(需要を見て) |

## 参照

- [0.5-text-ux.md](0.5-text-ux.md) — TUI / 出力 UX の設計(0.4.6 で大半実装済み、UX-3 のみ持ち越し)
- [cmap-support.md](cmap-support.md) — CMap 対応の優先度(D6 と連動)
- [performance-0.4.md](performance-0.4.md) — geom 高速化の経緯(A8 の前提)