Files
jap_web_ng/tools/gen_pwa_assets.py
2026-10-05 09:56:09 +03:00

125 lines
5.3 KiB
Python

#!/usr/bin/env python3
"""Генератор PWA-ассетов JapLearn: иконки + iOS splash-экраны.
Запуск (из корня проекта): python3 tools/gen_pwa_assets.py
Требуется Pillow (PIL) и ImageFont из системного Python.
Что делает:
- public/icons/*.png — 192/512 (any), 192/512 (maskable), apple-touch 180, favicon 32
- public/splash/*.png — apple-touch-startup-image под iPhone/iPad
- печатает готовый HTML-сниппет splash-ссылок для index.html
Палитра — Gruvbox Dark из src/style.css (--bg #282828, --primary-strong #49a8ac).
"""
from pathlib import Path
from PIL import Image, ImageDraw, ImageFont
ROOT = Path(__file__).resolve().parent.parent
FONT_PATH = ROOT / "public/fonts/NotoSansJP-VariableFont_wght.ttf"
ICONS_DIR = ROOT / "public/icons"
SPLASH_DIR = ROOT / "public/splash"
BG = (40, 40, 40) # --bg #282828
ACCENT = (73, 168, 172) # --primary-strong #49a8ac
MUTED = (144, 140, 124) # --muted #908c7c
GLYPH = "語" # «слово» — символ приложения
# Порядок: (файл, CSS-ширина, CSS-высота, DPR, подпись модели)
SPLASHES = [
("iphone-se", 375, 667, 2, "iPhone SE / 8"),
("iphone-8-plus", 414, 736, 3, "iPhone 8 Plus"),
("iphone-x", 375, 812, 3, "iPhone X/XS/11 Pro/12 mini/13 mini"),
("iphone-xr", 414, 896, 2, "iPhone XR/11"),
("iphone-xs-max", 414, 896, 3, "iPhone XS Max/11 Pro Max"),
("iphone-12", 390, 844, 3, "iPhone 12/13/14"),
("iphone-12-pro-max", 428, 926, 3, "iPhone 12/13/14 Pro Max"),
("iphone-14-pro", 393, 852, 3, "iPhone 14 Pro/15/15 Pro/16"),
("iphone-14-pro-max", 430, 932, 3, "iPhone 14 Pro Max/15 Plus/15 Pro Max"),
("iphone-16-pro", 402, 874, 3, "iPhone 16 Pro"),
("iphone-16-pro-max", 440, 956, 3, "iPhone 16 Pro Max/16 Plus"),
("ipad-9-7", 768, 1024, 2, "iPad 9.7/10.2"),
("ipad-pro-11", 834, 1194, 2, "iPad Pro 11\"/Air"),
("ipad-pro-12-9", 1024, 1366, 2, "iPad Pro 12.9\""),
]
def load_font(size: int, weight: int = 700) -> ImageFont.FreeTypeFont:
"""Noto Sans JP — variable, поэтому вес выставляем через ось wght."""
font = ImageFont.truetype(str(FONT_PATH), size)
try:
font.set_variation_by_axes([weight])
except Exception:
pass # статическая сборка шрифта — берём как есть
return font
def centered(draw: ImageDraw.ImageDraw, text: str, font, box_w: int, box_h: int, fill) -> None:
left, top, right, bottom = draw.textbbox((0, 0), text, font=font)
draw.text(
((box_w - (right - left)) / 2 - left, (box_h - (bottom - top)) / 2 - top),
text,
font=font,
fill=fill,
)
def make_icon(size: int, glyph_ratio: float, radius_ratio: float, opaque: bool) -> Image.Image:
img = Image.new("RGBA", (size, size), (*BG, 255 if opaque else 0))
draw = ImageDraw.Draw(img)
if radius_ratio > 0:
mask = Image.new("L", (size, size), 0)
ImageDraw.Draw(mask).rounded_rectangle(
(0, 0, size - 1, size - 1), radius=int(size * radius_ratio), fill=255
)
img.putalpha(mask)
draw = ImageDraw.Draw(img)
centered(draw, GLYPH, load_font(int(size * glyph_ratio)), size, size, ACCENT)
return img
def make_splash(w: int, h: int) -> Image.Image:
img = Image.new("RGB", (w, h), BG)
draw = ImageDraw.Draw(img)
glyph_size = int(w * 0.34)
centered(draw, GLYPH, load_font(glyph_size), w, int(h * 0.46), ACCENT)
centered(draw, "JapLearn", load_font(int(w * 0.075), 500), w, int(h * 0.60), MUTED)
return img
def main() -> None:
ICONS_DIR.mkdir(parents=True, exist_ok=True)
SPLASH_DIR.mkdir(parents=True, exist_ok=True)
# Иконки: any — со скруглением (как покажет Android/desktop), maskable —
# full-bleed без прозрачности и со «сжатым» логотипом (safe zone 80%),
# apple-touch — full-bleed без альфы, iOS скруглит сам.
for size in (192, 512):
make_icon(size, 0.56, 0.18, opaque=False).save(ICONS_DIR / f"icon-{size}.png")
make_icon(size, 0.44, 0.0, opaque=True).save(ICONS_DIR / f"maskable-{size}.png")
make_icon(180, 0.56, 0.0, opaque=True).save(ICONS_DIR / "apple-touch-icon-180.png")
make_icon(32, 0.62, 0.18, opaque=False).save(ICONS_DIR / "favicon-32.png")
for name, cw, ch, dpr, label in SPLASHES:
make_splash(cw * dpr, ch * dpr).save(SPLASH_DIR / f"{name}.png")
print("Иконки:")
for p in sorted(ICONS_DIR.glob("*.png")):
print(f" {p.relative_to(ROOT)} {p.stat().st_size // 1024} КБ")
print("Splash:")
total = sum(p.stat().st_size for p in SPLASH_DIR.glob("*.png"))
print(f" {len(list(SPLASH_DIR.glob('*.png')))} файлов, {total // 1024} КБ всего")
print("\nHTML для index.html:\n")
for name, cw, ch, dpr, label in SPLASHES:
print(
f' <link rel="apple-touch-startup-image" href="/splash/{name}.png"\n'
f' media="(device-width: {cw}px) and (device-height: {ch}px) '
f'and (-webkit-device-pixel-ratio: {dpr}) and (orientation: portrait)" />'
f" <!-- {label} -->"
)
if __name__ == "__main__":
main()