From 690beeb5614abe7bed00247fd9c97d04fc8d4784 Mon Sep 17 00:00:00 2001 From: OpenAI Codex Date: Thu, 17 Sep 2026 21:30:49 +0800 Subject: [PATCH] fix: add MapLibre retina sprite builder --- tools/cm95_maplibre_retina_sprite.py | 75 ++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100755 tools/cm95_maplibre_retina_sprite.py diff --git a/tools/cm95_maplibre_retina_sprite.py b/tools/cm95_maplibre_retina_sprite.py new file mode 100755 index 0000000..d1073d0 --- /dev/null +++ b/tools/cm95_maplibre_retina_sprite.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +import json +from pathlib import Path +import shutil +import subprocess +from typing import Any + + +def _scale_pair_list(value: Any, factor: int) -> Any: + if not isinstance(value, list): + return value + out = [] + for item in value: + if isinstance(item, list) and len(item) == 2: + out.append([item[0] * factor, item[1] * factor]) + else: + out.append(item) + return out + + +def _retina_entry(entry: dict[str, Any], factor: int) -> dict[str, Any]: + out = dict(entry) + for key in ("x", "y", "width", "height"): + if key in out: + out[key] = int(out[key]) * factor + if isinstance(out.get("content"), list) and len(out["content"]) == 4: + out["content"] = [v * factor for v in out["content"]] + for key in ("stretchX", "stretchY"): + if key in out: + out[key] = _scale_pair_list(out[key], factor) + out["pixelRatio"] = factor + return out + + +def build_retina(prefix: Path, *, factor: int = 2) -> dict[str, Any]: + src_png = prefix.with_suffix(".png") + src_json = prefix.with_suffix(".json") + if not src_png.is_file() or not src_json.is_file(): + raise FileNotFoundError(f"missing sprite pair for prefix {prefix}") + if shutil.which("convert") is None: + raise RuntimeError("ImageMagick convert is required") + + dst_png = prefix.with_name(prefix.name + f"@{factor}x").with_suffix(".png") + dst_json = prefix.with_name(prefix.name + f"@{factor}x").with_suffix(".json") + data = json.loads(src_json.read_text(encoding="utf-8")) + retina = {name: _retina_entry(dict(entry), factor) for name, entry in data.items()} + + subprocess.run( + ["convert", str(src_png), "-filter", "point", "-resize", f"{factor * 100}%", str(dst_png)], + check=True, + ) + dst_json.write_text(json.dumps(retina, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") + return { + "prefix": str(prefix), + "entry_count": len(retina), + "png": str(dst_png), + "json": str(dst_json), + "pixel_ratio": factor, + } + + +def main() -> int: + ap = argparse.ArgumentParser(description="Build MapLibre @2x sprite assets from a 1x sprite pair.") + ap.add_argument("prefix", nargs="+", type=Path) + args = ap.parse_args() + result = [build_retina(prefix) for prefix in args.prefix] + print(json.dumps(result, ensure_ascii=False, indent=2)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())