K1Guitar/tools/gen_ui0902_assets.py

339 lines
17 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 -*-
"""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
import sys
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
# 模式选择页无底栏:三行按设计稿 0902-01 撑满(约 y=43/135/225,高 88)
ROW_RESIZE = (222, 88)
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]
# 低透明 / 近黑 / 列表行底 #242B3A → 透明(焦点蓝底上避免黑块)
if a < 40 or (r < 12 and g < 12 and b < 12):
out[i] = 0
out[i + 1] = 0
elif abs(r - ROW_BG[0]) <= 18 and abs(g - ROW_BG[1]) <= 18 and abs(b - ROW_BG[2]) <= 18:
out[i] = 0
out[i + 1] = 0
else:
r = (r * a) // 255
g = (g * a) // 255
b = (b * a) // 255
v = ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3)
out[i] = (v >> 8) & 0xFF
out[i + 1] = v & 0xFF
i += 2
return bytes(out), w, h
def rgba_to_rgb565_opaque(im):
"""Full-screen boot/charge/flash: keep dark bg; flatten RGBA onto black (not white).
Design exports like 烧录模式.png are mostly transparent with dark glyphs; a bare
convert('RGB') drops alpha and turns transparent into white — wrong on device.
"""
rgba = im.convert("RGBA")
bg = Image.new("RGBA", rgba.size, (0, 0, 0, 255))
im = Image.alpha_composite(bg, rgba).convert("RGB")
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 = px[x, y]
v = ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3)
out[i] = (v >> 8) & 0xFF
out[i + 1] = v & 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 (keep L/R walls; runtime draws level fill)."""
im = Image.open(SRC / "02_顶部图标" / "电池图标-10.png").convert("RGBA")
px = im.load()
w, h = im.size
# 旧 range(2,27) 会抹掉右侧壳壁,导致显示残缺
for y in range(2, 10):
for x in range(3, 24):
px[x, y] = (0, 0, 0, 0)
for y in range(h):
for x in range(w):
r, g, b, a = px[x, y]
if a >= 40 and (r + g + b) >= 60:
px[x, y] = (255, 255, 255, 255)
elif a < 40 or (r < 12 and g < 12 and b < 12):
px[x, y] = (0, 0, 0, 0)
return im
def make_battery_fill():
"""Solid white 22x9 level strip (do NOT use 电池电量 mockup — it embeds \"80%\" text)."""
return Image.new("RGBA", (22, 9), (255, 255, 255, 255))
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) ----------------
# NOTE: MCU-internal icon set in Drv_ILI9341_Lcd_Image_UI0902.c/.h may include
# extra hand-tuned symbols (tri/btn). Prefer --ext-only to refresh ExtFlash map
# without wiping those arrays.
INTERNAL = [
("gImage_UI0902_Volume_20x15", "02_顶部图标/音量图标-10.png"),
("gImage_UI0902_Speaker_10x15", "02_顶部图标/音量图标-0924-喇叭.png"),
("gImage_UI0902_Bluetooth_On_13x20", "02_顶部图标/蓝牙图标-开启-0923.png"),
("gImage_UI0902_Bluetooth_Off_19x20", "02_顶部图标/蓝牙图标-未开启-0923.png"),
("gImage_UI0902_BatteryCharging_40x13", "02_顶部图标/电池图标-充电状态-0924.png"), # 0924 充电闪电电池 40x13
# BatteryFill generated by make_battery_fill() — not from mockup PNG
("gImage_Nav0924_Universal_Sel_33x46", "03_底部图标/底栏0924-universal-sel.png"),
("gImage_Nav0924_Universal_Not_33x46", "03_底部图标/底栏0924-universal-not.png"),
("gImage_Nav0924_Normal_Sel_34x46", "03_底部图标/底栏0924-normal-sel.png"),
("gImage_Nav0924_Normal_Not_34x46", "03_底部图标/底栏0924-normal-not.png"),
("gImage_Nav0924_Expert_Sel_33x46", "03_底部图标/底栏0924-expert-sel.png"),
("gImage_Nav0924_Expert_Not_33x46", "03_底部图标/底栏0924-expert-not.png"),
("gImage_Nav0924_Setting_Sel_34x46", "03_底部图标/底栏0924-setting-sel.png"),
("gImage_Nav0924_Setting_Not_34x46", "03_底部图标/底栏0924-setting-not.png"),
]
# ---------------- external flash (big images / text strips) ----------------
EXTERNAL = [
# Art numbering: 16/19=universal, 17/20=normal, 18/21=expert (not consecutive pairs)
("ROW_UNIVERSAL_NOT", "04_模式图标/完整版/模式界面栏背景-16.png"),
("ROW_UNIVERSAL_SEL", "04_模式图标/完整版/模式界面栏背景-19.png"),
("ROW_NORMAL_NOT", "04_模式图标/完整版/模式界面栏背景-17.png"),
("ROW_NORMAL_SEL", "04_模式图标/完整版/模式界面栏背景-20.png"),
("ROW_EXPERT_NOT", "04_模式图标/完整版/模式界面栏背景-18.png"),
("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"),
# BT rune / gear / chevron / restore CCW / power
("ICON_SET_BLUETOOTH", "06_控件/系统设置/系统设置图标-11.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_控件/系统设置/系统设置图标-13.png"),
("ICON_SYS_ABOUT", "06_控件/系统设置/系统设置图标-12.png"),
("ICON_SYS_VERSION", "06_控件/系统设置/系统设置图标-10.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_控件/语言-中文-选中-0924.png"),
("BTN_LANG_CN1", "06_控件/语言-中文-未选-0924.png"),
("BTN_LANG_CN2", "06_控件/语言-中文-选中-0924.png"),
("BTN_LANG_EN", "06_控件/语言-English-未选-0924.png"),
("BTN_LANG_EN_ON", "06_控件/语言-English-选中-0924.png"),
("ROW_HUB_MIXER", "05_设置图标/设置入口-调音台-0924.png"),
("ROW_HUB_LANG", "05_设置图标/设置入口-系统语言-0924.png"),
("ROW_HUB_SYSTEM", "05_设置图标/设置入口-系统设置-0924.png"),
("TXT_USB_CONFIRM", "06_控件/USB确认-文案-0924.png"),
("BTN_USB_CANCEL_DARK", "06_控件/USB确认-取消灰-0924.png"),
("BTN_USB_CONFIRM_BLUE", "06_控件/USB确认-确认蓝-0924.png"),
("BTN_USB_CANCEL_BLUE", "06_控件/USB确认-取消蓝-0924.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"),
# 文字文件:13伴奏 14弦 15麦 16麦混响(旧映射把 13/15 挂反)
("TXT_MIXER_BAND", "06_控件/调音台控件/文字/调音台文字-13.png"), # 伴奏
("TXT_MIXER_GUITAR", "06_控件/调音台控件/文字/调音台文字-14.png"), # 弦
("TXT_MIXER_MIC", "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"),
# 整屏开机 Logo / 充电画面 / 烧录模式(避开旧 0x0 / Charg.bin 0xCB70 区,挂在 ui0902 资源包)
("BOOT_LOGO", "07_开机/开机界面-0924.png"),
("CHARGE_SCREEN", "08_充电界面/充电界面-0924.png"),
("FLASH_MODE", "09_烧录模式/烧录模式-240x320.png"),
("LOWBAT_TIP", "10_低电量/低电量提醒-0924.png"),
]
def main():
ext_only = "--ext-only" in sys.argv
# ---- internal flash C arrays ----
if not ext_only:
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)
data, w, h = rgba_to_rgb565(make_battery_fill())
emit_c_array(c_lines, "gImage_UI0902_BatteryFill_22x9", data, w, h)
h_lines.append(f"extern const unsigned char gImage_UI0902_BatteryFill_22x9[{len(data)}];")
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)
extra_c = ROOT / "tools" / "ui0902_internal_extra.c"
extra_decl = [
"",
"extern const unsigned char gImage_UI0902_BtnMinus_23x22[1012];",
"extern const unsigned char gImage_UI0902_BtnPlus_23x22[1012];",
"extern const unsigned char gImage_UI0902_TabBack_30x24[1440];",
"extern const unsigned char gImage_UI0902_TriLeft_22x22[968];",
"extern const unsigned char gImage_UI0902_TriRight_23x22[1012];",
"extern const unsigned char gImage_UI0902_Txt_Confirm_50x24[2400];",
"extern const unsigned char gImage_UI0902_Txt_Cancel_50x24[2400];",
]
h_lines += extra_decl
h_lines += ["", "#endif", ""]
OUT_H.write_text("\n".join(h_lines), encoding="utf-8")
c_body = "\n".join(c_lines)
if extra_c.exists():
c_body += "\n" + extra_c.read_text(encoding="latin-1")
if not c_body.endswith("\n"):
c_body += "\n"
OUT_C.write_text(c_body, encoding="latin-1")
print(f"internal flash payload: {total_int} bytes + extras -> {OUT_C.name}")
else:
print("Skip internal C arrays (--ext-only)")
# ---- 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)
elif name in ("BOOT_LOGO", "CHARGE_SCREEN", "FLASH_MODE", "LOWBAT_TIP",
"TXT_USB_CONFIRM"):
if im.size[0] >= 241 and im.size[1] >= 321:
im = im.crop((0, 0, 240, 320))
elif name != "TXT_USB_CONFIRM" and im.size != (240, 320):
im = im.resize((240, 320), Image.LANCZOS)
data, w, h = rgba_to_rgb565_opaque(im)
elif name.startswith(("ROW_HUB_", "BTN_LANG_", "BTN_USB_")):
data, w, h = rgba_to_rgb565_opaque(im)
else:
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"external flash payload: {len(blob)} bytes ({len(entries)} images) -> {OUT_BIN.name}")
print(f"addr header -> {OUT_ADDR}")
if __name__ == "__main__":
main()