Module: VivlioStarter::CLI::PreProcessCommands::ThemeImageResolver
- Defined in:
- lib/vivlio_starter/cli/pre_process/theme_image_resolver.rb
Overview
テーマ画像パス解決モジュール
Constant Summary collapse
- THEME_IMAGE_EXTENSIONS =
%w[.webp .png .jpg .jpeg].freeze
- FALLBACK_THEME_IMAGE_SLUG =
扉絵・飾り画像の既定画像スラッグ。未指定時も無効な指定時もこの画像に寄せる。 (無効な color が yellow へフォールバックするのと揃えた挙動。バンドルの桜を使う)
'sakura'- FRONTISPIECE_DEFAULT_PATH =
万一バリアント解決に失敗したときの最終フォールバックパス(通常は到達しない)。 バリアントは生成キャッシュに出るため theme-images/ 形で指す(移設仕様 §3.1)。
'theme-images/bundled/sakura_portrait.webp'- ORNAMENT_DEFAULT_PATH =
'theme-images/bundled/sakura_landscape.webp'- DEFAULT_PAGE_WIDTH_MM =
210.0- DEFAULT_PAGE_HEIGHT_MM =
297.0- MIN_BINDING_RATIO =
1.35- MAX_BINDING_RATIO =
2.2- FRONTISPIECE_RATIO_TOLERANCE =
0.05- FRONTISPIECE_PLACEHOLDER_SVG =
<<~SVG <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 210 297" width="210" height="297"> <rect width="210" height="297" fill="#e3e3e3"/> <text x="105" y="150" font-family="monospace" font-size="14" fill="#666" text-anchor="middle">filename.webp</text> </svg> SVG
- ORNAMENT_PLACEHOLDER_SVG =
<<~SVG <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 297 210" width="297" height="210"> <rect width="297" height="210" fill="#e3e3e3"/> <text x="148.5" y="110" font-family="monospace" font-size="14" fill="#666" text-anchor="middle">filename.webp</text> </svg> SVG
Class Method Summary collapse
- .binding_safe_portrait_ratio ⇒ Object
-
.find_cached_theme_variant(base_slug, variant) ⇒ Object
生成キャッシュ内のバリアントを探索する。 ImageGenerator は images root からの相対サブパスを保って生成するため、 bundled 由来は theme-images/bundled/ に、ユーザー画像由来はルート直下に出る。.
-
.find_existing_theme_image(slug, location_order: %i[user bundled],, allowed_extensions: THEME_IMAGE_EXTENSIONS) ⇒ Object
既存テーマ画像を探索.
-
.find_existing_theme_variant(base_slug, variant) ⇒ Object
バリアント画像を探索。 ユーザーが stylesheets/images/ に手置きしたバリアント(意図的な上書き)を優先し、 次に生成キャッシュ(.cache/vs/theme-images/。ImageGenerator の出力先)を見る。.
- .frontispiece_allowed_ratios ⇒ Object
-
.image_ratio(path) ⇒ Object
画像のアスペクト比を取得.
-
.normalize_theme_image_slug(value) ⇒ Object
テーマ画像スラッグを正規化.
-
.placeholder_uri(base_slug, placeholder_svg) ⇒ Object
プレースホルダーURIを生成.
-
.ratio_accepted_for_frontispiece?(ratio) ⇒ Boolean
frontispiece 用の許容アスペクト比かチェック.
-
.resolve_frontispiece_path(raw, allow_generation: false) ⇒ Object
frontispiece (扉絵) の解決(未指定時は既定画像 sakura を生成して使う).
-
.resolve_image_path(raw, default_when_nil:, downcase_if: nil) ⇒ Object
汎用: 画像ライクな指定を解決して CSS 用相対パス/URL を返す.
-
.resolve_ornament_path(raw, allow_generation: false) ⇒ Object
ornament (装飾画像) の解決(未指定時は既定画像 sakura を生成して使う).
-
.resolve_theme_image_path(raw, variant:, default_path:, placeholder_svg:, allow_generation: false, slug_transform: nil, fallback_slug: nil) ⇒ Object
テーマ画像パスの解決.
-
.split_slug_and_variant(slug) ⇒ Object
スラッグをベース名、バリアント、拡張子に分割.
-
.svg_placeholder_uri(svg_template, filename) ⇒ Object
SVGプレースホルダーをdata URIに変換.
-
.svg_to_data_uri(svg_content) ⇒ Object
SVGをdata URIに変換.
-
.theme_image_available?(raw, variant:) ⇒ Boolean
指定されたテーマ画像名が実在する(またはバリアント生成の元になる画像が存在する)かを返す。 resolve_* が allow_generation: true でプレースホルダーではなく実画像を返せるかと一致する。 未指定・URL/url() 指定は検証対象外として true(既定画像・外部指定のため)。.
-
.theme_images_cache_root ⇒ Object
生成バリアントのキャッシュルート(.cache/vs/theme-images/)。 theme_images_root と同様に ivar でメモ化し、テストが一時 dir へ差し替えられるようにする。.
-
.theme_images_root ⇒ Object
テーマ画像のルートディレクトリ(ソース置き場: stylesheets/images/).
-
.theme_relative_path(path) ⇒ Object
テーマ画像の CSS 用相対パスを取得(返却 2 形はファイル冒頭コメント参照).
Class Method Details
.binding_safe_portrait_ratio ⇒ Object
352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 |
# File 'lib/vivlio_starter/cli/pre_process/theme_image_resolver.rb', line 352 def binding_safe_portrait_ratio # page の版面キー(width 等)は page_presets 由来で存在保証がないため [] で参照する page_cfg = Common::CONFIG.page width_mm = Units.length_to_mm(page_cfg[:width]) || DEFAULT_PAGE_WIDTH_MM height_mm = Units.length_to_mm(page_cfg[:height]) || DEFAULT_PAGE_HEIGHT_MM margin_inner_mm = Units.length_to_mm(page_cfg[:margin_inner]) || 0 margin_outer_mm = Units.length_to_mm(page_cfg[:margin_outer]) || 0 binding_delta = [margin_inner_mm - margin_outer_mm, 0].max effective_width = width_mm - binding_delta effective_width = width_mm * 0.4 if effective_width <= width_mm * 0.4 ratio = height_mm / [effective_width, 1.0].max ratio.clamp(MIN_BINDING_RATIO, MAX_BINDING_RATIO) rescue StandardError 1.414 end |
.find_cached_theme_variant(base_slug, variant) ⇒ Object
生成キャッシュ内のバリアントを探索する。 ImageGenerator は images root からの相対サブパスを保って生成するため、 bundled 由来は theme-images/bundled/ に、ユーザー画像由来はルート直下に出る。
254 255 256 257 258 259 260 261 |
# File 'lib/vivlio_starter/cli/pre_process/theme_image_resolver.rb', line 254 def find_cached_theme_variant(base_slug, variant) %W[#{base_slug}_#{variant}.webp bundled/#{File.basename(base_slug)}_#{variant}.webp].uniq.each do |candidate| path = File.join(theme_images_cache_root, candidate) return path if File.exist?(path) end nil end |
.find_existing_theme_image(slug, location_order: %i[user bundled],, allowed_extensions: THEME_IMAGE_EXTENSIONS) ⇒ Object
既存テーマ画像を探索
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/pre_process/theme_image_resolver.rb', line 283 def find_existing_theme_image(slug, location_order: %i[user bundled], allowed_extensions: THEME_IMAGE_EXTENSIONS) base = normalize_theme_image_slug(slug) ext = File.extname(base) stem = ext.empty? ? base : base.sub(/\.[^.]+\z/, '') candidates = if ext.empty? allowed_extensions.map { |e| "#{stem}#{e}" } else ["#{stem}#{ext}"] end location_order.each do |loc| dir = loc == :user ? theme_images_root : File.join(theme_images_root, 'bundled') candidates.each do |candidate| path = File.join(dir, candidate) return path if File.exist?(path) end end nil end |
.find_existing_theme_variant(base_slug, variant) ⇒ Object
バリアント画像を探索。 ユーザーが stylesheets/images/ に手置きしたバリアント(意図的な上書き)を優先し、 次に生成キャッシュ(.cache/vs/theme-images/。ImageGenerator の出力先)を見る。
245 246 247 248 249 |
# File 'lib/vivlio_starter/cli/pre_process/theme_image_resolver.rb', line 245 def find_existing_theme_variant(base_slug, variant) find_existing_theme_image("#{base_slug}_#{variant}", location_order: %i[user bundled], allowed_extensions: ['.webp']) || find_cached_theme_variant(base_slug, variant) end |
.frontispiece_allowed_ratios ⇒ Object
348 349 350 |
# File 'lib/vivlio_starter/cli/pre_process/theme_image_resolver.rb', line 348 def frontispiece_allowed_ratios [binding_safe_portrait_ratio, 1.414].uniq end |
.image_ratio(path) ⇒ Object
画像のアスペクト比を取得
325 326 327 328 329 330 331 332 333 334 335 336 337 |
# File 'lib/vivlio_starter/cli/pre_process/theme_image_resolver.rb', line 325 def image_ratio(path) out, status = Open3.capture2('magick', 'identify', '-format', '%w %h', path) return nil unless status.success? width_str, height_str = out.strip.split width = width_str.to_f height = height_str.to_f return nil if width <= 0 || height <= 0 height / width rescue StandardError nil end |
.normalize_theme_image_slug(value) ⇒ Object
テーマ画像スラッグを正規化
264 265 266 |
# File 'lib/vivlio_starter/cli/pre_process/theme_image_resolver.rb', line 264 def normalize_theme_image_slug(value) value.to_s.strip.sub(%r{\Aimages/}, '').sub(%r{\A/+}, '') end |
.placeholder_uri(base_slug, placeholder_svg) ⇒ Object
プレースホルダーURIを生成
371 372 373 374 375 |
# File 'lib/vivlio_starter/cli/pre_process/theme_image_resolver.rb', line 371 def placeholder_uri(base_slug, placeholder_svg) base_name = base_slug.to_s.strip.empty? ? 'missing' : File.basename(base_slug) filename = "#{base_name}.webp" svg_placeholder_uri(placeholder_svg, filename) end |
.ratio_accepted_for_frontispiece?(ratio) ⇒ Boolean
frontispiece 用の許容アスペクト比かチェック
340 341 342 343 344 345 346 |
# File 'lib/vivlio_starter/cli/pre_process/theme_image_resolver.rb', line 340 def ratio_accepted_for_frontispiece?(ratio) frontispiece_allowed_ratios.any? do |allowed| next false if allowed.zero? ((ratio - allowed).abs / allowed) <= FRONTISPIECE_RATIO_TOLERANCE end end |
.resolve_frontispiece_path(raw, allow_generation: false) ⇒ Object
frontispiece (扉絵) の解決(未指定時は既定画像 sakura を生成して使う)
72 73 74 75 76 77 78 79 80 81 82 83 84 85 |
# File 'lib/vivlio_starter/cli/pre_process/theme_image_resolver.rb', line 72 def resolve_frontispiece_path(raw, allow_generation: false) source = raw.nil? || raw.to_s.strip.empty? ? FALLBACK_THEME_IMAGE_SLUG : raw resolve_theme_image_path( source, variant: :portrait, default_path: FRONTISPIECE_DEFAULT_PATH, placeholder_svg: FRONTISPIECE_PLACEHOLDER_SVG, allow_generation: allow_generation, fallback_slug: FALLBACK_THEME_IMAGE_SLUG, slug_transform: lambda do |value| value =~ /^door[1-7](?:_portrait)?(?:\.[^.]+)?$/i ? value.downcase : value end ) end |
.resolve_image_path(raw, default_when_nil:, downcase_if: nil) ⇒ Object
汎用: 画像ライクな指定を解決して CSS 用相対パス/URL を返す
133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 |
# File 'lib/vivlio_starter/cli/pre_process/theme_image_resolver.rb', line 133 def resolve_image_path(raw, default_when_nil:, downcase_if: nil) return default_when_nil if raw.nil? || raw.to_s.strip.empty? s = raw.to_s.strip return s if s =~ /^url\(/i || s =~ %r{^https?://}i path = s path = path.downcase if downcase_if && path =~ downcase_if path = "images/#{path}" unless path.include?('/') styles_dir = Common::STYLESHEETS_DIR abs_path = File.join(styles_dir, path) base_noext = File.extname(abs_path).empty? ? abs_path : abs_path.sub(/\.[^.]+\z/, '') webp_abs = "#{base_noext}.webp" unless File.exist?(webp_abs) candidates = ["#{base_noext}.png", "#{base_noext}.jpg", "#{base_noext}.jpeg"] src = candidates.find { |p| File.exist?(p) } if src dir = File.dirname(src) Common.log_action("WebP を生成します: #{File.basename(src)} → #{File.basename(webp_abs)}") system("vs resize:high #{Shellwords.escape(dir)}") end end rel = base_noext.sub(%r{\A#{Regexp.escape(styles_dir)}/}, '') rel += '.webp' rel end |
.resolve_ornament_path(raw, allow_generation: false) ⇒ Object
ornament (装飾画像) の解決(未指定時は既定画像 sakura を生成して使う)
88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 |
# File 'lib/vivlio_starter/cli/pre_process/theme_image_resolver.rb', line 88 def resolve_ornament_path(raw, allow_generation: false) raw = FALLBACK_THEME_IMAGE_SLUG if raw.nil? || raw.to_s.strip.empty? value = raw.to_s.strip return value if value =~ /^url\(/i || value =~ %r{^https?://}i slug_value = value =~ /^frame-[a-z0-9_-]+(?:_landscape)?(?:\.[^.]+)?$/i ? value.downcase : value slug = normalize_theme_image_slug(slug_value) base_slug, requested_variant, ext = split_slug_and_variant(slug) # ornament用に landscape バリアント(2.39:1)を使用 if requested_variant == :landscape if (direct = find_existing_theme_image(slug, location_order: %i[user bundled])) return theme_relative_path(direct) end elsif (variant_specific = find_existing_theme_image("#{base_slug}_landscape", location_order: %i[user bundled])) return theme_relative_path(variant_specific) end base_query = ext.empty? ? base_slug : "#{base_slug}#{ext}" if (direct = find_existing_theme_image(base_query, location_order: %i[user bundled])) if allow_generation require_relative 'image_generator' # ornamentはlandscapeバリアントを生成 if (generated = ImageGenerator.ensure_variant_generated(direct, :landscape)) return theme_relative_path(generated) end end return theme_relative_path(direct) end resolve_theme_image_path( slug, variant: :landscape, default_path: ORNAMENT_DEFAULT_PATH, placeholder_svg: ORNAMENT_PLACEHOLDER_SVG, allow_generation: allow_generation, fallback_slug: FALLBACK_THEME_IMAGE_SLUG ) end |
.resolve_theme_image_path(raw, variant:, default_path:, placeholder_svg:, allow_generation: false, slug_transform: nil, fallback_slug: nil) ⇒ Object
テーマ画像パスの解決
164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 |
# File 'lib/vivlio_starter/cli/pre_process/theme_image_resolver.rb', line 164 def resolve_theme_image_path(raw, variant:, default_path:, placeholder_svg:, allow_generation: false, slug_transform: nil, fallback_slug: nil) return default_path if raw.nil? || raw.to_s.strip.empty? value = raw.to_s.strip return value if value =~ /^url\(/i || value =~ %r{^https?://}i slug_value = slug_transform ? slug_transform.call(value) : value slug = normalize_theme_image_slug(slug_value) base_slug, requested_variant, ext = split_slug_and_variant(slug) if (requested_variant == variant) && (direct = find_existing_theme_image(slug, location_order: %i[user bundled])) return theme_relative_path(direct) end if (variant_specific = find_existing_theme_variant(base_slug, variant)) return theme_relative_path(variant_specific) end base_query = ext.empty? ? base_slug : "#{base_slug}#{ext}" if (user_source = find_existing_theme_image(base_query, location_order: [:user])) ratio = image_ratio(user_source) return theme_relative_path(user_source) if ratio && ratio_accepted_for_frontispiece?(ratio) if allow_generation require_relative 'image_generator' if (generated = ImageGenerator.ensure_variant_generated(user_source, variant)) return theme_relative_path(generated) end end end if allow_generation && (bundled_source = find_existing_theme_image(base_query, location_order: [:bundled], allowed_extensions: ['.webp'])) require_relative 'image_generator' if (generated = ImageGenerator.ensure_variant_generated(bundled_source, variant)) return theme_relative_path(generated) end end # 指定名が解決できない場合は既定画像(sakura 等)へフォールバックする。 # フォールバック自身も無ければ(fallback_slug: nil の再帰)プレースホルダーを返す。 if fallback_slug && normalize_theme_image_slug(fallback_slug) != base_slug fallback = resolve_theme_image_path( fallback_slug, variant: variant, default_path: default_path, placeholder_svg: placeholder_svg, allow_generation: allow_generation ) return fallback unless fallback.start_with?('data:') end placeholder_uri(base_slug, placeholder_svg) end |
.split_slug_and_variant(slug) ⇒ Object
スラッグをベース名、バリアント、拡張子に分割
269 270 271 272 273 274 275 276 277 278 279 280 |
# File 'lib/vivlio_starter/cli/pre_process/theme_image_resolver.rb', line 269 def split_slug_and_variant(slug) ext = File.extname(slug) without_ext = ext.empty? ? slug : slug.sub(/\.[^.]+\z/, '') case without_ext.downcase when /_portrait\z/ [without_ext.sub(/_portrait\z/i, ''), :portrait, ext] when /_landscape\z/ [without_ext.sub(/_landscape\z/i, ''), :landscape, ext] else [without_ext, nil, ext] end end |
.svg_placeholder_uri(svg_template, filename) ⇒ Object
SVGプレースホルダーをdata URIに変換
378 379 380 381 382 383 384 |
# File 'lib/vivlio_starter/cli/pre_process/theme_image_resolver.rb', line 378 def svg_placeholder_uri(svg_template, filename) replaced = svg_template.gsub('filename.webp', CGI.escapeHTML(filename)) svg_to_data_uri(replaced) rescue StandardError => e Common.log_warn("プレースホルダー生成に失敗しました: #{e.}") 'data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%2F%3E' end |
.svg_to_data_uri(svg_content) ⇒ Object
SVGをdata URIに変換
387 388 389 390 391 |
# File 'lib/vivlio_starter/cli/pre_process/theme_image_resolver.rb', line 387 def svg_to_data_uri(svg_content) # シンプルにURL encoding encoded = URI.encode_www_form_component(svg_content) "data:image/svg+xml;charset=utf-8,#{encoded}" end |
.theme_image_available?(raw, variant:) ⇒ Boolean
指定されたテーマ画像名が実在する(またはバリアント生成の元になる画像が存在する)かを返す。 resolve_* が allow_generation: true でプレースホルダーではなく実画像を返せるかと一致する。 未指定・URL/url() 指定は検証対象外として true(既定画像・外部指定のため)。
225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 |
# File 'lib/vivlio_starter/cli/pre_process/theme_image_resolver.rb', line 225 def theme_image_available?(raw, variant:) return true if raw.nil? || raw.to_s.strip.empty? value = raw.to_s.strip return true if value =~ /^url\(/i || value =~ %r{^https?://}i slug = normalize_theme_image_slug(value) base_slug, requested_variant, ext = split_slug_and_variant(slug) base_query = ext.empty? ? base_slug : "#{base_slug}#{ext}" # 既に該当バリアントがある / バリアント名を直接指定している / 生成元の base 画像がある、のいずれか return true if find_existing_theme_variant(base_slug, variant) return true if requested_variant && find_existing_theme_image(slug) !find_existing_theme_image(base_query).nil? end |
.theme_images_cache_root ⇒ Object
生成バリアントのキャッシュルート(.cache/vs/theme-images/)。 theme_images_root と同様に ivar でメモ化し、テストが一時 dir へ差し替えられるようにする。
312 313 314 |
# File 'lib/vivlio_starter/cli/pre_process/theme_image_resolver.rb', line 312 def theme_images_cache_root @theme_images_cache_root ||= Common.theme_images_cache_dir end |
.theme_images_root ⇒ Object
テーマ画像のルートディレクトリ(ソース置き場: stylesheets/images/)
306 307 308 |
# File 'lib/vivlio_starter/cli/pre_process/theme_image_resolver.rb', line 306 def theme_images_root @theme_images_root ||= File.join(Common::STYLESHEETS_DIR, 'images') end |
.theme_relative_path(path) ⇒ Object
テーマ画像の CSS 用相対パスを取得(返却 2 形はファイル冒頭コメント参照)
317 318 319 320 321 322 |
# File 'lib/vivlio_starter/cli/pre_process/theme_image_resolver.rb', line 317 def theme_relative_path(path) cache_prefix = "#{theme_images_cache_root}/" return "theme-images/#{path.delete_prefix(cache_prefix)}" if path.start_with?(cache_prefix) path.sub(%r{\A#{Regexp.escape(theme_images_root)}/}, 'images/') end |