Files
nmemmertandClaude Sonnet 5.5 044589686c
build-container / build (push) Successful in 1m56s
PWA for iPhone and iPad: PNG icons, splash screens, manifest; stay signed in for 180 days
- iOS ignores SVG touch icons: add 120/152/167/180 apple-touch-icons, 192/512 and maskable manifest icons,
  and 25 splash screens (every iPhone size, iPad portrait+landscape incl. 12.9")
- Icons and splash screens are public so iOS can fetch them without a session
- Manifest: id, scope, categories, shortcuts; iOS standalone polish (no callout, no overscroll)
- Sessions last SESSION_DAYS (default 180) and renew daily while in use
- tools/make-icons.py regenerates the artwork

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-20 15:44:39 -04:00

77 lines
3.9 KiB
Python

#!/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'<link rel="apple-touch-startup-image" href="/icons/{name}" media="(device-width: {cw}px) and (device-height: {ch}px) and (-webkit-device-pixel-ratio: {pr}) and (orientation: {o})">\n')
print(len(links), 'splash screens')