#!/usr/bin/env python3 """生成品牌 PNG/ICO(Pillow),供 Chrome 快捷方式与 manifest 使用. 中控用通用监控图标;三所各自用交易所标识色+字标. """ from __future__ import annotations import os import shutil REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) OUT = os.path.join(REPO, "brand", "icons") BG = (12, 16, 25, 255) PANEL = (20, 27, 45, 255) CYAN = (34, 211, 238, 255) GREEN = (52, 211, 153, 255) RED = (248, 113, 113, 255) EXCHANGES = { "binance": { "label": "B", "accent": (240, 185, 11, 255), "panel": (26, 22, 10, 255), "svg_fill": "#F0B90B", }, "okx": { "label": "OKX", "accent": (255, 255, 255, 255), "panel": (18, 18, 18, 255), "svg_fill": "#FFFFFF", }, "gate": { "label": "G", "accent": (23, 230, 161, 255), "panel": (10, 28, 24, 255), "svg_fill": "#17E6A1", }, } def _lerp(c1: tuple[int, ...], c2: tuple[int, ...], t: float) -> tuple[int, int, int, int]: t = max(0.0, min(1.0, t)) return tuple(int(c1[i] + (c2[i] - c1[i]) * t) for i in range(4)) # type: ignore def _rounded_rect(draw, box, radius: int, fill) -> None: draw.rounded_rectangle(box, radius=radius, fill=fill) def _font(size: int): from PIL import ImageFont candidates = [ os.path.join(os.environ.get("WINDIR", r"C:\Windows"), "Fonts", "arialbd.ttf"), os.path.join(os.environ.get("WINDIR", r"C:\Windows"), "Fonts", "segoeuib.ttf"), "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", "/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf", "/System/Library/Fonts/Supplemental/Arial Bold.ttf", ] for path in candidates: if path and os.path.isfile(path): try: return ImageFont.truetype(path, size=size) except OSError: continue return ImageFont.load_default() def render_icon(size: int): from PIL import Image, ImageDraw img = Image.new("RGBA", (size, size), (0, 0, 0, 0)) draw = ImageDraw.Draw(img) m = max(6, size // 12) r = max(8, size // 6) _rounded_rect(draw, (m, m, size - m, size - m), r, BG) inner = m + max(2, size // 28) _rounded_rect(draw, (inner, inner, size - inner, size - inner), max(6, r - 4), PANEL) border = max(2, size // 42) for i in range(border): t0 = i / max(1, border - 1) for x in range(inner, size - inner): t = (x - inner) / max(1, size - 2 * inner) col = _lerp(CYAN, GREEN, (t + t0) * 0.5) draw.point((x, inner + i), fill=col) draw.point((x, size - inner - 1 - i), fill=col) for y in range(inner, size - inner): t = (y - inner) / max(1, size - 2 * inner) col = _lerp(CYAN, GREEN, (t + t0) * 0.5) draw.point((inner + i, y), fill=col) draw.point((size - inner - 1 - i, y), fill=col) def sx(v: float) -> int: return int(v * size / 512) def sy(v: float) -> int: return int(v * size / 512) pts = [(120, 320), (200, 248), (280, 272), (392, 168)] scaled = [(sx(x), sy(y)) for x, y in pts] draw.line(scaled, fill=CYAN, width=max(2, size // 26), joint="curve") ex, ey = scaled[-1] draw.ellipse( (ex - size // 28, ey - size // 28, ex + size // 28, ey + size // 28), fill=GREEN, ) def candle(cx, top, bottom, body_top, body_bottom, color): w = max(1, size // 64) bh = max(2, size // 32) draw.line((cx, top, cx, bottom), fill=color, width=w) draw.rounded_rectangle( (cx - bh, body_top, cx + bh, body_bottom), radius=max(1, bh // 3), fill=color, ) candle(sx(182), sy(248), sy(340), sy(268), sy(332), RED) candle(sx(282), sy(200), sy(340), sy(220), sy(316), GREEN) return img def render_exchange_icon(size: int, key: str): from PIL import Image, ImageDraw cfg = EXCHANGES[key] accent = cfg["accent"] panel = cfg["panel"] label = cfg["label"] img = Image.new("RGBA", (size, size), (0, 0, 0, 0)) draw = ImageDraw.Draw(img) m = max(6, size // 12) r = max(8, size // 6) _rounded_rect(draw, (m, m, size - m, size - m), r, BG) inner = m + max(2, size // 28) _rounded_rect(draw, (inner, inner, size - inner, size - inner), max(6, r - 4), panel) if size >= 32: border = max(1, size // 48) for i in range(border): x0 = inner + i y0 = inner + i x1 = size - inner - 1 - i y1 = size - inner - 1 - i if x1 <= x0 or y1 <= y0: break draw.rounded_rectangle( (x0, y0, x1, y1), radius=max(2, r - 4 - i), outline=accent, ) if key == "binance": # 币安菱形标识 cx = cy = size // 2 s = max(3, int(size * 0.22)) diamond = [(cx, cy - s), (cx + s, cy), (cx, cy + s), (cx - s, cy)] draw.polygon(diamond, fill=accent) s2 = max(1, int(s * 0.42)) if s2 < s: inner_d = [(cx, cy - s2), (cx + s2, cy), (cx, cy + s2), (cx - s2, cy)] draw.polygon(inner_d, fill=panel) elif key == "okx": # OKX 四格方块风格(右下留空) gap = max(1, size // 48) cell = max(2, int(size * 0.16)) cx = cy = size // 2 coords = [ (cx - cell - gap // 2, cy - cell - gap // 2), (cx + gap // 2, cy - cell - gap // 2), (cx - cell - gap // 2, cy + gap // 2), ] for x0, y0 in coords: draw.rectangle((x0, y0, x0 + cell, y0 + cell), fill=accent) else: # Gate: 大字 G font_size = max(10, int(size * 0.42)) font = _font(font_size) bbox = draw.textbbox((0, 0), label, font=font) tw, th = bbox[2] - bbox[0], bbox[3] - bbox[1] x = (size - tw) // 2 - bbox[0] y = (size - th) // 2 - bbox[1] - max(0, size // 64) draw.text((x, y), label, font=font, fill=accent) return img def write_exchange_svg(key: str, dest_dir: str) -> None: cfg = EXCHANGES[key] fill = cfg["svg_fill"] if key == "binance": mark = ( f'' f'' ) elif key == "okx": mark = ( f'' f'' f'' ) else: mark = ( f'G' ) svg = f""" {mark} """ with open(os.path.join(dest_dir, "icon.svg"), "w", encoding="utf-8", newline="\n") as f: f.write(svg) def _save_set(out_dir: str, render_fn) -> None: from PIL import Image os.makedirs(out_dir, exist_ok=True) sizes = [16, 32, 48, 180, 192, 512] images: dict[int, Image.Image] = {} for sz in sizes: im = render_fn(sz) images[sz] = im name = "apple-touch-icon.png" if sz == 180 else f"icon-{sz}.png" im.save(os.path.join(out_dir, name), format="PNG", optimize=True) ico_sizes = [16, 32, 48] ico_imgs = [images[s] for s in ico_sizes] ico_imgs[0].save( os.path.join(out_dir, "favicon.ico"), format="ICO", sizes=[(s, s) for s in ico_sizes], append_images=ico_imgs[1:], ) def main() -> None: os.makedirs(OUT, exist_ok=True) shutil.copy2(os.path.join(REPO, "brand", "icon.svg"), os.path.join(OUT, "icon.svg")) _save_set(OUT, render_icon) print(f"DONE hub {OUT}") for key in EXCHANGES: dest = os.path.join(OUT, key) os.makedirs(dest, exist_ok=True) write_exchange_svg(key, dest) _save_set(dest, lambda sz, k=key: render_exchange_icon(sz, k)) print(f"DONE {key} {dest}") if __name__ == "__main__": main()