diff --git a/scripts/dev/test_pillow_compat.py b/scripts/dev/test_pillow_compat.py new file mode 100755 index 00000000..ce0dfdd6 --- /dev/null +++ b/scripts/dev/test_pillow_compat.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 +""" +Pillow compatibility smoke test. + +Exercises the Pillow APIs used throughout LEDMatrix to verify a new +Pillow version doesn't break image rendering, font handling, or resize ops. + +Run after upgrading Pillow: + python3 scripts/dev/test_pillow_compat.py +""" + +import sys + + +def check(label, fn): + try: + result = fn() + print(f" ✓ {label}" + (f" — {result}" if result is not None else "")) + return True + except Exception as e: + print(f" ✗ {label} — {type(e).__name__}: {e}", file=sys.stderr) + return False + + +def main(): + from PIL import Image, ImageDraw, ImageFont, ImageFilter + import PIL + + print(f"Pillow {PIL.__version__} on Python {sys.version.split()[0]}\n") + + failures = 0 + + print("Image creation:") + failures += not check("Image.new RGB", + lambda: Image.new('RGB', (128, 32), (0, 0, 0)).size) + failures += not check("Image.new RGBA", + lambda: Image.new('RGBA', (64, 64), (255, 0, 0, 128)).size) + failures += not check("Image.new 1-bit", + lambda: Image.new('1', (16, 16)).size) + + print("\nDraw operations:") + img = Image.new('RGB', (128, 32), (0, 0, 0)) + draw = ImageDraw.Draw(img) + font = ImageFont.load_default() + failures += not check("draw.rectangle", + lambda: draw.rectangle([0, 0, 127, 31], outline=(255, 0, 0))) + failures += not check("draw.text", + lambda: draw.text((2, 2), "Hello", fill=(255, 255, 255), font=font)) + failures += not check("draw.line", + lambda: draw.line([0, 0, 127, 31], fill=(0, 255, 0))) + + print("\nFont metrics (used in text_helper, scroll_helper):") + failures += not check("draw.textlength", + lambda: f"{draw.textlength('Test', font=font):.1f}px") + failures += not check("draw.textbbox", + lambda: draw.textbbox((0, 0), "Test", font=font)) + + print("\nResampling (used in logo_helper, image_utils, sports base):") + logo = Image.new('RGBA', (200, 200), (255, 128, 0, 200)) + failures += not check("Image.Resampling.LANCZOS exists", + lambda: str(Image.Resampling.LANCZOS)) + failures += not check("thumbnail with LANCZOS", + lambda: (logo.thumbnail((64, 32), Image.Resampling.LANCZOS), logo.size)[1]) + big = Image.new('RGB', (300, 300), (0, 128, 255)) + failures += not check("resize with LANCZOS", + lambda: big.resize((128, 32), Image.Resampling.LANCZOS).size) + + print("\nComposite / paste (used in display rendering):") + base = Image.new('RGB', (128, 32), (0, 0, 0)) + overlay = Image.new('RGBA', (32, 32), (255, 0, 0, 128)) + failures += not check("paste RGBA onto RGB", + lambda: (base.paste(overlay.convert('RGB'), (0, 0)), base.size)[1]) + failures += not check("Image.alpha_composite", + lambda: Image.alpha_composite( + Image.new('RGBA', (32, 32)), overlay).size) + + print("\nImage I/O:") + import io + buf = io.BytesIO() + img.save(buf, format='PNG') + buf.seek(0) + failures += not check("save/load PNG roundtrip", + lambda: Image.open(buf).size) + + print() + if failures == 0: + print(f"All checks passed. Pillow {PIL.__version__} is compatible.") + return 0 + else: + print(f"{failures} check(s) failed — review output above.", file=sys.stderr) + return 1 + + +if __name__ == '__main__': + sys.exit(main())