Module: VivlioStarter::CLI::PreProcessCommands::ShowcaseTransformer
- Defined in:
- lib/vivlio_starter/cli/pre_process/showcase_transformer.rb
Overview
:::showcase ブロックを合成画像へ変換するモジュール
Defined Under Namespace
Classes: ImageTools
Constant Summary collapse
- REL_BASE =
生成物の出力先(images/ 配下)。著者画像(images/<章>/…)とは別系統に置く。
'showcase'- BLOCK_PATTERN =
ブロック全体(開きフェンス〜閉じフェンス)。中身を丸ごと消費する。
/^:::\s*\{\s*\.showcase\s*\}[ \t]*\n(.*?)^:::[ \t]*\n?/m- EMBED_MAX_EDGE =
SVG へ埋め込む元画像の最大長辺(スクリーンショットの文字が潰れない上限)。
2000- RASTER_SCALE =
ラスタライズ倍率と上限幅(リーダー側の縮小表示で鮮明に見せるため 2 倍で焼く)。
2- RASTER_MAX_WIDTH =
2600- PHOTO_COLOR_THRESHOLD =
元画像を「写真」とみなすユニーク色数の下限(冒頭コメントの実測値に基づく。 ロゴ 5,648 と写真 17,122 の谷に置いた)。これ以上なら JPEG、未満なら PNG。
8192
Class Method Summary collapse
-
.cache_key(source, block) ⇒ Object
画像内容をキーに含めるため、著者がスクリーンショットを撮り直せば再生成される。 v2: ラベル書体をキーに含める。SVG はサブセットを自身に抱くので、著者が typography.heading.font を変えたら作り直さないと古い書体のまま残る (mermaid 側は最初から font_family をキーに含めていた)。.
-
.cached_raster_ext(key) ⇒ Object
永続キャッシュに残るラスターの拡張子(png/jpg)。無ければ nil(=形式判定から決める)。.
-
.compose(image, lines, chapter_slug:, source_filename:, tools:) ⇒ Array(ShowcaseSvgBuilder::ShowcaseBlock, String, String)?
合成 SVG とラスターを(必要なら)生成し、参照パスを返す。縮退すべき場合は nil。.
-
.default_tools ⇒ Object
既定の外部ツール(magick + rsvg-convert)。.
- .escape_attr(str) ⇒ Object
-
.figure(svg_rel, raster_rel, alt, width) ⇒ Object
置換後の HTML。前後に空行を補い独立段落として組ませる。 ラスターの参照を data-vs-raster に明示して持たせる——形式が png / jpg のどちらにも なりうるため、EpubBuilder 側で拡張子を推測させない(推測させると --no-clean ビルドで 前回形式の残骸を拾いうる)。EpubBuilder は使用後にこの属性を取り除く。.
-
.label_font_subset(block) ⇒ String?
注釈ラベルに出る字だけへ絞った見出し書体(Bold)を返す。.
-
.plain_image(image) ⇒ Object
縮退時の出力。注釈を捨て、画像行だけを通常の画像記法として残す。.
- .raster_width(block, orig_w:, orig_h:) ⇒ Object
-
.render_block(lines, chapter_slug:, source_filename:, tools:) ⇒ Object
ブロック 1 つ分を HTML(または縮退した画像記法)へ変換する。.
-
.resolve_image_file(src, source_filename) ⇒ Object
正規化済みの画像参照(asset_prefix + images/…)から実ファイルパスを解決する。 ImagePathNormalizer が既に .webp 寄せ・存在チェックを済ませているため、 ここでは prefix を剥がして Common::IMAGES_DIR 基準で引き当てるだけでよい。.
-
.transform(content, chapter_slug:, source_filename:, tools: default_tools) ⇒ String
本文中の showcase ブロックをすべて
へ置換する。. - .warn_extra_images(source_filename) ⇒ Object
-
.warn_no_image(source_filename) ⇒ Object
--- 警告(§7.10: 修正例と出現位置を必ず添える) ---.
- .warn_remote_image(source_filename, src) ⇒ Object
- .warn_tools_missing(source_filename) ⇒ Object
- .warn_unparsable(line, source_filename) ⇒ Object
-
.write_assets!(block, key, out_dir, ext, source:, orig_w:, orig_h:, tools:) ⇒ Boolean
SVG とラスターを対でワークスペースへ用意する。合成は magick+rsvg で 1 枚 0.5〜1 秒級 のため、生成物は GeneratedAssetCache(.cache/vs/showcase/)に永続キャッシュされ、 元画像・注釈が変わらない限りクリーンビルドを跨いで再合成しない(キーは内容アドレス)。.
Class Method Details
.cache_key(source, block) ⇒ Object
画像内容をキーに含めるため、著者がスクリーンショットを撮り直せば再生成される。 v2: ラベル書体をキーに含める。SVG はサブセットを自身に抱くので、著者が typography.heading.font を変えたら作り直さないと古い書体のまま残る (mermaid 側は最初から font_family をキーに含めていた)。
189 190 191 192 193 194 195 196 197 198 |
# File 'lib/vivlio_starter/cli/pre_process/showcase_transformer.rb', line 189 def cache_key(source, block) payload = [ 'v2', Digest::SHA256.file(source).hexdigest, block.crop.join(','), JSON.generate(block.annotations.map(&:to_h)), SvgFontEmbedder.configured_heading_font ].join('|') Digest::SHA256.hexdigest(payload)[0, 16] end |
.cached_raster_ext(key) ⇒ Object
永続キャッシュに残るラスターの拡張子(png/jpg)。無ければ nil(=形式判定から決める)。
181 182 183 |
# File 'lib/vivlio_starter/cli/pre_process/showcase_transformer.rb', line 181 def cached_raster_ext(key) %w[png jpg].find { File.exist?(File.join(GeneratedAssetCache.dir(REL_BASE), "#{key}.#{it}")) } end |
.compose(image, lines, chapter_slug:, source_filename:, tools:) ⇒ Array(ShowcaseSvgBuilder::ShowcaseBlock, String, String)?
合成 SVG とラスターを(必要なら)生成し、参照パスを返す。縮退すべき場合は nil。
111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 |
# File 'lib/vivlio_starter/cli/pre_process/showcase_transformer.rb', line 111 def compose(image, lines, chapter_slug:, source_filename:, tools:) source = resolve_image_file(image.path, source_filename) return nil unless source unless tools.available? warn_tools_missing(source_filename) return nil end dims = tools.image_dimensions(source) unless dims warn_tools_missing(source_filename) return nil end orig_w, orig_h = dims block = ShowcaseSvgBuilder.parse(lines, orig_w:, orig_h:, on_warn: ->(line) { warn_unparsable(line, source_filename) }) key = cache_key(source, block) out_dir = File.join(Common::BUILD_HTML_DIR, 'images', REL_BASE, chapter_slug) # 元画像の素性でラスター形式を選ぶ(冒頭コメント参照)。同じ画像なら判定も同じに # なるため、キャッシュキーに形式を含める必要はない——ゆえに永続キャッシュに残る # 拡張子をそのまま信じてよく、ヒット時は magick identify(0.2〜0.35 秒)ごと省ける。 ext = cached_raster_ext(key) || (tools.photographic?(source) ? 'jpg' : 'png') return nil unless write_assets!(block, key, out_dir, ext, source:, orig_w:, orig_h:, tools:) # <img> の参照は消費者 dir 相対(asset_prefix 無し)。数式 SVG と同じ理由で、 # ビルド生成物は workspace 内実体のため EPUB の prefix 剥がしを素通りし、 # PDF は pdf/ ミラーで解決する(math_transformer.rb P4b §2.1 コメント参照)。 rel_dir = "images/#{REL_BASE}/#{chapter_slug}" [block, "#{rel_dir}/#{key}.svg", "#{rel_dir}/#{key}.#{ext}"] end |
.default_tools ⇒ Object
既定の外部ツール(magick + rsvg-convert)。
280 |
# File 'lib/vivlio_starter/cli/pre_process/showcase_transformer.rb', line 280 def default_tools = (@default_tools ||= ImageTools.new) |
.escape_attr(str) ⇒ Object
238 239 240 |
# File 'lib/vivlio_starter/cli/pre_process/showcase_transformer.rb', line 238 def escape_attr(str) str.to_s.gsub('&', '&').gsub('<', '<').gsub('>', '>').gsub('"', '"') end |
.figure(svg_rel, raster_rel, alt, width) ⇒ Object
置換後の HTML。前後に空行を補い独立段落として組ませる。 ラスターの参照を data-vs-raster に明示して持たせる——形式が png / jpg のどちらにも なりうるため、EpubBuilder 側で拡張子を推測させない(推測させると --no-clean ビルドで 前回形式の残骸を拾いうる)。EpubBuilder は使用後にこの属性を取り除く。
228 229 230 231 232 233 |
# File 'lib/vivlio_starter/cli/pre_process/showcase_transformer.rb', line 228 def figure(svg_rel, raster_rel, alt, width) "\n\n<figure class=\"vs-showcase\">\n" \ "<img class=\"vs-showcase\" src=\"#{svg_rel}\" data-vs-raster=\"#{raster_rel}\" " \ "alt=\"#{escape_attr(alt)}\" style=\"width: #{escape_attr(width)};\">\n" \ "</figure>\n\n" end |
.label_font_subset(block) ⇒ String?
注釈ラベルに出る字だけへ絞った見出し書体(Bold)を返す。
合成 SVG は <img> から参照される独立文書で、本文 HTML の @font-face が届かない。
素の sans-serif に任せると OS 既定の和文フォントへ落ち、Chromium がそれを
Type 3 で埋め込んでしまう(入稿で不可)。字を SVG 自身に持たせて断ち切る。
サブセットなので 8 文字で 3.3KB 程度、SVG は実質太らない。
経緯と実測は type3-font-embedding-notes.md。
フォントが見つからない・解析できない場合は nil を返し、SVG は従来どおり sans-serif で組まれる(Type 3 は残るが、ビルドは止めない)。
175 176 177 178 |
# File 'lib/vivlio_starter/cli/pre_process/showcase_transformer.rb', line 175 def label_font_subset(block) SvgFontEmbedder.subset(ShowcaseSvgBuilder.label_characters(block), SvgFontEmbedder.heading_font_path) end |
.plain_image(image) ⇒ Object
縮退時の出力。注釈を捨て、画像行だけを通常の画像記法として残す。
236 |
# File 'lib/vivlio_starter/cli/pre_process/showcase_transformer.rb', line 236 def plain_image(image) = "\n\n\n\n" |
.raster_width(block, orig_w:, orig_h:) ⇒ Object
200 201 202 203 |
# File 'lib/vivlio_starter/cli/pre_process/showcase_transformer.rb', line 200 def raster_width(block, orig_w:, orig_h:) width, = ShowcaseSvgBuilder.cropped_size(block, orig_w:, orig_h:) [(width * RASTER_SCALE).round, RASTER_MAX_WIDTH].min end |
.render_block(lines, chapter_slug:, source_filename:, tools:) ⇒ Object
ブロック 1 つ分を HTML(または縮退した画像記法)へ変換する。
93 94 95 96 97 98 99 100 101 102 103 104 105 106 |
# File 'lib/vivlio_starter/cli/pre_process/showcase_transformer.rb', line 93 def render_block(lines, chapter_slug:, source_filename:, tools:) image = ShowcaseSvgBuilder.scan_image(lines) unless image warn_no_image(source_filename) return '' end warn_extra_images(source_filename) if ShowcaseSvgBuilder.image_line_count(lines) > 1 composed = compose(image, lines, chapter_slug:, source_filename:, tools:) return plain_image(image) unless composed block, svg_rel, raster_rel = composed figure(svg_rel, raster_rel, ShowcaseSvgBuilder.alt_text(block), block.width) end |
.resolve_image_file(src, source_filename) ⇒ Object
正規化済みの画像参照(asset_prefix + images/…)から実ファイルパスを解決する。 ImagePathNormalizer が既に .webp 寄せ・存在チェックを済ませているため、 ここでは prefix を剥がして Common::IMAGES_DIR 基準で引き当てるだけでよい。
208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 |
# File 'lib/vivlio_starter/cli/pre_process/showcase_transformer.rb', line 208 def resolve_image_file(src, source_filename) # 画像不在時に normalizer が差し込む data URI プレースホルダ(既に 🔴 で報告済み) return nil if src.start_with?('data:') if src.start_with?('http://', 'https://') warn_remote_image(source_filename, src) return nil end rel = src.delete_prefix(Common.asset_prefix) return nil unless rel.start_with?('images/') path = File.(rel.delete_prefix('images/'), Common::IMAGES_DIR) File.exist?(path) ? path : nil end |
.transform(content, chapter_slug:, source_filename:, tools: default_tools) ⇒ String
本文中の showcase ブロックをすべて
コードスパンを退避してから走査する——記法そのものを解説する原稿(拡張記法 リファレンス)では ```markdown フェンスの中に showcase ブロックの書き方の例が 書かれており、退避しないと作例が変換に食われて消える。
82 83 84 85 86 87 88 89 90 |
# File 'lib/vivlio_starter/cli/pre_process/showcase_transformer.rb', line 82 def transform(content, chapter_slug:, source_filename:, tools: default_tools) return content unless content.match?(BLOCK_PATTERN) text, spans = MarkdownUtils.extract_code_spans(content) text = text.gsub(BLOCK_PATTERN) do render_block(::Regexp.last_match(1).lines, chapter_slug:, source_filename:, tools:) end MarkdownUtils.restore_code_spans(text, spans) end |
.warn_extra_images(source_filename) ⇒ Object
251 252 253 254 255 256 |
# File 'lib/vivlio_starter/cli/pre_process/showcase_transformer.rb', line 251 def warn_extra_images(source_filename) Common.log_warn( "[showcase] #{source_filename}: showcase ブロック内に画像が 2 枚あります。2 枚目以降は無視します", detail: '→ 1 ブロック 1 画像です。別の :::{.showcase} ブロックに分けてください' ) end |
.warn_no_image(source_filename) ⇒ Object
--- 警告(§7.10: 修正例と出現位置を必ず添える) ---
244 245 246 247 248 249 |
# File 'lib/vivlio_starter/cli/pre_process/showcase_transformer.rb', line 244 def warn_no_image(source_filename) Common.log_warn( "[showcase] #{source_filename}: showcase ブロックに画像がありません。ブロックを出力しません", detail: '→ 先頭に  を置いてください' ) end |
.warn_remote_image(source_filename, src) ⇒ Object
272 273 274 275 276 277 |
# File 'lib/vivlio_starter/cli/pre_process/showcase_transformer.rb', line 272 def warn_remote_image(source_filename, src) Common.log_warn( "[showcase] #{source_filename}: 外部 URL の画像(#{src})は注釈できません。通常の画像として出力します", detail: '→ 画像を images/ 配下へ置き  と書いてください' ) end |
.warn_tools_missing(source_filename) ⇒ Object
258 259 260 261 262 263 |
# File 'lib/vivlio_starter/cli/pre_process/showcase_transformer.rb', line 258 def warn_tools_missing(source_filename) Common.log_warn( "[showcase] #{source_filename}: ImageMagick / librsvg が見つからないため注釈なしの画像に縮退します", detail: '→ `vs doctor --fix` で導入できます(brew install imagemagick librsvg)' ) end |
.warn_unparsable(line, source_filename) ⇒ Object
265 266 267 268 269 270 |
# File 'lib/vivlio_starter/cli/pre_process/showcase_transformer.rb', line 265 def warn_unparsable(line, source_filename) Common.log_warn( %([showcase] #{source_filename} 内の記法を解釈できません: "#{line}"), detail: '→ 座標はカンマ区切りです。例: rect:1 190, 30, 360, 90 {pos=right} コメント' ) end |
.write_assets!(block, key, out_dir, ext, source:, orig_w:, orig_h:, tools:) ⇒ Boolean
SVG とラスターを対でワークスペースへ用意する。合成は magick+rsvg で 1 枚 0.5〜1 秒級 のため、生成物は GeneratedAssetCache(.cache/vs/showcase/)に永続キャッシュされ、 元画像・注釈が変わらない限りクリーンビルドを跨いで再合成しない(キーは内容アドレス)。
148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 |
# File 'lib/vivlio_starter/cli/pre_process/showcase_transformer.rb', line 148 def write_assets!(block, key, out_dir, ext, source:, orig_w:, orig_h:, tools:) GeneratedAssetCache.fetch(REL_BASE, ["#{key}.svg", "#{key}.#{ext}"], out_dir:) do |cache_dir| data_uri = tools.data_uri(source) next false unless data_uri svg = ShowcaseSvgBuilder.build(block, orig_w:, orig_h:, data_uri:, font_data: label_font_subset(block)) raster = tools.rasterize(svg, raster_width(block, orig_w:, orig_h:), format: ext.to_sym) next false unless raster File.write(File.join(cache_dir, "#{key}.svg"), svg, encoding: 'utf-8') File.binwrite(File.join(cache_dir, "#{key}.#{ext}"), raster) true end end |