K1Guitar/tools/gen_sysset_font.py

401 lines
13 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.

# -*- 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"
# 设置入口字墨高约 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
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)
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))
dr = ImageDraw.Draw(scratch)
try:
dr.text((origin, baseline), ch, font=font, fill=(255, 255, 255, 255), anchor="ls")
except TypeError:
dr.text((origin, 0), 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 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)
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, _, _ = 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)
)
ver_w = text_w("0.2.35", glyphs)
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)
print("back", back.size, back_x, back_y)
print("chev", chev.size, chev_x, chev_y)
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)
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 {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}",
"",
"#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(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)
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(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):
canvas = np.zeros((320, 240, 3), dtype=np.uint8)
canvas[:, :] = (0, 8, 19)
card = (36, 43, 58)
for i in range(4):
fill_round(canvas, CARD_X, CARD_Y0 + CARD_PITCH * i, CARD_W, CARD_H, CARD_R, card)
paste(canvas, back, back_xy[0], back_xy[1])
title = pics["Title"]
paste(canvas, title, (240 - title.size[0]) // 2, title_y)
labels = ["LblVer", "LblUsb", "LblAuto", "LblRestore"]
for i, name in enumerate(labels):
im = pics[name]
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))
ver = "0.2.35"
tw = text_w(ver, glyphs)
gh = glyphs["0"][0].size[1]
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)
num = "15"
tw = text_w(num, glyphs)
draw_text(canvas, val_x + (val_w - tw) // 2,
CARD_Y0 + CARD_PITCH * 2 + (CARD_H - 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)
print("preview", PREVIEW)
print("USB MIDI", pics["UsbMidi"].size, "关闭", pics["Off"].size)
if __name__ == "__main__":
main()