Module: VivlioStarter::CLI::Build::RotateTableImages

Defined in:
lib/vivlio_starter/cli/build/rotate_table_images.rb

Overview

回転テーブルのページ画像化(Kindle 専用の劣化経路)

Constant Summary collapse

OUT_DIR_NAME =

生成物の置き場(ワークスペース直下)。final clean がまとめて掃除する。

'rotate-tables'
CONTENT_WIDTH_PX =

版面幅が何 px になるようにラスタライズするか。HeadingImageComposer::RENDER_WIDTH と揃えてある(Kindle 端末幅 1072px を上回り、拡大しても粗が出ない)。

1400
TRIM_FUZZ =

余白の切り詰め。版面の中で表は centering + scale されており、素のままでは 周囲が大きく空く。Kindle は画像を画面幅へ伸ばすので、詰めるほど字が大きくなる。

'1%'
BORDER_PX =
12

Class Method Summary collapse

Class Method Details

.arm!(enabled) ⇒ Object

Parameters:

  • enabled (Boolean)

    PDF 枝が画像化を担うか



75
76
77
# File 'lib/vivlio_starter/cli/build/rotate_table_images.rb', line 75

def arm!(enabled)
  @gate = enabled ? Queue.new : nil
end

.armed?Boolean

Returns:

  • (Boolean)


79
# File 'lib/vivlio_starter/cli/build/rotate_table_images.rb', line 79

def armed? = !@gate.nil?

.available?(anchor_id) ⇒ Boolean

Returns:

  • (Boolean)


59
# File 'lib/vivlio_starter/cli/build/rotate_table_images.rb', line 59

def available?(anchor_id) = File.exist?(image_path(anchor_id))

.collect_anchor_ids(dir = Common::BUILD_HTML_DIR) ⇒ Hash{String => String}

html/ の全 HTML から回転テーブルの内部 ID を集める。

Returns:

  • (Hash{String => String})

    ID → その ID を含む HTML のパス



107
108
109
110
111
112
113
114
# File 'lib/vivlio_starter/cli/build/rotate_table_images.rb', line 107

def collect_anchor_ids(dir = Common::BUILD_HTML_DIR)
  prefix = PreProcessCommands::TableConverter::ROTATE_ID_PREFIX
  Dir.glob(File.join(dir, '*.html')).each_with_object({}) do |path, found|
    File.read(path, encoding: 'utf-8').scan(/\bid="(#{prefix}[^"]+)"/o).flatten.each do |id|
      found[id] ||= path
    end
  end
end

.crop(source, destination, geometry) ⇒ Object

版面(柱・ノンブルの内側)だけを切り出し、余白を詰めて細い白縁を付ける。

左右は inner/outer の狭いほうで対称に切る。柱とノンブルは天地の余白に 入るので、左右の切り幅は「版面を欠かない」ことだけ満たせばよく、 ページの表裏(recto/verso で inner と outer が入れ替わる)を判定しなくて済む。



180
181
182
183
184
185
186
187
188
189
# File 'lib/vivlio_starter/cli/build/rotate_table_images.rb', line 180

