视觉审核完成30%
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
"""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,
|
||||
}
|
||||
Reference in New Issue
Block a user