207 lines
6.8 KiB
Python
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()
|