2026-09-26 12:42:11 +08:00
|
|
|
|
# -*- 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"
|
|
|
|
|
|
|
2026-09-26 15:06:35 +08:00
|
|
|
|
# 设置入口字墨高约 19px。USB 行还要放下左右箭头,18px 是同一行里能对齐的最大字号。
|
|
|
|
|
|
TITLE_PX = 18
|
|
|
|
|
|
LABEL_PX = 18
|
|
|
|
|
|
VALUE_PX = 18
|
|
|
|
|
|
|
|
|
|
|
|
CARD_X = 8
|
|
|
|
|
|
CARD_W = 223
|
|
|
|
|
|
CARD_H = 62
|
|
|
|
|
|
CARD_Y0 = 42
|
|
|
|
|
|
CARD_PITCH = 70
|
|
|
|
|
|
CARD_R = 12
|
|
|
|
|
|
LABEL_DX = 12
|
|
|
|
|
|
RIGHT_PAD = 10
|
|
|
|
|
|
CTRL_GAP = 4
|
2026-09-26 12:42:11 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def render_glyph(ch, px):
|
|
|
|
|
|
"""Fixed-height cell so '.' shares the digit baseline. Horizontal crop only."""
|
|
|
|
|
|
font = ImageFont.truetype(str(FONT), px)
|
2026-09-26 15:06:35 +08:00
|
|
|
|
ascent, descent = font.getmetrics()
|
|
|
|
|
|
cell_h = ascent + max(descent, 1)
|
|
|
|
|
|
baseline = ascent
|
|
|
|
|
|
origin = 4
|
|
|
|
|
|
scratch = Image.new("RGBA", (96, cell_h), (0, 0, 0, 0))
|
2026-09-26 12:42:11 +08:00
|
|
|
|
dr = ImageDraw.Draw(scratch)
|
|
|
|
|
|
try:
|
2026-09-26 15:06:35 +08:00
|
|
|
|
dr.text((origin, baseline), ch, font=font, fill=(255, 255, 255, 255), anchor="ls")
|
2026-09-26 12:42:11 +08:00
|
|
|
|
except TypeError:
|
2026-09-26 15:06:35 +08:00
|
|
|
|
dr.text((origin, 0), ch, font=font, fill=(255, 255, 255, 255))
|
2026-09-26 12:42:11 +08:00
|
|
|
|
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:
|
2026-09-26 15:06:35 +08:00
|
|
|
|
im = Image.new("RGBA", (adv, cell_h), (0, 0, 0, 0))
|
2026-09-26 12:42:11 +08:00
|
|
|
|
return im, 0, adv
|
|
|
|
|
|
x0, x1 = int(cols[0]), int(cols[-1])
|
2026-09-26 15:06:35 +08:00
|
|
|
|
im = scratch.crop((x0, 0, x1 + 1, cell_h))
|
2026-09-26 12:42:11 +08:00
|
|
|
|
bear = x0 - origin
|
|
|
|
|
|
if bear < 0:
|
2026-09-26 15:06:35 +08:00
|
|
|
|
pad = Image.new("RGBA", (im.size[0] - bear, cell_h), (0, 0, 0, 0))
|
2026-09-26 12:42:11 +08:00
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-09-26 15:06:35 +08:00
|
|
|
|
def scale_to_h(im, h):
|
|
|
|
|
|
w = max(1, int(round(im.size[0] * h / im.size[1])))
|
|
|
|
|
|
return im.resize((w, h), Image.Resampling.LANCZOS)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-09-26 12:42:11 +08:00
|
|
|
|
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)
|
2026-09-26 15:06:35 +08:00
|
|
|
|
chev, _, _ = 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, _, _ = crop_widget(screen, 100, 132, 121, 152, is_card)
|
|
|
|
|
|
tri_r, _, _ = crop_widget(screen, 191, 132, 211, 152, is_card)
|
|
|
|
|
|
chev = scale_to_h(chev, 16)
|
|
|
|
|
|
tri_l = scale_to_h(tri_l, 18)
|
|
|
|
|
|
tri_r = scale_to_h(tri_r, 18)
|
|
|
|
|
|
|
|
|
|
|
|
midi_w = pics["UsbMidi"].size[0]
|
|
|
|
|
|
upd_w = pics["UsbUpdate"].size[0]
|
|
|
|
|
|
val_w = max(midi_w, upd_w, pics["Off"].size[0])
|
|
|
|
|
|
trir_x = CARD_X + CARD_W - RIGHT_PAD - tri_r.size[0]
|
|
|
|
|
|
val_x = trir_x - CTRL_GAP - val_w
|
|
|
|
|
|
tril_x = val_x - CTRL_GAP - tri_l.size[0]
|
|
|
|
|
|
tri_dy = (CARD_H - tri_l.size[1]) // 2
|
|
|
|
|
|
label_end = CARD_X + LABEL_DX + pics["LblUsb"].size[0]
|
|
|
|
|
|
if label_end + 4 > tril_x:
|
|
|
|
|
|
raise SystemExit(
|
|
|
|
|
|
"USB row overflow: label ends %d, left arrow at %d" % (label_end, tril_x)
|
|
|
|
|
|
)
|
2026-09-28 10:11:48 +08:00
|
|
|
|
ver_w = text_w("0.2.37", glyphs)
|
2026-09-26 15:06:35 +08:00
|
|
|
|
gh = glyphs["0"][0].size[1]
|
|
|
|
|
|
pill_h = gh + 8
|
|
|
|
|
|
pill_w = ver_w + 18
|
|
|
|
|
|
pill_dy = (CARD_H - pill_h) // 2
|
|
|
|
|
|
pill_dx = CARD_W - RIGHT_PAD - pill_w
|
|
|
|
|
|
if LABEL_DX + pics["LblVer"].size[0] + 8 > pill_dx:
|
|
|
|
|
|
raise SystemExit("version row overflow")
|
|
|
|
|
|
row3 = CARD_Y0 + CARD_PITCH * 3
|
|
|
|
|
|
chev_x = CARD_X + CARD_W - RIGHT_PAD - chev.size[0]
|
|
|
|
|
|
chev_y = row3 + (CARD_H - chev.size[1]) // 2
|
|
|
|
|
|
if LABEL_DX + pics["LblRestore"].size[0] + 8 > (chev_x - CARD_X):
|
|
|
|
|
|
raise SystemExit("restore row overflow")
|
|
|
|
|
|
title_y = max(6, (CARD_Y0 - pics["Title"].size[1]) // 2)
|
2026-09-26 12:42:11 +08:00
|
|
|
|
print("back", back.size, back_x, back_y)
|
|
|
|
|
|
print("chev", chev.size, chev_x, chev_y)
|
2026-09-26 15:06:35 +08:00
|
|
|
|
print("triL", tri_l.size, tril_x, "triR", tri_r.size, trir_x, "val", val_x, val_w)
|
|
|
|
|
|
print("pill", pill_dx, pill_dy, pill_w, pill_h, "title_y", title_y)
|
2026-09-26 12:42:11 +08:00
|
|
|
|
|
|
|
|
|
|
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}",
|
2026-09-26 15:06:35 +08:00
|
|
|
|
f"#define SYSSET_TRIL_X {tril_x}",
|
|
|
|
|
|
f"#define SYSSET_TRIL_DY {tri_dy}",
|
|
|
|
|
|
f"#define SYSSET_TRIR_X {trir_x}",
|
|
|
|
|
|
f"#define SYSSET_TRIR_DY {tri_dy}",
|
|
|
|
|
|
f"#define SYSSET_TITLE_Y {title_y}",
|
|
|
|
|
|
f"#define SYSSET_LABEL_DX {LABEL_DX}",
|
|
|
|
|
|
f"#define SYSSET_VAL_X {val_x}",
|
|
|
|
|
|
f"#define SYSSET_VAL_W {val_w}",
|
|
|
|
|
|
f"#define SYSSET_PILL_DX {pill_dx}",
|
|
|
|
|
|
f"#define SYSSET_PILL_DY {pill_dy}",
|
|
|
|
|
|
f"#define SYSSET_PILL_W {pill_w}",
|
|
|
|
|
|
f"#define SYSSET_PILL_H {pill_h}",
|
|
|
|
|
|
"#define SYSSET_PILL_R 6",
|
|
|
|
|
|
f"#define SYSSET_CARD_R {CARD_R}",
|
2026-09-26 12:42:11 +08:00
|
|
|
|
"",
|
|
|
|
|
|
"#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])
|
2026-09-26 15:06:35 +08:00
|
|
|
|
preview(pics, glyphs, back, (back_x, back_y), chev, (chev_x, chev_y),
|
|
|
|
|
|
tri_l, (tril_x, CARD_Y0 + CARD_PITCH + tri_dy),
|
|
|
|
|
|
tri_r, (trir_x, CARD_Y0 + CARD_PITCH + tri_dy),
|
|
|
|
|
|
val_x, val_w, pill_dx, pill_dy, pill_w, pill_h, title_y)
|
2026-09-26 12:42:11 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-09-26 15:06:35 +08:00
|
|
|
|
def preview(pics, glyphs, back, back_xy, chev, chev_xy, tri_l, tl, tri_r, tr,
|
|
|
|
|
|
val_x, val_w, pill_dx, pill_dy, pill_w, pill_h, title_y):
|
2026-09-26 12:42:11 +08:00
|
|
|
|
canvas = np.zeros((320, 240, 3), dtype=np.uint8)
|
2026-09-26 15:06:35 +08:00
|
|
|
|
canvas[:, :] = (0, 8, 19)
|
|
|
|
|
|
card = (36, 43, 58)
|
2026-09-26 12:42:11 +08:00
|
|
|
|
for i in range(4):
|
2026-09-26 15:06:35 +08:00
|
|
|
|
fill_round(canvas, CARD_X, CARD_Y0 + CARD_PITCH * i, CARD_W, CARD_H, CARD_R, card)
|
2026-09-26 12:42:11 +08:00
|
|
|
|
paste(canvas, back, back_xy[0], back_xy[1])
|
|
|
|
|
|
title = pics["Title"]
|
2026-09-26 15:06:35 +08:00
|
|
|
|
paste(canvas, title, (240 - title.size[0]) // 2, title_y)
|
2026-09-26 12:42:11 +08:00
|
|
|
|
labels = ["LblVer", "LblUsb", "LblAuto", "LblRestore"]
|
|
|
|
|
|
for i, name in enumerate(labels):
|
|
|
|
|
|
im = pics[name]
|
2026-09-26 15:06:35 +08:00
|
|
|
|
y = CARD_Y0 + CARD_PITCH * i + (CARD_H - im.size[1]) // 2
|
|
|
|
|
|
paste(canvas, im, CARD_X + LABEL_DX, y)
|
|
|
|
|
|
for row_y in (CARD_Y0 + CARD_PITCH, CARD_Y0 + CARD_PITCH * 2):
|
|
|
|
|
|
paste(canvas, tri_l, tl[0], row_y + (tl[1] - (CARD_Y0 + CARD_PITCH)))
|
|
|
|
|
|
paste(canvas, tri_r, tr[0], row_y + (tr[1] - (CARD_Y0 + CARD_PITCH)))
|
|
|
|
|
|
fill_round(canvas, CARD_X + pill_dx, CARD_Y0 + pill_dy, pill_w, pill_h, 6, (0, 8, 19))
|
2026-09-28 10:11:48 +08:00
|
|
|
|
ver = "0.2.37"
|
2026-09-26 12:42:11 +08:00
|
|
|
|
tw = text_w(ver, glyphs)
|
|
|
|
|
|
gh = glyphs["0"][0].size[1]
|
2026-09-26 15:06:35 +08:00
|
|
|
|
draw_text(canvas, CARD_X + pill_dx + (pill_w - tw) // 2,
|
|
|
|
|
|
CARD_Y0 + pill_dy + (pill_h - gh) // 2, ver, glyphs)
|
|
|
|
|
|
midi = pics["UsbMidi"]
|
|
|
|
|
|
paste(canvas, midi, val_x + (val_w - midi.size[0]) // 2,
|
|
|
|
|
|
CARD_Y0 + CARD_PITCH + (CARD_H - midi.size[1]) // 2)
|
2026-09-26 12:42:11 +08:00
|
|
|
|
num = "15"
|
|
|
|
|
|
tw = text_w(num, glyphs)
|
2026-09-26 15:06:35 +08:00
|
|
|
|
draw_text(canvas, val_x + (val_w - tw) // 2,
|
|
|
|
|
|
CARD_Y0 + CARD_PITCH * 2 + (CARD_H - gh) // 2, num, glyphs)
|
2026-09-26 12:42:11 +08:00
|
|
|
|
paste(canvas, chev, chev_xy[0], chev_xy[1])
|
|
|
|
|
|
PREVIEW.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
|
Image.fromarray(canvas).save(PREVIEW)
|
2026-09-26 15:06:35 +08:00
|
|
|
|
print("preview", PREVIEW)
|
|
|
|
|
|
print("USB MIDI", pics["UsbMidi"].size, "关闭", pics["Off"].size)
|
2026-09-26 12:42:11 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
|
main()
|