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()