K1Guitar/tools/gen_ui0902_assets.py

300 lines
15 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: keep dark background pixels (no near-black punch-through)."""
im = im.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_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 = [
# 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 最新全屏充电稿;百分比重叠由固件绘制
]
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"):
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()