99 lines
3.8 KiB
Python
99 lines
3.8 KiB
Python
#!/usr/bin/env python3
|
|
"""Verify fixed-size transparent PNG exports and detect green-key spill."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
from pathlib import Path
|
|
|
|
from PIL import Image
|
|
|
|
|
|
def parse_args() -> argparse.Namespace:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--input-dir", type=Path, required=True)
|
|
parser.add_argument("--output", type=Path, required=True)
|
|
parser.add_argument("--expected-count", type=int, required=True)
|
|
parser.add_argument("--visible-fraction-min", type=float)
|
|
parser.add_argument("--visible-fraction-max", type=float)
|
|
return parser.parse_args()
|
|
|
|
|
|
def inspect(
|
|
path: Path, fraction_min: float | None, fraction_max: float | None
|
|
) -> dict[str, object]:
|
|
image = Image.open(path).convert("RGBA")
|
|
width, height = image.size
|
|
pixels = list(image.getdata())
|
|
visible = [(r, g, b, a) for r, g, b, a in pixels if a > 0]
|
|
corners = [
|
|
image.getpixel((0, 0))[3],
|
|
image.getpixel((width - 1, 0))[3],
|
|
image.getpixel((0, height - 1))[3],
|
|
image.getpixel((width - 1, height - 1))[3],
|
|
]
|
|
# Aqua keeps blue close to green. A keyed-background remnant is instead
|
|
# strongly green-dominant over both neighboring channels.
|
|
green_spill = sum(1 for r, g, b, _ in visible if g - r > 50 and g - b > 50)
|
|
alpha_bound = image.getchannel("A").getbbox()
|
|
longest_fraction = 0.0
|
|
if alpha_bound:
|
|
longest_fraction = max(
|
|
alpha_bound[2] - alpha_bound[0], alpha_bound[3] - alpha_bound[1]
|
|
) / max(width, height)
|
|
errors: list[str] = []
|
|
expected = 128 if path.stem.endswith("@1x") else 256 if path.stem.endswith("@2x") else 0
|
|
if expected == 0 or image.size != (expected, expected):
|
|
errors.append(f"unexpected dimensions {image.size}")
|
|
if not visible:
|
|
errors.append("empty alpha content")
|
|
if any(corners):
|
|
errors.append(f"opaque corner alpha {corners}")
|
|
if green_spill:
|
|
errors.append(f"green spill pixels {green_spill}")
|
|
if fraction_min is not None and longest_fraction < fraction_min:
|
|
errors.append(f"visible fraction {longest_fraction:.4f} below {fraction_min:.4f}")
|
|
if fraction_max is not None and longest_fraction > fraction_max:
|
|
errors.append(f"visible fraction {longest_fraction:.4f} above {fraction_max:.4f}")
|
|
return {
|
|
"path": path.as_posix(),
|
|
"ok": not errors,
|
|
"errors": errors,
|
|
"dimensions": [width, height],
|
|
"corner_alpha": corners,
|
|
"visible_pixels": len(visible),
|
|
"content_bound": list(alpha_bound) if alpha_bound else None,
|
|
"longest_visible_fraction": round(longest_fraction, 6),
|
|
"green_spill_pixels": green_spill,
|
|
}
|
|
|
|
|
|
def main() -> None:
|
|
args = parse_args()
|
|
files = sorted(args.input_dir.glob("*.png"))
|
|
if (
|
|
args.visible_fraction_min is not None
|
|
and args.visible_fraction_max is not None
|
|
and args.visible_fraction_min > args.visible_fraction_max
|
|
):
|
|
raise SystemExit("visible fraction min cannot exceed max")
|
|
items = [
|
|
inspect(path, args.visible_fraction_min, args.visible_fraction_max)
|
|
for path in files
|
|
]
|
|
errors: list[str] = []
|
|
if len(files) != args.expected_count:
|
|
errors.append(f"expected {args.expected_count} PNGs, found {len(files)}")
|
|
errors.extend(error for item in items for error in item["errors"])
|
|
report = {"ok": not errors, "count": len(files), "errors": errors, "items": items}
|
|
args.output.parent.mkdir(parents=True, exist_ok=True)
|
|
args.output.write_text(json.dumps(report, indent=2) + "\n", encoding="utf-8")
|
|
if errors:
|
|
raise SystemExit("TRANSPARENT_ASSET_VERIFY_FAILED " + "; ".join(errors[:8]))
|
|
print(f"TRANSPARENT_ASSET_VERIFY_OK assets={len(files)} output={args.output}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|