Module: VivlioStarter::CLI::PreProcessCommands::ImagePathNormalizer
- Defined in:
- lib/vivlio_starter/cli/pre_process/image_path_normalizer.rb
Overview
画像パス正規化・プレースホルダー生成モジュール
Constant Summary collapse
- NO_IMAGE_PLACEHOLDER_SVG =
<<~SVG.freeze <svg width="600" height="400" viewBox="0 0 600 400" fill="none" xmlns="http://www.w3.org/2000/svg"> <defs> <linearGradient id="vivlioTextGradient" x1="0%" y1="0%" x2="100%" y2="0%"> <stop offset="0%" style="stop-color:#4a86e8;stop-opacity:1" /> <stop offset="100%" style="stop-color:#1c4587;stop-opacity:1" /> </linearGradient> <linearGradient id="starterTextGradient" x1="0%" y1="0%" x2="100%" y2="0%"> <stop offset="0%" style="stop-color:#6aa84f;stop-opacity:1" /> <stop offset="100%" style="stop-color:#38761d;stop-opacity:1" /> </linearGradient> <linearGradient id="backgroundGradient" x1="0%" y1="0%" x2="0%" y2="100%"> <stop offset="0%" style="stop-color:#E0F2F7;stop-opacity:1" /> <stop offset="100%" style="stop-color:#E8F5E9;stop-opacity:1" /> </linearGradient> </defs> <rect x="0" y="0" width="600" height="400" fill="url(#backgroundGradient)" /> <text#{' '} x="300"#{' '} y="140"#{' '} font-family="Arial, sans-serif"#{' '} font-size="72"#{' '} font-weight="bold" text-anchor="middle" dominant-baseline="middle" > <tspan fill="url(#vivlioTextGradient)">filename.webp</tspan> </text> <text#{' '} x="300"#{' '} y="260"#{' '} font-family="Arial, sans-serif"#{' '} font-size="72"#{' '} font-weight="bold" text-anchor="middle" dominant-baseline="middle" > <tspan fill="url(#starterTextGradient)">No Image</tspan> </text> </svg> SVG
- FALLBACK_EXTENSIONS =
拡張子違いを探すときの順。png / jpg を webp より先に見る——同名の原本が 残っていれば、そちらから派生を作るほうが劣化が一段で済む (
EpubBuilder#transcode_source_forと同じ流儀)。 %w[.png .jpg .jpeg .webp].freeze
Class Method Summary collapse
-
.build_source_image_line_map(source_path) ⇒ Object
元ファイルから画像名 → 行番号のマップを構築する。 pre_process で行数が変わる前の正しい行番号を取得するため。 コード領域の除外は Masking(唯一の実装)に委ね、地の文の画像記法のみ拾う。.
-
.fix_image_paths(content, filename, source_path: nil) ⇒ Object
Markdown 内の画像リンクを生成規約に合わせて正規化する コードブロック・インラインコード内は MarkdownUtils.extract_code_spans で退避し、 変換対象から除外する。.
-
.image_exists_for?(normalized_path) ⇒ Boolean
画像ディレクトリ内の拡張子違いを含めて存在を確認する.
-
.image_file_exists?(path) ⇒ Boolean
images/ 基準で実ファイルがあるか。.
-
.placeholder_image_path(missing_image_path = nil) ⇒ Object
プレースホルダーSVGを使用してデータURIを生成する.
-
.resolve_existing_image(normalized_path) ⇒ Object
原稿の参照を、実在するファイルへ解決する(見つからなければ nil)。.
-
.resolved_placeholder_or_path(alt_text, normalized_path, source_filename = nil, line_number = nil, original_image_name = nil) ⇒ Object
既存画像なら正規化パス(ワークスペース相対 = asset_prefix 前置)を、 無い場合はプレースホルダー(data URI・prefix 不要)を返す。 存在確認とログはルート基準の images/...
-
.sanitize_placeholder_text(filename) ⇒ Object
プレースホルダーに差し込むファイル名をサニタイズする.
-
.svg_to_data_uri(svg_content) ⇒ Object
SVGコンテンツをURLエンコードした data URI に変換する.
Class Method Details
.build_source_image_line_map(source_path) ⇒ Object
元ファイルから画像名 → 行番号のマップを構築する。 pre_process で行数が変わる前の正しい行番号を取得するため。 コード領域の除外は Masking(唯一の実装)に委ね、地の文の画像記法のみ拾う。
158 159 160 161 162 163 164 165 166 167 168 169 170 |
# File 'lib/vivlio_starter/cli/pre_process/image_path_normalizer.rb', line 158 def build_source_image_line_map(source_path) return {} unless source_path && File.exist?(source_path) map = {} source = File.read(source_path, encoding: 'utf-8') Masking.each_prose_line(source) do |line, lineno| line.scan(/!\[[^\]]*\]\(([^)]+)\)/) do image_name = File.basename(::Regexp.last_match(1)) map[image_name] ||= lineno end end map end |
.fix_image_paths(content, filename, source_path: nil) ⇒ Object
Markdown 内の画像リンクを生成規約に合わせて正規化する コードブロック・インラインコード内は MarkdownUtils.extract_code_spans で退避し、 変換対象から除外する。
80 81 82 83 84 85 86 87 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 |
# File 'lib/vivlio_starter/cli/pre_process/image_path_normalizer.rb', line 80 def fix_image_paths(content, filename, source_path: nil) chapter_dir = filename.sub(/\.md$/, '') # 元ファイルの行番号マップを構築(画像名 → 元ファイルでの行番号) source_line_map = build_source_image_line_map(source_path) # コードブロック・インラインコードを退避して変換対象から除外する protected_text, spans = MarkdownUtils.extract_code_spans(content) # 行番号を保持しながら画像記法を正規化する # プレースホルダーは改行を含まないため、行番号は元のコンテンツと一致する lines = protected_text.lines transformed_lines = lines.each_with_index.map do |line, idx| line_number = idx + 1 line.gsub(%r{!\[(.*?)\]\((?!https?://)([^)]+)\)}) do alt_text = ::Regexp.last_match(1) image_path = ::Regexp.last_match(2) # ワークスペース相対(asset_prefix 付き)で既に正規化済みなら再変換しない(冪等) prefix = Common.asset_prefix if !prefix.empty? && image_path.start_with?(prefix) "" elsif image_path.start_with?('images/data/') # DataImageResolver が確定させたビルド生成物参照(spec §3.4 の無 prefix 形)。 # 実体はワークスペース html/images/data/ にあり、ルート images/ には無い。 # asset_prefix 前置・.webp 寄せ・存在チェックのいずれも行わず素通しする。 "" else # すでに images/ から始まる場合はそのまま。相対パスは images/<章ディレクトリ>/ に正規化 normalized = if image_path.start_with?('images/') image_path else "images/#{chapter_dir}/#{image_path}" end original_image_name = File.basename(image_path) # 元ファイルの行番号があればそちらを使う source_ln = source_line_map[original_image_name] || line_number resolved_placeholder_or_path(alt_text, normalized, filename, source_ln, original_image_name) end end end # 退避したコードブロック・インラインコードを復元する MarkdownUtils.restore_code_spans(transformed_lines.join, spans) end |
.image_exists_for?(normalized_path) ⇒ Boolean
画像ディレクトリ内の拡張子違いを含めて存在を確認する
207 208 209 |
# File 'lib/vivlio_starter/cli/pre_process/image_path_normalizer.rb', line 207 def image_exists_for?(normalized_path) !resolve_existing_image(normalized_path).nil? end |
.image_file_exists?(path) ⇒ Boolean
images/ 基準で実ファイルがあるか。
201 202 203 204 |
# File 'lib/vivlio_starter/cli/pre_process/image_path_normalizer.rb', line 201 def image_file_exists?(path) relative_path = path.sub(%r{\Aimages/}, '') File.file?(File.(relative_path, Common::IMAGES_DIR)) end |
.placeholder_image_path(missing_image_path = nil) ⇒ Object
プレースホルダーSVGを使用してデータURIを生成する
212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 |
# File 'lib/vivlio_starter/cli/pre_process/image_path_normalizer.rb', line 212 def placeholder_image_path(missing_image_path = nil) unless missing_image_path return 'data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%2F%3E' end begin filename = File.basename(missing_image_path) replacement = sanitize_placeholder_text(filename) svg_with_filename = NO_IMAGE_PLACEHOLDER_SVG.gsub('filename.webp', replacement) svg_to_data_uri(svg_with_filename) rescue StandardError => e Common.log_warn("プレースホルダー画像の生成に失敗しました: #{e.class}: #{e.}") 'data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%2F%3E' end end |
.resolve_existing_image(normalized_path) ⇒ Object
原稿の参照を、実在するファイルへ解決する(見つからなければ nil)。
書かれた拡張子が最優先。 それが無いときだけ拡張子違いを探す——著者が
myawesome.jpg を置いて  と書いても拾えるようにするためで、
形式は本仕様が機械的に決めるもので著者の関心事ではない
(image-format-per-target-spec.md §3.1)。
かつては .png / .jpg を無条件で .webp へ読み替えていた。存在しない webp を
指す HTML が出来上がり、vs build の Step 1 が vs resize を呼んで webp を作る
ことでかろうじて成立していたが、素材を書き換えずに派生で解く方式にした以上、
その前提は要らない。
188 189 190 191 192 193 194 195 196 197 198 |
# File 'lib/vivlio_starter/cli/pre_process/image_path_normalizer.rb', line 188 def resolve_existing_image(normalized_path) return normalized_path if image_file_exists?(normalized_path) return nil if normalized_path.end_with?('.svg') stem = normalized_path.sub(/\.[^.]+\z/, '') FALLBACK_EXTENSIONS.each do |ext| candidate = "#{stem}#{ext}" return candidate if image_file_exists?(candidate) end nil end |
.resolved_placeholder_or_path(alt_text, normalized_path, source_filename = nil, line_number = nil, original_image_name = nil) ⇒ Object
既存画像なら正規化パス(ワークスペース相対 = asset_prefix 前置)を、 無い場合はプレースホルダー(data URI・prefix 不要)を返す。 存在確認とログはルート基準の images/... パスで行う(著者に分かる表記のため)。
131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 |
# File 'lib/vivlio_starter/cli/pre_process/image_path_normalizer.rb', line 131 def resolved_placeholder_or_path(alt_text, normalized_path, source_filename = nil, line_number = nil, original_image_name = nil) resolved = resolve_existing_image(normalized_path) return "" if resolved image_name = original_image_name || File.basename(normalized_path) if source_filename && line_number Common.log_error( "#{source_filename}:#{line_number} - 画像 '#{image_name}' が見つかりません(代替画像を使用します)", detail: "画像の場所: #{normalized_path}" ) else # 位置情報が取れない場合のフォールバック Common.log_error( "画像 '#{image_name}' が見つかりません(代替画像を使用します)", detail: "画像の場所: #{normalized_path}" ) end placeholder_path = placeholder_image_path(normalized_path) "" end |
.sanitize_placeholder_text(filename) ⇒ Object
プレースホルダーに差し込むファイル名をサニタイズする
229 230 231 232 233 |
# File 'lib/vivlio_starter/cli/pre_process/image_path_normalizer.rb', line 229 def sanitize_placeholder_text(filename) text = filename.to_s.strip text = 'missing image' if text.empty? CGI.escapeHTML(text) end |
.svg_to_data_uri(svg_content) ⇒ Object
SVGコンテンツをURLエンコードした data URI に変換する
236 237 238 239 240 |
# File 'lib/vivlio_starter/cli/pre_process/image_path_normalizer.rb', line 236 def svg_to_data_uri(svg_content) escaped = CGI.escape(svg_content.encode('utf-8')) escaped = escaped.gsub('+', '%20') "data:image/svg+xml;charset=utf-8,#{escaped}" end |