#!/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()