Class: VivlioStarter::CLI::PreProcessCommands::ShowcaseTransformer::ImageTools

Inherits:
Object
  • Object
show all
Defined in:
lib/vivlio_starter/cli/pre_process/showcase_transformer.rb

Overview

外部ツール依存をこのクラスへ隔離する(builder は純関数のまま保つ・§7.1)。

Instance Method Summary collapse

Instance Method Details

#available?Boolean

magick と rsvg-convert が両方揃っているか。

Returns:

  • (Boolean)


287
288
289
290
291
# File 'lib/vivlio_starter/cli/pre_process/showcase_transformer.rb', line 287

def available?
  return @available unless @available.nil?

  @available = tool_ok?('magick', '-version') && tool_ok?('rsvg-convert', '--version')
end

#data_uri(path) ⇒ Object

元画像を base64 data URI へ。EMBED_MAX_EDGE まで縮小して軽量化する (viewBox は元画像実寸で張るため、縮小しても座標計算には影響しない)。

写真は JPEG で埋める。 PNG は可逆なので、写真を埋めると PDF 内でも Flate のまま展開され、1 枚あたり 1,849 KB を占めていた(実測 2026-08-17・ 全章で 5 枚 9.2 MB。image-format-per-target-spec.md §4.2)。判定は EPUB 向けの ラスター形式と同じ photographic? を使う——同じ絵に別の基準を持つ理由がない。 透過は白へ落とす(rasterize の JPEG 枝と同じ流儀。SVG の地はページの白)。



325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
# File 'lib/vivlio_starter/cli/pre_process/showcase_transformer.rb', line 325

def data_uri(path)
  resize = ['-resize', "#{EMBED_MAX_EDGE}x#{EMBED_MAX_EDGE}>"]

  if photographic?(path)
    jpg, jpg_status = Open3.capture2('magick', path, *resize,
                                     '-background', 'white', '-alpha', 'remove', '-alpha', 'off',
                                     '-quality', '90', 'jpg:-', binmode: true)
    return "data:image/jpeg;base64,#{[jpg].pack('m0')}" if jpg_status.success? && !jpg.empty?
  end

  png, status = Open3.capture2('magick', path, *resize, 'png:-', binmode: true)
  return nil unless status.success? && !png.empty?

  # base64 は Ruby 3.4+ で default gem 外のため Array#pack('m0')(改行なし base64)で代替
  "data:image/png;base64,#{[png].pack('m0')}"
rescue StandardError
  nil
end

#image_dimensions(path) ⇒ Array(Integer, Integer)?

元画像の実寸(px→パーミル変換・viewBox 計算に使う)。

Returns:

  • (Array(Integer, Integer), nil)


295
296
297
298
299
300
301
302
303
# File 'lib/vivlio_starter/cli/pre_process/showcase_transformer.rb', line 295

def image_dimensions(path)
  out, status = Open3.capture2('magick', 'identify', '-format', '%w %h', path)
  return nil unless status.success?

  width, height = out.split.map(&:to_i)
  (width.to_i.positive? && height.to_i.positive?) ? [width, height] : nil
rescue StandardError
  nil
end

#photographic?(path) ⇒ Boolean

元画像が写真か(=ユニーク色数が多いか)。スクリーンショットは平坦色で色数が 桁違いに少ない(冒頭コメントの実測値)。判定はキャッシュ未ヒット時のみ走り、 実測 0.2〜0.35 秒。

Returns:

  • (Boolean)


308
309
310
311
312
313
314
315
# File 'lib/vivlio_starter/cli/pre_process/showcase_transformer.rb', line 308

def photographic?(path)
  out, status = Open3.capture2('magick', 'identify', '-format', '%k', path)
  return false unless status.success?

  out.strip.to_i > PHOTO_COLOR_THRESHOLD
rescue StandardError
  false
end

#rasterize(svg, width, format: :png) ⇒ Object

合成 SVG をラスター(バイト列)へ変換する。JPEG は透過を持てないため白で フラット化する(リーダーの白ページと馴染ませる。HeadingImageComposer と同じ流儀)。

Parameters:

  • format (Symbol) (defaults to: :png)

    :png(可逆・透過保持)/ :jpg(写真向け)



348
349
350
351
352
353
354
355
356
357
358
359
# File 'lib/vivlio_starter/cli/pre_process/showcase_transformer.rb', line 348

def rasterize(svg, width, format: :png)
  png, status = Open3.capture2('rsvg-convert', '-w', width.to_s, '-f', 'png',
                               stdin_data: svg, binmode: true)
  return nil unless status.success? && !png.empty?
  return png if format == :png

  jpg, jpg_status = Open3.capture2('magick', 'png:-', '-background', 'white', '-flatten',
                                   '-quality', '85', 'jpg:-', stdin_data: png, binmode: true)
  (jpg_status.success? && !jpg.empty?) ? jpg : nil
rescue StandardError
  nil
end