Module: VivlioStarter::CLI::Build::PdfMerger
- Defined in:
- lib/vivlio_starter/cli/build/pdf_merger.rb
Defined Under Namespace
Classes: Segment
Class Method Summary collapse
-
.add_outline_to_output_pdf!(entries_or_keep = nil) ⇒ Object
5.
-
.base_pdf_for(segments) ⇒ Object
メタデータ・しおりの引き継ぎ元。本文を優先する(ベース PDF の情報が 出力へ受け継がれるため、表紙 1 枚の情報で上書きされないようにする)。.
-
.body_and_matter_segments ⇒ Object
本文・前付・奥付の 3 区間。中間 PDF はワークスペース pdf/ 内に置かれる。.
-
.chapter_pagebreak_any? ⇒ Boolean
値の正規化と不正値の警告は BookSettingsCss が唯一の実装。ここは判定だけ借りる。.
-
.compute_front_matter_offset(segments) ⇒ Object
区間列のうち、前付より前のページ数を合算する。 前付が見つからない場合は 0 を返す(従来挙動と互換)。.
-
.cover_enhanced_segments ⇒ Object
結合順の区間列。前付・奥付が本文 PDF に相乗りしていれば同じファイルの 別範囲として、していなければ個別ファイルとして並べる。どちらも同じ Segment の列になるので、以降の工程は区別しなくてよい。.
-
.embedded_segments(path, ranges) ⇒ Object
1 本の PDF に相乗りした前付・奥付を 3 区間へ割る(閲覧用・入稿用で共用).
- .extract_size_from_preset(preset_name) ⇒ Object
-
.extract_targets(raw) ⇒ Object
2.
-
.front_matter_offset ⇒ Object
output.pdf 先頭に結合される表紙 PDF など、前付より前に並ぶページ数を返す。 アウトラインのページ位置計算の基点(本扉の実ページ番号 = offset + 1)を 補正するために用いる。merge_all_pdfs! 実行時に算出される。 未算出時は 0(表紙なし相当)。.
-
.insert_blank_page_before_colophon(segments) ⇒ Array<Segment>
奥付が偶数ページ(左ページ)始まりになるよう空白ページを挿入する。 閲覧用・入稿用のどちらの区間列にも使う。.
-
.merge_all_pdfs!(_entries_or_keep = nil) ⇒ Object
3.
-
.merge_pdfs_with_qpdf!(segments, output:, base_pdf: nil) ⇒ Boolean
区間列を qpdf で 1 本の PDF に結合する(閲覧用・入稿用ビルドの共通基盤)。.
-
.merged_output_pdf ⇒ Object
結合済み PDF のパス(ワークスペース pdf/ 内。最終リネームでルートへ出る).
- .qpdf_available? ⇒ Boolean
-
.ranged_segment(role, path, range) ⇒ Object
ページ範囲を 1 区間として扱う.
- .resolve_page_use(page_cfg) ⇒ Object
-
.separate_segments(front_matter:, body:, colophon:) ⇒ Object
前付・奥付を個別レンダしたときの 3 区間(フォールバック経路・閲覧用と入稿用で ファイル名が違うため basename を受け取る).
-
.whole_file_segment(role, path) ⇒ Object
ファイル全体を 1 区間として扱う.
Class Method Details
.add_outline_to_output_pdf!(entries_or_keep = nil) ⇒ Object
================================================================ 5. アウトライン付与 (Step 11) ================================================================
276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 |
# File 'lib/vivlio_starter/cli/build/pdf_merger.rb', line 276 def add_outline_to_output_pdf!(entries_or_keep = nil) return false unless File.exist?(merged_output_pdf) keep_numbers = Build::Utilities.chapter_numbers_for_outline(entries_or_keep) # 抽出対象HTMLの絞り込み(dedup 済み HTML はワークスペース pdf/ 内・P4 §5.1) special_pages = %w[_toc] special_pages.push('_glossarypage', '_indexpage') if IndexCommands.index_enabled? chapter_htmls = Dir.glob(File.join(Common::BUILD_PDF_DIR, '*.html')).select do |path| bn = File.basename(path, '.html') num = bn[/\A(\d+)-/, 1]&.to_i (num && (keep_numbers.nil? || keep_numbers.include?(num))) || special_pages.include?(bn) end if chapter_htmls.empty? Common.log_info('[Step 11] 本文HTMLなし。スキップします') return false end Common.log_action('[Step 11] PDF ブックマークを付与します…') # 表紙 PDF のページ数を基点に加味して、前付・本文・巻末のページ範囲を正しく算出する。 OutlineExtractor.add_outline_from_headings!(merged_output_pdf, chapter_htmls, max_level: 3, start_page: front_matter_offset + 1) true end |
.base_pdf_for(segments) ⇒ Object
メタデータ・しおりの引き継ぎ元。本文を優先する(ベース PDF の情報が 出力へ受け継がれるため、表紙 1 枚の情報で上書きされないようにする)。
170 171 172 |
# File 'lib/vivlio_starter/cli/build/pdf_merger.rb', line 170 def base_pdf_for(segments) segments.find { it.role == :body }&.path || segments.first.path end |
.body_and_matter_segments ⇒ Object
本文・前付・奥付の 3 区間。中間 PDF はワークスペース pdf/ 内に置かれる。
相乗り経路では 1 本の _sections.pdf から 3 範囲を切り出す。分割して
別ファイルにすると各ファイルがフォントを丸ごと抱え込み、実測で最終 PDF が
約 1.9MB 太る(front-back-matter-single-render-spec.md §3.1)。
70 71 72 73 74 75 76 77 78 |
# File 'lib/vivlio_starter/cli/build/pdf_merger.rb', line 70 def body_and_matter_segments sections = File.join(Common::BUILD_PDF_DIR, '_sections.pdf') ranges = Build::PdfBuilder.(sections) return (sections, ranges) if ranges separate_segments(front_matter: '_titlepage_legalpage.pdf', body: '_sections.pdf', colophon: '_colophon.pdf') end |
.chapter_pagebreak_any? ⇒ Boolean
値の正規化と不正値の警告は BookSettingsCss が唯一の実装。ここは判定だけ借りる。
242 243 244 245 246 247 248 249 |
# File 'lib/vivlio_starter/cli/build/pdf_merger.rb', line 242 def chapter_pagebreak_any? return false unless Common.configured? PreProcessCommands::BookSettingsCss .chapter_pagebreak_value(Common::CONFIG.page) == 'any' rescue StandardError false end |
.compute_front_matter_offset(segments) ⇒ Object
区間列のうち、前付より前のページ数を合算する。 前付が見つからない場合は 0 を返す(従来挙動と互換)。
266 267 268 269 270 271 |
# File 'lib/vivlio_starter/cli/build/pdf_merger.rb', line 266 def compute_front_matter_offset(segments) idx = segments.index { it.role == :front_matter } return 0 unless idx segments[0...idx].sum(&:pages) end |
.cover_enhanced_segments ⇒ Object
結合順の区間列。前付・奥付が本文 PDF に相乗りしていれば同じファイルの 別範囲として、していなければ個別ファイルとして並べる。どちらも同じ Segment の列になるので、以降の工程は区別しなくてよい。
31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
# File 'lib/vivlio_starter/cli/build/pdf_merger.rb', line 31 def cover_enhanced_segments segments = body_and_matter_segments cfg = Common::CONFIG # ターゲット判定 targets = extract_targets(cfg.output.targets) pdf_selected = targets.empty? || targets.any? { it.include?('pdf') } return segments unless pdf_selected return segments unless Common.pdf_combined? begin page_use = resolve_page_use(cfg.page) # カバー資産は共通前段の `prepare cover assets` が作り済み。ここでは並べるだけ。 # かつては本メソッドが生成も担っていたが、EPUB 枝・入稿用も同じ関数を呼んで # おり、枝を並列に走らせると同じパスへ同時に書く #(build-target-parallelization-spec.md §3.2)。 theme = Common.cover_theme size = extract_size_from_preset(page_use) # パス生成(生成物は cover_cache_dir に出ている) front = File.join(Common.cover_cache_dir, "frontcover_#{theme}_#{size}_rgb.pdf") back = File.join(Common.cover_cache_dir, "backcover_#{theme}_#{size}_rgb.pdf") segments.unshift(whole_file_segment(:cover_front, front)) if File.exist?(front) segments.push(whole_file_segment(:cover_back, back)) if File.exist?(back) rescue StandardError => e Common.log_warn("[Step 10] カバー結合設定の処理中にエラー: #{e.}") end segments end |
.embedded_segments(path, ranges) ⇒ Object
1 本の PDF に相乗りした前付・奥付を 3 区間へ割る(閲覧用・入稿用で共用)
81 82 83 84 85 86 87 |
# File 'lib/vivlio_starter/cli/build/pdf_merger.rb', line 81 def (path, ranges) [ ranged_segment(:front_matter, path, ranges[:front]), ranged_segment(:body, path, ranges[:body]), ranged_segment(:colophon, path, ranges[:colophon]) ] end |
.extract_size_from_preset(preset_name) ⇒ Object
128 129 130 131 132 133 134 135 |
# File 'lib/vivlio_starter/cli/build/pdf_merger.rb', line 128 def extract_size_from_preset(preset_name) case preset_name.to_s when /a4/ then 'a4' when /a5/ then 'a5' when /b5/ then 'b5' else 'a4' # デフォルト end end |
.extract_targets(raw) ⇒ Object
================================================================ 2. 補助メソッド (Data / Pattern Matching 活用) ================================================================
111 112 113 114 115 116 117 |
# File 'lib/vivlio_starter/cli/build/pdf_merger.rb', line 111 def extract_targets(raw) case raw in String => s then s.split(',').map(&:strip).reject(&:empty?) in Array => a then a.map(&:to_s).map(&:strip).reject(&:empty?) else [] end end |
.front_matter_offset ⇒ Object
output.pdf 先頭に結合される表紙 PDF など、前付より前に並ぶページ数を返す。 アウトラインのページ位置計算の基点(本扉の実ページ番号 = offset + 1)を 補正するために用いる。merge_all_pdfs! 実行時に算出される。 未算出時は 0(表紙なし相当)。
262 |
# File 'lib/vivlio_starter/cli/build/pdf_merger.rb', line 262 def front_matter_offset = @front_matter_offset || 0 |
.insert_blank_page_before_colophon(segments) ⇒ Array<Segment>
奥付が偶数ページ(左ページ)始まりになるよう空白ページを挿入する。 閲覧用・入稿用のどちらの区間列にも使う。
page.chapter_pagebreak: any(面を問わない)では挿入しない。奥付を左ページに 置くのは改丁とは別の慣習だが、「どちら側でもよい」と宣言した本で白紙だけが 残るのは一貫しない(chapter-pagebreak-spec.md §2.3)。
209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 |
# File 'lib/vivlio_starter/cli/build/pdf_merger.rb', line 209 def insert_blank_page_before_colophon(segments) if chapter_pagebreak_any? Common.log_debug('[Step 10] page.chapter_pagebreak: any のため奥付前の空白ページを挿入しません') return segments end colophon_idx = segments.index { it.role == :colophon } return segments unless colophon_idx # カバーはページ番号体系に含まれないため parity 計算から除外 preceding = segments[0...colophon_idx].reject { it.role == :cover_front } preceding.each { Common.log_debug("[Step 10] ページ数: #{it.path} #{it.range} = #{it.pages}p") } total = preceding.sum(&:pages) Common.log_debug("[Step 10] 奥付前の合計ページ数(カバー除外): #{total}") if total.zero? Common.log_debug('[Step 10] 奥付より前のPDFページ数を取得できませんでした') return segments end # total が偶数 → 次ページは奇数(右) → 空白ページを挿入して偶数に # total が奇数 → 次ページは偶数(左) → そのままでOK if total.even? blank = Build::Utilities.ensure_blank_page_pdf(File.join(Common::BUILD_PDF_DIR, '_blank_before_colophon.pdf')) Common.log_debug("[Step 10] 奥付を偶数ページに配置するため空白ページを挿入します(前方 #{total} ページ)") segments.dup.insert(colophon_idx, Segment.new(role: :blank, path: blank, range: '1-z', pages: 1)) else Common.log_debug("[Step 10] 奥付は偶数ページに配置されます(前方 #{total} ページ、空白挿入なし)") segments end end |
.merge_all_pdfs!(_entries_or_keep = nil) ⇒ Object
================================================================ 3. PDF 結合実行 (Step 10) ================================================================
140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 |
# File 'lib/vivlio_starter/cli/build/pdf_merger.rb', line 140 def merge_all_pdfs!(_entries_or_keep = nil) Common.log_action('[Step 10] 表紙、本文、奥付を結合します…') segments = cover_enhanced_segments.select { File.exist?(it.path) } if segments.empty? Common.log_error('[Step 10] 結合対象PDFがありません') return false end return false unless qpdf_available? # 奥付を偶数ページ(左ページ)に配置するため、必要なら空白ページを挿入 segments = insert_blank_page_before_colophon(segments) # アウトライン付与の基点補正用に、前付より前に結合される表紙 PDF の # ページ数を記録しておく(Step 11 で参照)。 @front_matter_offset = compute_front_matter_offset(segments) if merge_pdfs_with_qpdf!(segments, output: merged_output_pdf, base_pdf: base_pdf_for(segments)) Common.log_success('[Step 10] output.pdf を生成しました') true else Common.log_error('[Step 10] PDF結合に失敗しました') false end end |
.merge_pdfs_with_qpdf!(segments, output:, base_pdf: nil) ⇒ Boolean
区間列を qpdf で 1 本の PDF に結合する(閲覧用・入稿用ビルドの共通基盤)。
base_pdf を「結合のベース」として qpdf に渡すと、その PDF の メタデータが出力へ引き継がれる。指定がなければ先頭区間のファイルを使う。
同じファイルが複数区間に現れてよい(相乗り経路では 3 区間が同一ファイル)。 qpdf は入力ごとに 1 度だけ読み込むため、フォントなどの共有資源は重複しない。
189 190 191 192 193 194 195 196 197 198 |
# File 'lib/vivlio_starter/cli/build/pdf_merger.rb', line 189 def merge_pdfs_with_qpdf!(segments, output:, base_pdf: nil) return false if segments.empty? base_pdf ||= segments.first.path FileUtils.rm_f(output) pages = segments.map { %("#{it.path}" #{it.range}) }.join(' ') success = system(%(qpdf "#{base_pdf}" --pages #{pages} -- "#{output}" > /dev/null)) success && File.exist?(output) end |
.merged_output_pdf ⇒ Object
結合済み PDF のパス(ワークスペース pdf/ 内。最終リネームでルートへ出る)
175 |
# File 'lib/vivlio_starter/cli/build/pdf_merger.rb', line 175 def merged_output_pdf = File.join(Common::BUILD_PDF_DIR, 'output.pdf') |
.qpdf_available? ⇒ Boolean
251 252 253 254 255 256 |
# File 'lib/vivlio_starter/cli/build/pdf_merger.rb', line 251 def qpdf_available? return true if system('command -v qpdf >/dev/null 2>&1') Common.log_warn('[Step 10] qpdf が見つかりません。') false end |
.ranged_segment(role, path, range) ⇒ Object
ページ範囲を 1 区間として扱う
103 104 105 |
# File 'lib/vivlio_starter/cli/build/pdf_merger.rb', line 103 def ranged_segment(role, path, range) Segment.new(role:, path:, range: "#{range.first}-#{range.last}", pages: range.size) end |
.resolve_page_use(page_cfg) ⇒ Object
119 120 121 122 123 124 125 126 |
# File 'lib/vivlio_starter/cli/build/pdf_merger.rb', line 119 def resolve_page_use(page_cfg) # Data オブジェクトからプリセット名を優先順位付きで取得 %i[use preset preset_name size].each do |key| val = page_cfg&.[](key) return val.to_s if val && !val.to_s.strip.empty? end 'b5_standard' end |
.separate_segments(front_matter:, body:, colophon:) ⇒ Object
前付・奥付を個別レンダしたときの 3 区間(フォールバック経路・閲覧用と入稿用で ファイル名が違うため basename を受け取る)
91 92 93 94 95 |
# File 'lib/vivlio_starter/cli/build/pdf_merger.rb', line 91 def separate_segments(front_matter:, body:, colophon:) { front_matter:, body:, colophon: }.map do |role, name| whole_file_segment(role, File.join(Common::BUILD_PDF_DIR, name)) end end |
.whole_file_segment(role, path) ⇒ Object
ファイル全体を 1 区間として扱う
98 99 100 |
# File 'lib/vivlio_starter/cli/build/pdf_merger.rb', line 98 def whole_file_segment(role, path) Segment.new(role:, path:, range: '1-z', pages: Build::Utilities.page_count(path).to_i) end |