Files
KSP_project/scripts/wiki_embed_image_fallbacks.py
T

150 lines
6.2 KiB
Python

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"<img\b[^>]*>", 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())