#!/usr/bin/env python3 """ Kiwi logo compositor. Takes: - A kiwifruit disc SVG (generated by generate_disc.py) - A kiwi bird PNG (AI-generated via ComfyUI img2img then PIL-corrected) Composites bird over disc, removes white background, outputs icon sizes. Usage: conda run -n cf python3 scripts/logo/composite.py \ --disc scripts/logo/kiwi_disc.svg \ --bird scripts/logo/kiwi_bird_final.png \ --out frontend/public/icons/ Requirements: cairosvg, Pillow, numpy """ import argparse import io from pathlib import Path import cairosvg import numpy as np from PIL import Image, ImageDraw SIZES = [512, 256, 192, 64] def remove_background(bird_img: Image.Image) -> Image.Image: """Remove white/gray background from AI-generated bird PNG.""" arr = np.array(bird_img, dtype=np.float32) r, g, b = arr[:, :, 0], arr[:, :, 1], arr[:, :, 2] brightness = (r + g + b) / 3.0 saturation = np.max([r, g, b], axis=0) - np.min([r, g, b], axis=0) near_white = (brightness > 228) & (saturation < 28) # Remove gray ground shadow in bottom 30% of image only h = arr.shape[0] gray_shadow = np.zeros(arr.shape[:2], dtype=bool) gray_shadow[int(h * 0.70):, :] = ( (brightness[int(h * 0.70):, :] > 135) & (saturation[int(h * 0.70):, :] < 65) ) gray_shadow2 = np.zeros(arr.shape[:2], dtype=bool) gray_shadow2[int(h * 0.78):, :] = ( (brightness[int(h * 0.78):, :] > 110) & (saturation[int(h * 0.78):, :] < 75) ) transparent = near_white | gray_shadow | gray_shadow2 # Soft edge soft = (brightness > 205) & (saturation < 55) & ~transparent soft_alpha = np.where(soft, ((255.0 - brightness) / 50.0) * 255.0, 0.0) new_alpha = np.where(transparent, 0.0, np.where(soft, soft_alpha, 255.0)) arr[:, :, 3] = np.clip(new_alpha, 0, 255) return Image.fromarray(arr.astype(np.uint8)) def composite(disc_svg: Path, bird_png: Path, out_dir: Path) -> None: out_dir.mkdir(parents=True, exist_ok=True) # Render disc at max size max_size = max(SIZES) disc_bytes = cairosvg.svg2png(url=str(disc_svg), output_width=max_size, output_height=max_size) disc = Image.open(io.BytesIO(disc_bytes)).convert("RGBA") # Load and de-background bird bird_raw = Image.open(bird_png).convert("RGBA") bird_nobg = remove_background(bird_raw) bbox = bird_nobg.getbbox() bird_crop = bird_nobg.crop(bbox) # Scale bird to 80% of disc diameter target_w = int(max_size * 0.80) target_h = int(bird_crop.height * target_w / bird_crop.width) bird_scaled = bird_crop.resize((target_w, target_h), Image.LANCZOS) # Composite centered, slightly raised and shifted right (beak faces left) cx, cy = max_size // 2, max_size // 2 x_off = cx - bird_scaled.width // 2 + int(max_size * 0.06) y_off = cy - bird_scaled.height // 2 - int(max_size * 0.04) result = disc.copy() result.paste(bird_scaled, (x_off, y_off), bird_scaled) # Draw eye post-composite (eye was at 305,335 in 768px bird space) # Eye was at pixel (305, 335) in original 768px bird image # Adjust for crop bbox offset, then scale to composite space sx = target_w / bird_crop.width sy = target_h / bird_crop.height # Known eye coords from PIL correction pass (EX=305, EY=335 in 768px space) eye_x = int(x_off + (305 - bbox[0]) * sx) eye_y = int(y_off + (335 - bbox[1]) * sy) draw = ImageDraw.Draw(result) draw.ellipse([eye_x - 7, eye_y - 7, eye_x + 7, eye_y + 7], fill=(240, 235, 220, 255)) draw.ellipse([eye_x - 3, eye_y - 3, eye_x + 3, eye_y + 3], fill=(12, 5, 2, 255)) # Export all sizes for size in SIZES: out_path = out_dir / f"icon-{size}.png" resized = result.resize((size, size), Image.LANCZOS) resized.save(out_path) print(f" Saved {out_path} ({size}px)") # Maskable copies (same image — kiwi disc has safe-zone padding built in) for size in [192, 512]: src = out_dir / f"icon-{size}.png" dst = out_dir / f"maskable-{size}.png" dst.write_bytes(src.read_bytes()) print(f" Copied {dst}") if __name__ == "__main__": parser = argparse.ArgumentParser(description="Kiwi logo compositor") parser.add_argument("--disc", type=Path, default=Path("scripts/logo/kiwi_disc.svg")) parser.add_argument("--bird", type=Path, default=Path("scripts/logo/kiwi_bird_final.png")) parser.add_argument("--out", type=Path, default=Path("frontend/public/icons")) args = parser.parse_args() print(f"Compositing {args.bird.name} onto {args.disc.name} → {args.out}/") composite(args.disc, args.bird, args.out) print("Done.")