from __future__ import annotations import json from pathlib import Path from PIL import Image, ImageDraw, ImageFont ROOT = Path(__file__).resolve().parents[1] ASSET_DIR = ROOT / "client" / "dev" / "res" / "custom" / "47" SRC = ASSET_DIR / "wulin_juexue_generated_assets" / "source_generated_asset_sheet_magenta_alpha.png" OUT = ASSET_DIR / "wulin_juexue_generated_assets" / "transparent_slices" PREVIEW = ASSET_DIR / "wulin_juexue_generated_assets" / "reassembled_preview.png" CONTACT = OUT / "_contact_sheet.png" SLICES = [ ("outer_frame", (0, 40, 626, 320)), ("content_panel_dark", (660, 72, 454, 266)), ("secret_book_glow", (1175, 30, 300, 320)), ("green_vertical_tag", (80, 370, 105, 325)), ("open_manual_pages", (215, 370, 545, 315)), ("blue_skill_effect_panel", (785, 370, 675, 295)), ("long_name_plaque", (28, 708, 460, 88)), ("small_brown_button", (510, 708, 245, 90)), ("slot_card_blue", (795, 678, 150, 195)), ("slot_card_gold", (970, 678, 150, 195)), ("slot_card_locked_a", (1145, 678, 155, 195)), ("slot_card_locked_b", (1326, 678, 155, 195)), ("attribute_panel", (42, 838, 390, 148)), ("cost_bar", (460, 896, 420, 68)), ("reroll_button", (945, 880, 405, 118)), ] def trim(image: Image.Image, pad: int = 2) -> tuple[Image.Image, tuple[int, int, int, int]]: bbox = image.getchannel("A").getbbox() if not bbox: return image, (0, 0, image.width, image.height) left = max(0, bbox[0] - pad) top = max(0, bbox[1] - pad) right = min(image.width, bbox[2] + pad) bottom = min(image.height, bbox[3] + pad) return image.crop((left, top, right, bottom)), (left, top, right, bottom) def paste_fit(base: Image.Image, asset: Image.Image, box: tuple[int, int, int, int]) -> None: x, y, w, h = box scale = min(w / asset.width, h / asset.height) resized = asset.resize((max(1, int(asset.width * scale)), max(1, int(asset.height * scale))), Image.Resampling.LANCZOS) base.alpha_composite(resized, (x + (w - resized.width) // 2, y + (h - resized.height) // 2)) def draw_text(base: Image.Image) -> None: draw = ImageDraw.Draw(base) font_path = "C:/Windows/Fonts/NotoSansSC-VF.ttf" bold_path = "C:/Windows/Fonts/simhei.ttf" regular = ImageFont.truetype(font_path, 14) small = ImageFont.truetype(font_path, 12) bold = ImageFont.truetype(bold_path, 20) button = ImageFont.truetype(bold_path, 28) draw.text((89, 128), "佩戴中:武林秘籍", font=regular, fill=(255, 219, 127, 255)) draw.text((187, 370), "当前槽位", font=regular, fill=(240, 203, 127, 255)) draw.text((596, 349), "剑气纵横", font=bold, fill=(255, 220, 112, 255), anchor="mm") draw.text((548, 395), "基本剑术伤害提升 5%", font=small, fill=(249, 202, 104, 255)) draw.text((548, 424), "效果类型:增伤", font=small, fill=(249, 202, 104, 255)) draw.text((556, 461), "灵石 50", font=small, fill=(148, 255, 132, 255)) draw.text((659, 461), "金币 5000", font=small, fill=(148, 255, 132, 255)) draw.text((603, 503), "重新洗练", font=button, fill=(255, 226, 139, 255), anchor="mm") def make_contact(files: list[Path]) -> None: thumbs = [] for path in files: image = Image.open(path).convert("RGBA") scale = min(1, 180 / image.width, 120 / image.height) thumb = image.resize((max(1, int(image.width * scale)), max(1, int(image.height * scale))), Image.Resampling.LANCZOS) thumbs.append((path, image.size, thumb)) cols, cell = 3, (240, 170) rows = (len(thumbs) + cols - 1) // cols sheet = Image.new("RGBA", (cols * cell[0], rows * cell[1]), (46, 46, 46, 255)) draw = ImageDraw.Draw(sheet) for yy in range(0, sheet.height, 10): for xx in range(0, sheet.width, 10): color = (68, 68, 68, 255) if (xx // 10 + yy // 10) % 2 == 0 else (48, 48, 48, 255) draw.rectangle((xx, yy, xx + 9, yy + 9), fill=color) for index, (path, size, thumb) in enumerate(thumbs): col, row = index % cols, index // cols ox, oy = col * cell[0], row * cell[1] sheet.alpha_composite(thumb, (ox + (cell[0] - thumb.width) // 2, oy + 8)) draw.text((ox + 8, oy + 132), path.stem, fill=(240, 240, 240, 255)) draw.text((ox + 8, oy + 150), f"{size[0]}x{size[1]}", fill=(155, 225, 235, 255)) sheet.convert("RGB").save(CONTACT) def main() -> None: OUT.mkdir(parents=True, exist_ok=True) source = Image.open(SRC).convert("RGBA") manifest = [] outputs = {} for name, box in SLICES: x, y, w, h = box crop = source.crop((x, y, x + w, y + h)) final, trim_box = trim(crop) path = OUT / f"{name}.png" final.save(path) outputs[name] = final manifest.append( { "name": name, "source_box": {"x": x, "y": y, "width": w, "height": h}, "trim_box": { "x": trim_box[0], "y": trim_box[1], "width": trim_box[2] - trim_box[0], "height": trim_box[3] - trim_box[1], }, "output_size": {"width": final.width, "height": final.height}, "file": path.name, } ) (OUT / "manifest.json").write_text(json.dumps(manifest, ensure_ascii=False, indent=2), encoding="utf-8") make_contact([OUT / f"{name}.png" for name, _ in SLICES]) preview = Image.new("RGBA", (816, 582), (0, 0, 0, 0)) paste_fit(preview, outputs["outer_frame"], (0, 0, 816, 582)) paste_fit(preview, outputs["content_panel_dark"], (55, 104, 728, 447)) paste_fit(preview, outputs["long_name_plaque"], (68, 119, 220, 50)) paste_fit(preview, outputs["secret_book_glow"], (72, 150, 165, 165)) paste_fit(preview, outputs["green_vertical_tag"], (258, 132, 44, 112)) paste_fit(preview, outputs["open_manual_pages"], (320, 119, 150, 260)) paste_fit(preview, outputs["blue_skill_effect_panel"], (462, 118, 300, 205)) paste_fit(preview, outputs["long_name_plaque"], (466, 322, 218, 56)) paste_fit(preview, outputs["attribute_panel"], (466, 374, 218, 60)) paste_fit(preview, outputs["slot_card_blue"], (58, 392, 98, 146)) paste_fit(preview, outputs["slot_card_gold"], (165, 392, 98, 146)) paste_fit(preview, outputs["slot_card_locked_a"], (272, 392, 98, 146)) paste_fit(preview, outputs["slot_card_locked_b"], (379, 392, 98, 146)) paste_fit(preview, outputs["cost_bar"], (512, 438, 225, 36)) paste_fit(preview, outputs["reroll_button"], (510, 472, 185, 62)) draw_text(preview) preview.save(PREVIEW) print(OUT) print(PREVIEW) print(CONTACT) if __name__ == "__main__": main()