"""Encode the supplied Freddy iris as a 178 px LVGL RGB565+alpha image. The source WebP stays unchanged. Run from the project root with the bundled Python (Pillow required): python firmware/tools/make_iris.py """ from pathlib import Path from PIL import Image, ImageDraw import hashlib import math ROOT = Path(__file__).resolve().parents[2] SOURCE = ROOT / "reference_images/official_freddy_eye_texture.webp" OUTPUT = ROOT / "firmware/eye_test/FreddyIris.cpp" PREVIEW = ROOT / "firmware/assets/freddy_iris_preview.png" DISPLAY_PREVIEW = ROOT / "firmware/assets/freddy_eye_display_preview.png" BLINK_PREVIEW = ROOT / "firmware/assets/freddy_eye_blink_preview.png" RED_PREVIEW = ROOT / "firmware/assets/freddy_eye_red_preview.png" SIZE = 178 def main() -> None: source = Image.open(SOURCE).convert("RGB") if source.size != (508, 466): raise ValueError(f"Unexpected source size: {source.size}") # The artwork's painted disc spans roughly x=159..372, y=126..337. # Keep an equal square around that disc, then scale to the renderer's iris. iris = source.crop((159, 125, 373, 339)).resize( (SIZE, SIZE), Image.Resampling.LANCZOS ) preview = Image.new("RGBA", (SIZE, SIZE)) encoded = bytearray() radius = SIZE / 2 for y in range(SIZE): for x in range(SIZE): r, g, b = iris.getpixel((x, y)) distance = math.hypot(x + 0.5 - radius, y + 0.5 - radius) # Two-pixel feather removes the source's white rectangular backing. alpha = round(max(0.0, min(1.0, (radius - distance) / 2.0)) * 255) preview.putpixel((x, y), (r, g, b, alpha)) rgb565 = ((r >> 3) << 11) | ((g >> 2) << 5) | (b >> 3) encoded.extend((rgb565 & 0xFF, rgb565 >> 8, alpha)) PREVIEW.parent.mkdir(parents=True, exist_ok=True) preview.save(PREVIEW) display = Image.new("RGBA", (480, 480), (57, 39, 29, 255)) draw = ImageDraw.Draw(display) draw.ellipse((18, 18, 461, 461), fill=(116, 56, 36, 255)) draw.ellipse((24, 24, 455, 455), fill=(245, 244, 234, 255)) display.alpha_composite(preview, (151, 151)) DISPLAY_PREVIEW.parent.mkdir(parents=True, exist_ok=True) display.save(DISPLAY_PREVIEW) blink = display.copy() blink_draw = ImageDraw.Draw(blink) blink_draw.rectangle((0, 0, 479, 139), fill=(116, 56, 36, 255)) blink_draw.rectangle((0, 340, 479, 479), fill=(116, 56, 36, 255)) blink.save(BLINK_PREVIEW) red = Image.new("RGBA", (480, 480), (57, 39, 29, 255)) red_draw = ImageDraw.Draw(red) red_draw.ellipse((18, 18, 461, 461), fill=(116, 56, 36, 255)) red_draw.ellipse((24, 24, 455, 455), fill=(33, 25, 27, 255)) for diameter, color, alpha in ( (178, (21, 9, 9), 255), (144, (110, 11, 8), 34), (116, (190, 27, 19), 55), (82, (226, 45, 30), 90), (48, (250, 65, 43), 165), (28, (255, 58, 43), 255), (13, (255, 242, 227), 255), ): layer = Image.new("RGBA", red.size) offset = (480 - diameter) // 2 ImageDraw.Draw(layer).ellipse( (offset, offset, offset + diameter - 1, offset + diameter - 1), fill=(*color, alpha), ) red = Image.alpha_composite(red, layer) red.save(RED_PREVIEW) lines = [ "// Generated from reference_images/official_freddy_eye_texture.webp.", f"// Source SHA-256: {hashlib.sha256(SOURCE.read_bytes()).hexdigest()}", "// Regenerate with firmware/tools/make_iris.py; do not edit this array.", '#include "FreddyIris.h"', "", "static const uint8_t kFreddyIrisPixels[] = {", ] for offset in range(0, len(encoded), 18): lines.append( " " + ", ".join(f"0x{byte:02x}" for byte in encoded[offset : offset + 18]) + "," ) lines += [ "};", "", "const lv_img_dsc_t kFreddyIris = {", " .header = {.cf = LV_IMG_CF_TRUE_COLOR_ALPHA, .always_zero = 0,", f" .reserved = 0, .w = {SIZE}, .h = {SIZE}}},", " .data_size = sizeof(kFreddyIrisPixels),", " .data = kFreddyIrisPixels,", "};", ] OUTPUT.write_text("\n".join(lines) + "\n") print(f"Wrote {OUTPUT} ({len(encoded):,} image bytes) and previews in {PREVIEW.parent}") if __name__ == "__main__": main()