#!/usr/bin/env python3 """Генерация иконок приложения (фавиконка + PWA) из логотипа факультета. Квадрат с градиентом боковой панели сайта (#1b2dd8 → #167cf2), внутрь помещается белая сова. Результат: img/app-icon.svg — самодостаточный svg (favicon) img/icon-192.png — иконка манифеста img/icon-512.png — иконка манифеста (maskable) img/apple-touch-icon.png — 180x180 для iOS img/favicon-32.png — растровая запасная фавиконка Запуск (нужен rsvg-convert из librsvg2-bin): python3 tools/make_icons.py """ from __future__ import annotations import subprocess import sys import xml.etree.ElementTree as ET from pathlib import Path ROOT = Path(__file__).resolve().parents[1] IMG = ROOT / "src" / "frontend" / "static" / "img" SVG_NS = "http://www.w3.org/2000/svg" SODIPODI_NS = "http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" INKSCAPE_NS = "http://www.inkscape.org/namespaces/inkscape" SIZE = 512 GRADIENT = (("#1b2dd8", "0%"), ("#167cf2", "100%")) # как .side в светлой теме OWL_SCALE = 0.62 # доля стороны иконки под логотип def strip_ns(el: ET.Element) -> None: """Убирает inkscape/sodipodi-атрибуты (в чужом документе они не нужны).""" for key in list(el.attrib): if key.startswith(f"{{{SODIPODI_NS}}}") or key.startswith(f"{{{INKSCAPE_NS}}}"): del el.attrib[key] for child in el: strip_ns(child) def build_icon_svg() -> str: owl_root = ET.parse(IMG / "owl-white.svg").getroot() view_box = owl_root.get("viewBox", "0 0 109.34 109.87") _, _, ow, oh = (float(v) for v in view_box.replace(",", " ").split()) # размеры и положение логотипа внутри квадрата target = SIZE * OWL_SCALE scale = target / max(ow, oh) tx = (SIZE - ow * scale) / 2 ty = (SIZE - oh * scale) / 2 ET.register_namespace("", SVG_NS) icon = ET.Element( f"{{{SVG_NS}}}svg", { "viewBox": f"0 0 {SIZE} {SIZE}", "width": str(SIZE), "height": str(SIZE), }, ) defs = ET.SubElement(icon, f"{{{SVG_NS}}}defs") grad = ET.SubElement( defs, f"{{{SVG_NS}}}linearGradient", {"id": "bg", "x1": "0", "y1": "0", "x2": "0", "y2": "1"}, ) for color, offset in GRADIENT: ET.SubElement( grad, f"{{{SVG_NS}}}stop", {"offset": offset, "stop-color": color} ) ET.SubElement( icon, f"{{{SVG_NS}}}rect", {"width": str(SIZE), "height": str(SIZE), "fill": "url(#bg)"} ) group = ET.SubElement( icon, f"{{{SVG_NS}}}g", {"transform": f"translate({tx:.2f} {ty:.2f}) scale({scale:.6f})"}, ) for child in owl_root: tag = child.tag if tag.endswith("namedview") or tag.endswith("metadata"): continue clone = ET.fromstring(ET.tostring(child)) strip_ns(clone) group.append(clone) xml = ET.tostring(icon, encoding="unicode") return '\n' + xml + "\n" def rasterize(svg_path: Path, size: int, out_path: Path) -> None: subprocess.run( ["rsvg-convert", "-w", str(size), "-h", str(size), "-o", str(out_path), str(svg_path)], check=True, ) def main() -> int: svg = build_icon_svg() icon = IMG / "app-icon.svg" icon.write_text(svg, encoding="utf-8") if not subprocess.run(["which", "rsvg-convert"], capture_output=True).stdout: print("нет rsvg-convert — svg создан, png не сгенерированы", file=sys.stderr) return 1 rasterize(icon, 192, IMG / "icon-192.png") rasterize(icon, 512, IMG / "icon-512.png") rasterize(icon, 180, IMG / "apple-touch-icon.png") rasterize(icon, 32, IMG / "favicon-32.png") for name in ("app-icon.svg", "icon-192.png", "icon-512.png", "apple-touch-icon.png", "favicon-32.png"): print("готово:", (IMG / name).relative_to(ROOT)) return 0 if __name__ == "__main__": raise SystemExit(main())