#!/usr/bin/env python3 # -*- coding: utf-8 -*- """gen_action_icons.py — 从 Material Symbols Rounded 官方 SVG 生成 sb_icons.h 位图动作图标。 背景:MeowOS SDK topbar(W_TOPAPPBAR)动作此前用字体码点(⚙↻⌕)绘制,4k 字库无这些码点→豆腐。 正确路线 = 位图图标:与状态栏同一张 sb_icons[](Material Symbols Rounded 32x32 单通道 alpha), 动作项经 meow_topappbar_set_item_icon(w, idx, MEOW_ICON_*) 指定索引后即画位图。 版权/来源:图标位图源自 google/material-design-icons 的 Material Symbols Rounded(Apache-2.0)。 本脚本只做下载(或读缓存)+ 栅格化成 32x32 单通道 alpha + 拼接 C 数组。 生成方式:svg(24px 网格,viewBox 0 -960 960 960)→ cairosvg 512x512 → LANCZOS 降到 32x32 取 alpha 通道(每像素 0..255 亮度/alpha,与状态栏同源——绘制颜色由调用方主题表提供)。 用 FILL=1(实心)以匹配现有状态栏图标的实心风格(WiFi/BT/BATT 均为实心)。 可复现性:SVG 缓存于本目录 icons_src/{symbol}_fill1_24px.svg(缺则联网下载,命中缓存即断网可跑); 栅格参数(512/32/LANCZOS/FILL=1)固定 → 重跑输出一致。 用法(在 patch_v2/sdk/ 下): python3 tools/gen_action_icons.py # 用 venv 的 cairosvg 运行 依赖:Python3 + Pillow + cairosvg(isolated venv:python3 -m venv && /bin/pip install cairosvg)。 输出:重写 patch_v2/sdk/sb_icons.h——保留现有 15 个状态栏位图(0-14,原样),追加 15 个动作 图标(15-29),索引兼容(既有索引不变,只追加)。 """ import io import os import sys import time import urllib.request try: import cairosvg from PIL import Image except Exception: # noqa sys.exit("缺少 cairosvg / Pillow,请先安装:python3 -m venv && /bin/pip install cairosvg") THIS_DIR = os.path.dirname(os.path.abspath(__file__)) SDK_DIR = os.path.abspath(os.path.join(THIS_DIR, os.pardir)) SB_ICONS_H = os.path.join(SDK_DIR, "sb_icons.h") SRC_DIR = os.path.join(THIS_DIR, "icons_src") OUT_RES = 512 # cairosvg 栅格目标边长(先大后小 → 抗锯齿 alpha) TARGET = 32 # 最终位图边长(= SB_ICON_SIZE) FILL = 1 # Material Symbols FILL 轴:1=实心(与状态栏一致) BASE_URL = "https://raw.githubusercontent.com/google/material-design-icons/master/symbols/web/" UA = {"User-Agent": "Mozilla/5.0"} # 现有状态栏图标 C 数组名(0-14,按 sb_icons[] 现有顺序;勿改序)。 # 本脚本不改动它们(原样保留),仅在生成表格/枚举时按此顺序引用。 BASE_ICONS = [ "SB_ICON_WIFI", "SB_ICON_BT", "SB_ICON_BATT", "SB_ICON_VOL", "SB_ICON_BRIGHT", "SB_ICON_WIFI_STRONG", "SB_ICON_WIFI_MEDIUM", "SB_ICON_WIFI_WEAK", "SB_ICON_WIFI_NO", "SB_ICON_WIFI_OFF", "SB_ICON_BT_NO", "SB_ICON_BT_CONN", "SB_ICON_BT_DIS", "SB_ICON_BT_SEARCH", "SB_ICON_BATT_CHG", ] # 动作图标:c_array_name, enum_name, material_symbols 图标名。索引 15-29,勿改序。 ACTIONS = [ ("SB_ICON_SETTINGS", "SB_I_SETTINGS", "settings"), ("SB_ICON_REFRESH", "SB_I_REFRESH", "refresh"), ("SB_ICON_SEARCH", "SB_I_SEARCH", "search"), ("SB_ICON_PLAY_ARROW", "SB_I_PLAY_ARROW", "play_arrow"), ("SB_ICON_PAUSE", "SB_I_PAUSE", "pause"), ("SB_ICON_SKIP_NEXT", "SB_I_SKIP_NEXT", "skip_next"), ("SB_ICON_SKIP_PREV", "SB_I_SKIP_PREV", "skip_previous"), ("SB_ICON_ARROW_BACK", "SB_I_ARROW_BACK", "arrow_back"), ("SB_ICON_CLOSE", "SB_I_CLOSE", "close"), ("SB_ICON_ADD", "SB_I_ADD", "add"), ("SB_ICON_CHECK", "SB_I_CHECK", "check"), ("SB_ICON_LYRICS", "SB_I_LYRICS", "lyrics"), ("SB_ICON_QUEUE_MUSIC", "SB_I_QUEUE_MUSIC", "queue_music"), ("SB_ICON_DOWNLOAD", "SB_I_DOWNLOAD", "download"), ("SB_ICON_HEART", "SB_I_HEART", "favorite"), # Material Symbols 用 "favorite" 表心形 ] HEADER_COMMENT = """/* sb_icons.h — 状态栏 + 动作位图图标(Material Symbols Rounded, MD3),单通道 alpha 位图 32x32。 * 生成自 material-symbols rounded(google/material-design-icons,Apache-2.0)SVG 栅格化: * 图标族 = FILL=1 实心(与状态栏 WiFi/BT/BATT 现有实心风格一致);每像素 0..255(亮度/alpha)。 * ★ 为日/夜间预留:位图只存亮度;绘制颜色由调用方主题表提供,换主题仅改配色。 * 索引布局(兼容):状态栏 SB_I_WIFI=0..SB_I_BATT_CHG=14(原样保留);TopAppBar 动作 * MEOW_ICON_SETTINGS=15..MEOW_ICON_HEART=29(只追加,既有索引不变)。动作图标为 15-29, * 经 meow_topappbar_set_item_icon(w, idx, MEOW_ICON_*) 使 W_TOPAPPBAR 动作项画位图。 * 重新生成:python3 tools/gen_action_icons.py(SVG 缓存 tools/icons_src/,可复现)。 */ #ifndef SB_ICONS_H #define SB_ICONS_H #define SB_ICON_SIZE 32 typedef struct { const unsigned char *data; } sb_icon_t; """ ACTION_BLOCK_MARKER = "/* ===== TopAppBar \u52a8\u4f5c\u56fe\u6807" # "/* ===== TopAppBar 动作图标" def fetch(url, path, retries=6): """带重试的下载(网络不稳):命中缓存直接返回,失败退避重试。""" if os.path.exists(path) and os.path.getsize(path) > 200: return open(path, "rb").read() for k in range(retries): try: req = urllib.request.Request(url, headers=UA) data = urllib.request.urlopen(req, timeout=25).read() if len(data) < 200: raise ValueError("too small %d" % len(data)) open(path, "wb").write(data) return data except Exception as e: # noqa sys.stderr.write(" retry %d %s: %s\n" % (k, path, repr(e)[:80])) time.sleep(1.5 * (k + 1)) raise RuntimeError("download failed: " + url) def svg_path_for(symbol): return os.path.join(SRC_DIR, symbol + "_fill%d_24px.svg" % FILL) def ensure_svg(symbol): path = svg_path_for(symbol) if not (os.path.exists(path) and os.path.getsize(path) > 200): url = BASE_URL + symbol + "/materialsymbolsrounded/" + symbol + "_fill%d_24px.svg" % FILL fetch(url, path) return path def rasterize_alpha32(symbol): """SVG → 32x32 单通道 alpha(0..255)。512 渲大图再 LANCZOS 降采样,得到平滑边缘。""" svg = open(ensure_svg(symbol), "rb").read() png = cairosvg.svg2png(bytestring=svg, output_width=OUT_RES, output_height=OUT_RES) alpha = Image.open(io.BytesIO(png)).convert("RGBA").getchannel("A") return alpha.resize((TARGET, TARGET), Image.LANCZOS) def fmt_alpha_array(name, alpha): """把 32x32 alpha 排成 C 数组(每行 32 个像素,便于目检)。""" lines = ["static const unsigned char %s[%d]={" % (name, TARGET * TARGET)] for y in range(TARGET): row = ",".join(str(alpha.getpixel((x, y))) for x in range(TARGET)) lines.append(row + ",") lines.append("};") return "\n".join(lines) def build_action_block(): outs = [ACTION_BLOCK_MARKER + "(Material Symbols Rounded, MD3, Apache-2.0)====\n"] outs.append(" * 复用状态栏同源 32x32 单通道 alpha;绘制颜色/按压态由 meow_ui_widget.c 主题表提供。 */\n") for cname, _enum, symbol in ACTIONS: a = rasterize_alpha32(symbol) outs.append(fmt_alpha_array(cname, a)) return "\n".join(outs) def build_table_and_enum(): table = ["static const sb_icon_t sb_icons[] = {"] for name in BASE_ICONS: table.append(" { %s }," % name) for cname, _enum, _symbol in ACTIONS: table.append(" { %s }," % cname) table.append("};") table = "\n".join(table) enum = ["/* 枚举索引(与 sb_icons[] 对应)。状态栏 0-14(勿改序);动作 15-29(TopAppBar 用)。 */"] enum.append("enum { SB_I_WIFI=0, SB_I_BT=1, SB_I_BATT=2, SB_I_VOL=3, SB_I_BRIGHT=4,") enum.append(" SB_I_WIFI_STRONG=5, SB_I_WIFI_MEDIUM=6, SB_I_WIFI_WEAK=7,") enum.append(" SB_I_WIFI_NO=8, SB_I_WIFI_OFF=9, SB_I_BT_NO=10, SB_I_BT_CONN=11,") enum.append(" SB_I_BT_DIS=12, SB_I_BT_SEARCH=13, SB_I_BATT_CHG=14,") # 动作枚举从 15 起,与 meow_ui_widgets.h 的 MEOW_ICON_* 同值(勿改序) for i, (_c, e, _s) in enumerate(ACTIONS): last = (i == len(ACTIONS) - 1) enum.append(" %s=%d%s" % (e, 15 + i, " }; " if last else ",")) return table + "\n\n" + "\n".join(enum) + "\n\n#endif /* SB_ICONS_H */\n" def regenerate_sb_icons_h(): old = open(SB_ICONS_H, encoding="utf-8").read() # body = 现有状态栏数组(原样保留)。HEADER_COMMENT 已提供 ifndef/define/size/typedef, # 故从首个状态栏数组起切;终点:优先动作块标记(幂等重跑),缺失则 sb_icons[] 表格(首跑)。 body_start = old.find("static const unsigned char " + BASE_ICONS[0] + "[1024]") if body_start < 0: sys.exit("sb_icons.h 缺少首个状态栏数组,拒绝重写") pr = old.find(ACTION_BLOCK_MARKER) if pr < 0: pr = old.find("static const sb_icon_t sb_icons[]") if pr < 0 or pr <= body_start: sys.exit("sb_icons.h 缺少 sb_icons[] 表格,拒绝重写") body = old[body_start:pr] out = HEADER_COMMENT + body.rstrip("\n") + "\n\n" out += build_action_block() + "\n\n" out += build_table_and_enum() open(SB_ICONS_H, "w", encoding="utf-8").write(out) print("wrote %s (%d bytes)" % (SB_ICONS_H, len(out))) def main(): os.makedirs(SRC_DIR, exist_ok=True) regenerate_sb_icons_h() if __name__ == "__main__": main()