Support transparent full-canvas HD rounding and rebuild runtimes
Accept strict common-scale integer rounding for complete transparent PNG canvases while retaining source-aware alpha checks and existing opaque/exact cache identities. Add separate cache identity for newly eligible rounded transparent frames and matching Rust/Python regression coverage. Bundle rebuilt Linux and GNU Windows executables against the unchanged reviewed4978 asset pack. Validation: 31 Rust HD tests, 7 Python audit tests, and 341 portable tests passed; zero failures. Original-data integration validated all4978 remasters at scales1–4 with bounded caches. Three fresh software SDL1920x1080 menu/mission captures are pixel-identical to published controls. Windows PE/dependency and complete shared data hashes verified; fresh Wine asset and45-mod validation passed. Artwork geometry remains independently reviewed; no held candidate is admitted by the format change.
This commit is contained in:
1 parent
e6a3d552f5
commit
5956d17785
8 files changed
+404
-25
No files matched your search
@@ -63,15 +63,14 @@ def validate_hd_frame(root, frame, payload, picture):
|
||||
image = image.crop((x, y, x + width, y + height))
|
||||
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
|
||||
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")
|
||||
raise ValueError("PNG aspect differs from original canvas; only a full 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 rounded and (alpha_min, alpha_max) != (255, 255):
|
||||
if rounded and source_alpha_min != 0 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")
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
"""Full-canvas integer rounding eligibility; this is not artwork approval."""
|
||||
import hashlib
|
||||
import struct
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from PIL import Image
|
||||
from hd_art_audit import integer_rounded_aspect, validate_hd_frame
|
||||
from hd_art_source import decode
|
||||
|
||||
|
||||
class FullCanvasRoundingAuditTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
temporary = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(temporary.cleanup)
|
||||
self.root = Path(temporary.name)
|
||||
|
||||
def case(self, logical=(8, 5), natural=(101, 63), transparent=True):
|
||||
width, height = logical
|
||||
payload = bytearray(struct.pack("<iiiii", 0 if transparent else 1, 0, 0, width, height))
|
||||
if transparent:
|
||||
# Real legacy span payload with both occupied and empty source cells.
|
||||
start = 1 if width > 2 else 0
|
||||
length = max(1, width - 2)
|
||||
rows = range(1, height - 1) if height > 2 else range(1)
|
||||
for y in rows:
|
||||
payload.extend(struct.pack("<iiii", start, y, y * width + start, length))
|
||||
payload.extend(bytes([7] * length))
|
||||
payload.extend(struct.pack("<iiii", 0, 0, -1, 0))
|
||||
else:
|
||||
payload.extend(bytes([7] * (width * height)))
|
||||
payload = bytes(payload)
|
||||
source = self.root / "source.bin"
|
||||
source.write_bytes(payload)
|
||||
picture = decode(source, [(80, 100, 120)] * 256)
|
||||
raw = Image.new("RGBA", natural, (80, 100, 120, 0 if transparent else 255))
|
||||
if transparent:
|
||||
x0, y0 = natural[0] // 4, natural[1] // 4
|
||||
raw.paste((80, 100, 120, 255), (x0, y0, natural[0] - x0, natural[1] - y0))
|
||||
raw.save(self.root / "ship.png")
|
||||
frame = {"frame": 0, "file": "ship.png", "logical_width": width,
|
||||
"logical_height": height, "origin": [0, 0],
|
||||
"source_sha256": hashlib.sha256(payload).hexdigest()}
|
||||
return frame, payload, picture
|
||||
|
||||
def validate(self, frame, payload, picture):
|
||||
return validate_hd_frame(self.root, frame, payload, picture)
|
||||
|
||||
def test_full_canvas_and_explicit_full_crop_accept_both_original_alpha_classes(self):
|
||||
for logical, natural in [((8, 5), (101, 63)), ((5, 8), (63, 101)),
|
||||
((40, 32), (1402, 1122))]:
|
||||
for transparent in [False, True]:
|
||||
with self.subTest(logical=logical, natural=natural, transparent=transparent):
|
||||
frame, payload, picture = self.case(logical, natural, transparent)
|
||||
expected = min(natural[0], logical[0] * 4) * min(natural[1], logical[1] * 4) * 4
|
||||
self.assertEqual(self.validate(frame, payload, picture), expected)
|
||||
frame["crop"] = [0, 0, *natural]
|
||||
self.assertEqual(self.validate(frame, payload, picture), expected)
|
||||
|
||||
def test_inexact_partial_crop_rejected_but_exact_partial_crop_unchanged(self):
|
||||
for transparent in [False, True]:
|
||||
frame, payload, picture = self.case(transparent=transparent)
|
||||
for crop in [[0, 0, 100, 63], [1, 0, 100, 63], [0, 0, 101, 62]]:
|
||||
with self.subTest(transparent=transparent, crop=crop):
|
||||
changed = {**frame, "crop": crop}
|
||||
with self.assertRaisesRegex(ValueError, "aspect differs"):
|
||||
self.validate(changed, payload, picture)
|
||||
frame["crop"] = [1, 1, 80, 50]
|
||||
self.assertEqual(self.validate(frame, payload, picture), 32 * 20 * 4)
|
||||
|
||||
def test_strict_ties_aspect_and_minimum_resolution_are_not_relaxed(self):
|
||||
self.assertFalse(integer_rounded_aspect(5, 6, 2, 2))
|
||||
self.assertFalse(integer_rounded_aspect(0, 6, 2, 2))
|
||||
self.assertFalse(integer_rounded_aspect(5, 6, 0, 2))
|
||||
for logical, natural, message in [((2, 2), (5, 6), "aspect differs"),
|
||||
((8, 5), (101, 64), "aspect differs"),
|
||||
((8, 5), (15, 10), "twice")]:
|
||||
with self.subTest(logical=logical, natural=natural):
|
||||
frame, payload, picture = self.case(logical, natural)
|
||||
with self.assertRaisesRegex(ValueError, message):
|
||||
self.validate(frame, payload, picture)
|
||||
|
||||
def test_transparent_original_keeps_background_and_visible_alpha_checks(self):
|
||||
frame, payload, picture = self.case()
|
||||
for alpha, message in [(255, "transparent HD background"),
|
||||
(0, "no visible pixels"), (127, "no visible pixels")]:
|
||||
with self.subTest(alpha=alpha):
|
||||
raw = Image.new("RGBA", (101, 63), (80, 100, 120, alpha))
|
||||
if alpha == 127:
|
||||
raw.putpixel((0, 0), (0, 0, 0, 0))
|
||||
raw.save(self.root / "ship.png")
|
||||
with self.assertRaisesRegex(ValueError, message):
|
||||
self.validate(frame, payload, picture)
|
||||
|
||||
def test_rounded_opaque_original_still_requires_every_raw_alpha_to_be_255(self):
|
||||
frame, payload, picture = self.case(transparent=False)
|
||||
for alpha in [0, 128, 254]:
|
||||
with self.subTest(alpha=alpha):
|
||||
raw = Image.new("RGBA", (101, 63), (80, 100, 120, 255))
|
||||
raw.putpixel((100, 62), (80, 100, 120, alpha))
|
||||
raw.save(self.root / "ship.png")
|
||||
with self.assertRaisesRegex(ValueError, "fully opaque"):
|
||||
self.validate(frame, payload, picture)
|
||||
|
||||
def test_source_canvas_anchor_hash_path_and_crop_contracts_are_retained(self):
|
||||
frame, payload, picture = self.case()
|
||||
for fields, message in [({"logical_width": 9}, "logical canvas"),
|
||||
({"origin": [1, 0]}, "top-left anchor"),
|
||||
({"source_sha256": "changed"}, "source hash"),
|
||||
({"file": "../escape.png"}, "stay relative"),
|
||||
({"crop": [1, 0, 101, 63]}, "crop exceeds"),
|
||||
({"crop": [0, 0, 0, 63]}, "crop exceeds")]:
|
||||
with self.subTest(fields=fields):
|
||||
with self.assertRaisesRegex(ValueError, message):
|
||||
self.validate({**frame, **fields}, payload, picture)
|
||||
|
||||
def test_malformed_or_non_png_art_stays_rejected(self):
|
||||
frame, payload, picture = self.case()
|
||||
(self.root / "ship.png").write_bytes(b"not an image")
|
||||
with self.assertRaises(OSError):
|
||||
self.validate(frame, payload, picture)
|
||||
Image.new("RGB", (101, 63), (80, 100, 120)).save(self.root / "ship.png", format="JPEG")
|
||||
with self.assertRaisesRegex(ValueError, "must be PNG"):
|
||||
self.validate(frame, payload, picture)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user