#!/usr/bin/env python3 """Makes the app's icons for every platform from its own, the kingfisher on white (halkyon_learn_flutter/assets/icon/halkyon_icon.png). - The snap's, snap/gui/halkyon-learn.png at the repository's root (512 x 512 pixels, the Snap Store's size), which the Store and the desktop file show as they are: the picture with rounded corners, as other apps' white tiles have (radius 142.222 of 512, about 28 %). - Through flutter_launcher_icons, configured in the app's pubspec: Android's adaptive icon and its older square one, iOS's, macOS's (the rounded tile on Apple's grid, 824 of 1024 pixels) and Windows' (the rounded tile). - Windows' .ico once more, with every size Windows shows (16 to 256 pixels): the tool writes only the 256 one and leaves Windows to scale it down for its small views. - The iOS project file as it was: the tool sets every build setting with ASSETCATALOG in its name to the icon set's name, Xcode's yes-or-no ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS too, which breaks the project (flutter_launcher_icons issue 634); the project names its icon set already. The derived images it hands to the tool go to halkyon_learn_flutter/build/icons/, which git ignores. The rounded corners' mask is drawn at four times the size and scaled down, so their edges are smooth. Run from apps/learn with Python 3 and Pillow (python3-pil on Ubuntu), the app's dependencies fetched: python3 tool/app_icons.py """ import subprocess from pathlib import Path from PIL import Image, ImageDraw APP = Path("halkyon_learn_flutter") SOURCE = APP / "assets/icon/halkyon_icon.png" DERIVED = APP / "build/icons" SNAP_ICON = Path("../../snap/gui/halkyon-learn.png") WINDOWS_ICON = APP / "windows/runner/resources/app_icon.ico" IOS_PROJECT = APP / "ios/Runner.xcodeproj/project.pbxproj" RADIUS = 142.222 / 512 SUPERSAMPLING = 4 # Apple's grid: the tile is 824 of the icon's 1024 pixels, centred. MACOS_CANVAS = 1024 MACOS_TILE = 824 WINDOWS_SIZES = [16, 20, 24, 32, 40, 48, 64, 128, 256] def rounded(size: int) -> Image.Image: """The icon at [size] pixels, its corners rounded off to transparency.""" icon = Image.open(SOURCE).convert("RGBA").resize((size, size), Image.LANCZOS) large = size * SUPERSAMPLING mask = Image.new("L", (large, large), 0) ImageDraw.Draw(mask).rounded_rectangle( (0, 0, large - 1, large - 1), radius=RADIUS * large, fill=255 ) mask = mask.resize((size, size), Image.LANCZOS) # Keeps what the icon itself makes transparent, and takes the corners away. transparent = Image.new("L", (size, size), 0) icon.putalpha(Image.composite(icon.getchannel("A"), transparent, mask)) return icon def main() -> None: rounded(512).save(SNAP_ICON, optimize=True) print(f"Wrote {SNAP_ICON}.") DERIVED.mkdir(parents=True, exist_ok=True) rounded(1024).save(DERIVED / "rounded.png") macos = Image.new("RGBA", (MACOS_CANVAS, MACOS_CANVAS), (0, 0, 0, 0)) margin = (MACOS_CANVAS - MACOS_TILE) // 2 macos.alpha_composite(rounded(MACOS_TILE), (margin, margin)) macos.save(DERIVED / "macos.png") ios_project = IOS_PROJECT.read_bytes() subprocess.run(["dart", "run", "flutter_launcher_icons"], cwd=APP, check=True) IOS_PROJECT.write_bytes(ios_project) rounded(256).save( WINDOWS_ICON, format="ICO", sizes=[(size, size) for size in WINDOWS_SIZES] ) print(f"Wrote {WINDOWS_ICON} with {len(WINDOWS_SIZES)} sizes.") if __name__ == "__main__": main()