181 lines
5 KiB
Python
181 lines
5 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import struct
|
|
from pathlib import Path
|
|
|
|
from PIL import Image
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
ASSET_DIR = ROOT / "client" / "dev" / "res" / "custom" / "47"
|
|
LAYER_DIR = ASSET_DIR / "wulin_juexue_psd_layers"
|
|
SOURCE_PSD = ASSET_DIR / "\u6b66\u6797\u7edd\u5b66.psd"
|
|
OUTPUT_PSD = ASSET_DIR / "wulin_juexue_layered.psd"
|
|
OUTPUT_PSDTOOLS_PSD = ASSET_DIR / "wulin_juexue_layered_psdtools.psd"
|
|
|
|
|
|
def u32(value: int) -> bytes:
|
|
return struct.pack(">I", value)
|
|
|
|
|
|
def i32(value: int) -> bytes:
|
|
return struct.pack(">i", value)
|
|
|
|
|
|
def u16(value: int) -> bytes:
|
|
return struct.pack(">H", value)
|
|
|
|
|
|
def i16(value: int) -> bytes:
|
|
return struct.pack(">h", value)
|
|
|
|
|
|
def pascal_name(name: str) -> bytes:
|
|
raw = name.encode("ascii", "replace")[:255]
|
|
data = bytes([len(raw)]) + raw
|
|
while len(data) % 4:
|
|
data += b"\0"
|
|
return data
|
|
|
|
|
|
def unicode_layer_name(name: str) -> bytes:
|
|
encoded = name.encode("utf-16-be")
|
|
data = u32(len(name)) + encoded
|
|
if len(data) % 2:
|
|
data += b"\0"
|
|
return b"8BIM" + b"luni" + u32(len(data)) + data
|
|
|
|
|
|
def layer_channels(image: Image.Image) -> list[tuple[int, bytes]]:
|
|
rgba = image.convert("RGBA")
|
|
r, g, b, a = rgba.split()
|
|
return [
|
|
(0, b"\0\0" + r.tobytes()),
|
|
(1, b"\0\0" + g.tobytes()),
|
|
(2, b"\0\0" + b.tobytes()),
|
|
(-1, b"\0\0" + a.tobytes()),
|
|
]
|
|
|
|
|
|
def layer_record(name: str, image: Image.Image, x: int, y: int) -> tuple[bytes, bytes]:
|
|
width, height = image.size
|
|
channels = layer_channels(image)
|
|
|
|
record = bytearray()
|
|
record += i32(y) + i32(x) + i32(y + height) + i32(x + width)
|
|
record += u16(len(channels))
|
|
for channel_id, channel_data in channels:
|
|
record += i16(channel_id) + u32(len(channel_data))
|
|
|
|
record += b"8BIM" + b"norm"
|
|
record += bytes([255, 0, 8, 0])
|
|
|
|
extra = bytearray()
|
|
extra += u32(0)
|
|
extra += u32(0)
|
|
extra += pascal_name(name)
|
|
extra += unicode_layer_name(name)
|
|
record += u32(len(extra)) + extra
|
|
|
|
channel_image_data = b"".join(channel_data for _, channel_data in channels)
|
|
return bytes(record), channel_image_data
|
|
|
|
|
|
def full_canvas_layer(path: Path, canvas_size: tuple[int, int], offset: tuple[int, int]) -> Image.Image:
|
|
image = Image.open(path).convert("RGBA")
|
|
canvas = Image.new("RGBA", canvas_size, (0, 0, 0, 0))
|
|
canvas.alpha_composite(image, offset)
|
|
return canvas
|
|
|
|
|
|
def main() -> None:
|
|
width, height = 816, 582
|
|
|
|
base = Image.open(SOURCE_PSD).convert("RGBA")
|
|
if base.size != (width, height):
|
|
raise ValueError(f"Unexpected source PSD size: {base.size}")
|
|
|
|
layers: list[tuple[str, Image.Image, int, int]] = [
|
|
("Frame_WulinJuexue", base, 0, 0),
|
|
(
|
|
"Content_Juexue",
|
|
Image.open(LAYER_DIR / "layer_01_main_content_733x447_at_51_104.png").convert("RGBA"),
|
|
51,
|
|
104,
|
|
),
|
|
(
|
|
"Modal_Dim",
|
|
Image.open(LAYER_DIR / "layer_02_modal_dim_816x582_at_0_0.png").convert("RGBA"),
|
|
0,
|
|
0,
|
|
),
|
|
(
|
|
"Popup_UnlockSlot",
|
|
Image.open(LAYER_DIR / "layer_03_unlock_popup_420x261_at_198_172.png").convert("RGBA"),
|
|
198,
|
|
172,
|
|
),
|
|
]
|
|
|
|
records = bytearray()
|
|
channel_data = bytearray()
|
|
for name, image, x, y in layers:
|
|
record, data = layer_record(name, image, x, y)
|
|
records += record
|
|
channel_data += data
|
|
|
|
layer_info = i16(len(layers)) + records + channel_data
|
|
if len(layer_info) % 2:
|
|
layer_info += b"\0"
|
|
|
|
layer_and_mask = u32(len(layer_info)) + layer_info + u32(0)
|
|
|
|
composite = Image.new("RGBA", (width, height), (0, 0, 0, 0))
|
|
for _, image, x, y in layers:
|
|
composite.alpha_composite(image, (x, y))
|
|
r, g, b, _ = composite.split()
|
|
merged_image_data = b"\0\0" + r.tobytes() + g.tobytes() + b.tobytes()
|
|
|
|
header = (
|
|
b"8BPS"
|
|
+ u16(1)
|
|
+ b"\0" * 6
|
|
+ u16(3)
|
|
+ u32(height)
|
|
+ u32(width)
|
|
+ u16(8)
|
|
+ u16(3)
|
|
)
|
|
|
|
psd = header + u32(0) + u32(0) + u32(len(layer_and_mask)) + layer_and_mask + merged_image_data
|
|
OUTPUT_PSD.write_bytes(psd)
|
|
|
|
manifest = {
|
|
"output": OUTPUT_PSD.name,
|
|
"canvas": {"width": width, "height": height},
|
|
"layers_bottom_to_top": [
|
|
{"name": name, "x": x, "y": y, "width": image.size[0], "height": image.size[1]}
|
|
for name, image, x, y in layers
|
|
],
|
|
}
|
|
(LAYER_DIR / "layered_psd_manifest.json").write_text(
|
|
json.dumps(manifest, ensure_ascii=False, indent=2),
|
|
encoding="utf-8",
|
|
)
|
|
try:
|
|
from psd_tools import PSDImage
|
|
|
|
psd = PSDImage.new("RGB", (width, height), color=(0, 0, 0))
|
|
for name, image, x, y in layers:
|
|
psd.create_pixel_layer(image, name=name, top=y, left=x)
|
|
psd.save(OUTPUT_PSDTOOLS_PSD)
|
|
print(OUTPUT_PSDTOOLS_PSD)
|
|
except ImportError:
|
|
print("psd-tools is not installed; skipped psd-tools PSD output.")
|
|
|
|
print(OUTPUT_PSD)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|