#!/usr/bin/env python3 """从 brand/icon.svg 逻辑绘制 PNG/ICO,供 Chrome 快捷方式、PWA manifest 使用。""" from __future__ import annotations import os import struct import zlib REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) OUT = os.path.join(REPO, "brand", "icons") def _png_chunk(tag: bytes, data: bytes) -> bytes: return ( struct.pack(">I", len(data)) + tag + data + struct.pack(">I", zlib.crc32(tag + data) & 0xFFFFFFFF) ) def _write_png(path: str, size: int, rgba: bytes) -> None: raw = b"" stride = size * 4 for y in range(size): raw += b"\x00" + rgba[y * stride : (y + 1) * stride] ihdr = struct.pack(">IIBBBBB", size, size, 8, 6, 0, 0, 0) idat = zlib.compress(raw, 9) png = ( b"\x89PNG\r\n\x1a\n" + _png_chunk(b"IHDR", ihdr) + _png_chunk(b"IDAT", idat) + _png_chunk(b"IEND", b"") ) with open(path, "wb") as f: f.write(png) def _lerp(a: float, b: float, t: float) -> float: return a + (b - a) * t def _gradient(t: float) -> tuple[int, int, int]: if t < 0.55: u = t / 0.55 return ( int(_lerp(0, 0, u)), int(_lerp(212, 139, u)), int(_lerp(255, 255, u)), ) u = (t - 0.55) / 0.45 return ( int(_lerp(61, 0, u)), int(_lerp(255, 255, u)), int(_lerp(157, 157, u)), ) def _inside_round_rect(x: int, y: int, size: int, pad: int, radius: int) -> bool: if x < pad or y < pad or x >= size - pad or y >= size - pad: return False r = radius if x < pad + r and y < pad + r: return (x - pad - r) ** 2 + (y - pad - r) ** 2 <= r * r if x >= size - pad - r and y < pad + r: return (x - (size - pad - r)) ** 2 + (y - pad - r) ** 2 <= r * r if x < pad + r and y >= size - pad - r: return (x - pad - r) ** 2 + (y - (size - pad - r)) ** 2 <= r * r if x >= size - pad - r and y >= size - pad - r: return (x - (size - pad - r)) ** 2 + (y - (size - pad - r)) ** 2 <= r * r return True def _draw_candle( buf: bytearray, size: int, cx: int, top: int, bottom: int, body_top: int, body_bottom: int, rgb: tuple[int, int, int], wick_w: int = 2, body_half: int = 7, ) -> None: for y in range(max(0, top), min(size, bottom + 1)): for x in range(cx - wick_w, cx + wick_w + 1): if 0 <= x < size: i = (y * size + x) * 4 buf[i : i + 3] = bytes((*rgb, 255)) for y in range(max(0, body_top), min(size, body_bottom + 1)): for x in range(cx - body_half, cx + body_half + 1): if 0 <= x < size: i = (y * size + x) * 4 buf[i : i + 3] = bytes((*rgb, 255)) def render_icon_rgba(size: int) -> bytes: buf = bytearray(size * size * 4) pad = max(4, size // 14) radius = size // 5 inner_pad = pad + max(2, size // 32) ring_w = max(2, size // 48) for y in range(size): for x in range(size): i = (y * size + x) * 4 if not _inside_round_rect(x, y, size, pad, radius): buf[i : i + 4] = b"\x00\x00\x00\x00" continue if _inside_round_rect(x, y, size, inner_pad, radius - 4): buf[i : i + 4] = bytes((18, 24, 42, 255)) else: t = (x + y) / (2 * size) r, g, b = _gradient(t) buf[i : i + 4] = bytes((r, g, b, 220)) s = size / 512.0 def sc(v: int) -> int: return int(v * s) _draw_candle(buf, size, sc(148), sc(228), sc(332), sc(252), sc(304), (255, 77, 109), sc(8), sc(16)) _draw_candle(buf, size, sc(228), sc(196), sc(352), sc(220), sc(308), (0, 255, 157), sc(8), sc(16)) _draw_candle(buf, size, sc(308), sc(240), sc(320), sc(264), sc(304), (0, 255, 157), sc(8), sc(14)) _draw_candle(buf, size, sc(384), sc(180), sc(300), sc(208), sc(272), (0, 255, 157), sc(8), sc(16)) ccx, ccy = sc(256), sc(256) cr = sc(52) for y in range(size): for x in range(size): d = ((x - ccx) ** 2 + (y - ccy) ** 2) ** 0.5 if cr - ring_w <= d <= cr: t = (x + y) / (2 * size) r, g, b = _gradient(t) i = (y * size + x) * 4 buf[i : i + 4] = bytes((r, g, b, 200)) tri = [ (sc(236), sc(276)), (sc(256), sc(220)), (sc(276), sc(276)), ] def _in_tri(px: int, py: int) -> bool: x1, y1 = tri[0] x2, y2 = tri[1] x3, y3 = tri[2] d1 = (px - x2) * (y1 - y2) - (x1 - x2) * (py - y2) d2 = (px - x3) * (y2 - y3) - (x2 - x3) * (py - y3) d3 = (px - x1) * (y3 - y1) - (x3 - x1) * (py - y1) has_neg = d1 < 0 or d2 < 0 or d3 < 0 has_pos = d1 > 0 or d2 > 0 or d3 > 0 return not (has_neg and has_pos) for y in range(size): for x in range(size): if _in_tri(x, y): t = (x + y) / (2 * size) r, g, b = _gradient(t) i = (y * size + x) * 4 buf[i : i + 4] = bytes((r, g, b, 255)) return bytes(buf) def _write_ico(path: str, sizes: list[int]) -> None: images = [] for sz in sizes: rgba = render_icon_rgba(sz) # BGRA bottom-up for ICO row = sz * 4 pixels = bytearray() for y in range(sz - 1, -1, -1): for x in range(sz): i = (y * sz + x) * 4 pixels.extend([rgba[i + 2], rgba[i + 1], rgba[i], rgba[i + 3]]) and_row = (sz * 4 + 3) & ~3 if and_row > sz * 4: pixels.extend(b"\x00" * (and_row - sz * 4)) bmp = b"BM" + struct.pack(" None: os.makedirs(OUT, exist_ok=True) import shutil shutil.copy2( os.path.join(REPO, "brand", "icon.svg"), os.path.join(OUT, "icon.svg"), ) for sz in (16, 32, 180, 192, 512): _write_png(os.path.join(OUT, f"icon-{sz}.png"), sz, render_icon_rgba(sz)) shutil.copy2(os.path.join(OUT, "icon-180.png"), os.path.join(OUT, "apple-touch-icon.png")) _write_ico(os.path.join(OUT, "favicon.ico"), [16, 32, 48]) print(f"DONE {OUT}") if __name__ == "__main__": main()