#!/usr/bin/env python3 """scripts/inline_images.py — base64-embed all relative-path images in an HTML file so it becomes self-contained (redistributable without the assets/ folder). Usage: python scripts/inline_images.py Stdlib only (base64, re, mimetypes, sys, pathlib). """ from __future__ import annotations import base64 import mimetypes import re import sys from pathlib import Path IMG_SRC_RE = re.compile( r'(]*\bsrc=")(assets/[^"]+)("[^>]*>)', re.IGNORECASE, ) def _mime_for(path: Path) -> str: ext = path.suffix.lower() if ext == ".svg": return "image/svg+xml" guessed, _ = mimetypes.guess_type(str(path)) return guessed or "application/octet-stream" def inline(html_path: Path) -> int: html = html_path.read_text(encoding="utf-8") repo_root = html_path.parent.parent.parent count = 0 def replacer(match: re.Match[str]) -> str: nonlocal count prefix, rel_src, suffix = match.group(1), match.group(2), match.group(3) img_path = html_path.parent / rel_src if not img_path.exists(): print(f" WARNING: image not found: {rel_src}", file=sys.stderr) return match.group(0) mime = _mime_for(img_path) data = base64.b64encode(img_path.read_bytes()).decode("ascii") count += 1 return f'{prefix}data:{mime};base64,{data}{suffix}' new_html = IMG_SRC_RE.sub(replacer, html) if count > 0: html_path.write_text(new_html, encoding="utf-8") return count def main() -> int: if len(sys.argv) != 2: print("Usage: python scripts/inline_images.py ", file=sys.stderr) return 1 html_path = Path(sys.argv[1]) if not html_path.exists(): print(f"ERROR: {html_path} not found", file=sys.stderr) return 1 count = inline(html_path) print(f"Inlined {count} image(s) into {html_path}", file=sys.stderr) return 0 if __name__ == "__main__": raise SystemExit(main())