125 lines
4.4 KiB
Python
125 lines
4.4 KiB
Python
#!/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())
|