2026-09-02 13:37:25 +08:00
|
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
|
"""K1 0902 UI assets -> RGB565 C arrays (MCU flash) + packed bin (W25Q128 external flash).
|
|
|
|
|
|
|
|
|
|
|
|
Outputs:
|
|
|
|
|
|
device/LCD_ILI9341/Drv_ILI9341_Lcd_Image_UI0902.c/.h small icons (internal flash)
|
|
|
|
|
|
project/inc/UI0902_Res_Addr.h external flash address map
|
|
|
|
|
|
tools/out/ui0902_res.bin packed images, base UI0902_RES_BASE
|
|
|
|
|
|
tools/out/preview_*.png page previews
|
|
|
|
|
|
"""
|
|
|
|
|
|
from PIL import Image, ImageDraw, ImageFont
|
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
|
|
|
|
SRC = ROOT / "tools" / "assets_0902"
|
|
|
|
|
|
OUT = ROOT / "tools" / "out"
|
|
|
|
|
|
OUT.mkdir(exist_ok=True)
|
|
|
|
|
|
OUT_C = ROOT / "device" / "LCD_ILI9341" / "Drv_ILI9341_Lcd_Image_UI0902.c"
|
|
|
|
|
|
OUT_H = ROOT / "device" / "LCD_ILI9341" / "Drv_ILI9341_Lcd_Image_UI0902.h"
|
|
|
|
|
|
OUT_ADDR = ROOT / "project" / "inc" / "UI0902_Res_Addr.h"
|
|
|
|
|
|
OUT_BIN = OUT / "ui0902_res.bin"
|
|
|
|
|
|
|
|
|
|
|
|
RES_BASE = 0x00100000 # W25Q128 offset; avoids logo(0x0)/charge(0xCB70)/preset(<~0xA8B60) regions
|
|
|
|
|
|
ALIGN = 0x1000
|
|
|
|
|
|
|
|
|
|
|
|
# 222x89 rows do not fit 3x between status bar (26) and bottom bar (285); squash to 84px
|
|
|
|
|
|
ROW_RESIZE = (222, 84)
|
|
|
|
|
|
|
|
|
|
|
|
MISANS = r"c:\Users\qjyu\Documents\SoundWalker\一诺国际吉他\Doc\UI\MiSans\MiSans 字体\MiSans-Medium.ttf"
|
|
|
|
|
|
PAGE_BG = (0, 8, 19) # #000813
|
|
|
|
|
|
ROW_BG = (36, 43, 58) # #242B3A
|
|
|
|
|
|
SEL_BLUE = (3, 78, 213) # #034ED5
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def rgba_to_rgb565(im):
|
|
|
|
|
|
im = im.convert("RGBA")
|
|
|
|
|
|
px = im.load()
|
|
|
|
|
|
w, h = im.size
|
|
|
|
|
|
out = bytearray(w * h * 2)
|
|
|
|
|
|
i = 0
|
|
|
|
|
|
for y in range(h):
|
|
|
|
|
|
for x in range(w):
|
|
|
|
|
|
r, g, b, a = px[x, y]
|
|
|
|
|
|
if a < 40 or (r < 12 and g < 12 and b < 12):
|
|
|
|
|
|
out[i] = 0
|
|
|
|
|
|
out[i + 1] = 0
|
|
|
|
|
|
else:
|
|
|
|
|
|
r = (r * a) // 255
|
|
|
|
|
|
g = (g * a) // 255
|
|
|
|
|
|
b = (b * a) // 255
|
|
|
|
|
|
c = ((r >> 3) << 11) | ((g >> 2) << 5) | (b >> 3)
|
|
|
|
|
|
out[i] = (c >> 8) & 0xFF
|
|
|
|
|
|
out[i + 1] = c & 0xFF
|
|
|
|
|
|
i += 2
|
|
|
|
|
|
return bytes(out), w, h
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def emit_c_array(lines, name, data, w, h):
|
|
|
|
|
|
lines.append(f"const unsigned char {name}[{len(data)}] = {{ /* {w}x{h} RGB565 BE */")
|
|
|
|
|
|
for i in range(0, len(data), 16):
|
|
|
|
|
|
lines.append(",".join(f"0x{b:02X}" for b in data[i:i + 16]) + ",")
|
|
|
|
|
|
lines.append("};")
|
|
|
|
|
|
lines.append("")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def make_battery_outline():
|
|
|
|
|
|
"""电池图标-10 -> outline only (interior fill removed; runtime draws level fill)."""
|
|
|
|
|
|
im = Image.open(SRC / "02_顶部图标" / "电池图标-10.png").convert("RGBA")
|
|
|
|
|
|
px = im.load()
|
|
|
|
|
|
for y in range(2, 10):
|
|
|
|
|
|
for x in range(2, 27):
|
|
|
|
|
|
px[x, y] = (0, 0, 0, 0)
|
|
|
|
|
|
return im
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def make_cancel_button():
|
|
|
|
|
|
"""Generate 取消 106x56 dialog button matching 控件-10 style."""
|
|
|
|
|
|
im = Image.new("RGBA", (106, 56), (0, 0, 0, 0))
|
|
|
|
|
|
d = ImageDraw.Draw(im)
|
|
|
|
|
|
d.rounded_rectangle([0, 0, 105, 55], radius=12, fill=ROW_BG + (255,))
|
|
|
|
|
|
f = ImageFont.truetype(MISANS, 24)
|
|
|
|
|
|
t = "取消"
|
|
|
|
|
|
bb = d.textbbox((0, 0), t, font=f)
|
|
|
|
|
|
d.text(((106 - (bb[2] - bb[0])) // 2 - bb[0], (56 - (bb[3] - bb[1])) // 2 - bb[1]),
|
|
|
|
|
|
t, fill=(255, 255, 255, 255), font=f)
|
|
|
|
|
|
return im
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------- internal flash (small, high-frequency icons) ----------------
|
|
|
|
|
|
INTERNAL = [
|
|
|
|
|
|
("gImage_UI0902_Volume_20x15", "02_顶部图标/音量图标-10.png"),
|
|
|
|
|
|
("gImage_UI0902_Bluetooth_11x17", "02_顶部图标/蓝牙图标-10.png"),
|
|
|
|
|
|
("gImage_UI0902_BatteryFill_22x9", "02_顶部图标/电池电量-10-11.png"),
|
|
|
|
|
|
("gImage_UI0902_TabSetting_Sel_36x28", "03_底部图标/底部图标-10.png"),
|
|
|
|
|
|
("gImage_UI0902_TabSetting_Not_35x28", "03_底部图标/底部图标-11.png"),
|
|
|
|
|
|
("gImage_UI0902_TabMixer_Sel_35x27", "03_底部图标/底部图标-12.png"),
|
|
|
|
|
|
("gImage_UI0902_TabMixer_Not_18x28", "03_底部图标/底部图标-13.png"),
|
|
|
|
|
|
("gImage_UI0902_TabMode_Sel_36x28", "03_底部图标/底部图标-14.png"),
|
|
|
|
|
|
("gImage_UI0902_TabMode_Not_35x29", "03_底部图标/底部图标-15.png"),
|
|
|
|
|
|
("gImage_UI0902_TabBack_Sel_35x27", "03_底部图标/底部图标-16.png"),
|
|
|
|
|
|
("gImage_UI0902_TabBack_Not_18x28", "03_底部图标/底部图标-17.png"),
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------- external flash (big images / text strips) ----------------
|
|
|
|
|
|
EXTERNAL = [
|
2026-09-02 15:24:49 +08:00
|
|
|
|
# Art numbering: 16/19=universal, 17/20=normal, 18/21=expert (not consecutive pairs)
|
2026-09-02 13:37:25 +08:00
|
|
|
|
("ROW_UNIVERSAL_NOT", "04_模式图标/完整版/模式界面栏背景-16.png"),
|
2026-09-02 15:24:49 +08:00
|
|
|
|
("ROW_UNIVERSAL_SEL", "04_模式图标/完整版/模式界面栏背景-19.png"),
|
|
|
|
|
|
("ROW_NORMAL_NOT", "04_模式图标/完整版/模式界面栏背景-17.png"),
|
|
|
|
|
|
("ROW_NORMAL_SEL", "04_模式图标/完整版/模式界面栏背景-20.png"),
|
|
|
|
|
|
("ROW_EXPERT_NOT", "04_模式图标/完整版/模式界面栏背景-18.png"),
|
2026-09-02 13:37:25 +08:00
|
|
|
|
("ROW_EXPERT_SEL", "04_模式图标/完整版/模式界面栏背景-21.png"),
|
|
|
|
|
|
("ICON_UNIVERSAL_SEL", "04_模式图标/万能模式图标-1.png"),
|
|
|
|
|
|
("ICON_UNIVERSAL_NOT", "04_模式图标/万能模式图标-2.png"),
|
|
|
|
|
|
("ICON_NORMAL_SEL", "04_模式图标/普通模式图标-3.png"),
|
|
|
|
|
|
("ICON_NORMAL_NOT", "04_模式图标/普通模式图标-4.png"),
|
|
|
|
|
|
("ICON_EXPERT_SEL", "04_模式图标/专业模式图标-5.png"),
|
|
|
|
|
|
("ICON_EXPERT_NOT", "04_模式图标/专业模式图标-6.png"),
|
|
|
|
|
|
("TXT_MODE_UNIVERSAL_NOT", "04_模式图标/完整版/文字/模式选择文字-10.png"),
|
|
|
|
|
|
("TXT_MODE_UNIVERSAL_SEL", "04_模式图标/完整版/文字/模式选择文字-11.png"),
|
|
|
|
|
|
("TXT_MODE_NORMAL_NOT", "04_模式图标/完整版/文字/模式选择文字-12.png"),
|
|
|
|
|
|
("TXT_MODE_NORMAL_SEL", "04_模式图标/完整版/文字/模式选择文字-13.png"),
|
|
|
|
|
|
("TXT_MODE_EXPERT_NOT", "04_模式图标/完整版/文字/模式选择文字-14.png"),
|
|
|
|
|
|
("TXT_MODE_EXPERT_SEL", "04_模式图标/完整版/文字/模式选择文字-15.png"),
|
|
|
|
|
|
("ICON_SET_BLUETOOTH", "05_设置图标/设置图标-01.png"),
|
|
|
|
|
|
("ICON_SET_GEAR", "05_设置图标/设置图标-02.png"),
|
|
|
|
|
|
("ICON_CHEVRON", "05_设置图标/设置图标-03.png"),
|
|
|
|
|
|
("ROW_SET_BLUETOOTH", "05_设置图标/设置图标 完整-10.png"),
|
|
|
|
|
|
("ROW_SET_SYSTEM", "05_设置图标/设置图标 完整-11.png"),
|
|
|
|
|
|
("ICON_SYS_LANG", "06_控件/系统设置/系统设置图标-10.png"),
|
|
|
|
|
|
("ICON_SYS_RESTORE", "06_控件/系统设置/系统设置图标-11.png"),
|
|
|
|
|
|
("ICON_SYS_ABOUT", "06_控件/系统设置/系统设置图标-12.png"),
|
|
|
|
|
|
("ICON_SYS_VERSION", "06_控件/系统设置/系统设置图标-13.png"),
|
|
|
|
|
|
("TXT_SET_LANG", "06_控件/系统设置/文字/设置界面文字-14.png"),
|
|
|
|
|
|
("TXT_SET_RESTORE", "06_控件/系统设置/文字/设置界面文字-15.png"),
|
|
|
|
|
|
("TXT_SET_ABOUT", "06_控件/系统设置/文字/设置界面文字-16.png"),
|
|
|
|
|
|
("TXT_SET_VERSION", "06_控件/系统设置/文字/设置界面文字-17.png"),
|
|
|
|
|
|
("TXT_SET2_A", "06_控件/系统设置/文字/设置界面文字 - 二级-14.png"),
|
|
|
|
|
|
("TXT_SET2_B", "06_控件/系统设置/文字/设置界面文字 - 二级-15.png"),
|
|
|
|
|
|
("TXT_SET2_C", "06_控件/系统设置/文字/设置界面文字 - 二级-16.png"),
|
|
|
|
|
|
("TXT_SET2_D", "06_控件/系统设置/文字/设置界面文字 - 二级-17.png"),
|
|
|
|
|
|
("BTN_TYPE_ACOUSTIC_SEL", "06_控件/控件 类型选择-10.png"),
|
|
|
|
|
|
("BTN_TYPE_SING_NOT", "06_控件/控件 类型选择-11.png"),
|
|
|
|
|
|
("BTN_TYPE_FINGER_NOT", "06_控件/控件 类型选择-12.png"),
|
|
|
|
|
|
("BTN_TYPE_STRUM_NOT", "06_控件/控件 类型选择-13.png"),
|
|
|
|
|
|
("BTN_MINUS_BLUE", "06_控件/控件-11.png"),
|
|
|
|
|
|
("BTN_PLUS_BLUE", "06_控件/控件-12.png"),
|
|
|
|
|
|
("BTN_MINUS_GRAY", "06_控件/控件-13.png"),
|
|
|
|
|
|
("BTN_PLUS_GRAY", "06_控件/控件-14.png"),
|
|
|
|
|
|
("SLIDER_WIDGET", "06_控件/控件-15.png"),
|
|
|
|
|
|
("BTN_LANG_SEL", "06_控件/控件-16.png"),
|
|
|
|
|
|
("BTN_LANG_CN1", "06_控件/控件-17.png"),
|
|
|
|
|
|
("BTN_LANG_CN2", "06_控件/控件-18.png"),
|
|
|
|
|
|
("BTN_LANG_EN", "06_控件/控件-19.png"),
|
|
|
|
|
|
("BTN_CANCEL_SM", "06_控件/控件 2-20.png"),
|
|
|
|
|
|
("BTN_CONFIRM_SM", "06_控件/控件 2-21.png"),
|
|
|
|
|
|
("BTN_CANCEL_SM2", "06_控件/控件 2-22.png"),
|
|
|
|
|
|
("BTN_CONFIRM_SM2", "06_控件/控件 2-23.png"),
|
|
|
|
|
|
("BTN_CONFIRM_BIG", "06_控件/控件-10.png"),
|
|
|
|
|
|
("MIXER_KNOB", "06_控件/调音台控件/调音台控件-1.png"),
|
|
|
|
|
|
("MIXER_KNOB2", "06_控件/调音台控件/调音台控件-2.png"),
|
|
|
|
|
|
("MIXER_TRACK", "06_控件/调音台控件/调音台控件-3.png"),
|
|
|
|
|
|
("TXT_MIXER_MIC", "06_控件/调音台控件/文字/调音台文字-13.png"),
|
|
|
|
|
|
("TXT_MIXER_GUITAR", "06_控件/调音台控件/文字/调音台文字-14.png"),
|
|
|
|
|
|
("TXT_MIXER_BAND", "06_控件/调音台控件/文字/调音台文字-15.png"),
|
|
|
|
|
|
("TXT_MIXER_VOCALFX", "06_控件/调音台控件/文字/调音台文字-16.png"),
|
|
|
|
|
|
("TXT_MIXER_GUITARFX", "06_控件/调音台控件/文字/调音台文字-17.png"),
|
|
|
|
|
|
("TXT_EFFECT_AMOUNT", "06_控件/文字/文字-10.png"),
|
|
|
|
|
|
("TXT_BAND_VOLUME", "06_控件/文字/文字-11.png"),
|
|
|
|
|
|
("TXT_MIC_VOLUME", "06_控件/文字/文字-12.png"),
|
|
|
|
|
|
("TXT_RESTORE_V1", "06_控件/文字/文字-24.png"),
|
|
|
|
|
|
("TXT_RESTORE_V2", "06_控件/文字/文字-25.png"),
|
|
|
|
|
|
("TXT_RESTORE_H", "06_控件/文字/文字-26.png"),
|
|
|
|
|
|
("TXT_RESTORE_CONFIRM", "06_控件/文字/文字-27.png"),
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def main():
|
|
|
|
|
|
# ---- internal flash C arrays ----
|
|
|
|
|
|
c_lines = ['#include "Drv_ILI9341_Lcd_Image_UI0902.h"', "",
|
|
|
|
|
|
"/* Auto-generated by tools/gen_ui0902_assets.py. Do not hand-edit. */", ""]
|
|
|
|
|
|
h_lines = ["#ifndef __DRV_ILI9341_LCD_IMAGE_UI0902_H",
|
|
|
|
|
|
"#define __DRV_ILI9341_LCD_IMAGE_UI0902_H", "",
|
|
|
|
|
|
'#include "stdint.h"', ""]
|
|
|
|
|
|
total_int = 0
|
|
|
|
|
|
|
|
|
|
|
|
data, w, h = rgba_to_rgb565(make_battery_outline())
|
|
|
|
|
|
emit_c_array(c_lines, "gImage_UI0902_BatteryOutline_29x12", data, w, h)
|
|
|
|
|
|
h_lines.append("extern const unsigned char gImage_UI0902_BatteryOutline_29x12[];")
|
|
|
|
|
|
total_int += len(data)
|
|
|
|
|
|
|
|
|
|
|
|
for name, rel in INTERNAL:
|
|
|
|
|
|
data, w, h = rgba_to_rgb565(Image.open(SRC / rel))
|
|
|
|
|
|
emit_c_array(c_lines, name, data, w, h)
|
|
|
|
|
|
h_lines.append(f"extern const unsigned char {name}[{len(data)}];")
|
|
|
|
|
|
total_int += len(data)
|
|
|
|
|
|
h_lines += ["", "#endif", ""]
|
|
|
|
|
|
OUT_H.write_text("\n".join(h_lines), encoding="utf-8")
|
|
|
|
|
|
OUT_C.write_text("\n".join(c_lines), encoding="utf-8")
|
|
|
|
|
|
|
|
|
|
|
|
# ---- external flash bin ----
|
|
|
|
|
|
addr_lines = ["#ifndef __UI0902_RES_ADDR_H", "#define __UI0902_RES_ADDR_H", "",
|
|
|
|
|
|
"/* Auto-generated by tools/gen_ui0902_assets.py. W25Q128 resource map. */",
|
|
|
|
|
|
f"#define UI0902_RES_BASE 0x{RES_BASE:08X}UL", ""]
|
|
|
|
|
|
blob = bytearray()
|
|
|
|
|
|
offset = 0
|
|
|
|
|
|
entries = {}
|
|
|
|
|
|
for name, rel in EXTERNAL:
|
|
|
|
|
|
im = Image.open(SRC / rel)
|
|
|
|
|
|
if name in ("ROW_UNIVERSAL_NOT", "ROW_UNIVERSAL_SEL", "ROW_NORMAL_NOT", "ROW_NORMAL_SEL", "ROW_EXPERT_NOT", "ROW_EXPERT_SEL"):
|
|
|
|
|
|
im = im.resize(ROW_RESIZE, Image.LANCZOS)
|
|
|
|
|
|
data, w, h = rgba_to_rgb565(im)
|
|
|
|
|
|
entries[name] = (offset, w, h, len(data))
|
|
|
|
|
|
blob += data
|
|
|
|
|
|
pad = (-len(blob)) % ALIGN
|
|
|
|
|
|
blob += b"\x00" * pad
|
|
|
|
|
|
offset += len(data) + pad
|
|
|
|
|
|
# derived cancel button
|
|
|
|
|
|
data, w, h = rgba_to_rgb565(make_cancel_button())
|
|
|
|
|
|
entries["BTN_CANCEL_BIG"] = (offset, w, h, len(data))
|
|
|
|
|
|
blob += data
|
|
|
|
|
|
for name, (off, w, h, size) in entries.items():
|
|
|
|
|
|
addr_lines.append(f"#define UI0902_{name}_ADDR (UI0902_RES_BASE + 0x{off:06X}UL) /* {size} bytes */")
|
|
|
|
|
|
addr_lines.append(f"#define UI0902_{name}_W {w}")
|
|
|
|
|
|
addr_lines.append(f"#define UI0902_{name}_H {h}")
|
|
|
|
|
|
addr_lines.append("")
|
|
|
|
|
|
addr_lines += ["#endif", ""]
|
|
|
|
|
|
OUT_ADDR.write_text("\n".join(addr_lines), encoding="utf-8")
|
|
|
|
|
|
OUT_BIN.write_bytes(bytes(blob))
|
|
|
|
|
|
|
|
|
|
|
|
print(f"internal flash payload: {total_int} bytes -> {OUT_C.name}")
|
|
|
|
|
|
print(f"external flash payload: {len(blob)} bytes ({len(entries)} images) -> {OUT_BIN.name}")
|
|
|
|
|
|
print(f"addr header -> {OUT_ADDR}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
|
main()
|