Files
greyhaven-design-system/htmx-demo/compare.py

101 lines
3.0 KiB
Python

#!/usr/bin/env python3
"""
Image comparator for React vs HTMX showcase validation.
Compares two PNG/WEBP screenshots and produces:
1. Similarity percentage (pixels within tolerance / total pixels)
2. A diff image with mismatches highlighted magenta on a faded background
Tolerance is per-channel: anti-aliasing / sub-pixel hinting is accepted
(default 12 of 255 per channel, tweakable via --tol). Font / layout / color
changes produce large regions of divergence that will exceed the tolerance.
Usage:
python3 compare.py react.webp htmx.webp [--out diff.webp] [--tol 12]
"""
import argparse
import sys
from pathlib import Path
from PIL import Image, ImageChops, ImageDraw
def load(path):
img = Image.open(path).convert("RGB")
return img
def compare(react_path, htmx_path, out_path, tol):
a = load(react_path)
b = load(htmx_path)
if a.size != b.size:
# Pad the smaller one with transparent/white so we can still diff
w, h = max(a.width, b.width), max(a.height, b.height)
pad_a = Image.new("RGB", (w, h), (255, 255, 255))
pad_b = Image.new("RGB", (w, h), (255, 255, 255))
pad_a.paste(a, (0, 0))
pad_b.paste(b, (0, 0))
a, b = pad_a, pad_b
size_mismatch = True
else:
size_mismatch = False
diff = ImageChops.difference(a, b)
# Per-pixel max channel diff
total = a.width * a.height
differing = 0
mask = Image.new("L", a.size, 0)
mask_pixels = mask.load()
diff_pixels = diff.load()
for y in range(a.height):
for x in range(a.width):
r, g, bl = diff_pixels[x, y]
if max(r, g, bl) > tol:
differing += 1
mask_pixels[x, y] = 255
similarity = 100.0 * (total - differing) / total
# Build diff image: React screenshot faded 50%, with diffs in magenta
faded = Image.eval(a, lambda v: int(v * 0.4 + 0.6 * 255))
magenta = Image.new("RGB", a.size, (255, 0, 180))
out = Image.composite(magenta, faded, mask)
# Add a header text
draw = ImageDraw.Draw(out)
header = (
f"similarity={similarity:.2f}% "
f"differing={differing}/{total} "
f"tol={tol}"
+ (" (SIZE MISMATCH — padded)" if size_mismatch else "")
)
draw.rectangle([0, 0, a.width, 24], fill=(0, 0, 0))
draw.text((8, 4), header, fill=(255, 255, 255))
out.save(out_path)
return similarity, differing, total
def main():
p = argparse.ArgumentParser()
p.add_argument("react")
p.add_argument("htmx")
p.add_argument("--out", default="diff.webp")
p.add_argument("--tol", type=int, default=12)
args = p.parse_args()
sim, diff_px, total = compare(args.react, args.htmx, args.out, args.tol)
print(f"react = {args.react}")
print(f"htmx = {args.htmx}")
print(f"diff -> {args.out}")
print(f" similarity = {sim:.2f}% ({diff_px} / {total} pixels differ > {args.tol})")
if sim < 99.5:
sys.exit(1)
sys.exit(0)
if __name__ == "__main__":
main()