vivlio-starter-pdf 1.0.1 → 1.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.ruby-version +1 -0
- data/CHANGELOG.md +33 -10
- data/README.md +14 -4
- data/RELEASE_NOTE.md +34 -112
- data/Rakefile +66 -4
- data/exe/vivlio-starter-pdf +37 -39
- data/lib/vivlio_starter/cli/pdf/enhanced_provider.rb +139 -0
- data/lib/vivlio_starter/cli/pdf/log_helper.rb +38 -0
- data/lib/vivlio_starter/cli/pdf/outline_writer.rb +145 -0
- data/lib/vivlio_starter/cli/pdf/reader.rb +1253 -0
- data/lib/vivlio_starter/cli/pdf/utilities.rb +43 -0
- data/lib/vivlio_starter/cli/pdf/version.rb +7 -0
- data/lib/{vivlio/starter → vivlio_starter}/pdf.rb +5 -7
- data/vivlio-starter-pdf.gemspec +12 -8
- metadata +18 -16
- data/lib/vivlio/starter/cli/pdf/enhanced_provider.rb +0 -120
- data/lib/vivlio/starter/cli/pdf/log_helper.rb +0 -40
- data/lib/vivlio/starter/cli/pdf/outline_writer.rb +0 -118
- data/lib/vivlio/starter/cli/pdf/utilities.rb +0 -45
- data/lib/vivlio/starter/pdf/reader.rb +0 -1255
- data/lib/vivlio/starter/pdf/utilities.rb +0 -11
- data/lib/vivlio/starter/pdf/version.rb +0 -11
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0335b9ec25ee9a373f9d4c261c1a4a610e87fb2ac538b2207b3dd8f741bc82c1
|
|
4
|
+
data.tar.gz: 9645fdbcb4575cae982bf48ef0c60f061b6f26afbbe101bd4530f95262c6da32
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 01f2c370439f11cf3012f32b092415a8aeebab059386f09462292952e7026b8b616db82e00aae11489465d562c0f90f89ea548f6df98d89523e05c7b0f1e8a00
|
|
7
|
+
data.tar.gz: 215ff34974f8bc3f2fd81f929bd39413ff79ad454db5993ee36c350dd48af12b091372b9b1ca42c6b048e29d5633e88b29bfb5dca3aebcaa8726d4d594773f4e
|
data/.ruby-version
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
4.0.6
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,39 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
### Planned
|
|
9
|
+
- パフォーマンス最適化(大規模 PDF 対応)
|
|
10
|
+
- 追加 OCR エンジン対応
|
|
11
|
+
- クラウド OCR サービス連携
|
|
12
|
+
- PDF 暗号化対応
|
|
13
|
+
|
|
14
|
+
## [1.1.2] - 2026-08-07
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
- **必要な Ruby を 4.0 以上から 3.4 以上へ緩めた** (`vivlio-starter-pdf.gemspec`): Ruby 4.0 固有の機能は使っていない。Prism のバージョン指定パースで全 13 ファイルを解析したところ 4.0.0 / 3.4.0 / 3.3.0 いずれも構文エラー 0 件で、実際の下限を決めていたのは `it`(暗黙ブロック引数・38 箇所)**単独**だった。`it` は 3.3 でも構文エラーにならず実行時 `NameError` になる——静的解析では見つからないので、Ruby 3.4.10 で実走して全テスト通過を確認している。コード変更はゼロ(gemspec 1 行のみ)。本体 `vivlio-starter` の緩和に合わせたもので、本体だけ緩めてもプラグインが `>= 4.0` のままでは 3.4 環境に入らない。
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
- **壊れたアウトライン項目 1 件が、しおりを全滅させていたのを直した** (`outline_writer.rb`): 見出しのタイトルに不正な UTF-8 バイトが混ざると `String#strip` が `Encoding::CompatibilityError` を投げ、`add_outline!` の rescue まで飛んで **PDF からしおりが丸ごと消えていた**。範囲外ページ・空タイトル・深すぎる階層はいずれも `on_skip` で 1 件ずつ飛ばす設計なのに、ここだけ全体を巻き添えにしていた。
|
|
21
|
+
- `add_outline!` は `false` を返すだけで PDF 自体は出来上がるため、**成果物を開くまで誰も気付けない**種類の失敗だった。
|
|
22
|
+
- 該当項目だけを飛ばし、理由(`title is not valid UTF-8, skipping outline entry: …`)を `on_skip` で報告する。原稿のどの見出しを直せばよいか分かる形にした。
|
|
23
|
+
- `EncodingError` と `ArgumentError` の両方を捕まえる。壊れたバイト列に Ruby がどちらを投げるかはメソッド次第で(`strip` は `Encoding::CompatibilityError`、`match?` は `ArgumentError`)、その差でしおりの有無が決まってはならない。
|
|
24
|
+
- **アウトライン項目に `nil` 以外の想定外の型が混ざると全体が止まっていたのを直した** (`outline_writer.rb`): `fetch` が `respond_to?(:[])` だけで添字アクセスしていたため、配列に `String` や `Integer` が紛れると `TypeError: no implicit conversion of Symbol into Integer`(`Integer#[]` はビット参照)で落ちていた。Hash 風オブジェクトは従来どおり受けつつ、添字アクセスの失敗は既定値へ倒す。
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
- **堅牢性テストを整備した** (`test/robustness/`): 本体 `vivlio-starter` の `test/vivlio_starter/robustness/` と同じ位置づけで 14 件を追加。守る約束は 2 つ——**公開 API は例外を外へ出さない**(ビルドパイプラインの途中から呼ばれるため、失敗は戻り値で伝える)、**失敗しても入力を壊さない**(読み込んだのと同じパスへ書き戻すため、途中失敗で元 PDF が切り詰められると最も気付きにくい事故になる)。上記 2 件の修正はこのテストを書く過程で見つかったもの。
|
|
28
|
+
- `broken_pdf_test.rb` — PDF でないファイル・0 バイト・存在しないパス・暗号化 PDF・書き込み不可ファイル。戻り値と、**入力のバイト列が不変であること**を固定
|
|
29
|
+
- `outline_entry_test.rb` — 壊れた項目を 1 件ずつ飛ばして残りを書くこと、飛ばした理由を報告すること、全滅時に空のしおりツリーを残さないこと
|
|
30
|
+
|
|
31
|
+
## [1.1.1] - 2026-06-23
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
- 隠しノンブルのフォントを埋め込み可能な TTF に変更(FT-02)。HexaPDF 組込の標準 14 フォント `Helvetica` は PDF に埋め込まれず、印刷所入稿で「非埋め込みフォント」事故になっていた。書籍プロジェクト直下に同梱の `stylesheets/fonts/hackgen35/HackGen35ConsoleNF-Regular.ttf` があれば `font.map` へ登録して使用し、数字グリフのみをサブセット埋め込みする(極小)。同梱フォントが無い場合は従来どおり `Helvetica` にフォールバックしてビルドは継続する。`EnhancedProviderTest` を追加。
|
|
35
|
+
|
|
36
|
+
## [1.1.0] - 2026-06-10
|
|
37
|
+
|
|
38
|
+
### Changed
|
|
39
|
+
- 本体側に合わせ、lib/vivlio_starter/cli/pdf/配下にファイル構成を調整した。
|
|
40
|
+
|
|
8
41
|
## [1.0.1] - 2026-03-21
|
|
9
42
|
|
|
10
43
|
### Added
|
|
@@ -59,13 +92,3 @@ vivlio-starter-pdf 1.0.0 としてリリース!HexaPDF を活用した高度
|
|
|
59
92
|
- 日本語テキストの空白圧縮処理
|
|
60
93
|
- 括弧の正規化と誤読修正
|
|
61
94
|
- 出版向けの文字処理最適化
|
|
62
|
-
|
|
63
|
-
---
|
|
64
|
-
|
|
65
|
-
## [Unreleased]
|
|
66
|
-
|
|
67
|
-
### Planned
|
|
68
|
-
- パフォーマンス最適化(大規模 PDF 対応)
|
|
69
|
-
- 追加 OCR エンジン対応
|
|
70
|
-
- クラウド OCR サービス連携
|
|
71
|
-
- PDF 暗号化対応
|
data/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# vivlio-starter-pdf
|
|
2
2
|
|
|
3
3
|
[](https://www.gnu.org/licenses/agpl-3.0)
|
|
4
|
-
[](https://www.ruby-lang.org/)
|
|
5
5
|
|
|
6
6
|
**vivlio-starter** の AGPL 拡張プラグイン。HexaPDF を活用した高度な PDF 解析・後処理機能を提供します。
|
|
7
7
|
|
|
@@ -46,24 +46,34 @@ brew install tesseract tesseract-lang poppler vips
|
|
|
46
46
|
vs pdf:read document.pdf
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
+
標準(MIT)実装に戻して動作を比べたいときは、環境変数で無効化できます。
|
|
50
|
+
|
|
51
|
+
```zsh
|
|
52
|
+
VIVLIO_PDF_PLUGIN=disable vs build
|
|
53
|
+
```
|
|
54
|
+
|
|
49
55
|
### Ruby API
|
|
50
56
|
|
|
51
57
|
```ruby
|
|
52
|
-
require "
|
|
58
|
+
require "vivlio_starter/pdf"
|
|
53
59
|
|
|
54
60
|
# PDF → Markdown 変換
|
|
55
|
-
result =
|
|
61
|
+
result = VivlioStarter::Pdf::Reader.new("input.pdf",
|
|
56
62
|
ocr: { mode: "auto", languages: ["jpn"], dpi: 300 }
|
|
57
63
|
).execute
|
|
58
64
|
|
|
59
65
|
# アウトライン付与
|
|
60
|
-
provider =
|
|
66
|
+
provider = VivlioStarter::Pdf::EnhancedProvider.new
|
|
61
67
|
provider.add_outline!(pdf_path, items, max_level: 3)
|
|
62
68
|
|
|
63
69
|
# 隠しノンブル
|
|
64
70
|
provider.stamp_nombre!(pdf_path, bleed_pt: 8.5)
|
|
65
71
|
```
|
|
66
72
|
|
|
73
|
+
`stamp_nombre!` / `add_outline!` は成功で `true`、失敗で `false` を返し、`page_count` は失敗で `nil` を返します。**例外は外へ出しません**(本体のビルドパイプラインの途中から呼ばれるため)。失敗しても入力 PDF は書き換えません。
|
|
74
|
+
|
|
75
|
+
`add_outline!` の `items` は `{ level:, text:, page: }` の配列です。壊れた項目(不正な UTF-8 のタイトル・範囲外ページ・空タイトル)は 1 件ずつ飛ばして残りを書き込み、飛ばした理由をログに出します。
|
|
76
|
+
|
|
67
77
|
## 設定
|
|
68
78
|
|
|
69
79
|
`config/book.yml` の `pdf_read` セクションで挙動を制御できます。
|
data/RELEASE_NOTE.md
CHANGED
|
@@ -1,133 +1,55 @@
|
|
|
1
|
-
# vivlio-starter-pdf
|
|
1
|
+
# vivlio-starter-pdf リリースノート
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
書籍制作ツール **vivlio-starter** の AGPL 拡張プラグインです。HexaPDF を使い、隠しノンブル・PDF アウトライン・PDF → Markdown 変換(OCR 連携)を担当します。使い方は [README](README.md)、網羅的な変更履歴は [CHANGELOG](CHANGELOG.md) をご覧ください。
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
新しい版から順に並べています。
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
### PDF → Markdown 変換
|
|
10
|
-
- HexaPDF でテキスト・画像を高精度に抽出
|
|
11
|
-
- 精密な座標解析による行・段落の再構成
|
|
12
|
-
- Markdown 形式での構造化出力
|
|
13
|
-
|
|
14
|
-
### OCR 連携と日本語対応 🆕
|
|
15
|
-
- スキャン PDF を自動検出
|
|
16
|
-
- Tesseract による日本語 OCR 実行
|
|
17
|
-
- OCR テキストの空白圧縮と括弧正規化
|
|
18
|
-
- prh 辞書による誤読修正
|
|
19
|
-
|
|
20
|
-
### 画像抽出と位置合わせ
|
|
21
|
-
- PDF 内の XObject を解析し、WebP 形式で書き出し
|
|
22
|
-
- テキスト行と画像の精密な座標マッピング
|
|
23
|
-
- イラスト領域の自動検出と除外
|
|
24
|
-
|
|
25
|
-
### 出版向け機能
|
|
26
|
-
- **隠しノンブル**: 入稿用 PDF の塗り足し領域にページ番号をオーバーレイ
|
|
27
|
-
- **PDF アウトライン**: HTML 見出しを解析し、PDF のブックマークツリーを構築
|
|
28
|
-
|
|
29
|
-
## 📦 インストール
|
|
30
|
-
|
|
31
|
-
```ruby
|
|
32
|
-
# Gemfile
|
|
33
|
-
gem 'vivlio-starter-pdf', '~> 1.0.0'
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
```bash
|
|
37
|
-
bundle install
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
### 外部ツール(OCR 利用時)
|
|
41
|
-
|
|
42
|
-
```bash
|
|
43
|
-
brew install tesseract tesseract-lang poppler vips
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
## 💡 使用例
|
|
47
|
-
|
|
48
|
-
### vivlio-starter との連携(プラグイン専用)
|
|
49
|
-
|
|
50
|
-
```bash
|
|
51
|
-
# Enhanced Mode で自動的に HexaPDF/OCR パイプラインを使用
|
|
52
|
-
vs pdf:read document.pdf
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
### Ruby API
|
|
56
|
-
|
|
57
|
-
```ruby
|
|
58
|
-
require "vivlio/starter/pdf"
|
|
59
|
-
|
|
60
|
-
# PDF → Markdown 変換
|
|
61
|
-
result = Vivlio::Starter::PDF::Reader.new("input.pdf",
|
|
62
|
-
ocr: { mode: "auto", languages: ["jpn"], dpi: 300 }
|
|
63
|
-
).execute
|
|
7
|
+
---
|
|
64
8
|
|
|
65
|
-
|
|
66
|
-
provider = Vivlio::Starter::Pdf::EnhancedProvider.new
|
|
67
|
-
provider.add_outline!(pdf_path, items, max_level: 3)
|
|
9
|
+
## 1.1.2 — 2026-08-07
|
|
68
10
|
|
|
69
|
-
|
|
70
|
-
provider.stamp_nombre!(pdf_path, bleed_pt: 8.5)
|
|
71
|
-
```
|
|
11
|
+
**細かな不具合修正が中心のリリースです。** ただし 1 件は成果物に直接影響するため、しおり付き PDF を作る方は更新をおすすめします。
|
|
72
12
|
|
|
73
|
-
|
|
13
|
+
- **壊れた見出しが 1 つあると、しおりが丸ごと消えていた問題を直しました。** 見出しに不正な UTF-8 バイトが混ざると例外が最上位まで飛び、**PDF からしおりが全滅**していました。範囲外ページや空タイトルは 1 件ずつ飛ばす設計だったのに、ここだけ全体を巻き添えにしていたことになります。しかも PDF 自体は出来上がるため、**開いてみるまで誰も気付けません**。今は該当項目だけを飛ばし、どの見出しが原因かをログで知らせます
|
|
14
|
+
- **想定外の型が項目に混ざると、アウトライン付与が止まっていた問題を直しました**(`TypeError`)
|
|
15
|
+
- **必要な Ruby を 4.0 以上から 3.4 以上へ緩めました**(コード変更はゼロ、gemspec 1 行のみ)。本体の緩和に合わせたものです。本体だけ緩めても、プラグインが 4.0 以上のままでは 3.4 環境に入りません
|
|
74
16
|
|
|
75
|
-
`
|
|
17
|
+
あわせて、これらを二度と起こさないための**堅牢性テスト 14 件**を `test/robustness/` に追加しました。守る約束は 2 つです。
|
|
76
18
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
text_area:
|
|
80
|
-
top_margin: 18
|
|
81
|
-
bottom_margin: 20
|
|
82
|
-
inner_margin: 15
|
|
83
|
-
outer_margin: 12
|
|
84
|
-
page_separator: false
|
|
85
|
-
ocr:
|
|
86
|
-
mode: auto
|
|
87
|
-
languages:
|
|
88
|
-
- japanese
|
|
89
|
-
dpi: 300
|
|
90
|
-
psm: 3
|
|
91
|
-
inline_image_text: include
|
|
92
|
-
```
|
|
19
|
+
1. **公開 API は例外を外へ出さない。** 本体のビルドパイプラインの途中から呼ばれるため、1 つの PDF 操作が例外で飛ぶとビルド全体が落ちます。失敗は戻り値で伝えます
|
|
20
|
+
2. **失敗しても入力を壊さない。** 読み込んだのと同じパスへ書き戻すので、途中で失敗して元 PDF が切り詰められると「ビルドは続いたのに成果物が壊れている」という最も気付きにくい事故になります
|
|
93
21
|
|
|
94
|
-
|
|
22
|
+
上の 2 件は、このテストを書く過程で見つかりました。
|
|
95
23
|
|
|
96
|
-
-
|
|
97
|
-
- **実環境での動作検証**
|
|
98
|
-
- **外部ツール連携のテスト**
|
|
24
|
+
## 1.1.1 — 2026-06-23
|
|
99
25
|
|
|
100
|
-
|
|
26
|
+
- **隠しノンブルを埋め込み可能なフォントで描画するようにしました(FT-02)。** HexaPDF 組込の標準 14 フォント `Helvetica` は PDF に埋め込まれず、印刷所入稿で「非埋め込みフォント」の差し戻しになっていました
|
|
27
|
+
- 書籍プロジェクト直下に同梱の `stylesheets/fonts/hackgen35/HackGen35ConsoleNF-Regular.ttf` があれば、数字グリフのみをサブセット埋め込みします(増分は極小)。無ければ従来どおり `Helvetica` にフォールバックしてビルドを続けます
|
|
101
28
|
|
|
102
|
-
|
|
103
|
-
- 完全な OCR 連携と日本語対応
|
|
104
|
-
- 画像位置合わせの精密化
|
|
105
|
-
- PDF アウトライン生成機能
|
|
106
|
-
- 隠しノンブル機能
|
|
29
|
+
## 1.1.0 — 2026-06-10
|
|
107
30
|
|
|
108
|
-
|
|
109
|
-
- gemspec の RubyGems 公開設定
|
|
110
|
-
- ドキュメントの整備
|
|
111
|
-
- 外部ツールの自動案内
|
|
31
|
+
- **名前空間を `VivlioStarter::Pdf` へ統一し、`lib/vivlio_starter/` 配下へ再編しました。** 1.0.0 以降で**唯一の非互換変更**です
|
|
112
32
|
|
|
113
|
-
|
|
33
|
+
本体(`vs`)経由で使っている場合、影響はありません。**Ruby API を直接呼んでいる場合のみ**書き換えが必要です。
|
|
114
34
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
35
|
+
| | 1.0.x | 1.1.0 以降 |
|
|
36
|
+
|:---|:---|:---|
|
|
37
|
+
| require | `require "vivlio/starter/pdf"` | `require "vivlio_starter/pdf"` |
|
|
38
|
+
| 名前空間 | `Vivlio::Starter::PDF` と `Vivlio::Starter::Pdf` が混在 | `VivlioStarter::Pdf` に統一 |
|
|
119
39
|
|
|
120
|
-
|
|
40
|
+
1.0.x では `PDF` と `Pdf` の 2 つの綴りが同居しており、どちらを書けばよいか実物を見ないと分からない状態でした。
|
|
121
41
|
|
|
122
|
-
|
|
123
|
-
- **依存**: hexapdf (~> 1.0), ruby-vips (~> 2.2)
|
|
124
|
-
- **外部ツール**: Tesseract, poppler, libvips(OCR 使用時)
|
|
125
|
-
- **セマンティックバージョニング**: 1.x.x 系は後方互換性を保証
|
|
42
|
+
## 1.0.1 — 2026-03-21
|
|
126
43
|
|
|
127
|
-
|
|
44
|
+
- **CLI を追加しました**(`vivlio-starter-pdf --version` / `--help`)。インストール直後に動作確認できます
|
|
128
45
|
|
|
129
|
-
|
|
46
|
+
## 1.0.0 — 2026-03-21
|
|
130
47
|
|
|
131
|
-
|
|
48
|
+
最初のメジャーリリースです。
|
|
132
49
|
|
|
133
|
-
|
|
50
|
+
- **隠しノンブル** — 入稿用 PDF の塗り足し領域に、断裁されて消えるページ番号を刷り込みます
|
|
51
|
+
- **PDF アウトライン** — 見出しから PDF のブックマークツリーを構築します
|
|
52
|
+
- **PDF → Markdown 変換** — HexaPDF でテキスト・画像を座標付きで抽出します
|
|
53
|
+
- **OCR 連携** — スキャン PDF を自動検出し、Tesseract で日本語 OCR を実行。空白圧縮・括弧正規化・prh 辞書による誤読修正まで行います
|
|
54
|
+
- **画像抽出** — PDF 内の XObject を解析し、WebP 形式で書き出します
|
|
55
|
+
- プラグイン専用設計。インストールされていれば本体が自動検出し、Standard Mode から Enhanced Mode へ切り替わります
|
data/Rakefile
CHANGED
|
@@ -3,10 +3,72 @@
|
|
|
3
3
|
require "bundler/gem_tasks"
|
|
4
4
|
require "rake/testtask"
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
# gemspec が Ruby 3.4 以上を謳う以上、それが本当かは実際に走らせないと分からない。
|
|
7
|
+
# `it`(暗黙ブロック引数)は 3.3 以下でも構文エラーにならず「it というメソッドの
|
|
8
|
+
# 呼び出し」として通り、実行時に NameError になる——静的解析では捕まらない。
|
|
9
|
+
SUPPORTED_RUBY_VERSIONS = %w[3.4.10 4.0.6].freeze
|
|
10
|
+
|
|
11
|
+
# 別の Ruby を子プロセスで起動する以上、親の bundler 環境は必ず捨てる。
|
|
12
|
+
# `bundle exec rake test` から呼ばれると、親(現在の Ruby)の bundler を
|
|
13
|
+
# 子(別の Ruby)が読みに行き、テストが始まる前に LoadError で落ちる。
|
|
14
|
+
# nil を渡すと、その環境変数は子から取り除かれる。
|
|
15
|
+
UNBUNDLED_ENV = {
|
|
16
|
+
"RUBYOPT" => nil, "RUBYLIB" => nil, "GEM_HOME" => nil, "GEM_PATH" => nil,
|
|
17
|
+
"BUNDLE_GEMFILE" => nil, "BUNDLE_BIN_PATH" => nil, "BUNDLER_VERSION" => nil,
|
|
18
|
+
"BUNDLER_SETUP" => nil
|
|
19
|
+
}.freeze
|
|
20
|
+
|
|
21
|
+
namespace :test do
|
|
22
|
+
# テストの実体。現在の Ruby で 1 回だけ走る(test:versions が各版でこれを呼ぶ)
|
|
23
|
+
Rake::TestTask.new(:current) do |t|
|
|
24
|
+
t.libs << "test"
|
|
25
|
+
t.libs << "lib"
|
|
26
|
+
t.test_files = FileList["test/**/*_test.rb"]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
desc "対応する全 Ruby 版(#{SUPPORTED_RUBY_VERSIONS.join(' / ')})でテストする"
|
|
30
|
+
task :versions do
|
|
31
|
+
missing = SUPPORTED_RUBY_VERSIONS - `rbenv versions --bare`.split("\n")
|
|
32
|
+
unless missing.empty?
|
|
33
|
+
abort <<~MESSAGE
|
|
34
|
+
次の Ruby が rbenv に入っていません: #{missing.join(', ')}
|
|
35
|
+
#{missing.map { "rbenv install #{it}" }.join("\n ")}
|
|
36
|
+
MESSAGE
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# 版ごとに bundle が要る。未導入なら黙って入れる(その版の初回だけ時間がかかる)
|
|
40
|
+
failed = SUPPORTED_RUBY_VERSIONS.reject do |version|
|
|
41
|
+
puts "\n=== Ruby #{version} ==="
|
|
42
|
+
env = UNBUNDLED_ENV.merge("RBENV_VERSION" => version)
|
|
43
|
+
|
|
44
|
+
unless system(env, "rbenv exec bundle check", out: File::NULL, err: File::NULL)
|
|
45
|
+
next false unless system(env, "rbenv exec bundle install --quiet")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
system(env, "rbenv exec bundle exec rake test:current")
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
abort "\n失敗した Ruby: #{failed.join(', ')}" unless failed.empty?
|
|
52
|
+
puts "\n全 Ruby 版で通過: #{SUPPORTED_RUBY_VERSIONS.join(' / ')}"
|
|
53
|
+
end
|
|
10
54
|
end
|
|
11
55
|
|
|
56
|
+
# 手元では常に対応全版を回す(1 版あたり 10 秒弱で終わるため、分けるほどの負担にならない)。
|
|
57
|
+
# CI(GitHub Actions)は版をマトリクスで分担するので、各ジョブは 1 回でよい。
|
|
58
|
+
task test: (ENV["CI"] ? "test:current" : "test:versions")
|
|
12
59
|
task default: :test
|
|
60
|
+
|
|
61
|
+
# gem のアンインストール → ビルド → インストールを一括実行
|
|
62
|
+
task :reinstall do
|
|
63
|
+
gemspec = Dir['*.gemspec'].first
|
|
64
|
+
raise 'gemspec が見つかりません' unless gemspec
|
|
65
|
+
|
|
66
|
+
require_relative 'lib/vivlio_starter/cli/pdf/version'
|
|
67
|
+
version = VivlioStarter::Pdf::VERSION
|
|
68
|
+
gem_name = 'vivlio-starter-pdf'
|
|
69
|
+
gem_file = "#{gem_name}-#{version}.gem"
|
|
70
|
+
|
|
71
|
+
sh "gem uninstall #{gem_name} --version #{version} --executables --ignore-dependencies 2>/dev/null || true"
|
|
72
|
+
sh "gem build #{gemspec}"
|
|
73
|
+
sh "gem install #{gem_file}"
|
|
74
|
+
end
|
data/exe/vivlio-starter-pdf
CHANGED
|
@@ -1,52 +1,50 @@
|
|
|
1
1
|
#!/usr/bin/env ruby
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
-
require '
|
|
4
|
+
require 'vivlio_starter/pdf'
|
|
5
5
|
|
|
6
6
|
# vivlio-starter-pdf CLI
|
|
7
7
|
# Provides basic version information for debugging and user confirmation
|
|
8
8
|
|
|
9
|
-
module
|
|
10
|
-
module
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
exit 1
|
|
45
|
-
end
|
|
9
|
+
module VivlioStarter
|
|
10
|
+
module Pdf
|
|
11
|
+
class CLI
|
|
12
|
+
def self.run(args = [])
|
|
13
|
+
case args[0]
|
|
14
|
+
when '--version', '-v'
|
|
15
|
+
puts "vivlio-starter-pdf #{VERSION}"
|
|
16
|
+
when '--help', '-h', nil
|
|
17
|
+
puts <<~HELP
|
|
18
|
+
vivlio-starter-pdf #{VERSION} - Advanced PDF processor for vivlio-starter
|
|
19
|
+
|
|
20
|
+
Usage:
|
|
21
|
+
vivlio-starter-pdf [options]
|
|
22
|
+
|
|
23
|
+
Options:
|
|
24
|
+
--version, -v Print version and exit
|
|
25
|
+
--help, -h Print this help and exit
|
|
26
|
+
|
|
27
|
+
Description:
|
|
28
|
+
vivlio-starter-pdf is an AGPL-licensed plugin for vivlio-starter
|
|
29
|
+
that provides advanced PDF processing capabilities including:
|
|
30
|
+
- PDF to Markdown conversion using HexaPDF
|
|
31
|
+
- Japanese OCR support with Tesseract
|
|
32
|
+
- Image extraction and precise positioning
|
|
33
|
+
- PDF outline generation and hidden pagination
|
|
34
|
+
|
|
35
|
+
This is a plugin and should be used through vivlio-starter:
|
|
36
|
+
vs pdf:read document.pdf
|
|
37
|
+
|
|
38
|
+
For more information, visit: https://github.com/Atelier-Mirai/vivlio-starter-pdf
|
|
39
|
+
HELP
|
|
40
|
+
else
|
|
41
|
+
puts "Error: Unknown option '#{args[0]}'"
|
|
42
|
+
puts "Use 'vivlio-starter-pdf --help' for usage information."
|
|
43
|
+
exit 1
|
|
46
44
|
end
|
|
47
45
|
end
|
|
48
46
|
end
|
|
49
47
|
end
|
|
50
48
|
end
|
|
51
49
|
|
|
52
|
-
|
|
50
|
+
VivlioStarter::Pdf::CLI.run(ARGV)
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "hexapdf"
|
|
4
|
+
require_relative "utilities"
|
|
5
|
+
require_relative "log_helper"
|
|
6
|
+
require_relative "outline_writer"
|
|
7
|
+
|
|
8
|
+
module VivlioStarter
|
|
9
|
+
module Pdf
|
|
10
|
+
# vivlio-starter 本体から呼び出される HexaPDF ベースのプロバイダ
|
|
11
|
+
#
|
|
12
|
+
# 隠しノンブル書き込み・PDF アウトライン付与など、
|
|
13
|
+
# Enhanced Mode 固有の PDF 操作を提供する。
|
|
14
|
+
class EnhancedProvider
|
|
15
|
+
# 隠しノンブルに使う埋め込み可能フォント(書籍プロジェクト同梱の TTF)。
|
|
16
|
+
# HexaPDF 組込の "Helvetica" は標準 14 フォントで PDF に埋め込まれず、印刷所入稿で
|
|
17
|
+
# 「非埋め込みフォント」事故になる(FT-02)。プロジェクト直下に同梱の
|
|
18
|
+
# HackGen35ConsoleNF があれば登録してサブセット埋め込みする(数字のみで極小)。
|
|
19
|
+
NOMBRE_FONT_RELATIVE = File.join("stylesheets", "fonts", "hackgen35", "HackGen35ConsoleNF-Regular.ttf")
|
|
20
|
+
# document.config の font.map へ登録する内部フォント名
|
|
21
|
+
NOMBRE_FONT_NAME = "VivlioNombre"
|
|
22
|
+
# 同梱フォントが見つからない場合のフォールバック(非埋め込み標準フォント)
|
|
23
|
+
FALLBACK_FONT_NAME = "Helvetica"
|
|
24
|
+
# 隠しノンブルのフォントサイズ(pt)
|
|
25
|
+
FONT_SIZE_PT = 6
|
|
26
|
+
|
|
27
|
+
# PDF のページ数を取得する
|
|
28
|
+
def page_count(pdf_path)
|
|
29
|
+
Utilities.page_count(pdf_path)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# 空白ページ PDF が存在しなければ生成する
|
|
33
|
+
def ensure_blank_page_pdf(path, width_pt, height_pt)
|
|
34
|
+
Utilities.ensure_blank_page_pdf(path, width_pt, height_pt)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# PDF の各ページに隠しノンブル(ページ番号)を書き込む
|
|
38
|
+
# 奇数ページは左端、偶数ページは右端に 90° 回転して配置する
|
|
39
|
+
# @param pdf_path [String] 対象 PDF のパス
|
|
40
|
+
# @param bleed_pt [Float] 塗り足し幅(pt)
|
|
41
|
+
# @return [Boolean] 成功なら true
|
|
42
|
+
def stamp_nombre!(pdf_path, bleed_pt:)
|
|
43
|
+
return false unless File.exist?(pdf_path)
|
|
44
|
+
|
|
45
|
+
document = HexaPDF::Document.open(pdf_path)
|
|
46
|
+
total = document.pages.count
|
|
47
|
+
return false if total.zero?
|
|
48
|
+
|
|
49
|
+
LogHelper.log_action("[NombreStamper] 隠しノンブルを書き込みます(#{total} ページ)[Enhanced Mode]…")
|
|
50
|
+
|
|
51
|
+
font = register_nombre_font(document)
|
|
52
|
+
|
|
53
|
+
document.pages.each_with_index do |page, idx|
|
|
54
|
+
stamp_page(page, idx + 1, bleed_pt: bleed_pt.to_f, font:)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
document.write(pdf_path, optimize: true)
|
|
58
|
+
LogHelper.log_success("[NombreStamper] 隠しノンブル書き込み完了(#{total} ページ)")
|
|
59
|
+
true
|
|
60
|
+
rescue StandardError => e
|
|
61
|
+
LogHelper.log_error("[NombreStamper] 隠しノンブル書き込みに失敗: #{e.message}")
|
|
62
|
+
false
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# PDF にアウトライン(しおり)を付与する
|
|
66
|
+
# OutlineWriter を使い、階層構造を HexaPDF のアウトラインツリーに変換する
|
|
67
|
+
# @param pdf_path [String] 対象 PDF のパス
|
|
68
|
+
# @param items [Array<Hash>] アウトライン項目(:level, :text, :page)
|
|
69
|
+
# @param max_level [Integer] アウトラインの最大階層深度
|
|
70
|
+
# @return [Boolean] 成功なら true
|
|
71
|
+
def add_outline!(pdf_path, items, max_level:)
|
|
72
|
+
return false unless File.exist?(pdf_path)
|
|
73
|
+
|
|
74
|
+
document = HexaPDF::Document.open(pdf_path)
|
|
75
|
+
writer = OutlineWriter.new(document, max_level:, on_skip: method(:log_outline_skip))
|
|
76
|
+
inserted = writer.write(items)
|
|
77
|
+
if inserted.zero?
|
|
78
|
+
LogHelper.log_warn("[OutlineWriter] 有効なアウトライン項目が存在しないためスキップしました")
|
|
79
|
+
return false
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
document.write(pdf_path, optimize: true)
|
|
83
|
+
LogHelper.log_success("[OutlineWriter] PDF にアウトラインを #{inserted} 件追加しました")
|
|
84
|
+
true
|
|
85
|
+
rescue StandardError => e
|
|
86
|
+
LogHelper.log_error("[OutlineWriter] PDF アウトライン付与に失敗: #{e.message}")
|
|
87
|
+
false
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
private
|
|
91
|
+
|
|
92
|
+
# 隠しノンブル用フォントを document へ登録し、canvas に渡すフォント名を返す。
|
|
93
|
+
# 同梱 TTF(NOMBRE_FONT_RELATIVE)があればサブセット埋め込みされる名前を、
|
|
94
|
+
# 見つからなければ非埋め込みの Helvetica(FALLBACK_FONT_NAME)を返す。
|
|
95
|
+
# @param document [HexaPDF::Document]
|
|
96
|
+
# @return [String] canvas.font に渡すフォント名
|
|
97
|
+
def register_nombre_font(document)
|
|
98
|
+
return FALLBACK_FONT_NAME unless File.exist?(NOMBRE_FONT_RELATIVE)
|
|
99
|
+
|
|
100
|
+
(document.config["font.map"] ||= {})[NOMBRE_FONT_NAME] = { none: NOMBRE_FONT_RELATIVE }
|
|
101
|
+
NOMBRE_FONT_NAME
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# 1 ページに隠しノンブルを描画する
|
|
105
|
+
# 奇数ページは左端 90°、偶数ページは右端 -90° に回転配置する
|
|
106
|
+
def stamp_page(page, page_number, bleed_pt:, font:)
|
|
107
|
+
canvas = page.canvas(type: :overlay)
|
|
108
|
+
box = page.box(:media)
|
|
109
|
+
|
|
110
|
+
canvas.font(font, size: FONT_SIZE_PT)
|
|
111
|
+
canvas.fill_color(0)
|
|
112
|
+
|
|
113
|
+
x_offset = bleed_pt / 2.0
|
|
114
|
+
y_center = box.height / 2.0
|
|
115
|
+
|
|
116
|
+
if page_number.odd?
|
|
117
|
+
draw_rotated_text(canvas, page_number.to_s, x: x_offset, y: y_center, angle: 90)
|
|
118
|
+
else
|
|
119
|
+
draw_rotated_text(canvas, page_number.to_s, x: box.width - x_offset, y: y_center, angle: -90)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# キャンバス上の指定座標にテキストを回転描画する
|
|
124
|
+
def draw_rotated_text(canvas, text, x:, y:, angle:)
|
|
125
|
+
canvas
|
|
126
|
+
.save_graphics_state
|
|
127
|
+
.translate(x, y)
|
|
128
|
+
.rotate(angle)
|
|
129
|
+
.text(text, at: [0, 0])
|
|
130
|
+
.restore_graphics_state
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# OutlineWriter のスキップ通知をログに出力するコールバック
|
|
134
|
+
def log_outline_skip(message)
|
|
135
|
+
LogHelper.log_warn("[OutlineWriter] #{message}")
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module VivlioStarter
|
|
4
|
+
module Pdf
|
|
5
|
+
# Logging helper that integrates with vivlio-starter CLI when available
|
|
6
|
+
module LogHelper
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def log_action(message)
|
|
10
|
+
dispatch(:log_action, message) { puts(message) }
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def log_info(message)
|
|
14
|
+
dispatch(:log_info, message) { puts(message) }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def log_success(message)
|
|
18
|
+
dispatch(:log_success, message) { puts(message) }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def log_warn(message)
|
|
22
|
+
dispatch(:log_warn, message) { warn(message) }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def log_error(message)
|
|
26
|
+
dispatch(:log_error, message) { warn(message) }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def dispatch(method, message)
|
|
30
|
+
if defined?(VivlioStarter::CLI::Common)
|
|
31
|
+
VivlioStarter::CLI::Common.public_send(method, message)
|
|
32
|
+
else
|
|
33
|
+
yield if block_given?
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|