hpdft-0.4.6.0: dev/0.4-roadmap.md
# hpdft 0.3.1 / 0.4 ロードマップ
`master` @ 0.3.0.0(2026-07-05)以降の方針。
## リリース方針
| 版 | 位置づけ | 主な内容 |
|----|---------|---------|
| **0.3.1** | 0.3 の拡張 | ルビ抽出(青空文庫記法) |
| **0.4.0** | API 基盤 | E0 公開 API 整備 |
| **0.4.x** | 機能拡張 | D7 → D5+E2 → E3(順次) |
0.3 残タスク(D2–D7)は **0.4 目標との関連で取捨選択** する。下記「0.3 残タスクの扱い」参照。
---
## 0.3.1 — ルビ抽出
0.4 API 整備とは独立して進められる。Layout / Interpret の延長として **0.3.1 で先行リリース** する。
### 出力形式(青空文庫)
[青空文庫 注記一覧・ルビ](https://www.aozora.gr.jp/annotation/etc.html) に準拠。
| ケース | 出力例 |
|--------|--------|
| 単一文字種の基底 | `青空文庫《あおぞらぶんこ》` |
| 混在文字種の基底 | `霧の|ロンドン警視庁《スコットランドヤード》` |
- ルビ本体は `《…》`(全角括弧)
- 基底が漢字・仮名・英字など **複数文字種混在** するときは基底直前に `|`(全角縦棒)を付ける
- 平仮名と片仮名は別文字種として扱う(青空文庫マニュアル準拠)
- **0.3.1 の MVP** は上記の横書き・縦書き geometry ヒューリスティック + Structure パス。左付きルビ・ママ注記・外字注記は **注記形式**(`[#…]`)として後回し可
### 抽出パイプライン(優先順)
```
Structure(/Ruby, /RB, /RT 等が利用可能)
↓ 不可・不完全
geometry ヒューリスティック(Layout 拡張)
```
- **Tagged PDF に Ruby 構造がある場合は Structure パスを優先**(MCID 紐付けと同様の方針)
- geometry フォールバック:
- 横書き (wmode 0): 親行の直上、小字体、親文字 span と X 範囲が重なる行
- 縦書き (wmode 1): 親列の右側、小字体、親文字 span と Y 範囲が重なる行
- 脚注・上付き処理(`--footnotes`)と競合しないよう、**ルビ行の分離を脚注処理より前** に行う
- CLI: 既存テキスト抽出に `--ruby` を追加(既定 off)。有効時は行内に青空記法を埋め込む
### 0.3.1 で触るモジュール(想定)
- `PDF.Structure` — Ruby 要素の解析・MCID/座標との対応
- `PDF.Layout` — ルビ行検出・基底への結合、`aozoraRuby` 整形(`|` 判定含む)
- `PDF.Text` — `--ruby` 配線
- `hpdft.hs` — フラグ追加
- `test/Unit.hs` — synthetic glyph 配置の unit テスト
### 0.3.1 スコープ外
- 左付きルビ(青空の `[#…の左に…]` 注記形式)
- 外字・長基底の開始/終了型注記
- wmode 混在ページの interleave(既知制限のまま)
---
## 0.4 — 全体像
```mermaid
flowchart LR
subgraph v031 [0.3.1]
Ruby[ルビ]
end
subgraph v040 [0.4.0 E0]
API[公開 API 整備]
end
subgraph v04x [0.4.x]
D7[CLI サブコマンド]
D5E2[D5 Filter + テキスト diff]
E3[画像抽出]
end
Ruby --> API
API --> D7
D7 --> D5E2
D5E2 --> E3
```
**作業順(合意):** E0 → D7 → D5+E2 → E3
D2(CI)は E0 と並行または直後。API とテスト基盤が整うと E2 の fixture 化が容易になる。
---
## E0 — ライブラリ API 整備(0.4.0)
`scripts/` が `DocumentStructure` 内部関数に直接依存している状態を解消し、**安定した公開境界** を設ける。
### 目標 API(案)
```haskell
-- PDF.Page(新規)
pageCount :: Document -> PdfResult Int
pageRefAt :: Document -> Int -> PdfResult PageRef -- 1-based
pageItems :: Document -> PageRef -> PdfResult [PageItem]
pageGlyphs :: Document -> PageRef -> PdfResult [Glyph]
pageLines :: Document -> PageRef -> LayoutOptions -> PdfResult [Line]
pageParagraphs :: Document -> PageRef -> LayoutOptions -> PdfResult [T.Text]
-- 構造化出力(diff 共用)
data PageRegion = PageRegion
{ regionPage :: Int
, regionParagraph :: Int -- ページ内段落番号
, regionBBox :: Rect -- 段落 bbox 概算
, regionText :: T.Text
}
pageRegions :: Document -> PageRef -> LayoutOptions -> PdfResult [PageRegion]
```
- `interpret-page` / `inspect_font` / 将来の `diff` が **公開 API のみ** で書けるようにする
- `hpdft.cabal` の `exposed-modules` を明示的に整理
- breaking change は 0.4.0 で一括(CHANGELOG に移行ガイド)
---
## D7 — CLI サブコマンド化
フラグ連鎖を廃止し、サブコマンドで整理。
```
hpdft extract [OPTIONS] FILE # テキスト(現行 default)
hpdft extract text [OPTIONS] FILE # 明示的テキスト
hpdft extract images -p PAGE FILE # E3: 画像書き出し
hpdft diff FILE_A FILE_B # E2: 段落 diff
hpdft inspect page FILE PAGE # interpret-page 相当
hpdft info FILE # メタデータ
```
- 0.3 の `-p`, `--geom`, `--legacy`, `--ruby` 等は `extract` サブコマンド配下へ
- 旧フラグ形式は **1 版 deprecated 警告** のうえ互換維持を検討
---
## E2 — テキスト diff(段落単位)
### 粒度
**段落単位**。ページ番号 + ページ内段落 index + bbox 概算で「どこが変わったか」を返す。
### 設計
```haskell
data TextChange
= TextChange
{ changePageA :: Maybe Int
, changePageB :: Maybe Int
, changeParaA :: Maybe Int
, changeParaB :: Maybe Int
, changeBBox :: Maybe Rect
, changeOld :: T.Text
, changeNew :: T.Text
}
| PageCountMismatch { pagesA :: Int, pagesB :: Int }
compareDocuments :: LayoutOptions -> Document -> Document -> PdfResult [TextChange]
```
- 各ページを `layoutPageText` / `pageParagraphs` で段落列に分解(**document-level 結合は diff では使わない**)
- 正規化(空白・ヘッダ除去は LayoutOptions で制御)後、段落列の LCS
- CLI: `hpdft diff a.pdf b.pdf [--json]`
- ライブラリファースト。CLI は薄いラッパー
---
## E3 — 画像抽出
### 意図する挙動
```bash
hpdft extract images -p 3 FILE.pdf -o ./out/
# → FILE.pdf 3 ページ目の Image XObject を out/page3-001.jpg 等として書き出す
```
- **ファイル書き出しが本体**。bbox はファイル名や sidecar JSON に付随させる程度
- `/Image` XObject の stream を Filter に応じてデコードし、JPEG はそのまま `.jpg`、raw/Flate は PNG 化または raw 保存
- Form XObject 内のネスト画像は **再帰追跡**(Interpret の XObject 走査を拡張)
- インライン画像は 0.4.x 後半または 0.5 候補
- **D5 前提:** 最低限 `/DCTDecode`(JPEG)。Flate+PNG predictor は既存。LZW 等はコーパス需要に応じて追加
---
## 0.3 残タスクの扱い
| ID | 0.4 関連 | 判断 |
|----|---------|------|
| **D2** CI | E0/E2 のテスト自動化 | E0 前後で導入。0.3.1 ルビ PR から optional で可 |
| **D3** パーサ統一 | なし | **延期**(痛みが出たら) |
| **D5** Filter | E3 必須(DCTDecode) | **E3 直前に DCTDecode を先行**。LZW/ASCII85 は後追い |
| **D6** AES-256 | 暗号化 PDF 対象時 | コーパス次第。**0.4 スコープ外可** |
| **D7** CLI | E2/E3 の CLI | **E0 直後**(合意順序) |
0.3 フォローアップ:
| 項目 | 判断 |
|------|------|
| MediaBox 外フィルタ | E2/diff ノイズ削減のため **E0〜E2 で検討** |
| wmode 混在 interleave | **0.3.1 ルビでは既知制限のまま** |
| RTL / 行内逆順 | スコープ外継続 |
---
## 推奨マイルストーン
| 順 | 版 | 内容 | 状態 |
|----|-----|------|------|
| 1 | **0.3.1** | ルビ(Structure 優先 → geometry、`--ruby`、青空記法) | **完了** v0.3.1.0 |
| 2 | **0.4.0** | E0 公開 API + exposed-modules 整理 | **完了** |
| 3 | **0.4.0/1** | D2 CI(並行可) | **完了** v0.4.4.0 |
| 4 | **0.4.1** | D7 CLI サブコマンド | **完了** |
| 5 | **0.4.2** | D5(DCTDecode)+ E2 段落 diff | **完了** |
| 6 | **0.4.3** | E3 `extract images` | **完了** v0.4.3.0 |
---
## 参照
- [青空文庫 ルビとルビのように付く文字](https://www.aozora.gr.jp/annotation/etc.html)
- [0.3 ロードマップ](0.3-roadmap.md) — 0.3.0.0 までの完了内容