start of testing and adding all display support

This commit is contained in:
2026-05-10 22:48:16 -04:00
parent 3a68e40ada
commit 746a2cc4aa
9 changed files with 928 additions and 32 deletions
+159
View File
@@ -0,0 +1,159 @@
#!/usr/bin/env python3
"""
compare.py <baseline.png> <candidate.png>
Pixel-level comparison of two PNG screenshots.
Exit codes:
0 images are identical (or within tolerance)
1 images differ beyond tolerance
2 usage / file error
Outputs a one-line summary and, on failure, saves a diff image alongside
the candidate as <candidate>_diff.png highlighting changed pixels in red.
"""
import struct
import sys
import zlib
TOLERANCE = 10 # per-channel delta considered noise
THRESHOLD = 0.01 # max fraction of differing pixels before failure (1 %)
# ── Minimal PNG reader ────────────────────────────────────────────────────────
def _read_chunk(data, pos):
length = struct.unpack_from('>I', data, pos)[0]
tag = data[pos+4:pos+8]
body = data[pos+8:pos+8+length]
pos += 12 + length
return tag, body, pos
def load_png(path):
with open(path, 'rb') as f:
raw = f.read()
assert raw[:8] == b'\x89PNG\r\n\x1a\n', f"{path}: not a PNG"
ihdr = width = height = None
idat_chunks = []
pos = 8
while pos < len(raw):
tag, body, pos = _read_chunk(raw, pos)
if tag == b'IHDR':
width, height = struct.unpack('>II', body[:8])
bit_depth, colour_type = body[8], body[9]
assert bit_depth == 8 and colour_type == 2, \
f"{path}: only 8-bit RGB PNGs supported"
elif tag == b'IDAT':
idat_chunks.append(body)
elif tag == b'IEND':
break
assert width and height, f"{path}: no IHDR"
raw_pixels = zlib.decompress(b''.join(idat_chunks))
# Reconstruct pixels via PNG filter (only filter 0 = None is used here)
stride = width * 3
pixels = []
idx = 0
prev_row = bytes(stride)
for _ in range(height):
filt = raw_pixels[idx]; idx += 1
row = bytearray(raw_pixels[idx:idx+stride]); idx += stride
if filt == 0:
pass
elif filt == 1: # Sub
for i in range(3, stride):
row[i] = (row[i] + row[i-3]) & 0xFF
elif filt == 2: # Up
for i in range(stride):
row[i] = (row[i] + prev_row[i]) & 0xFF
elif filt == 3: # Average
for i in range(stride):
a = row[i-3] if i >= 3 else 0
row[i] = (row[i] + (a + prev_row[i]) // 2) & 0xFF
elif filt == 4: # Paeth
for i in range(stride):
a = row[i-3] if i >= 3 else 0
b = prev_row[i]
c = prev_row[i-3] if i >= 3 else 0
p = a + b - c
pa = abs(p - a); pb = abs(p - b); pc = abs(p - c)
pr = a if pa <= pb and pa <= pc else (b if pb <= pc else c)
row[i] = (row[i] + pr) & 0xFF
prev_row = bytes(row)
pixels.append([tuple(row[i:i+3]) for i in range(0, stride, 3)])
return width, height, pixels
def save_png(path, pixels):
h = len(pixels)
w = len(pixels[0])
rows = bytearray()
for row in pixels:
rows.append(0) # filter = None
for p in row:
rows += bytearray(p)
def chunk(tag, data):
crc = zlib.crc32(tag + data) & 0xffffffff
return struct.pack('>I', len(data)) + tag + data + struct.pack('>I', crc)
out = b'\x89PNG\r\n\x1a\n'
out += chunk(b'IHDR', struct.pack('>IIBBBBB', w, h, 8, 2, 0, 0, 0))
out += chunk(b'IDAT', zlib.compress(bytes(rows), 9))
out += chunk(b'IEND', b'')
with open(path, 'wb') as f:
f.write(out)
# ── Comparison logic ──────────────────────────────────────────────────────────
def compare(baseline_path, candidate_path):
try:
bw, bh, base_px = load_png(baseline_path)
cw, ch, cand_px = load_png(candidate_path)
except Exception as e:
print(f"ERROR: {e}")
return 2
if bw != cw or bh != ch:
print(f"FAIL size mismatch: baseline={bw}×{bh} candidate={cw}×{ch}")
return 1
total = bw * bh
diff_px = 0
diff_img = [[(0, 0, 0)] * bw for _ in range(bh)]
for y in range(bh):
for x in range(bw):
br, bg, bb = base_px[y][x]
cr, cg, cb = cand_px[y][x]
if abs(br-cr) > TOLERANCE or abs(bg-cg) > TOLERANCE or abs(bb-cb) > TOLERANCE:
diff_px += 1
diff_img[y][x] = (220, 0, 0) # red highlight on diff image
else:
# Dim the matching pixel so diffs stand out
diff_img[y][x] = (cr//3, cg//3, cb//3)
frac = diff_px / total
if frac > THRESHOLD:
diff_path = candidate_path.replace('.png', '_diff.png')
save_png(diff_path, diff_img)
print(f"FAIL {diff_px}/{total} pixels differ ({frac:.2%}) "
f"— diff saved to {diff_path}")
return 1
else:
print(f"PASS {diff_px}/{total} pixels differ ({frac:.2%}) "
f"[tolerance ≤{THRESHOLD:.0%}]")
return 0
if __name__ == '__main__':
if len(sys.argv) != 3:
print(f"Usage: {sys.argv[0]} <baseline.png> <candidate.png>")
sys.exit(2)
sys.exit(compare(sys.argv[1], sys.argv[2]))