Previously the HTML whitepaper referenced diagrams via relative paths (src="diagrams/X.png"), which 404'd because the docroot does not carry a mirror of whitepaper/diagrams/. The "HTML whitepaper" thus showed broken image stubs instead of the architecture figures. Fix: a post-processor (whitepaper/embed-images.py, stdlib Python) runs after rst2html5. Every <img> with a relative src gets base64-inlined as a data: URI with inferred MIME. Any .svg reference would splice in as an inline <svg> element, surfacing alt text as <title>; the current RST only uses the .png variant of gnu-logo so no SVGs inline for now, but the path works when we switch. Makefile's whitepaper-html target chains the embed step after the existing sed passes for the uncloseai.js script tag and the meaningful <title>. Title tweaked to "feedback as a primitive" to match the homepage tagline. Result: whitepaper/lumbda-whitepaper.html grows from 180 KB to 4.6 MB (base64 inflation on ~2 MB of diagrams), and it now opens offline as a single file — no network fetches for figures, no docroot mirror needed.
95 lines
3 KiB
Python
95 lines
3 KiB
Python
#!/usr/bin/env python3
|
|
# Embed every <img src="..."> in an HTML file as an inline asset so a
|
|
# single-file whitepaper travels without a server. SVGs splice in as
|
|
# inline <svg>. Raster images (png/jpg/etc.) base64-encode into a
|
|
# data: URI. Absolute URLs (http://, https://, data:) pass through.
|
|
#
|
|
# Usage: embed-images.py <html-file>
|
|
|
|
import base64
|
|
import mimetypes
|
|
import os
|
|
import re
|
|
import sys
|
|
|
|
|
|
def _is_absolute(src):
|
|
return bool(re.match(r"^(https?:)?//|^data:", src))
|
|
|
|
|
|
def _svg_inline(path, attrs):
|
|
"""Read a .svg file and return an <svg> element ready to splice in,
|
|
preserving any alt text from the <img> tag via <title>."""
|
|
with open(path, "r") as f:
|
|
svg = f.read()
|
|
# Strip the XML prolog and the <!DOCTYPE svg ...> declaration;
|
|
# keeping them inside an HTML5 document is harmless but messy.
|
|
svg = re.sub(r"^\s*<\?xml[^>]*\?>\s*", "", svg)
|
|
svg = re.sub(r"^\s*<!DOCTYPE[^>]*>\s*", "", svg, flags=re.IGNORECASE)
|
|
# If the original <img> carried alt="..." surface it as <title> so
|
|
# hover-text and a11y tools still get the description.
|
|
alt = attrs.get("alt", "").strip()
|
|
if alt and "<title" not in svg[:512]:
|
|
svg = re.sub(r"(<svg\b[^>]*>)", r"\1<title>" + alt + "</title>", svg, count=1)
|
|
return svg
|
|
|
|
|
|
def _data_uri(path):
|
|
mime, _ = mimetypes.guess_type(path)
|
|
if not mime:
|
|
mime = "application/octet-stream"
|
|
with open(path, "rb") as f:
|
|
payload = base64.b64encode(f.read()).decode()
|
|
return f"data:{mime};base64,{payload}"
|
|
|
|
|
|
_IMG_RE = re.compile(r"<img\b([^>]*)>", re.IGNORECASE)
|
|
_ATTR_RE = re.compile(r'([a-zA-Z_:][a-zA-Z0-9_:.-]*)\s*=\s*"([^"]*)"')
|
|
|
|
|
|
def _parse_attrs(blob):
|
|
return dict(_ATTR_RE.findall(blob))
|
|
|
|
|
|
def _render_attrs(attrs):
|
|
return " " + " ".join(f'{k}="{v}"' for k, v in attrs.items()) if attrs else ""
|
|
|
|
|
|
def embed(html, base_dir):
|
|
def replace(match):
|
|
attr_blob = match.group(1)
|
|
attrs = _parse_attrs(attr_blob)
|
|
src = attrs.get("src", "")
|
|
if not src or _is_absolute(src):
|
|
return match.group(0)
|
|
fs_path = os.path.join(base_dir, src)
|
|
if not os.path.isfile(fs_path):
|
|
sys.stderr.write(f"embed-images: missing {src}\n")
|
|
return match.group(0)
|
|
ext = os.path.splitext(src)[1].lower()
|
|
if ext == ".svg":
|
|
# Inline the SVG in place of the <img>. Drops the img entirely.
|
|
return _svg_inline(fs_path, attrs)
|
|
attrs["src"] = _data_uri(fs_path)
|
|
return f"<img{_render_attrs(attrs)}>"
|
|
|
|
return _IMG_RE.sub(replace, html)
|
|
|
|
|
|
def main(argv):
|
|
if len(argv) != 2:
|
|
sys.stderr.write("usage: embed-images.py <html-file>\n")
|
|
return 2
|
|
path = argv[1]
|
|
base_dir = os.path.dirname(os.path.abspath(path))
|
|
with open(path, "r") as f:
|
|
html = f.read()
|
|
out = embed(html, base_dir)
|
|
with open(path, "w") as f:
|
|
f.write(out)
|
|
print(f"embed-images: rewrote {path} ({len(out)} bytes)")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main(sys.argv))
|