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

153 lines
6.6 KiB
Python

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