bayuMIR/tools/slice_wulin_mock_assets.py
2026-06-24 21:00:14 +08:00

109 lines
3.9 KiB
Python

from __future__ import annotations
from collections import deque
from pathlib import Path
from PIL import Image
ROOT = Path(__file__).resolve().parents[1]
SOURCE = Path(r"C:/Users/Administrator/AppData/Local/Temp/codex-clipboard-1a698e57-4da7-4217-9a9f-4b81d7cb7fb5.png")
OUT_DIR = ROOT / "client" / "dev" / "res" / "custom" / "47" / "wulin_juexue_sliced_assets"
SLICES = [
("00_full_mock_with_frame", (0, 0, 816, 582), False),
("01_outer_frame", (0, 0, 816, 582), True),
("02_inner_content_area", (51, 104, 733, 447), False),
("03_top_equip_plaque", (65, 122, 220, 43), True),
("04_left_book_showcase_panel", (66, 119, 251, 232), False),
("05_secret_book_with_glow", (93, 158, 136, 143), True),
("06_green_trainable_tag", (262, 130, 38, 99), True),
("07_middle_manual_pages", (333, 119, 151, 259), True),
("08_right_effect_book_panel", (444, 116, 319, 253), True),
("09_skill_name_plaque", (463, 323, 219, 55), True),
("10_skill_attr_panel", (462, 372, 222, 60), True),
("11_slot_header", (162, 365, 165, 27), True),
("12_slot_01_active_blue", (61, 390, 98, 148), True),
("13_slot_02_active_gold", (164, 390, 98, 148), True),
("14_slot_03_locked", (270, 390, 98, 148), True),
("15_slot_04_locked", (371, 390, 98, 148), True),
("16_cost_bar", (512, 439, 223, 34), True),
("17_reroll_button", (510, 472, 183, 59), True),
("18_bottom_pattern_strip", (52, 494, 733, 57), True),
]
def crop_box(spec: tuple[int, int, int, int]) -> tuple[int, int, int, int]:
x, y, w, h = spec
return x, y, x + w, y + h
def trim_alpha(image: Image.Image, padding: int = 2) -> Image.Image:
bbox = image.getchannel("A").getbbox()
if not bbox:
return image
left = max(0, bbox[0] - padding)
top = max(0, bbox[1] - padding)
right = min(image.width, bbox[2] + padding)
bottom = min(image.height, bbox[3] + padding)
return image.crop((left, top, right, bottom))
def transparent_edge_dark(image: Image.Image, threshold: int = 3) -> Image.Image:
"""Remove black/near-black pixels connected to the crop edge."""
image = image.convert("RGBA")
pixels = image.load()
width, height = image.size
seen: set[tuple[int, int]] = set()
queue: deque[tuple[int, int]] = deque()
def is_edge_dark(x: int, y: int) -> bool:
r, g, b, a = pixels[x, y]
return a > 0 and r <= threshold and g <= threshold and b <= threshold
for x in range(width):
for y in (0, height - 1):
if is_edge_dark(x, y):
queue.append((x, y))
seen.add((x, y))
for y in range(height):
for x in (0, width - 1):
if (x, y) not in seen and is_edge_dark(x, y):
queue.append((x, y))
seen.add((x, y))
while queue:
x, y = queue.popleft()
r, g, b, _ = pixels[x, y]
pixels[x, y] = (r, g, b, 0)
for nx, ny in ((x - 1, y), (x + 1, y), (x, y - 1), (x, y + 1)):
if 0 <= nx < width and 0 <= ny < height and (nx, ny) not in seen and is_edge_dark(nx, ny):
seen.add((nx, ny))
queue.append((nx, ny))
return image
def main() -> None:
OUT_DIR.mkdir(parents=True, exist_ok=True)
source = Image.open(SOURCE).convert("RGBA")
if source.size != (816, 582):
raise ValueError(f"Unexpected source size: {source.size}")
lines = ["name,x,y,width,height,file"]
for name, box, key_dark_edges in SLICES:
x, y, w, h = box
image = source.crop(crop_box(box))
if key_dark_edges:
image = transparent_edge_dark(image)
image = trim_alpha(image)
out = OUT_DIR / f"{name}.png"
image.save(out)
lines.append(f"{name},{x},{y},{w},{h},{out.name}")
(OUT_DIR / "slices_manifest.csv").write_text("\n".join(lines), encoding="utf-8")
print(OUT_DIR)
if __name__ == "__main__":
main()