현재 작업 상태 저장
This commit is contained in:
parent
07cc67761e
commit
6bd91b0d5e
674 changed files with 8726 additions and 298 deletions
125
docs/avatar-art/seoyeon/live2d-psb/export_psb_layers.py
Normal file
125
docs/avatar-art/seoyeon/live2d-psb/export_psb_layers.py
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
#!/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())
|
||||
Loading…
Add table
Add a link
Reference in a new issue