Files
MeowOS-For-R3P2/patch_v2/sdk/tools/gen_action_icons.py
T
AskaEth 3611cbbbde sdk: sb_icons 扩充动作图标 + TopAppBar 位图图标支持
TopAppBar 动作项此前用字体码点(⚙↻⌕)绘制,4k 字库无这些码点→豆腐。改走位图:
- sb_icons.h 追加 15 个 Material Symbols Rounded 动作位图(settings/refresh/search/
  play_arrow/pause/skip_next/skip_previous/arrow_back/close/add/check/lyrics/
  queue_music/download/heart),FILL=1 实心与状态栏一致;sb_icons[] 只追加(0-14 不变),
  动作索引 15-29。
- 生成脚本 tools/gen_action_icons.py(cairosvg svg→32x32 单通道 alpha,SVG 缓存
  icons_src/ 可复现、断网可跑)。
- meow_ui_gfx.c 抽取图标 blit 像素核心 icon_blit_px,新增 meow_gfx_blit_icon_ink
  (控件路径不标顶栏脏区);meow_gfx_blit_icon 状态栏路径行为不变。
- meow_ui_widget.c W_TOPAPPBAR 项支持位图模式:新 API meow_topappbar_set_item_icon
  (w, idx, icon_idx),绘制取主题 onSurface/按压向栏底色淡 12%,文本项行为不变;
  hit 用统一项宽。
- 公共头 widgets.h 加 MEOW_ICON_*(15-29) 枚举与 set_item_icon 声明;internal.h 声明
  meow_gfx_blit_icon_ink。
- 宿主测试(test_meow_ui_widgets)加 topbar 位图图标绘制/命中/按压变淡 + 状态栏 blit
  回归断言;test_umbrella 加 MEOW_ICON_* 编译期锁;MEOW_APP_SDK.md 补说明+索引表。
- 验证:宿主测试 ALL PASS(14项);交叉 zig 构建 0 warning;meow_ui.a 新 md5
  1d69cdae55ab5dde3a98f5e2124f3656。
2026-09-02 03:24:46 +08:00

203 lines
9.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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 <v> && <v>/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 <v> && <v>/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()