from __future__ import annotations from collections import deque from pathlib import Path from PIL import Image ROOT = Path(__file__).resolve().parents[1] SOURCE_ROOT = Path("D:/") OUT_ROOT = ROOT / "public" / "assets" / "brain" LANCZOS = getattr(getattr(Image, "Resampling", Image), "LANCZOS") ASSETS = { "memory/bread.png": "ChatGPT Image 2026年7月23日 21_31_30 (1).png", "memory/carrot.png": "ChatGPT Image 2026年7月23日 21_31_32 (2).png", "memory/coffee.png": "ChatGPT Image 2026年7月23日 21_31_34 (4).png", "memory/cookie.png": "ChatGPT Image 2026年7月23日 21_31_35 (5).png", "memory/egg.png": "ChatGPT Image 2026年7月23日 21_31_36 (6).png", "memory/fish.png": "ChatGPT Image 2026年7月23日 21_31_38 (7).png", "memory/orange.png": "ChatGPT Image 2026年7月23日 21_31_39 (8).png", "memory/cheese.png": "ChatGPT Image 2026年7月23日 21_31_40.png", "planes/teal.png": "ChatGPT Image 2026年7月23日 21_31_44 (9).png", "planes/coral.png": "ChatGPT Image 2026年7月23日 21_31_45 (10).png", "symbols/camera.png": "ChatGPT Image 2026年7月23日 21_40_52 (1).png", "symbols/sun.png": "ChatGPT Image 2026年7月23日 21_40_52 (2).png", "symbols/squares.png": "ChatGPT Image 2026年7月23日 21_40_53 (3).png", "symbols/lock.png": "ChatGPT Image 2026年7月23日 21_40_53 (4).png", "symbols/lightning.png": "ChatGPT Image 2026年7月23日 21_40_53 (5).png", "symbols/moon.png": "ChatGPT Image 2026年7月23日 21_40_53 (6).png", "symbols/gear.png": "ChatGPT Image 2026年7月23日 21_40_53 (7).png", "symbols/star.png": "ChatGPT Image 2026年7月23日 21_40_53 (8).png", "symbols/puzzle.png": "ChatGPT Image 2026年7月23日 21_40_53 (9).png", "symbols/coffee.png": "ChatGPT Image 2026年7月23日 21_40_53 (10).png", } def has_real_alpha(image: Image.Image) -> bool: if "A" not in image.getbands(): return False alpha = image.getchannel("A") lo, hi = alpha.getextrema() return lo < 255 and hi == 255 def remove_generated_checkerboard(image: Image.Image) -> Image.Image: rgba = image.convert("RGBA") rgba.thumbnail((720, 720), LANCZOS) pixels = rgba.load() width, height = rgba.size def is_background(x: int, y: int) -> bool: red, green, blue, _ = pixels[x, y] high = max(red, green, blue) low = min(red, green, blue) return low >= 218 and high - low <= 18 # Flood only from the canvas edge. This removes the connected checkerboard # without punching holes into pale objects such as the teal plane or egg. queue: deque[tuple[int, int]] = deque() visited = bytearray(width * height) for x in range(width): queue.append((x, 0)) queue.append((x, height - 1)) for y in range(height): queue.append((0, y)) queue.append((width - 1, y)) while queue: x, y = queue.popleft() offset = y * width + x if visited[offset] or not is_background(x, y): continue visited[offset] = 1 if x: queue.append((x - 1, y)) if x + 1 < width: queue.append((x + 1, y)) if y: queue.append((x, y - 1)) if y + 1 < height: queue.append((x, y + 1)) for y in range(height): for x in range(width): red, green, blue, _ = pixels[x, y] alpha = 0 if visited[y * width + x] else 255 pixels[x, y] = (red, green, blue, alpha) bounds = rgba.getbbox() if bounds: rgba = rgba.crop(bounds) return rgba def copy_or_convert(source: Path, destination: Path) -> None: destination.parent.mkdir(parents=True, exist_ok=True) with Image.open(source) as image: if has_real_alpha(image): prepared = image.convert("RGBA") bounds = prepared.getbbox() if bounds: prepared = prepared.crop(bounds) prepared.thumbnail((720, 720), LANCZOS) else: prepared = remove_generated_checkerboard(image) prepared.save(destination, optimize=True) def main() -> None: missing: list[str] = [] for output, source_name in ASSETS.items(): source = SOURCE_ROOT / source_name destination = OUT_ROOT / output if not source.exists(): missing.append(source_name) continue copy_or_convert(source, destination) if missing: raise SystemExit("Missing assets:\n" + "\n".join(missing)) if __name__ == "__main__": main()