K1Guitar/tools/rebuild_nav_icons_0909.py

207 lines
6.8 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Bottom nav: 0909 enlarged icons + original design text strips, fixed baseline."""
from __future__ import annotations
import re
import subprocess
from io import BytesIO
from pathlib import Path
from PIL import Image
PROJ = Path(__file__).resolve().parents[1]
ROOT = PROJ.parent.parent
SRC_DIR = next(
p
for p in (ROOT / "Doc" / "UI" / "K1标准界面图 0904").iterdir()
if p.is_dir() and "0909" in p.name
)
ASSETS = PROJ / "tools" / "assets_0902" / "03_底部图标"
DOC_ASSETS = ROOT / "Doc" / "UI" / "k1_ui_png 0902" / "03_底部图标"
OUT_C = PROJ / "device" / "LCD_ILI9341" / "Drv_ILI9341_Lcd_Image_UI0902.c"
OUT_H = PROJ / "device" / "LCD_ILI9341" / "Drv_ILI9341_Lcd_Image_UI0902.h"
ROW_BG = (36, 43, 58)
CANVAS_H = 32
ICON_AREA_H = 20
TEXT_TOP = 22 # all labels share this top row
# png_idx, src_glyph_num, symbol_base, width
MAPPING = [
(10, 21, "gImage_UI0902_TabSetting_Sel", 40),
(11, 23, "gImage_UI0902_TabSetting_Not", 40),
(12, 25, "gImage_UI0902_TabMixer_Sel", 40),
(13, 27, "gImage_UI0902_TabMixer_Not", 22),
(14, 20, "gImage_UI0902_TabMode_Sel", 40),
(15, 22, "gImage_UI0902_TabMode_Not", 40),
(16, 24, "gImage_UI0902_TabBack_Sel", 40),
(17, 26, "gImage_UI0902_TabBack_Not", 22),
]
def git_png(rel: str) -> Image.Image:
data = subprocess.check_output(["git", "show", f"HEAD:{rel}"], cwd=PROJ)
return Image.open(BytesIO(data)).convert("RGBA")
def clear_bg(im: Image.Image) -> Image.Image:
im = im.convert("RGBA")
px = im.load()
for y in range(im.height):
for x in range(im.width):
r, g, b, a = px[x, y]
if a < 40 or (r < 12 and g < 12 and b < 12):
px[x, y] = (0, 0, 0, 0)
return im
def row_has_ink(im: Image.Image, y: int) -> bool:
w = im.width
for x in range(w):
r, g, b, a = im.getpixel((x, y))
if a > 40 and (r + g + b) > 40:
return True
return False
def extract_text(im: Image.Image) -> Image.Image:
"""Crop label band below the icon/gap from original composite."""
im = clear_bg(im)
h = im.height
# find last empty gap row in upper half, then text starts after
empty = [y for y in range(h) if not row_has_ink(im, y)]
# text region: from first non-empty after mid-gap
mid_empties = [y for y in empty if 10 <= y <= 20]
if mid_empties:
text_y0 = mid_empties[-1] + 1
else:
text_y0 = h // 2 + 2
while text_y0 < h and not row_has_ink(im, text_y0):
text_y0 += 1
text_y1 = h - 1
while text_y1 > text_y0 and not row_has_ink(im, text_y1):
text_y1 -= 1
band = im.crop((0, text_y0, im.width, text_y1 + 1))
# trim horizontal transparent
bbox = band.getbbox()
if bbox:
band = band.crop(bbox)
return clear_bg(band)
def fit_icon(im: Image.Image) -> Image.Image:
im = clear_bg(im)
nw, nh = im.size
if nw > 24 or nh > ICON_AREA_H:
scale = min(24 / nw, ICON_AREA_H / nh)
nw = max(1, int(round(nw * scale)))
nh = max(1, int(round(nh * scale)))
im = clear_bg(im.resize((nw, nh), Image.Resampling.NEAREST))
nw, nh = im.size
return im
def rgba_to_rgb565(im: Image.Image) -> tuple[bytes, int, int]:
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] = 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] = 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 emit_c_array(name: str, data: bytes, w: int, h: int) -> str:
lines = [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("")
return "\n".join(lines)
def replace_c_array(text: str, old_name: str, new_block: str) -> str:
pat = re.compile(
rf"const unsigned char {re.escape(old_name)}\[\d+\] = \{{.*?\n\}};\n?",
re.S,
)
if not pat.search(text):
raise SystemExit(f"array not found: {old_name}")
return pat.sub(new_block, text, count=1)
def replace_h_decl(text: str, old_name: str, new_name: str, nbytes: int) -> str:
pat = re.compile(rf"extern const unsigned char {re.escape(old_name)}\[\d+\];")
if not pat.search(text):
raise SystemExit(f"decl not found: {old_name}")
return pat.sub(
f"extern const unsigned char {new_name}[{nbytes}];", text, count=1
)
def main() -> None:
by_num = {int(f.stem.rsplit("-", 1)[-1]): f for f in SRC_DIR.glob("*.png")}
c_text = OUT_C.read_text(encoding="utf-8")
h_text = OUT_H.read_text(encoding="utf-8")
sizes: dict[int, tuple[int, int]] = {}
for out_idx, src_num, base, width in MAPPING:
icon = fit_icon(Image.open(by_num[src_num]))
old = git_png(f"tools/assets_0902/03_底部图标/底部图标-{out_idx}.png")
text = extract_text(old)
# widen canvas if text wider
cw = max(width, icon.width, text.width)
canvas = Image.new("RGBA", (cw, CANVAS_H), (0, 0, 0, 0))
iy = (ICON_AREA_H - icon.height) // 2
canvas.paste(icon, ((cw - icon.width) // 2, max(0, iy)), icon)
# pin text BOTTOM to same row so labels sit on one baseline
text_bottom = CANVAS_H - 1
ty = text_bottom - text.height + 1
if ty < TEXT_TOP:
ty = TEXT_TOP
canvas.paste(text, ((cw - text.width) // 2, ty), text)
out = ASSETS / f"底部图标-{out_idx}.png"
canvas.save(out)
if DOC_ASSETS.exists():
canvas.save(DOC_ASSETS / f"底部图标-{out_idx}.png")
data, w, h = rgba_to_rgb565(canvas)
new_name = f"{base}_{w}x{h}"
m = re.search(rf"{re.escape(base)}_\d+x\d+", c_text)
if not m:
raise SystemExit(f"cannot locate {base}")
old_name = m.group(0)
c_text = replace_c_array(c_text, old_name, emit_c_array(new_name, data, w, h))
h_text = replace_h_decl(h_text, old_name, new_name, len(data))
sizes[out_idx] = (w, h)
print(f"{out_idx}: {w}x{h} text={text.size} -> {new_name}")
OUT_C.write_text(c_text, encoding="utf-8")
OUT_H.write_text(h_text, encoding="utf-8")
print("SIZES", sizes)
if __name__ == "__main__":
main()