현재 작업 상태 저장

This commit is contained in:
Yun Chan 2026-06-27 11:20:24 +09:00
parent 07cc67761e
commit 6bd91b0d5e
674 changed files with 8726 additions and 298 deletions

View file

@ -0,0 +1,158 @@
#!/usr/bin/env python3
"""Export top-level PSB groups as normalized app-ready raster layers."""
from __future__ import annotations
import json
from pathlib import Path
from typing import Iterable
import numpy as np
from PIL import Image, ImageDraw
from psd_tools import PSDImage
HERE = Path(__file__).resolve().parent
PSB = Path(r"C:\Users\encep\OneDrive\문서\카카오톡 받은 파일\라투디 여캐.psb")
OUT = HERE / "top-groups"
PARTS = OUT / "parts"
SOURCE_CANVAS = (1080, 1920)
TARGET_CANVAS = (900, 1125)
CROP_BOX = (0, 240, 1080, 1590)
LAYER_ORDER = [
"hair-back",
"body",
"outfit",
"face",
"eye-left",
"eye-right",
"hair-front",
"mouth",
]
GROUP_MAP = {
"뒷머리": "hair-back",
"": "body",
"의상": "outfit",
"얼굴": "face",
"좌 눈": "eye-left",
"우 눈": "eye-right",
"머리카락": "hair-front",
"": "mouth",
}
EXTRA_BACK_HAIR = {"레이어 110", "레이어 110 복사"}
def alpha_bbox(im: Image.Image, threshold: int = 8) -> tuple[int, int, int, int] | None:
alpha = np.array(im.convert("RGBA").getchannel("A"))
ys, xs = np.where(alpha > threshold)
if len(xs) == 0:
return None
return int(xs.min()), int(ys.min()), int(xs.max() + 1), int(ys.max() + 1)
def resize_rgba(im: Image.Image, size: tuple[int, int]) -> Image.Image:
arr = np.array(im.convert("RGBA")).astype(np.float32)
alpha = arr[:, :, 3:4] / 255.0
premultiplied = arr.copy()
premultiplied[:, :, :3] *= alpha
resized = Image.fromarray(np.clip(premultiplied, 0, 255).astype(np.uint8), "RGBA").resize(
size,
Image.Resampling.LANCZOS,
)
out = np.array(resized).astype(np.float32)
out_alpha = out[:, :, 3:4] / 255.0
out[:, :, :3] = np.where(out_alpha > 0.001, out[:, :, :3] / np.maximum(out_alpha, 0.001), 0)
out[out[:, :, 3] < 1, :3] = 0
return Image.fromarray(np.clip(out, 0, 255).astype(np.uint8), "RGBA")
def normalize(full_canvas: Image.Image) -> Image.Image:
return resize_rgba(full_canvas.crop(CROP_BOX), TARGET_CANVAS)
def composite_layers(layers: Iterable[Image.Image]) -> Image.Image:
canvas = Image.new("RGBA", SOURCE_CANVAS, (0, 0, 0, 0))
for layer in layers:
canvas.alpha_composite(layer.convert("RGBA"))
return canvas
def make_contact(entries: list[dict[str, object]]) -> None:
tile_w, tile_h = 180, 180
sheet = Image.new("RGBA", (tile_w * 4, tile_h * 2), (30, 39, 36, 255))
draw = ImageDraw.Draw(sheet)
for i, entry in enumerate(entries):
layer = Image.open(PARTS / str(entry["file"])).convert("RGBA")
box = alpha_bbox(layer)
thumb = Image.new("RGBA", (1, 1), (0, 0, 0, 0)) if box is None else layer.crop(box)
thumb.thumbnail((tile_w - 22, tile_h - 46), Image.Resampling.LANCZOS)
x = (i % 4) * tile_w + (tile_w - thumb.width) // 2
y = (i // 4) * tile_h + 12
sheet.alpha_composite(thumb, (x, y))
draw.text(((i % 4) * tile_w + 8, (i // 4) * tile_h + tile_h - 28), str(entry["id"]), fill=(210, 222, 216, 255))
draw.text(((i % 4) * tile_w + 8, (i // 4) * tile_h + tile_h - 14), str(entry["source"]), fill=(150, 166, 160, 255))
sheet.save(OUT / "top-groups-contact.png", optimize=True)
def main() -> int:
psd = PSDImage.open(PSB)
PARTS.mkdir(parents=True, exist_ok=True)
top = {layer.name: layer for layer in psd}
entries: list[dict[str, object]] = []
extra_back = [top[name].composite(viewport=(0, 0, psd.width, psd.height)) for name in EXTRA_BACK_HAIR if name in top]
for source_name, part_id in GROUP_MAP.items():
if source_name not in top:
raise KeyError(f"Missing top-level group: {source_name}")
image = top[source_name].composite(viewport=(0, 0, psd.width, psd.height))
if part_id == "hair-back":
image = composite_layers([*extra_back, image])
normalized = normalize(image)
file_name = f"{part_id}.png"
normalized.save(PARTS / file_name, optimize=True)
entries.append(
{
"id": part_id,
"source": source_name,
"file": file_name,
"alphaBox": list(alpha_bbox(normalized) or ()),
}
)
preview = Image.new("RGBA", TARGET_CANVAS, (30, 39, 36, 255))
for part_id in LAYER_ORDER:
preview.alpha_composite(Image.open(PARTS / f"{part_id}.png").convert("RGBA"))
preview.save(OUT / "preview-psb-groups.png", optimize=True)
transparent = Image.new("RGBA", TARGET_CANVAS, (0, 0, 0, 0))
for part_id in LAYER_ORDER:
transparent.alpha_composite(Image.open(PARTS / f"{part_id}.png").convert("RGBA"))
transparent.save(OUT / "preview-psb-groups-transparent.png", optimize=True)
(OUT / "top-groups-manifest.json").write_text(
json.dumps(
{
"source": str(PSB),
"sourceCanvas": SOURCE_CANVAS,
"targetCanvas": TARGET_CANVAS,
"cropBox": CROP_BOX,
"layerOrder": LAYER_ORDER,
"parts": entries,
},
ensure_ascii=False,
indent=2,
),
encoding="utf-8",
)
make_contact(entries)
print(f"exported {len(entries)} top-level groups")
print(f"preview {OUT / 'preview-psb-groups.png'}")
return 0
if __name__ == "__main__":
raise SystemExit(main())