Files
blockminers-printer/scripts/assets.py
T

115 lines
3.5 KiB
Python

import json
import re
import struct
from functools import lru_cache
from io import BytesIO
from pathlib import Path
from PIL import Image, UnidentifiedImageError
from .settings import DATA_DIR
def load_json(path: Path) -> dict:
with path.open("r", encoding="utf-8") as f:
return json.load(f)
def load_project_version() -> str:
project_file = DATA_DIR / "project.json"
if not project_file.exists():
return ""
return str(load_json(project_file).get("projectVersion", ""))
def find_art_for_json(json_path: Path) -> Path | None:
base = json_path.with_suffix("")
for ext in (".png", ".jpg", ".jpeg"):
candidate = base.with_suffix(ext)
if candidate.exists():
return candidate
return None
@lru_cache(maxsize=256)
def _load_rgb_cached(path: str, mtime_ns: int, size: int) -> Image.Image:
try:
return Image.open(path).convert("RGBA")
except UnidentifiedImageError:
cleaned = strip_png_ancillary_chunks(Path(path))
if cleaned is None:
raise
return Image.open(BytesIO(cleaned)).convert("RGBA")
def load_rgb(path: Path) -> Image.Image:
stat = path.stat()
return _load_rgb_cached(str(path), stat.st_mtime_ns, stat.st_size).copy()
def save_temp_png(img: Image.Image, path: Path, dpi: int) -> None:
img.save(path, dpi=(dpi, dpi), compress_level=1, optimize=False)
def strip_png_ancillary_chunks(path: Path) -> bytes | None:
"""Return a PNG with nonessential chunks removed.
A few source PNG templates carry metadata chunks that Pillow refuses to
parse, while the pixel data itself is valid. Keeping critical chunks gives
us a deterministic source-data repair without touching the copied assets.
"""
data = path.read_bytes()
signature = b"\x89PNG\r\n\x1a\n"
if not data.startswith(signature):
return None
keep = {b"IHDR", b"PLTE", b"IDAT", b"IEND", b"tRNS"}
out = bytearray(signature)
pos = len(signature)
try:
while pos + 12 <= len(data):
size = struct.unpack(">I", data[pos : pos + 4])[0]
chunk_type = data[pos + 4 : pos + 8]
chunk_end = pos + 8 + size + 4
if chunk_end > len(data):
return None
if chunk_type in keep:
out.extend(data[pos:chunk_end])
pos = chunk_end
if chunk_type == b"IEND":
return bytes(out)
except struct.error:
return None
return None
def cover_resize(img: Image.Image, size: tuple[int, int]) -> Image.Image:
target_w, target_h = size
src = img.convert("RGBA")
ratio = max(target_w / src.width, target_h / src.height)
new_size = (round(src.width * ratio), round(src.height * ratio))
resized = src.resize(new_size, Image.Resampling.LANCZOS)
left = max(0, (resized.width - target_w) // 2)
top = max(0, (resized.height - target_h) // 2)
return resized.crop((left, top, left + target_w, top + target_h))
def meta_unit_scale(meta: dict) -> float:
return float(meta.get("cardWidth", 825)) / 825.0
def hex_color(value: str | None, fallback: str = "#ffffff") -> tuple[int, int, int]:
value = value or fallback
value = value.strip()
if value.startswith("#"):
value = value[1:]
if len(value) != 6:
return hex_color(fallback)
return tuple(int(value[i : i + 2], 16) for i in (0, 2, 4))
def safe_name(value: str) -> str:
value = re.sub(r'[\\/:*?"<>|]', "", value)
value = re.sub(r"\s+", "_", value.strip())
return value or "untitled"