import argparse import base64 import re import sys from pathlib import Path from PIL import Image, ImageOps ASSET_SOURCES = { "/vulture/vulture_shuttle_full_stack_original.png": "data/KSP/wiki-upload/vulture_shuttle_full_stack_at_launch_pad.jpg", "/vulture/vulture_shuttle_ignition_original.png": "data/KSP/wiki-upload/vulture_shuttle_ignition.jpg", "/vulture/vulture_ascent_original.png": "data/KSP/wiki-upload/vulture_ascent.jpg", "/vulture/vulture_booster_recovery_original.png": "data/KSP/wiki-upload/vulture_booster_recovery.jpg", "/vulture/vulture_booster_separation_original.png": "data/KSP/wiki-upload/vulture_booster_separation.jpg", "/echo/echo_takeoff_side_view.png": "data/KSP/wiki-upload-echo-current/echo_takeoff_side_view.png", "/echo/echo_mk2_cross_section.png": "data/KSP/wiki-upload-echo-current/echo_mk2_cross_section.png", "/echo/echo_vtvl.png": "data/KSP/wiki-upload-echo-current/echo_vtvl.png", "/echo/echo_approach_guanghan_station.png": "data/KSP/wiki-upload-echo-current/echo_approach_guanghan_station.png", "/echo/echo_high_energy_return_braking.png": "data/KSP/wiki-upload-echo-current/echo_high_energy_return_braking.png", "/enterprise/enterprise_takeoff_side_view.png": "data/KSP/wiki-upload-enterprise-current/enterprise_takeoff_side_view.png", "/enterprise/enterprise_payload_bay_view.png": "data/KSP/wiki-upload-enterprise-current/enterprise_payload_bay_view.png", "/enterprise/enterprise_dorsal_airframe_view.png": "data/KSP/wiki-upload-enterprise-current/enterprise_dorsal_airframe_view.png", } HTML_FILES = [ "data/wiki/vulture_shuttle_zh.html", "data/wiki/vulture_shuttle_en.html", "data/wiki/echo_shuttle_zh.html", "data/wiki/echo_shuttle_en.html", "data/wiki/enterprise_shuttle_zh.html", "data/wiki/enterprise_shuttle_en.html", ] IMG_RE = re.compile(r"]*>", re.IGNORECASE) SRC_RE = re.compile(r'src="[^"]*"', re.IGNORECASE) ASSET_RE = re.compile(r'data-wiki-asset="([^"]+)"', re.IGNORECASE) def jpeg_name(asset_path): return asset_path.strip("/").replace("/", "__").rsplit(".", 1)[0] + ".jpg" def write_compressed_jpeg(repo_root, source, target, max_dimension, quality): image = Image.open(repo_root / source) image = ImageOps.exif_transpose(image) image.thumbnail((max_dimension, max_dimension), Image.Resampling.LANCZOS) if image.mode in ("RGBA", "LA"): background = Image.new("RGB", image.size, (255, 255, 255)) background.paste(image, mask=image.getchannel("A")) image = background elif image.mode != "RGB": image = image.convert("RGB") target.parent.mkdir(parents=True, exist_ok=True) image.save(target, format="JPEG", quality=quality, optimize=True, progressive=True) def build_data_uris(repo_root, embed_dir, max_dimension, quality): data_uris = {} for asset_path, source in ASSET_SOURCES.items(): source_path = repo_root / source if not source_path.exists(): raise FileNotFoundError(f"Missing source image for {asset_path}: {source}") target = repo_root / embed_dir / jpeg_name(asset_path) write_compressed_jpeg(repo_root, source, target, max_dimension, quality) encoded = base64.b64encode(target.read_bytes()).decode("ascii") data_uris[asset_path] = f"data:image/jpeg;base64,{encoded}" print(f"embedded_source asset={asset_path} file={target.as_posix()} bytes={target.stat().st_size}") return data_uris def rewrite_img_tag(match, data_uris): tag = match.group(0) asset_match = ASSET_RE.search(tag) if not asset_match: return tag asset_path = asset_match.group(1) data_uri = data_uris.get(asset_path) if not data_uri: return tag if not SRC_RE.search(tag): return tag return SRC_RE.sub(f'src="{data_uri}"', tag, count=1) def restore_img_tag(match): tag = match.group(0) asset_match = ASSET_RE.search(tag) if not asset_match or not SRC_RE.search(tag): return tag return SRC_RE.sub(f'src="{asset_match.group(1)}"', tag, count=1) def rewrite_html(repo_root, html_files, data_uris): changed = 0 for relative in html_files: path = repo_root / relative original = path.read_text(encoding="utf-8") updated = IMG_RE.sub(lambda match: rewrite_img_tag(match, data_uris), original) if updated != original: path.write_text(updated, encoding="utf-8", newline="\n") changed += 1 count = updated.count('src="data:image/jpeg;base64,') print(f"html_updated file={relative} data_uri_count={count}") else: print(f"html_unchanged file={relative}") return changed def restore_html(repo_root, html_files): changed = 0 for relative in html_files: path = repo_root / relative original = path.read_text(encoding="utf-8") updated = IMG_RE.sub(restore_img_tag, original) if updated != original: path.write_text(updated, encoding="utf-8", newline="\n") changed += 1 count = updated.count('src="data:image/jpeg;base64,') print(f"html_restored file={relative} remaining_data_uri_count={count}") else: print(f"html_unchanged file={relative}") return changed def parse_args(): parser = argparse.ArgumentParser(description="Embed compressed image fallbacks in Wiki.js HTML pages.") parser.add_argument("--embed-dir", default="data/KSP/wiki-embed-current") parser.add_argument("--max-dimension", type=int, default=1600) parser.add_argument("--quality", type=int, default=82) parser.add_argument("--restore-asset-src", action="store_true", help="Restore image src attributes from data-wiki-asset paths") return parser.parse_args() def main(): args = parse_args() repo_root = Path.cwd() if args.restore_asset_src: changed = restore_html(repo_root, HTML_FILES) print(f"restore_summary html_changed={changed}") return 0 data_uris = build_data_uris(repo_root, Path(args.embed_dir), args.max_dimension, args.quality) changed = rewrite_html(repo_root, HTML_FILES, data_uris) print(f"embed_summary assets={len(data_uris)} html_changed={changed}") return 0 if __name__ == "__main__": sys.exit(main())