K1Guitar/tools/gen_ui0902_assets.py

307 lines
15 KiB
Python
Raw Permalink Normal View History

#!/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_Bluetooth_11x17", "02_顶部图标/蓝牙图标-10.png"),
# BatteryFill generated by make_battery_fill() — not from mockup PNG
("gImage_UI0902_TabSetting_Sel_40x32", "03_底部图标/底部图标-10.png"), # 万能白
("gImage_UI0902_TabSetting_Not_40x32", "03_底部图标/底部图标-11.png"), # 普通白
("gImage_UI0902_TabMixer_Sel_40x32", "03_底部图标/底部图标-12.png"), # 专业白
("gImage_UI0902_TabMixer_Not_22x32", "03_底部图标/底部图标-13.png"), # 设置白
("gImage_UI0902_TabMode_Sel_40x32", "03_底部图标/底部图标-14.png"), # 万能蓝
("gImage_UI0902_TabMode_Not_40x32", "03_底部图标/底部图标-15.png"), # 普通蓝
("gImage_UI0902_TabBack_Sel_40x32", "03_底部图标/底部图标-16.png"), # 专业蓝
("gImage_UI0902_TabBack_Not_22x32", "03_底部图标/底部图标-17.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_控件/控件-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"),
# 文字文件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_开机/开机界面-20.png"),
("CHARGE_SCREEN", "08_充电界面/充电界面-0902-09.png"), # 0904 最新全屏充电稿;百分比重叠由固件绘制
("FLASH_MODE", "09_烧录模式/烧录模式-240x320.png"), # Boot IAP 烧录等待画面
]
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)
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")
print(f"internal flash payload: {total_int} bytes -> {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"):
if im.size[0] >= 241 and im.size[1] >= 321:
im = im.crop((0, 0, 240, 320))
elif im.size != (240, 320):
im = im.resize((240, 320), Image.LANCZOS)
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()