def crop(source, destination, geometry)
  command = %(magick "#{source}" ) +
            %(-crop #{geometry[:width]}x#{geometry[:height]}+#{geometry[:left]}+#{geometry[:top]} +repage ) +
            %(-fuzz #{TRIM_FUZZ} -trim +repage ) +
            %(-bordercolor white -border #{BORDER_PX} "#{destination}" > /dev/null 2>&1)
  return true if system(command) && File.exist?(destination)

  Common.log_warn("[rotate-table] 画像の切り出しに失敗しました: #{File.basename(destination, '.png')}")
  false
end

.crop_geometryHash?

版面の切り出し寸法(px)と、そのために必要な解像度。

Returns:

  • (Hash, nil)

    book.yml から版面を決められないときは nil



193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
# File 'lib/vivlio_starter/cli/build/rotate_table_images.rb', line 193

def crop_geometry
  page = Common.configured? ? Common::CONFIG.page.to_h : nil
  return nil unless page

  page_w, page_h = Common.resolve_page_size(page).map { Units.length_to_mm(it) }
  side = [mm(page[:margin_inner]), mm(page[:margin_outer])].min
  top = mm(page[:margin_top])
  bottom = mm(page[:margin_bottom])
  return nil unless page_w && page_h

  content_w = page_w - mm(page[:margin_inner]) - mm(page[:margin_outer])
  return nil unless content_w.positive?

  dpi = (CONTENT_WIDTH_PX / (content_w / Units::MM_PER_INCH)).round
  px = ->(value) { (value * dpi / Units::MM_PER_INCH).round }
  { dpi:, left: px.call(side), top: px.call(top),
    width: px.call(page_w - (side * 2)), height: px.call(page_h - top - bottom) }
end

.extract!(pdf_path) ⇒ Integer

回転テーブルのページを切り出して画像化する。

Parameters:

  • pdf_path (String)

    切り出し元(dedup 前の 1 回目のレンダ)

Returns:

  • (Integer)

    生成できた画像の数



120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
# File 'lib/vivlio_starter/cli/build/rotate_table_images.rb', line 120

def extract!(pdf_path)
  ids = collect_anchor_ids.keys.sort
  return 0 if ids.empty?

  unless File.exist?(pdf_path)
    warn_missing_pdf(ids)
    return 0
  end

  pages = PdfPageMapExtractor.new(pdf_path).pages_for(ids)
  warn_unresolved(ids - pages.keys) if pages.size < ids.size
  return 0 if pages.empty?

  rasterize_pages(pdf_path, pages)
end

.extract_subset(pdf_path, pages, output) ⇒ Object



154
155
156
157
158
159
160
# File 'lib/vivlio_starter/cli/build/rotate_table_images.rb', line 154

def extract_subset(pdf_path, pages, output)
  command = %(qpdf "#{pdf_path}" --pages "#{pdf_path}" #{pages.join(',')} -- "#{output}" > /dev/null 2>&1)
  return true if system(command) && File.exist?(output)

  Common.log_warn('[rotate-table] 回転テーブルのページを取り出せませんでした(qpdf)')
  false
end

.image_path(anchor_id) ⇒ Object



57
# File 'lib/vivlio_starter/cli/build/rotate_table_images.rb', line 57

def image_path(anchor_id) = File.join(output_dir, "#{anchor_id}.png")

.mm(value) ⇒ Object



212
# File 'lib/vivlio_starter/cli/build/rotate_table_images.rb', line 212

def mm(value) = Units.length_to_mm(value) || 0.0

.output_dirObject



55
# File 'lib/vivlio_starter/cli/build/rotate_table_images.rb', line 55

def output_dir = File.join(Common::BUILD_DIR, OUT_DIR_NAME)

.rasterize_pages(pdf_path, pages) ⇒ Object

必要なページだけを 1 本の小さな PDF へ抜いてからラスタライズする。 100MB 級の本文 PDF を表の数だけ開き直すのは無駄が大きいため。



138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
# File 'lib/vivlio_starter/cli/build/rotate_table_images.rb', line 138

def rasterize_pages(pdf_path, pages)
  FileUtils.mkdir_p(output_dir)
  geometry = crop_geometry
  return 0 unless geometry

  Dir.mktmpdir('vs-rotate') do |tmp|
    ordered = pages.values.uniq.sort
    subset = File.join(tmp, 'pages.pdf')
    return 0 unless extract_subset(pdf_path, ordered, subset)

    pages.count do |anchor_id, page|
      render_one(subset, ordered.index(page) + 1, anchor_id, geometry, tmp)
    end
  end
end

.release!Object

待っている枝を解放する。PDF 枝が例外で落ちても必ず呼ぶこと(デッドロック防止)。



82
# File 'lib/vivlio_starter/cli/build/rotate_table_images.rb', line 82

def release! = @gate&.close

.render_one(subset_pdf, index, anchor_id, geometry, tmp) ⇒ Object

1 ページを PNG 化し、版面の外(柱・ノンブル)を落として余白を詰める。



163
164
165
166
167
168
169
170
171
172
173
# File 'lib/vivlio_starter/cli/build/rotate_table_images.rb', line 163

def render_one(subset_pdf, index, anchor_id, geometry, tmp)
  raw = File.join(tmp, "#{anchor_id}-raw")
  rasterize = %(pdftoppm -png -r #{geometry[:dpi]} -f #{index} -l #{index} -singlefile ) +
              %("#{subset_pdf}" "#{raw}" > /dev/null 2>&1)
  unless system(rasterize) && File.exist?("#{raw}.png")
    Common.log_warn("[rotate-table] ページの画像化に失敗しました: #{anchor_id}")
    return false
  end

  crop("#{raw}.png", image_path(anchor_id), geometry)
end

.wait_until_ready!Object

待った時間を必ず報告する。

待っているのは本文 PDF が組み上がることであって、画像化そのものではない (実測 1.8 秒)。この待ちは PDF 枝が長い構成では枝の陰に隠れて壁時計に現れないが、 索引・用語集を切ると PDF 枝が半分になって臨界経路が EPUB 側へ移り、そのまま 壁時計に乗る。ステップ計時には現れない時間なので、ここで出さないと 「EPUB 枝が遅い」と読み違える。



91
92
93
94
95
96
97
98
99
# File 'lib/vivlio_starter/cli/build/rotate_table_images.rb', line 91

def wait_until_ready!
  return unless @gate

  Common.log_info('[rotate-table] 本文 PDF の完成を待っています…')
  started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
  @gate.pop
  waited = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started
  Common.log_info(format('[rotate-table] 本文 PDF を %.2f 秒待ちました', waited))
end

.warn_missing_pdf(ids) ⇒ Object

=============================================================

縮退の案内(warning-messages-actionable の方針)



218
219
220
221
222
223
# File 'lib/vivlio_starter/cli/build/rotate_table_images.rb', line 218

def warn_missing_pdf(ids)
  Common.log_warn("回転テーブル #{ids.size} 件を Kindle 用に画像化できませんでした(本文 PDF がありません)",
                  detail: "対処: config/book.yml の output.targets に pdf を足すと画像化されます。\n" \
                          '(例: targets: pdf, epub, kindle)そのままでも Kindle は素の表として読めますが、' \
                          '回転せず横長の表は見切れます。')
end

.warn_unresolved(ids) ⇒ Object



225
226
227
228
229
# File 'lib/vivlio_starter/cli/build/rotate_table_images.rb', line 225

def warn_unresolved(ids)
  Common.log_warn("回転テーブルのページ位置を特定できませんでした: #{ids.join(', ')}",
                  detail: '対処: vs build --clean で中間生成物を作り直してください。' \
                          '解消しない場合は該当の :::{.rotate-table} ブロックをお知らせください。')
end