subtree pull sdk 来自主仓 pub/sdk(主仓 commit 68ce46a 等)。新增: - 布局引擎 meow_ui_box / 动画基座 meow_ui_anim / 滚动物理 meow_ui_scroll - §6.4 事件分发形式化 / W_LISTVIEW 虚拟化多行 / meow_str 字符串资源 i18n - sb_icons 动作图标扩充 + topbar 位图图标 / meow_img_downscale - tools/ 图标生成脚本与 SVG 源 - 对应宿主 test_*.c 与 demo_box_scroll.c build_sdk.sh 冲突解析:保留公开仓自包含版(vendored meow_font + -Iinclude -I.), OBJS 手工并入 4 个新模块(meow_ui_box/anim/scroll/str)。保留 vendored meow_font.c/ meow_ipc.h/meow_font.h/meow_notif.h,剔除宿主测试二进制与编译产物。
203 lines
9.6 KiB
Python
203 lines
9.6 KiB
Python
#!/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()
|