#!/usr/bin/env python3 """Generate MiSans bitmap font C arrays for ILI9341 LCD driver.""" import glob import math import os import re import sys from PIL import Image, ImageDraw, ImageFont ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) HEADER_IN = os.path.join(ROOT, "device", "LCD_ILI9341", "Drv_ILI9341_Lcd_ChineseFont.h") HEADER_OUT = HEADER_IN ENG_HEADER_IN = os.path.join(ROOT, "device", "LCD_ILI9341", "Drv_ILI9341_Lcd_EnglishFont.h") ENG_HEADER_OUT = ENG_HEADER_IN MISANS_DIR = os.path.join(os.path.dirname(ROOT), "..", "Doc", "UI", "MiSans") MISANS_DIR = os.path.normpath(MISANS_DIR) def find_ttf(name_part): for path in glob.glob(os.path.join(MISANS_DIR, "**", "*.ttf"), recursive=True): if name_part.lower() in os.path.basename(path).lower(): return path raise FileNotFoundError(f"TTF matching {name_part} not found under {MISANS_DIR}") def parse_chs_tables(text): tables = {} for m in re.finditer(r"const char (CHS\d+Table)\[\]\[2\] = \{(.*?)\};", text, re.S): name = m.group(1) body = m.group(2) entries = [] comments = [] for line in body.splitlines(): cm = re.search(r"\{(0x[0-9A-Fa-f]{2})\s*,\s*(0x[0-9A-Fa-f]{2})\}", line) if cm: entries.append((int(cm.group(1), 16), int(cm.group(2), 16))) cmt = re.search(r"//\s*(.+)", line) comments.append(cmt.group(1).strip() if cmt else "") tables[name] = list(zip(entries, comments)) return tables def gbk_char(h, l): return bytes([h, l]).decode("gbk", errors="replace") def _dilate(pixels, w, h, times=1): cur = pixels[:] for _ in range(times): nxt = cur[:] for y in range(h): for x in range(w): if cur[y * w + x]: continue hit = False for dy in (-1, 0, 1): for dx in (-1, 0, 1): nx, ny = x + dx, y + dy if 0 <= nx < w and 0 <= ny < h and cur[ny * w + nx]: nxt[y * w + x] = 1 hit = True break if hit: break cur = nxt return cur def render_glyph(ch, size, font, square=True, threshold=90, pack_ink=False, thicken=0): """Rasterize into size×size (CJK) or (size/2)×size (ASCII). pack_ink=True (recommended for size<=13): crop FreeType ink bbox, fit into cell, lower threshold — keeps strokes at tiny LCD sizes where MiSans Medium otherwise collapses to sparse fragments. """ if square: w = h = size else: w = max(3, size // 2) h = size if not pack_ink: scale = 4 big = Image.new("L", (w * scale, h * scale), 0) draw = ImageDraw.Draw(big) bbox = draw.textbbox((0, 0), ch, font=font) tw = max(1, bbox[2] - bbox[0]) th = max(1, bbox[3] - bbox[1]) pad = scale inner_w = w * scale - 2 * pad inner_h = h * scale - 2 * pad x = pad + (inner_w - tw) // 2 - bbox[0] y = pad + (inner_h - th) // 2 - bbox[1] draw.text((x, y), ch, fill=255, font=font) img = big.resize((w, h), Image.Resampling.LANCZOS) pixels = [1 if img.getpixel((col, row)) > threshold else 0 for row in range(h) for col in range(w)] if thicken: pixels = _dilate(pixels, w, h, times=thicken) return pixels, w, h # --- small-size path: oversample → threshold → crop ink → fit cell --- try: ttf_path = font.path except Exception: ttf_path = None scale = 8 pt = max(size + 2, int(size * 1.35)) * scale font_big = ImageFont.truetype(ttf_path, pt) if ttf_path else font canvas = Image.new("L", (w * scale * 2, h * scale * 2), 0) draw = ImageDraw.Draw(canvas) bbox = draw.textbbox((0, 0), ch, font=font_big) margin = scale draw.text((margin - bbox[0], margin - bbox[1]), ch, fill=255, font=font_big) ink = canvas.point(lambda p: 255 if p > threshold else 0) ib = ink.getbbox() if ib is None: return [0] * (w * h), w, h cropped = ink.crop(ib) cw, chh = cropped.size pad = 1 if size >= 11 else 0 tw = max(1, w - 2 * pad) th = max(1, h - 2 * pad) fit = min(tw / cw, th / chh) nw = max(1, int(round(cw * fit))) nh = max(1, int(round(chh * fit))) resized = cropped.resize((nw, nh), Image.Resampling.LANCZOS) out = Image.new("L", (w, h), 0) out.paste(resized, ((w - nw) // 2, (h - nh) // 2)) pixels = [1 if out.getpixel((col, row)) > 128 else 0 for row in range(h) for col in range(w)] if thicken: pixels = _dilate(pixels, w, h, times=thicken) return pixels, w, h def cn_to_bytes_msb(pixels, width, height): row_bytes = (width + 7) // 8 out = [] for row in range(height): for bi in range(row_bytes): val = 0 for bit in range(8): col = bi * 8 + bit if col < width and pixels[row * width + col]: val |= (0x80 >> bit) elif col >= width: pass out.append(val) return out def ascii_to_bytes_lsb(pixels, width, height): row_bytes = (width + 7) // 8 out = [] for row in range(height): for bi in range(row_bytes): val = 0 for bit in range(8): col = bi * 8 + bit if col < width and pixels[row * width + col]: val |= (0x01 << bit) out.append(val) return out def fmt_bytes(data): parts = [] for i, b in enumerate(data): parts.append(f"0x{b:02X}") lines = [] for i in range(0, len(parts), 16): lines.append(",".join(parts[i:i + 16])) return "{" + ",".join(lines) + "}" def generate_chinese_array(table_name, entries, size, font): # Small CJK: pack ink + lower threshold (Semibold preferred by caller) pack = size <= 13 thr = 40 if pack else 90 lines = [f"const unsigned char Chinese_font_{size}[][{((size + 7)//8)*size}] = {{"] for idx, ((h, l), comment) in enumerate(entries): ch = gbk_char(h, l) pixels, w, hgt = render_glyph( ch, size, font, square=True, threshold=thr, pack_ink=pack, thicken=0) data = cn_to_bytes_msb(pixels, w, hgt) cmt = comment or ch lines.append(f"{fmt_bytes(data)},/*\"{cmt}\",{idx}*/") lines.append("};") return "\n".join(lines) def generate_ascii_array(size, font, array_name=None): sizex = size // 2 row_bytes = (sizex + 7) // 8 total = row_bytes * size suffix = f"{size}{sizex}" name = array_name or f"ascii_{suffix}" # Tiny ASCII (KEY/BPM/digits): Semibold + ink-pack; no dilate (avoids blob) pack = size <= 13 thr = 40 if pack else 90 lines = [f"const unsigned char {name}[][{total}]={{"] for num in range(95): ch = chr(num + 32) pixels, w, hgt = render_glyph( ch, size, font, square=False, threshold=thr, pack_ink=pack, thicken=0) data = ascii_to_bytes_lsb(pixels, w, hgt) lines.append(f"{fmt_bytes(data)},/*\"{ch}\",{num}*/") lines.append("};") return name, "\n".join(lines) def patch_header_chinese(text, tables, fonts_by_size): # Replace each Chinese_font_XX array for size, font in fonts_by_size.items(): table_key = { 8: "CHS8Table", 11: "CHS11Table", 12: "CHS12Table", 13: "CHS13Table", 15: "CHS15Table", 16: "CHS16Table", 28: "CHS24Table", # reuse 24 table chars for 28 if no CHS28 }.get(size) if size == 28: table_key = "CHS24Table" if table_key not in tables: print(f"Skip size {size}: no {table_key}") continue entries = tables[table_key] new_array = generate_chinese_array(table_key, entries, size, font) pat = rf"const unsigned char Chinese_font_{size}\[\]\[\d+\] = \{{.*?\}};" if re.search(pat, text, re.S): text = re.sub(pat, new_array, text, count=1, flags=re.S) else: # insert before Chinese_font for next size or at end of tables section insert_after = f"const char {table_key}" # add new table for CHS8/CHS15 if missing pass return text def ensure_chs8_table(text, tables): if "CHS8Table" in tables: return text, tables if "CHS12Table" not in tables: return text, tables # Insert CHS8Table copy of CHS12Table before CHS11Table entries = tables["CHS12Table"] block = "const char CHS8Table[][2] = {\n" for (h, l), cmt in entries: block += f" {{0x{h:02X},0x{l:02X}}}, // {cmt}\n" block += "};\n\n" text = text.replace("const char CHS11Table", block + "const char CHS11Table", 1) tables["CHS8Table"] = entries return text, tables def ensure_chs9_table(text, tables): if "CHS9Table" in tables: return text, tables src = "CHS11Table" if "CHS11Table" in tables else ("CHS12Table" if "CHS12Table" in tables else None) if not src: return text, tables entries = tables[src] block = "const char CHS9Table[][2] = {\n" for (h, l), cmt in entries: block += f" {{0x{h:02X},0x{l:02X}}}, // {cmt}\n" block += "};\n\n" text = text.replace("const char CHS11Table", block + "const char CHS11Table", 1) tables["CHS9Table"] = entries return text, tables def ensure_chs15_table(text, tables): if "CHS15Table" in tables: return text, tables if "CHS13Table" not in tables: return text, tables entries = tables["CHS13Table"] block = "const char CHS15Table[][2] = {\n" for (h, l), cmt in entries: block += f" {{0x{h:02X},0x{l:02X}}}, // {cmt}\n" block += "};\n\n" text = text.replace("const char CHS16Table", block + "const char CHS16Table", 1) tables["CHS15Table"] = entries return text, tables def main(): medium = find_ttf("MiSans-Medium") semibold = find_ttf("MiSans-Semibold") print("Medium:", medium) print("Semibold:", semibold) only = None for a in sys.argv[1:]: if a.startswith("--only="): only = {int(x) for x in a.split("=", 1)[1].split(",") if x.strip()} with open(HEADER_IN, "r", encoding="utf-8", errors="replace") as f: text = f.read() tables = parse_chs_tables(text) print("Tables:", {k: len(v) for k, v in tables.items()}) text, tables = ensure_chs8_table(text, tables) text, tables = ensure_chs9_table(text, tables) text, tables = ensure_chs15_table(text, tables) # Regenerate all Chinese font arrays cn_sizes = [8, 9, 11, 12, 13, 15, 16, 24, 28] if only: cn_sizes = [s for s in cn_sizes if s in only] for size in cn_sizes: if size in (24, 28): table_key = "CHS24Table" font = ImageFont.truetype(semibold if size == 28 else medium, size - 2) else: table_key = { 8: "CHS8Table", 9: "CHS9Table", 11: "CHS11Table", 12: "CHS12Table", 13: "CHS13Table", 15: "CHS15Table", 16: "CHS16Table", }[size] # ≤13: Semibold + pack_ink inside generate_*; larger: Medium oversample ttf = semibold if size <= 13 else medium pt = max(8, size + 2) if size <= 13 else max(8, size + 2) * 4 font = ImageFont.truetype(ttf, pt) if table_key not in tables: continue entries = tables[table_key] new_array = generate_chinese_array(table_key, entries, size, font) pat = rf"const unsigned char Chinese_font_{size}\[\]\[\d+\] =[\s\S]*?\}};" if re.search(pat, text, re.S): text = re.sub(pat, new_array, text, count=1, flags=re.S) print(f"Updated Chinese_font_{size} ({len(entries)} glyphs)") else: # Insert new array before Chinese_font for next existing or after CHS tables anchor = "const unsigned char Chinese_font_11" if size == 8: anchor = "const unsigned char Chinese_font_11" elif size == 9: anchor = "const unsigned char Chinese_font_11" elif size == 15: anchor = "const unsigned char Chinese_font_16" elif size == 28: anchor = None if anchor and anchor in text: text = text.replace(anchor, new_array + "\n\n" + anchor, 1) print(f"Inserted Chinese_font_{size}") elif size == 28: text = text.rstrip() + "\n\n" + new_array + "\n" print(f"Appended Chinese_font_{size}") with open(HEADER_OUT, "w", encoding="utf-8", newline="\n") as f: f.write(text) # English / ASCII:默认跳过,避免破坏已校准的数字字模;需要时传 --ascii # --only= 仅过滤要生成的字号,仍须配合 --ascii 才会写英文头文件 if "--ascii" not in sys.argv: print("Skip English fonts (pass --ascii to regenerate).") print("Done.") return with open(ENG_HEADER_IN, "r", encoding="utf-8", errors="replace") as f: eng = f.read() # ≤13 ASCII: Semibold (thicker strokes survive 4×8 / 5×11 cells) ascii_targets = [ (8, semibold, None), (9, semibold, None), (11, semibold, None), (12, semibold, None), (13, semibold, "ascii_1306"), (15, medium, None), (16, medium, "ascii_1608"), (24, medium, "ascii_2412"), (28, semibold, None), ] if only: ascii_targets = [t for t in ascii_targets if t[0] in only] for size, ttf, legacy_name in ascii_targets: font = ImageFont.truetype(ttf, max(8, size + 1) if size <= 13 else max(6, size - 1)) name, arr = generate_ascii_array(size, font, legacy_name) pat = rf"const unsigned char {name}\[\]\[\d+\][\s\S]*?\}};" if re.search(pat, eng, re.S): eng = re.sub(pat, arr, eng, count=1, flags=re.S) print(f"Updated {name}") else: auto_name, _ = generate_ascii_array(size, font) pat2 = rf"const unsigned char {auto_name}\[\]\[\d+\][\s\S]*?\}};" if auto_name != name and re.search(pat2, eng, re.S): eng = re.sub(pat2, arr, eng, count=1, flags=re.S) print(f"Updated {auto_name} -> {name}") else: eng = eng.rstrip() + "\n\n" + arr + "\n" print(f"Appended {name}") with open(ENG_HEADER_OUT, "w", encoding="utf-8", newline="\n") as f: f.write(eng) print("Done.") if __name__ == "__main__": main()