90 lines
3.3 KiB
Python
90 lines
3.3 KiB
Python
"""Rebuild A01 button skins with deterministic horizontal three-slice scaling."""
|
|
|
|
import argparse
|
|
import json
|
|
from pathlib import Path
|
|
|
|
from PIL import Image, PngImagePlugin
|
|
|
|
|
|
def rebuild_button(
|
|
source_path: Path,
|
|
output_path: Path,
|
|
*,
|
|
output_size: tuple[int, int],
|
|
trim: int,
|
|
padding: int,
|
|
cap_width: int,
|
|
) -> None:
|
|
"""Trim contaminated edges, preserve both caps, and stretch only the center."""
|
|
with Image.open(source_path) as opened:
|
|
source = opened.convert("RGBA")
|
|
|
|
if trim < 0 or trim * 2 >= min(source.size):
|
|
raise ValueError(f"invalid trim {trim} for source size {source.size}")
|
|
if trim:
|
|
source = source.crop((trim, trim, source.width - trim, source.height - trim))
|
|
|
|
if cap_width <= 0 or cap_width * 2 >= source.width:
|
|
raise ValueError(f"invalid capWidth {cap_width} for cropped width {source.width}")
|
|
|
|
output_width, output_height = output_size
|
|
inner_width = output_width - padding * 2
|
|
inner_height = output_height - padding * 2
|
|
if inner_width <= 0 or inner_height <= 0:
|
|
raise ValueError(f"padding {padding} leaves no drawable area in {output_size}")
|
|
|
|
target_cap_width = max(1, round(cap_width * inner_height / source.height))
|
|
if target_cap_width * 2 >= inner_width:
|
|
raise ValueError("scaled caps leave no room for the center slice")
|
|
|
|
left = source.crop((0, 0, cap_width, source.height))
|
|
center = source.crop((cap_width, 0, source.width - cap_width, source.height))
|
|
right = source.crop((source.width - cap_width, 0, source.width, source.height))
|
|
|
|
resampling = Image.Resampling.LANCZOS
|
|
left = left.resize((target_cap_width, inner_height), resampling)
|
|
center = center.resize((inner_width - target_cap_width * 2, inner_height), resampling)
|
|
right = right.resize((target_cap_width, inner_height), resampling)
|
|
|
|
output = Image.new("RGBA", output_size, (0, 0, 0, 0))
|
|
output.alpha_composite(left, (padding, padding))
|
|
output.alpha_composite(center, (padding + target_cap_width, padding))
|
|
output.alpha_composite(right, (output_width - padding - target_cap_width, padding))
|
|
|
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
png_info = PngImagePlugin.PngInfo()
|
|
png_info.add(b"sRGB", b"\x00")
|
|
output.save(output_path, format="PNG", optimize=True, pnginfo=png_info)
|
|
|
|
|
|
def rebuild_manifest(manifest_path: Path, workspace_root: Path) -> None:
|
|
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
|
for asset in manifest["assets"]:
|
|
processing = asset["processing"]
|
|
alpha = asset["alpha"]
|
|
pixels = asset["outputPixels"]
|
|
source = workspace_root / asset["source"]
|
|
output = workspace_root / asset["output"]
|
|
rebuild_button(
|
|
source,
|
|
output,
|
|
output_size=(pixels["width"], pixels["height"]),
|
|
trim=processing["trim"],
|
|
padding=alpha["transparentOuterPadding"],
|
|
cap_width=processing["capWidth"],
|
|
)
|
|
print(f"REBUILT {asset['id']} -> {asset['output']}")
|
|
|
|
|
|
def main() -> None:
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("manifest", type=Path)
|
|
parser.add_argument("--workspace", type=Path, required=True)
|
|
args = parser.parse_args()
|
|
rebuild_manifest(args.manifest.resolve(), args.workspace.resolve())
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|