#!/usr/bin/env python3 """Export PSB layers through ImageMagick and preserve canvas offsets.""" from __future__ import annotations import json import re import subprocess from pathlib import Path import numpy as np from PIL import Image, ImageDraw, ImageFont HERE = Path(__file__).resolve().parent OUT_RAW = HERE / "layers-raw" OUT_CANVAS = HERE / "layers-canvas" MANIFEST = HERE / "layers-manifest.json" PSB = Path(r"C:\Users\encep\OneDrive\문서\카카오톡 받은 파일\라투디 여캐.psb") CANVAS_SIZE = (1080, 1920) def run_magick(args: list[str]) -> str: result = subprocess.run( ["magick", *args], cwd=HERE, text=True, capture_output=True, check=False, ) if result.returncode != 0: raise RuntimeError(result.stderr.strip() or result.stdout.strip()) return result.stdout def identify_layers() -> list[dict[str, object]]: output = run_magick(["identify", "-format", "%p\t%w\t%h\t%g\t%X\t%Y\n", str(PSB)]) layers: list[dict[str, object]] = [] for line in output.splitlines(): if not line.strip(): continue index_s, width_s, height_s, page, x_s, y_s = line.split("\t") layers.append( { "index": int(index_s), "width": int(width_s), "height": int(height_s), "page": page, "x": int(x_s), "y": int(y_s), } ) return layers def alpha_bbox(im: Image.Image, threshold: int = 8) -> tuple[int, int, int, int] | None: arr = np.array(im.convert("RGBA").getchannel("A")) ys, xs = np.where(arr > threshold) if len(xs) == 0: return None return int(xs.min()), int(ys.min()), int(xs.max() + 1), int(ys.max() + 1) def export_layer(layer: dict[str, object]) -> dict[str, object]: index = int(layer["index"]) raw = OUT_RAW / f"layer-{index:03d}.png" full = OUT_CANVAS / f"layer-{index:03d}.png" run_magick([f"{PSB}[{index}]", "-background", "none", "-alpha", "on", "png32:" + str(raw)]) src = Image.open(raw).convert("RGBA") full_canvas = Image.new("RGBA", CANVAS_SIZE, (0, 0, 0, 0)) full_canvas.alpha_composite(src, (int(layer["x"]), int(layer["y"]))) full_canvas.save(full, optimize=True) bbox = alpha_bbox(full_canvas) return { **layer, "raw": raw.relative_to(HERE).as_posix(), "canvas": full.relative_to(HERE).as_posix(), "alphaBox": list(bbox) if bbox else None, } def make_contact(layers: list[dict[str, object]]) -> None: visible = [layer for layer in layers if layer.get("alphaBox") is not None] tile_w, tile_h = 180, 220 cols = 6 rows = (len(visible) + cols - 1) // cols sheet = Image.new("RGBA", (cols * tile_w, rows * tile_h), (30, 39, 36, 255)) draw = ImageDraw.Draw(sheet) for i, layer in enumerate(visible): full = Image.open(HERE / str(layer["canvas"])).convert("RGBA") box = tuple(layer["alphaBox"]) thumb = full.crop(box) thumb.thumbnail((tile_w - 18, tile_h - 54), Image.Resampling.LANCZOS) x = (i % cols) * tile_w + (tile_w - thumb.width) // 2 y = (i // cols) * tile_h + 12 sheet.alpha_composite(thumb, (x, y)) label = f"{int(layer['index']):03d} {layer['width']}x{layer['height']} {layer['page']}" draw.text(((i % cols) * tile_w + 8, (i // cols) * tile_h + tile_h - 36), label, fill=(210, 222, 216, 255)) draw.text(((i % cols) * tile_w + 8, (i // cols) * tile_h + tile_h - 20), str(layer["alphaBox"]), fill=(150, 166, 160, 255)) sheet.save(HERE / "layers-contact.png", optimize=True) def main() -> int: if not PSB.exists(): raise FileNotFoundError(PSB) OUT_RAW.mkdir(parents=True, exist_ok=True) OUT_CANVAS.mkdir(parents=True, exist_ok=True) layers = identify_layers() exported = [export_layer(layer) for layer in layers] MANIFEST.write_text(json.dumps({"source": str(PSB), "canvas": CANVAS_SIZE, "layers": exported}, indent=2), encoding="utf-8") make_contact(exported) flattened = HERE / "flattened.png" run_magick([f"{PSB}[0]", "-background", "none", "-alpha", "on", "png32:" + str(flattened)]) print(f"exported {len(exported)} layers") print(f"manifest {MANIFEST}") print(f"contact {HERE / 'layers-contact.png'}") print(f"flattened {flattened}") return 0 if __name__ == "__main__": raise SystemExit(main())