#!/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] 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 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 (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_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"), ("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"), # 整屏开机 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()