Files
MealQuest/web/scripts/generate-icons.py
T
dev_agentandClaude Opus 5.5 46cc7500de
CI / ci (push) Successful in 1m34s
Independent review Codex gpt-6-sol high: APPROVED (0 findings)
CI / ci (pull_request) Successful in 1m36s
Add the web app manifest, icons and service worker via vite-plugin-pwa
vite-plugin-pwa runs in injectManifest mode over a minimal src/sw.ts that
caches the injected precache list on install. The manifest lives in
web/manifest.json (MealQuest, standalone at /, Grape theme over the page
background) with 192, 512 and maskable 512 PNG icons and a 180 px
apple-touch-icon, all generated from design/logos/logo_v1_2Oct2026.jpg by
web/scripts/generate-icons.py.

The plugin links the manifest and adds the registerSW.js script only in
production builds. src/pwa.test.ts builds and previews the app to assert
the shipped manifest, sw.js, icon sizes and registration, and checks that
the dev server registers nothing.

Maestro task: 58b74786-937a-45a5-850f-7913273beba2

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 16:12:18 -04:00

60 lines
2.2 KiB
Python

"""Generates the PWA icons in web/public from the MealQuest logo.
Run from the repository root with Pillow installed:
python3 web/scripts/generate-icons.py
The logo fills the regular icons edge to edge. The maskable icon shrinks it
onto the colour of its own parchment, so the banner stays inside
the circular safe zone (radius 40% of the icon) that launchers may crop to.
"""
from pathlib import Path
from PIL import Image, ImageDraw, ImageFilter, ImageStat
ROOT = Path(__file__).resolve().parents[2]
SOURCE = ROOT / "design" / "logos" / "logo_v1_2Oct2026.jpg"
PUBLIC = ROOT / "web" / "public"
MASKABLE_SCALE = 0.66
def resized(logo: Image.Image, size: int) -> Image.Image:
return logo.resize((size, size), Image.Resampling.LANCZOS)
def maskable(logo: Image.Image, size: int) -> Image.Image:
# The padding is the median colour of the logo's top edge, which is bare
# parchment; a feathered edge hides the seam.
top_edge = ImageStat.Stat(logo.crop((0, 0, logo.width, logo.height // 40)))
canvas = Image.new("RGB", (size, size), tuple(round(c) for c in top_edge.median))
inner = round(size * MASKABLE_SCALE)
offset = (size - inner) // 2
feather = Image.new("L", (inner, inner), 0)
margin = inner // 24
ImageDraw.Draw(feather).rectangle((margin, margin, inner - margin, inner - margin), fill=255)
feather = feather.filter(ImageFilter.GaussianBlur(margin / 2))
canvas.paste(resized(logo, inner), (offset, offset), feather)
return canvas
def main() -> None:
logo = Image.open(SOURCE).convert("RGB")
if logo.width != logo.height:
raise SystemExit(f"{SOURCE} is {logo.width}x{logo.height}; the logo must be square")
outputs = {
PUBLIC / "icons" / "icon-192.png": resized(logo, 192),
PUBLIC / "icons" / "icon-512.png": resized(logo, 512),
PUBLIC / "icons" / "icon-maskable-512.png": maskable(logo, 512),
PUBLIC / "apple-touch-icon.png": resized(logo, 180),
}
for path, image in outputs.items():
path.parent.mkdir(parents=True, exist_ok=True)
image.save(path, optimize=True)
print(f"wrote {path.relative_to(ROOT)} ({image.width}x{image.height})")
if __name__ == "__main__":
main()