docs(P3a): inline images for self-contained HTML (REQ-268)
New scripts/inline_images.py (stdlib only: base64, re, mimetypes) — base64-embeds all relative-path <img src='assets/...'> images into the rendered HTML so it's redistributable without the assets/ folder. MIME-sniffs by extension (.png->image/png, .svg->image/svg+xml, etc). render_slides.sh Step 3 invokes it after the MARP HTML render, before staging. Verified: 2 images inlined, 0 file-path refs remaining. ---ci--- project: acdl phase: 3 milestone: v1.23 status: execute phase_role: execution ---/ci---
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
#!/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 <html-path>
|
||||
|
||||
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'(<img\b[^>]*\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 <html-path>", 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())
|
||||
Reference in New Issue
Block a user