Initial Blockminers printer
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
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 render_work_scale(meta: dict, target_size: tuple[int, int]) -> float:
|
||||
sx = target_size[0] / float(meta["cardWidth"])
|
||||
sy = target_size[1] / float(meta["cardHeight"])
|
||||
return max(2.0, sx, sy)
|
||||
|
||||
|
||||
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"
|
||||
Reference in New Issue
Block a user