# -*- coding: utf-8 -*- """System-settings text as MiSans-Medium RGB565, same pipeline as the settings hub cards.""" from pathlib import Path import numpy as np from PIL import Image, ImageDraw, ImageFont ROOT = Path(r"C:\Users\qjyu\Documents\SoundWalker\一诺国际吉他") CODE = ROOT / "Code" / "YNGJ-GT1-M - AT32F403ARCT7" FONT = next((ROOT / "Doc" / "UI" / "MiSans").rglob("MiSans-Medium.ttf")) SCREEN = CODE / "screen.png" OUT_C = CODE / "device" / "LCD_ILI9341" / "Drv_ILI9341_Lcd_Image_SysSet.c" OUT_H = CODE / "device" / "LCD_ILI9341" / "Drv_ILI9341_Lcd_Image_SysSet.h" PREVIEW = CODE / "tools" / "out" / "sysset_preview.png" TITLE_PX = 16 LABEL_PX = 13 VALUE_PX = 13 def render_phrase(text, px): font = ImageFont.truetype(str(FONT), px) scratch = Image.new("RGBA", (220, 48), (0, 0, 0, 0)) dr = ImageDraw.Draw(scratch) dr.text((2, 2), text, font=font, fill=(255, 255, 255, 255)) bbox = scratch.getbbox() if bbox is None: raise SystemExit("empty " + text) return scratch.crop(bbox) CELL_H = 13 BASELINE = 11 def render_glyph(ch, px): """Fixed-height cell so '.' shares the digit baseline. Horizontal crop only.""" font = ImageFont.truetype(str(FONT), px) origin = 8 scratch = Image.new("RGBA", (72, CELL_H), (0, 0, 0, 0)) dr = ImageDraw.Draw(scratch) try: dr.text((origin, BASELINE), ch, font=font, fill=(255, 255, 255, 255), anchor="ls") except TypeError: ascent, _ = font.getmetrics() dr.text((origin, BASELINE - ascent), ch, font=font, fill=(255, 255, 255, 255)) alpha = np.array(scratch)[:, :, 3] cols = np.where(alpha.max(axis=0) >= 40)[0] adv = max(1, int(round(font.getlength(ch)))) if len(cols) == 0: im = Image.new("RGBA", (adv, CELL_H), (0, 0, 0, 0)) return im, 0, adv x0, x1 = int(cols[0]), int(cols[-1]) im = scratch.crop((x0, 0, x1 + 1, CELL_H)) bear = x0 - origin if bear < 0: pad = Image.new("RGBA", (im.size[0] - bear, CELL_H), (0, 0, 0, 0)) pad.paste(im, (-bear, 0)) im = pad bear = 0 adv = max(bear + im.size[0], adv) return im, bear, adv def to_rgb565(im): im = im.convert("RGBA") w, h = im.size out = bytearray(w * h * 2) i = 0 px = im.load() for y in range(h): for x in range(w): r, g, b, a = px[x, y] if a < 40: 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) def crop_widget(im, x0, y0, x1, y1, key_fn): """RGB crop; keyed pixels become transparent black.""" sub = im.crop((x0, y0, x1 + 1, y1 + 1)).convert("RGBA") px = sub.load() w, h = sub.size for y in range(h): for x in range(w): r, g, b, a = px[x, y] if key_fn(r, g, b): px[x, y] = (0, 0, 0, 0) else: px[x, y] = (r, g, b, 255) bbox = sub.getbbox() if bbox is None: raise SystemExit("empty widget") ox, oy = bbox[0], bbox[1] return sub.crop(bbox), x0 + ox, y0 + oy def is_page_bg(r, g, b): return r < 14 and g < 18 and b < 28 def is_card(r, g, b): return 24 <= r <= 48 and 32 <= g <= 56 and 44 <= b <= 72 def to_1bit(im): """MSB-first ink. About 1/16 the size of RGB565, which is what internal flash can hold.""" im = im.convert("RGBA") w, h = im.size stride = (w + 7) // 8 out = bytearray(stride * h) px = im.load() for y in range(h): for x in range(w): r, g, b, a = px[x, y] if a >= 96 and (r + g + b) > 180: out[y * stride + (x >> 3)] |= 0x80 >> (x & 7) return bytes(out) def emit_array(lines, name, data, note): lines.append(f"const unsigned char {name}[{len(data)}] = {{ /* {note} */") hexes = [f"0x{b:02X}" for b in data] for i in range(0, len(hexes), 16): lines.append(" " + ",".join(hexes[i:i + 16]) + ",") lines.append("};") lines.append("") def main(): screen = Image.open(SCREEN).convert("RGB") phrases = { "Title": ("系统设置", TITLE_PX), "LblVer": ("固件版本", LABEL_PX), "LblUsb": ("USB模式", LABEL_PX), "LblAuto": ("自动关机", LABEL_PX), "LblRestore": ("恢复出厂设置", LABEL_PX), "UsbUpdate": ("USB更新", VALUE_PX), "UsbMidi": ("USB MIDI", VALUE_PX), "Off": ("关闭", VALUE_PX), } pics = {} for name, (text, px) in phrases.items(): im = render_phrase(text, px) pics[name] = im print(f"{name:12} {text} {im.size}") glyphs = {} for ch in "0123456789.": im, bear, adv = render_glyph(ch, VALUE_PX) glyphs[ch] = (im, bear, adv) print(f"glyph {ch!r:4} {im.size} bear {bear} adv {adv}") back, back_x, back_y = crop_widget(screen, 8, 6, 36, 32, is_page_bg) chev, chev_x, chev_y = crop_widget(screen, 198, 255, 218, 282, lambda r, g, b: is_card(r, g, b) or is_page_bg(r, g, b)) tri_l, tlx, tly = crop_widget(screen, 100, 132, 121, 152, is_card) tri_r, trx, try_ = crop_widget(screen, 191, 132, 211, 152, is_card) print("back", back.size, back_x, back_y) print("chev", chev.size, chev_x, chev_y) print("triL", tri_l.size, tlx, tly, "triR", tri_r.size, trx, try_) h_lines = [ "#ifndef __DRV_ILI9341_LCD_IMAGE_SYSSET_H", "#define __DRV_ILI9341_LCD_IMAGE_SYSSET_H", "", '#include "stdint.h"', "", "/* MiSans-Medium strips, same blit path as the settings hub cards. */", "typedef struct {", " const unsigned char *data;", " uint8_t w;", " uint8_t h;", " uint8_t adv;", " int8_t bear;", " uint8_t mode; /* 0 = 1-bit ink, 1 = RGB565 */", "} SysSetGlyph;", "", ] c_lines = [ '#include "Drv_ILI9341_Lcd_Image_SysSet.h"', "", "/* Auto-generated by tools/gen_sysset_font.py — MiSans Medium */", "", ] payload = [0] def add_pic(sym, im, bear=0, adv=None, rgb565=False): w, h = im.size if adv is None: adv = w if rgb565: data = to_rgb565(im) note = f"{w}x{h} RGB565 BE" mode = 1 else: data = to_1bit(im) note = f"{w}x{h} 1-bit MSB" mode = 0 payload[0] += len(data) arr = f"gImage_SysSet_{sym}_{w}x{h}" emit_array(c_lines, arr, data, note) c_lines.append( f"const SysSetGlyph g_SysSet_{sym} = {{ {arr}, {w}, {h}, {adv}, {bear}, {mode} }};" ) c_lines.append("") h_lines.append(f"extern const SysSetGlyph g_SysSet_{sym};") for name, im in pics.items(): add_pic(name, im) for ch, (im, bear, adv) in glyphs.items(): sym = "Dot" if ch == "." else f"D{ch}" add_pic(sym, im, bear, adv) add_pic("Back", back) add_pic("Chevron", chev) add_pic("TriL", tri_l, rgb565=True) add_pic("TriR", tri_r, rgb565=True) h_lines += [ "", f"#define SYSSET_BACK_X {back_x}", f"#define SYSSET_BACK_Y {back_y}", f"#define SYSSET_CHEV_X {chev_x}", f"#define SYSSET_CHEV_Y {chev_y}", f"#define SYSSET_TRIL_X {tlx}", f"#define SYSSET_TRIL_DY {tly - 114}", f"#define SYSSET_TRIR_X {trx}", f"#define SYSSET_TRIR_DY {try_ - 114}", "#define SYSSET_TITLE_Y 12", "#define SYSSET_LABEL_DX 12", "#define SYSSET_VAL_X 122", "#define SYSSET_VAL_W 69", "#define SYSSET_PILL_DX 143", "#define SYSSET_PILL_DY 18", "#define SYSSET_PILL_W 54", "#define SYSSET_PILL_H 22", "#define SYSSET_PILL_R 4", "#define SYSSET_CARD_R 7", "", "#endif", "", ] OUT_H.write_text("\n".join(h_lines), encoding="utf-8", newline="\n") OUT_C.write_text("\n".join(c_lines), encoding="utf-8", newline="\n") print("wrote", OUT_C.stat().st_size, OUT_H.stat().st_size, "payload", payload[0]) preview(screen, pics, glyphs, back, (back_x, back_y), chev, (chev_x, chev_y), tri_l, (tlx, tly), tri_r, (trx, try_)) def fill_round(canvas, x, y, w, h, r, color): yy, xx = np.ogrid[0:h, 0:w] m = np.ones((h, w), dtype=bool) for cx, cy, sx, sy in ( (r, r, 1, 1), (w - 1 - r, r, -1, 1), (r, h - 1 - r, 1, -1), (w - 1 - r, h - 1 - r, -1, -1), ): corner = (xx - cx) ** 2 + (yy - cy) ** 2 > r * r region = np.zeros_like(m) if sx > 0: region[:, :r] = True else: region[:, w - r:] = True if sy > 0: band = yy < r else: band = yy >= h - r m[region & band & corner] = False canvas[y:y + h, x:x + w][m] = color def paste(canvas, im, x, y): im = im.convert("RGBA") px = im.load() w, h = im.size for yy in range(h): for xx in range(w): r, g, b, a = px[xx, yy] if a < 40: continue if 0 <= y + yy < 320 and 0 <= x + xx < 240: canvas[y + yy, x + xx] = (r * a // 255, g * a // 255, b * a // 255) def text_w(s, glyphs): w = 0 for ch in s: im, bear, adv = glyphs[ch] w += adv return w def draw_text(canvas, x, y, s, glyphs): """y is the top of the tallest glyph's bitmap, using bear within advance.""" # Align by bitmap top: caller passes top y. Glyphs share similar height. for ch in s: im, bear, adv = glyphs[ch] paste(canvas, im, x + bear, y) x += adv def preview(screen, pics, glyphs, back, back_xy, chev, chev_xy, tri_l, tl, tri_r, tr): canvas = np.zeros((320, 240, 3), dtype=np.uint8) canvas[:, :] = (0, 8, 16) card = (32, 40, 56) for i in range(4): fill_round(canvas, 16, 51 + 63 * i, 207, 58, 7, card) paste(canvas, back, back_xy[0], back_xy[1]) title = pics["Title"] paste(canvas, title, (240 - title.size[0]) // 2, 12) labels = ["LblVer", "LblUsb", "LblAuto", "LblRestore"] for i, name in enumerate(labels): im = pics[name] y = 51 + 63 * i + (58 - im.size[1]) // 2 paste(canvas, im, 16 + 12, y) # buttons on rows 1 and 2 for row_y in (114, 177): paste(canvas, tri_l, tl[0], row_y + (tl[1] - 114)) paste(canvas, tri_r, tr[0], row_y + (tr[1] - 114)) # version fill_round(canvas, 159, 51 + 18, 54, 22, 4, (0, 8, 16)) ver = "0.2.35" tw = text_w(ver, glyphs) gh = glyphs["0"][0].size[1] draw_text(canvas, 159 + (54 - tw) // 2, 51 + 18 + (22 - gh) // 2, ver, glyphs) upd = pics["UsbUpdate"] paste(canvas, upd, 122 + (69 - upd.size[0]) // 2, 114 + (58 - upd.size[1]) // 2) num = "15" tw = text_w(num, glyphs) draw_text(canvas, 122 + (69 - tw) // 2, 177 + (58 - gh) // 2, num, glyphs) paste(canvas, chev, chev_xy[0], chev_xy[1]) PREVIEW.parent.mkdir(parents=True, exist_ok=True) Image.fromarray(canvas).save(PREVIEW) ref = np.array(screen.convert("RGB")) d = np.abs(canvas.astype(int) - ref.astype(int)).sum(axis=2) print("preview", PREVIEW, "mean", round(float(d.mean()), 2), "pct>40", round(float((d > 40).mean()), 4)) print("USB MIDI w", pics["UsbMidi"].size, "关闭", pics["Off"].size) if __name__ == "__main__": main()