"""Compose the boot splash: chroma-keyed mascot plus VT323 "Mat Games" lettering. The raw mascot comes from an image generator on a flat magenta background (generators do not emit real alpha). This script keys that background to transparency, removes magenta spill, then draws the title with the OFL-licensed VT323 font and saves a transparent PNG ready for application/boot_splash/image. """ import argparse import hashlib import json import pathlib from PIL import Image, ImageDraw, ImageFilter, ImageFont parser = argparse.ArgumentParser() parser.add_argument('raw', type=pathlib.Path, help='raw generated image (magenta background)') parser.add_argument('--title', default='MatGames') parser.add_argument('--canvas', type=int, default=1024) parser.add_argument('--title-height', type=int, default=190) parser.add_argument('--bottom-fade', type=int, default=50, help='soften the generator frame cut at the bottom of the bust') parser.add_argument('--colors', type=int, default=256, help='palette colors for the shipped PNG; 0 keeps 32-bit RGBA') parser.add_argument('--generated-from', type=pathlib.Path, default=None, help='pre-edit generation this file was derived from, for provenance') args = parser.parse_args() args.raw = args.raw.resolve() root = pathlib.Path(__file__).resolve().parents[1] branding = root / 'assets/branding' branding.mkdir(parents=True, exist_ok=True) source_tools = root / 'tools/branding' font_path = source_tools / 'fonts/VT323-Regular.ttf' # ---------------------------------------------------------------- chroma key source = Image.open(args.raw).convert('RGB') corners = [source.getpixel(p) for p in [(2, 2), (source.width - 3, 2), (2, source.height - 3), (source.width - 3, source.height - 3)]] background = tuple(sorted(channel)[1] for channel in zip(*corners)) # median per channel T0, T1 = 55, 130 alpha = Image.new('L', source.size) spill = Image.new('L', source.size) pixels = source.load() alpha_pixels = alpha.load() spill_pixels = spill.load() for y in range(source.height): for x in range(source.width): r, g, b = pixels[x, y] distance = ((r - background[0]) ** 2 + (g - background[1]) ** 2 + (b - background[2]) ** 2) ** 0.5 alpha_pixels[x, y] = max(0, min(255, round((distance - T0) / (T1 - T0) * 255))) # Magenta contamination raises both red and blue above green. spill_pixels[x, y] = max(0, min(r, b) - g) mask = alpha.filter(ImageFilter.MedianFilter(3)) fixed = Image.new('RGB', source.size) fixed_pixels = fixed.load() for y in range(source.height): for x in range(source.width): r, g, b = pixels[x, y] amount = spill_pixels[x, y] fixed_pixels[x, y] = (max(0, r - amount), g, max(0, b - amount)) mascot = fixed.convert('RGBA') mascot.putalpha(mask) mascot = mascot.crop(mascot.getbbox()) if args.bottom_fade > 0: fade = min(args.bottom_fade, mascot.height) ramp = Image.new('L', (1, fade)) for y in range(fade): ramp.putpixel((0, y), round(255 * (1.0 - y / fade))) band = mascot.crop((0, mascot.height - fade, mascot.width, mascot.height)) band.putalpha(Image.composite(band.getchannel('A'), Image.new('L', band.size, 0), ramp.resize((1, fade)).resize(band.size, Image.NEAREST))) mascot.paste(band, (0, mascot.height - fade)) # ---------------------------------------------------------------- composition canvas_width = args.canvas title_area = args.title_height scale = (canvas_width - 2 * 40) / mascot.width mascot = mascot.resize((round(mascot.width * scale), round(mascot.height * scale)), Image.LANCZOS) height = mascot.height + title_area canvas = Image.new('RGBA', (canvas_width, height), (0, 0, 0, 0)) canvas.alpha_composite(mascot, ((canvas_width - mascot.width) // 2, 0)) font_size = int(title_area * 0.95) font = ImageFont.truetype(str(font_path), font_size) text = args.title while font.getbbox(text)[2] - font.getbbox(text)[0] > canvas_width - 60 and font_size > 8: font_size -= 2 font = ImageFont.truetype(str(font_path), font_size) box = font.getbbox(text) text_width = box[2] - box[0] position = ((canvas_width - text_width) // 2 - box[0], mascot.height + (title_area - box[3]) // 2 - box[1]) glow = Image.new('RGBA', canvas.size, (0, 0, 0, 0)) ImageDraw.Draw(glow).text(position, text, font=font, fill=(255, 150, 20, 190)) glow = glow.filter(ImageFilter.GaussianBlur(9)) canvas.alpha_composite(glow) ImageDraw.Draw(canvas).text(position, text, font=font, fill=(255, 196, 64, 255)) out = branding / 'boot_splash.png' if args.colors > 0: # Flat cartoon art tolerates a palette well; keeps ~95 alpha levels for edges. canvas = canvas.quantize(colors=args.colors, method=Image.FASTOCTREE, dither=Image.Dither.NONE) canvas.save(out, optimize=True) manifest = json.loads((source_tools / 'manifest.json').read_text()) if (source_tools / 'manifest.json').exists() else {} manifest.update({ 'boot_splash': {'file': str(out.relative_to(root)), 'size': list(canvas.size), 'mode': canvas.mode, 'palette_colors': args.colors, 'sha256': hashlib.sha256(out.read_bytes()).hexdigest()}, 'mascot_source': {'file': str(args.raw.relative_to(root)), 'sha256': hashlib.sha256(args.raw.read_bytes()).hexdigest()}, 'generator': { 'tool': 'agy_generate (Antigravity direct API)', 'model': 'gemini-3.1-flash-image', 'date': '2026-10-11', 'prompt': ('single cartoon zombie head-and-bust mascot, unsettling wide grin, thumbs up, ' '2D cartoon style with bold outlines, flat magenta chroma-key background, ' 'no text and no logo; then edited to a bald scalp with a sparse hair fringe ' 'on the sides only'), 'terms_note': ('generated from no third-party source material; the provider terms do not claim ' 'ownership of the output. The generator emits no real alpha channel, so the ' 'background is keyed out locally by this script.'), }, 'chroma_key': {'background_rgb': list(background), 'transparent_below': T0, 'opaque_above': T1}, 'title': {'text': args.title, 'font': 'tools/branding/fonts/VT323-Regular.ttf', 'license': 'SIL Open Font License 1.1 (tools/branding/fonts/OFL.txt)'}, 'bottom_fade_px': args.bottom_fade, }) if args.generated_from is not None: chain = manifest.get('mascot_derived_from', []) manifest['mascot_derived_from'] = chain + [{ 'file': str(args.generated_from.resolve().relative_to(root)), 'sha256': hashlib.sha256(args.generated_from.read_bytes()).hexdigest(), 'edit': 'AI edit (agy_edit): keep face, grin, thumbs up, shirt, pose and flat background; ' 'change the hair to a bald scalp with a sparse fringe on the sides only', }] (source_tools / 'manifest.json').write_text(json.dumps(manifest, indent=2) + '\n') print(out, canvas.size, canvas.mode, 'mascot', mascot.size)