K1Guitar/tools/gen_misans_font.py

408 lines
14 KiB
Python
Raw Permalink 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.

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