130 lines
4.5 KiB
Python
130 lines
4.5 KiB
Python
#!/usr/bin/env python3
|
|
"""Convert the repository's runtime image assets to WebP.
|
|
|
|
This is intentionally a one-time migration helper. It keeps the source files
|
|
untouched and writes converted files into a caller-provided temporary folder.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import hashlib
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
|
|
from PIL import Image, ImageOps
|
|
|
|
|
|
IMAGE_EXTENSIONS = {".jpg", ".jpeg", ".png"}
|
|
LOSSLESS_PATH_PARTS = {"routes", "train-intro", "volumes", "products", "source"}
|
|
|
|
|
|
def parse_args() -> argparse.Namespace:
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--root", type=Path, required=True)
|
|
parser.add_argument("--output", type=Path, required=True)
|
|
parser.add_argument("--manifest", type=Path, required=True)
|
|
return parser.parse_args()
|
|
|
|
|
|
def source_files(root: Path) -> list[tuple[Path, str]]:
|
|
public_root = root / "public" / "assets"
|
|
files: list[tuple[Path, str]] = []
|
|
for path in sorted(public_root.rglob("*")):
|
|
if path.is_file() and path.suffix.lower() in IMAGE_EXTENSIONS:
|
|
relative = path.relative_to(public_root).as_posix()
|
|
files.append((path, relative))
|
|
|
|
logo = root / "apps" / "miniprogram" / "src" / "assets" / "wanderq-logo.png"
|
|
if logo.is_file():
|
|
files.append((logo, "brand/wanderq-logo.png"))
|
|
return files
|
|
|
|
|
|
def should_use_lossless(source: Path, relative: str) -> bool:
|
|
if source.suffix.lower() == ".png":
|
|
return True
|
|
parts = set(Path(relative).parts)
|
|
return bool(parts & LOSSLESS_PATH_PARTS) or relative.endswith("train/price-overview.jpg")
|
|
|
|
|
|
def convert(source: Path, target: Path, lossless: bool) -> None:
|
|
target.parent.mkdir(parents=True, exist_ok=True)
|
|
with Image.open(source) as opened:
|
|
image = ImageOps.exif_transpose(opened)
|
|
save_options = {
|
|
"format": "WEBP",
|
|
"method": 6,
|
|
"lossless": lossless,
|
|
}
|
|
if not lossless:
|
|
save_options["quality"] = 84
|
|
icc_profile = image.info.get("icc_profile")
|
|
if icc_profile:
|
|
save_options["icc_profile"] = icc_profile
|
|
image.save(target, **save_options)
|
|
|
|
|
|
def main() -> None:
|
|
args = parse_args()
|
|
root = args.root.resolve()
|
|
output = args.output.resolve()
|
|
output.mkdir(parents=True, exist_ok=True)
|
|
|
|
manifest: list[dict[str, object]] = []
|
|
files = source_files(root)
|
|
if not files:
|
|
raise SystemExit("未找到本地静态图片源;为避免生成空清单,请确认源素材仍在 public/assets 或小程序 logo 路径中")
|
|
|
|
for source, relative in files:
|
|
output_relative = f"{Path(relative).with_suffix('')}.webp"
|
|
target = output / output_relative
|
|
lossless = should_use_lossless(source, relative)
|
|
convert(source, target, lossless)
|
|
source_bytes = source.stat().st_size
|
|
output_bytes = target.stat().st_size
|
|
digest = hashlib.sha256(target.read_bytes()).hexdigest()
|
|
if relative == "brand/wanderq-logo.png":
|
|
reference = None
|
|
object_key = "wanqu/miniapp/static/brand/wanderq-logo.webp"
|
|
source_path = "apps/miniprogram/src/assets/wanderq-logo.png"
|
|
else:
|
|
reference = f"/assets/{relative}"
|
|
object_key = f"wanqu/miniapp/static/{output_relative}"
|
|
source_path = f"public/assets/{relative}"
|
|
manifest.append(
|
|
{
|
|
"sourcePath": source_path,
|
|
"reference": reference,
|
|
"outputPath": output_relative,
|
|
"objectKey": object_key,
|
|
"lossless": lossless,
|
|
"sourceBytes": source_bytes,
|
|
"outputBytes": output_bytes,
|
|
"sha256": digest,
|
|
}
|
|
)
|
|
|
|
args.manifest.parent.mkdir(parents=True, exist_ok=True)
|
|
args.manifest.write_text(json.dumps({"assets": manifest}, ensure_ascii=False, indent=2) + os.linesep)
|
|
source_total = sum(int(item["sourceBytes"]) for item in manifest)
|
|
output_total = sum(int(item["outputBytes"]) for item in manifest)
|
|
print(
|
|
json.dumps(
|
|
{
|
|
"assets": len(manifest),
|
|
"sourceBytes": source_total,
|
|
"outputBytes": output_total,
|
|
"savedBytes": source_total - output_total,
|
|
"lossless": sum(1 for item in manifest if item["lossless"]),
|
|
"lossy": sum(1 for item in manifest if not item["lossless"]),
|
|
},
|
|
ensure_ascii=False,
|
|
)
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|