Preserve full opaque HD backgrounds and source-supported small cues
This commit is contained in:
1 parent
a02c9b0679
commit
e95d060b0c
13 files changed
+1022
-33
No files matched your search
+23
-4
@@ -12,6 +12,7 @@ import struct
|
||||
from pathlib import Path
|
||||
from PIL import Image
|
||||
from hd_art_source import decode
|
||||
from hd_frame_policy import FRAME_KEYS, validate_frame_policy
|
||||
|
||||
CATEGORIES = ["Player banking", "Bosses", "Enemies and ground objects", "Player and enemy projectiles", "Explosions and effects", "Equipment and pickups", "Terrain", "Menus, portraits, HUD and other pictures"]
|
||||
|
||||
@@ -23,8 +24,16 @@ def checked_palette(payload):
|
||||
raise ValueError("invalid VGA palette")
|
||||
return [tuple((min(v, 63) << 2) | (min(v, 63) >> 4) for v in payload[n:n + 3]) for n in range(0, 768, 3)]
|
||||
|
||||
def integer_rounded_aspect(width, height, logical_width, logical_height):
|
||||
"""One common scale rounded by less than half a pixel on each axis."""
|
||||
return min(width, height, logical_width, logical_height) > 0 and (
|
||||
2 * abs(width * logical_height - height * logical_width)
|
||||
< logical_width + logical_height
|
||||
)
|
||||
|
||||
def validate_hd_frame(root, frame, payload, picture):
|
||||
"""Mirror loader checks; artistic approval is deliberately a separate step."""
|
||||
validate_frame_policy(frame, payload)
|
||||
if frame["source_sha256"] != hashlib.sha256(payload).hexdigest():
|
||||
raise ValueError("original source hash changed")
|
||||
if [frame["logical_width"], frame["logical_height"]] != list(picture.size):
|
||||
@@ -45,17 +54,26 @@ def validate_hd_frame(root, frame, payload, picture):
|
||||
if max(source.size) > 4096 or source.width * source.height > 4_194_304:
|
||||
raise ValueError("PNG dimensions exceed runtime limit")
|
||||
image = source.convert("RGBA")
|
||||
png_size = image.size
|
||||
source_alpha_min, source_alpha_max = picture.getchannel("A").getextrema()
|
||||
if frame.get("crop") is not None:
|
||||
x, y, width, height = frame["crop"]
|
||||
if min(x, y) < 0 or min(width, height) <= 0 or x + width > image.width or y + height > image.height:
|
||||
raise ValueError("atlas crop exceeds PNG")
|
||||
image = image.crop((x, y, x + width, y + height))
|
||||
if image.width * picture.height != image.height * picture.width:
|
||||
raise ValueError("PNG aspect differs from original canvas")
|
||||
rounded = image.width * picture.height != image.height * picture.width
|
||||
if rounded and not (
|
||||
source_alpha_min == source_alpha_max == 255
|
||||
and image.size == png_size
|
||||
and integer_rounded_aspect(image.width, image.height, picture.width, picture.height)
|
||||
):
|
||||
raise ValueError("PNG aspect differs from original canvas; only a full opaque frame may use subpixel integer rounding")
|
||||
if image.width < picture.width * 2 or image.height < picture.height * 2:
|
||||
raise ValueError("artwork needs at least twice the logical resolution")
|
||||
alpha_min, alpha_max = image.getchannel("A").getextrema()
|
||||
if picture.getchannel("A").getextrema()[0] == 0 and alpha_min != 0:
|
||||
if rounded and (alpha_min, alpha_max) != (255, 255):
|
||||
raise ValueError("HD integer-aspect rounding requires a fully opaque source and PNG")
|
||||
if source_alpha_min == 0 and alpha_min != 0:
|
||||
raise ValueError("transparent original needs a transparent HD background")
|
||||
if alpha_max < 128:
|
||||
raise ValueError("artwork has no visible pixels")
|
||||
@@ -176,8 +194,9 @@ def audit(data, project, runtime_path=None):
|
||||
if not 1 <= sequence["frame_count"] <= 512 or len(sequence["frames"]) > sequence["frame_count"]:
|
||||
raise ValueError("HD pack has invalid frame count")
|
||||
for frame in sequence["frames"]:
|
||||
if set(frame) - {"frame","file","logical_width","logical_height","origin","source_sha256","crop"}:
|
||||
if set(frame) - FRAME_KEYS:
|
||||
raise ValueError("unknown HD frame fields")
|
||||
validate_frame_policy(frame)
|
||||
asset = sequence["base_asset"] + frame["frame"]
|
||||
if not 0 <= frame["frame"] < sequence["frame_count"] or asset in supplied:
|
||||
raise ValueError("HD pack has duplicate ID or invalid frame index")
|
||||
|
||||
@@ -15,7 +15,7 @@ from pathlib import Path
|
||||
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
FRAME_KEYS = {"frame", "file", "logical_width", "logical_height", "origin", "source_sha256", "crop"}
|
||||
from hd_frame_policy import FRAME_KEYS, validate_frame_policy
|
||||
|
||||
|
||||
def proposal_frames(proposal, expected=None):
|
||||
@@ -30,6 +30,7 @@ def proposal_frames(proposal, expected=None):
|
||||
for frame in sequence["frames"]:
|
||||
if set(frame) - FRAME_KEYS:
|
||||
raise ValueError("unknown manifest frame fields")
|
||||
validate_frame_policy(frame)
|
||||
if not 0 <= frame["frame"] < sequence["frame_count"]:
|
||||
raise ValueError("frame outside sequence")
|
||||
identity = f"{base + frame['frame']:05x}"
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
"""Strict optional HD presentation metadata; source fidelity requires peer review."""
|
||||
import struct
|
||||
|
||||
FRAME_KEYS = {"frame", "file", "logical_width", "logical_height", "origin",
|
||||
"source_sha256", "crop", "preserve_reserved_coverage", "native_nearest_cues"}
|
||||
|
||||
def _indices(payload):
|
||||
if len(payload) < 20:
|
||||
raise ValueError("truncated original picture")
|
||||
kind, _, _, width, height = struct.unpack_from("<iiiii", payload)
|
||||
if not (1 <= width <= 4096 and 1 <= height <= 4096 and width * height <= 1_048_576):
|
||||
raise ValueError("invalid original canvas")
|
||||
if kind == 1:
|
||||
if len(payload) < 20 + width * height:
|
||||
raise ValueError("truncated original raw picture")
|
||||
return width, height, payload[20:20 + width * height]
|
||||
if kind != 0:
|
||||
raise ValueError("unsupported original picture type")
|
||||
out = [256] * (width * height)
|
||||
off = 20
|
||||
runs = 0
|
||||
while True:
|
||||
if off + 16 > len(payload):
|
||||
raise ValueError("truncated original picture span")
|
||||
x, y, dest, length = struct.unpack_from("<iiii", payload, off)
|
||||
off += 16
|
||||
if dest == -1:
|
||||
break
|
||||
if not (x >= 0 and 0 <= y < height and length > 0 and x + length <= width
|
||||
and off + length <= len(payload)):
|
||||
raise ValueError("invalid original picture span")
|
||||
start = y * width + x
|
||||
out[start:start + length] = payload[off:off + length]
|
||||
off += length
|
||||
runs += 1
|
||||
if runs > len(out):
|
||||
raise ValueError("too many original picture spans")
|
||||
return width, height, out
|
||||
|
||||
def validate_frame_policy(frame, source_payload=None):
|
||||
if set(frame) - FRAME_KEYS:
|
||||
raise ValueError("unknown HD frame fields")
|
||||
if type(frame.get("preserve_reserved_coverage", False)) is not bool:
|
||||
raise ValueError("preserve_reserved_coverage must be a boolean")
|
||||
cues = frame.get("native_nearest_cues", [])
|
||||
if not isinstance(cues, list) or len(cues) > 64:
|
||||
raise ValueError("native_nearest_cues must contain at most64 cells")
|
||||
width, height = frame["logical_width"], frame["logical_height"]
|
||||
if type(width) is not int or type(height) is not int or not (0 < width < 2**32 and 0 < height < 2**32):
|
||||
raise ValueError("logical dimensions must be nonzero u32")
|
||||
previous = None
|
||||
for cue in cues:
|
||||
if not isinstance(cue, list) or len(cue) != 2 or any(type(n) is not int or not 0 <= n < 2**32 for n in cue):
|
||||
raise ValueError("native nearest cue must be a pair of u32 coordinates")
|
||||
if cue[0] >= width or cue[1] >= height:
|
||||
raise ValueError("native nearest cue outside original logical canvas")
|
||||
key = tuple(cue)
|
||||
if previous is not None and key <= previous:
|
||||
raise ValueError("native nearest cues must be sorted and unique")
|
||||
previous = key
|
||||
if source_payload is not None and cues:
|
||||
source_width, source_height, indices = _indices(source_payload)
|
||||
if (source_width, source_height) != (width, height):
|
||||
raise ValueError("logical canvas differs from original")
|
||||
if any(index == 256 for index in indices):
|
||||
raise ValueError("native nearest cues require an opaque original canvas")
|
||||
if any(indices[y * width + x] >= 240 for x, y in cues):
|
||||
raise ValueError("native nearest cue must be an ordinary original source cell")
|
||||
Reference in new issue
Block a user