"""Deterministic PNG quality checks for generated design assets.""" from pathlib import Path from typing import Any from PIL import Image def _outer_ring_pixels(image: Image.Image, thickness: int): width, height = image.size thickness = max(0, min(thickness, width // 2, height // 2)) for y in range(height): for x in range(width): if x < thickness or x >= width - thickness or y < thickness or y >= height - thickness: yield image.getpixel((x, y)) def analyze_asset(path: Path, spec: dict[str, Any]) -> dict[str, Any]: """Analyze one PNG against its manifest v2 specification.""" path = Path(path) errors: list[str] = [] warnings: list[str] = [] metrics: dict[str, int] = {} with Image.open(path) as opened: width, height = opened.size mode = opened.mode info = dict(opened.info) has_alpha = "A" in opened.getbands() or "transparency" in opened.info image = opened.convert("RGBA") expected = spec["outputPixels"] if (width, height) != (expected["width"], expected["height"]): errors.append( f"dimensions expected {expected['width']}x{expected['height']}, got {width}x{height}" ) alpha = spec.get("alpha", {}) if alpha.get("required") and not has_alpha: errors.append(f"alpha-required asset has no alpha channel or transparency table, got {mode}") padding = int(alpha.get("transparentOuterPadding", 0)) max_alpha = int(alpha.get("cornerMaxAlpha", 0)) opaque_padding = sum(1 for pixel in _outer_ring_pixels(image, padding) if pixel[3] > max_alpha) metrics["outerPaddingViolations"] = opaque_padding if opaque_padding: errors.append(f"outer padding contains {opaque_padding} pixels above alpha {max_alpha}") edge = spec.get("edge", {}) pixels = list(image.get_flattened_data()) chroma_residue = sum( 1 for red, green, blue, pixel_alpha in pixels if pixel_alpha > 8 and green > 120 and green - max(red, blue) > 80 ) metrics["chromaResiduePixels"] = chroma_residue if edge.get("forbidChromaResidue") and chroma_residue: errors.append(f"chroma residue detected in {chroma_residue} visible pixels") light_fringe = sum( 1 for red, green, blue, pixel_alpha in pixels if 0 < pixel_alpha < 255 and red > 235 and green > 235 and blue > 235 ) metrics["lightFringePixels"] = light_fringe if edge.get("forbidLightFringe") and light_fringe: errors.append(f"light fringe detected in {light_fringe} partially transparent pixels") transparent_rgb = sum( 1 for red, green, blue, pixel_alpha in pixels if pixel_alpha == 0 and (red != 0 or green != 0 or blue != 0) ) metrics["transparentRgbPixels"] = transparent_rgb if edge.get("premultipliedAlphaCheck") and transparent_rgb: errors.append(f"transparent RGB detected in {transparent_rgb} fully transparent pixels") byte_size = path.stat().st_size max_bytes = int(spec.get("quality", {}).get("maxBytes", 0)) if max_bytes and byte_size > max_bytes: errors.append(f"maxBytes {max_bytes} exceeded by file size {byte_size}") expected_color_space = spec.get("quality", {}).get("colorSpace") if expected_color_space == "sRGB" and not ("srgb" in info or "icc_profile" in info): warnings.append("PNG does not declare an sRGB chunk or ICC profile") return { "id": spec.get("id", path.stem), "path": str(path), "width": width, "height": height, "mode": mode, "bytes": byte_size, "errors": errors, "warnings": warnings, "metrics": metrics, }