#!/usr/bin/env python3 """Generates the Android launcher icon and splash screen art from ../../logo.png (the repo's brand mark + "fuelish" wordmark). Only the graphic mark (not the wordmark) works as a launcher/splash icon, so this first isolates it: crop the upper portion of the source image (where the mark lives) and auto-crop that to its actual non-transparent bounding box, rather than relying on any hardcoded pixel offsets that would break if logo.png is ever redrawn at a different resolution. Produces: - mipmap-/ic_launcher_foreground.png - the mark centered, scaled to the adaptive-icon safe zone (66% of a 108dp canvas), transparent background - paired at runtime with @color/ic_launcher_background (see mipmap-anydpi-v26/ic_launcher.xml, committed - a static reference, not generated) for API 26+. - mipmap-/ic_launcher.png / ic_launcher_round.png - the same mark flattened onto ic_launcher_background, for API <26 devices that can't use the adaptive-icon XML at all. - drawable-nodpi/splash_icon.png - the mark on a transparent background, referenced by Theme.Fuelish.Splash's windowSplashScreenAnimatedIcon (see values/themes.xml). Generated output, not committed (see the repo's root .gitignore's /android/app/src/main/res/{mipmap-*,drawable-nodpi} entries) - regenerate by running this script directly or via Gradle's generateAppIcons task (wired into app's preBuild, see ../app/build.gradle.kts). Requires Pillow. """ import os import sys from PIL import Image SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) LOGO_PATH = os.path.normpath(os.path.join(SCRIPT_DIR, "..", "..", "logo.png")) RES_DIR = os.path.normpath(os.path.join(SCRIPT_DIR, "..", "app", "src", "main", "res")) BACKGROUND_RGBA = (255, 255, 255, 255) # matches @color/ic_launcher_background # (density qualifier, adaptive-icon canvas px, legacy launcher px) DENSITIES = [ ("mdpi", 108, 48), ("hdpi", 162, 72), ("xhdpi", 216, 96), ("xxhdpi", 324, 144), ("xxxhdpi", 432, 192), ] # Adaptive icons render only the inner ~66% "safe zone" reliably across # launchers that apply their own mask shape. SAFE_ZONE_RATIO = 0.66 # Fraction of logo.png's height (from the top) that contains the graphic # mark, before the "fuelish" wordmark begins. MARK_REGION_HEIGHT_RATIO = 0.60 SPLASH_CANVAS_PX = 480 SPLASH_ICON_RATIO = 0.75 def extract_mark(logo: Image.Image) -> Image.Image: width, height = logo.size region = logo.crop((0, 0, width, round(height * MARK_REGION_HEIGHT_RATIO))) bbox = region.getchannel("A").getbbox() if bbox is None: raise ValueError(f"{LOGO_PATH}: no non-transparent pixels found in the top " f"{MARK_REGION_HEIGHT_RATIO:.0%} of the image") mark = region.crop(bbox) # Pad to square so downstream scaling doesn't distort the aspect ratio. side = max(mark.width, mark.height) square = Image.new("RGBA", (side, side), (0, 0, 0, 0)) square.paste(mark, ((side - mark.width) // 2, (side - mark.height) // 2), mark) return square def resized(image: Image.Image, target_px: int) -> Image.Image: ratio = target_px / max(image.width, image.height) size = (round(image.width * ratio), round(image.height * ratio)) return image.resize(size, Image.LANCZOS) def centered_on(canvas_px: int, content: Image.Image, background: tuple) -> Image.Image: canvas = Image.new("RGBA", (canvas_px, canvas_px), background) offset = ((canvas_px - content.width) // 2, (canvas_px - content.height) // 2) canvas.paste(content, offset, content) return canvas def main(): logo = Image.open(LOGO_PATH).convert("RGBA") mark = extract_mark(logo) for density, canvas_px, legacy_px in DENSITIES: mipmap_dir = os.path.join(RES_DIR, f"mipmap-{density}") os.makedirs(mipmap_dir, exist_ok=True) foreground = centered_on(canvas_px, resized(mark, round(canvas_px * SAFE_ZONE_RATIO)), (0, 0, 0, 0)) foreground.save(os.path.join(mipmap_dir, "ic_launcher_foreground.png")) legacy = centered_on(legacy_px, resized(mark, round(legacy_px * SAFE_ZONE_RATIO)), BACKGROUND_RGBA) legacy.convert("RGB").save(os.path.join(mipmap_dir, "ic_launcher.png")) legacy.convert("RGB").save(os.path.join(mipmap_dir, "ic_launcher_round.png")) drawable_dir = os.path.join(RES_DIR, "drawable-nodpi") os.makedirs(drawable_dir, exist_ok=True) splash = centered_on(SPLASH_CANVAS_PX, resized(mark, round(SPLASH_CANVAS_PX * SPLASH_ICON_RATIO)), (0, 0, 0, 0)) splash.save(os.path.join(drawable_dir, "splash_icon.png")) print(f"wrote launcher icons for {len(DENSITIES)} densities + splash icon to {RES_DIR}", file=sys.stderr) if __name__ == "__main__": main()