#!/usr/bin/env python3 """Generates the PWA icons and iOS splash screens into public/icons. Needs Pillow: pip install pillow""" import os from PIL import Image, ImageDraw OUT = os.path.join(os.path.dirname(__file__), '..', 'public', 'icons') os.makedirs(OUT, exist_ok=True) SS = 4 # supersampling for smooth edges def glyph(size, fg_scale=0.56, bg=((0x5b, 0x86, 0xff), (0x3a, 0x5c, 0xe6)), rounded=0.0): """The app mark: four rounded tiles on a blue gradient. rounded>0 rounds the outer corners (for splash screens).""" S = size * SS img = Image.new('RGB', (S, S)) px = img.load() if False else None # vertical gradient top, bot = bg grad = Image.new('RGB', (1, S)) for y in range(S): t = y / (S - 1) grad.putpixel((0, y), tuple(int(top[i] + (bot[i] - top[i]) * t) for i in range(3))) img = grad.resize((S, S)) overlay = Image.new('RGBA', (S, S), (0, 0, 0, 0)) d = ImageDraw.Draw(overlay) g = S * fg_scale # glyph box gap = g * 0.10 tile = (g - gap) / 2 x0 = (S - g) / 2 r = tile * 0.24 for i, (cx, cy, a) in enumerate([(0, 0, 255), (1, 0, 180), (0, 1, 180), (1, 1, 255)]): x = x0 + cx * (tile + gap); y = x0 + cy * (tile + gap) d.rounded_rectangle([x, y, x + tile, y + tile], radius=r, fill=(255, 255, 255, a)) img = Image.alpha_composite(img.convert('RGBA'), overlay) if rounded: mask = Image.new('L', (S, S), 0) ImageDraw.Draw(mask).rounded_rectangle([0, 0, S - 1, S - 1], radius=S * rounded, fill=255) out = Image.new('RGBA', (S, S), (0, 0, 0, 0)); out.paste(img, (0, 0), mask); img = out return img.resize((size, size), Image.LANCZOS) def save(img, name, flatten=True): if flatten and img.mode == 'RGBA': # iOS fills transparency with black, so icons must be opaque bg = Image.new('RGB', img.size, (0x3a, 0x5c, 0xe6)); bg.paste(img, mask=img.split()[3]); img = bg img.save(os.path.join(OUT, name), optimize=True) # ---- Icons for n in (180, 167, 152, 120): save(glyph(n), f'apple-touch-icon-{n}.png') save(glyph(180), 'apple-touch-icon.png') # iOS default lookup save(glyph(192), 'icon-192.png'); save(glyph(512), 'icon-512.png') save(glyph(512, fg_scale=0.50), 'icon-maskable-512.png') # artwork stays inside the 80% safe zone save(glyph(192, fg_scale=0.50), 'icon-maskable-192.png') save(glyph(32), 'favicon-32.png') # ---- iOS splash screens: dark background with the app mark. (width, height, css width, css height, pixel ratio) BG = (0x0f, 0x11, 0x15) PHONES = [(1320, 2868, 440, 956, 3), (1206, 2622, 402, 874, 3), (1290, 2796, 430, 932, 3), (1179, 2556, 393, 852, 3), (1284, 2778, 428, 926, 3), (1170, 2532, 390, 844, 3), (1125, 2436, 375, 812, 3), (1242, 2688, 414, 896, 3), (828, 1792, 414, 896, 2), (1242, 2208, 414, 736, 3), (750, 1334, 375, 667, 2)] PADS = [(2048, 2732, 1024, 1366, 2), (1668, 2388, 834, 1194, 2), (1668, 2224, 834, 1112, 2), (1640, 2360, 820, 1180, 2), (1620, 2160, 810, 1080, 2), (1488, 2266, 744, 1133, 2), (1536, 2048, 768, 1024, 2)] links = [] def splash(w, h, cw, ch, pr, orient): img = Image.new('RGB', (w, h), BG) m = int(min(w, h) * 0.22) mark = glyph(m, rounded=0.225) img.paste(mark, ((w - m) // 2, (h - m) // 2), mark) name = f'splash-{w}x{h}.png' img.save(os.path.join(OUT, name), optimize=True) links.append((name, cw, ch, pr, orient)) for (w, h, cw, ch, pr) in PHONES: splash(w, h, cw, ch, pr, 'portrait') for (w, h, cw, ch, pr) in PADS: splash(w, h, cw, ch, pr, 'portrait') splash(h, w, ch, cw, pr, 'landscape') # the same device on its side with open(os.path.join(OUT, 'splash-links.html'), 'w') as f: for name, cw, ch, pr, o in links: f.write(f'\n') print(len(links), 'splash screens')