#!/usr/bin/env python3 # -*- coding: utf-8 -*- """生成中文子集位图字体 ui_font_cn_12.c(LVGL 格式,输出到 Drivers/BSP/UI/)。 前置依赖: npm i -g lv_font_conv # 离线 TTF/OTF -> LVGL 字体 转换器 pip install fonttools # 仅当字体源是 .ttc 集合时需要(用于抽取单字重) 用法: python tools/font_gen/make_font.py # 默认:宋体, 12px, 1bpp python tools/font_gen/make_font.py --bpp 2 # 宋体细笔画发虚时改用 2bpp python tools/font_gen/make_font.py --font C:/path/to/font.ttf """ import os import sys import argparse import tempfile import subprocess ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) OUT = os.path.join(ROOT, 'Drivers', 'BSP', 'UI', 'ui_font_cn_12.c') CHARS_FILE = os.path.join(ROOT, 'ui_cn_chars.txt') FONT_NAME = 'ui_font_cn_12' def resolve_lv_font_conv(): # 1) 全局 node 模块路径(npm i -g) try: groot = subprocess.check_output(['npm', 'root', '-g'], text=True).strip() p = os.path.join(groot, 'lv_font_conv', 'lv_font_conv.js') if os.path.exists(p): return ['node', p] except Exception: pass # 2) PATH 上的可执行 for cmd in ('lv_font_conv.cmd', 'lv_font_conv'): if subprocess.run(['where', cmd], capture_output=True, shell=True).returncode == 0: return [cmd] sys.exit('ERROR: 找不到 lv_font_conv。请先执行: npm i -g lv_font_conv') def resolve_font(path): if path: if not os.path.exists(path): sys.exit('ERROR: --font 指定的文件不存在: ' + path) return path # 默认字体:Windows 宋体集合 simsun.ttc(lv_font_conv 不支持 .ttc,需先抽取 face0) ttc = r'C:\Windows\Fonts\simsun.ttc' if os.path.exists(ttc): try: from fontTools.ttLib import TTCollection coll = TTCollection(ttc) tmp = os.path.join(tempfile.gettempdir(), 'simsun_regular.ttf') coll.fonts[0].save(tmp) print('extracted SimSun regular (face0) ->', tmp) return tmp except Exception as e: sys.exit('ERROR: 抽取 simsun.ttc 失败,请 pip install fonttools,或用 --font 指定 ttf: ' + str(e)) sys.exit('ERROR: 未找到 C:\\Windows\\Fonts\\simsun.ttc,请用 --font 指定字体 ttf/otf 路径') # --------------------------------------------------------------------------- # 箭头字形合并 # 宋体(simsun.ttc)没有 U+25B6(▶)字形,lv_font_conv 会静默跳过 → 真机显示成方框。 # 该字形从 Segoe UI Symbol(seguisym.ttf,Windows 自带)提取,全量重生成字体后 # 自动补回,避免"重生成字体箭头又变方框"。 # --------------------------------------------------------------------------- ARROW_CP_DEC = 9654 # U+25B6 ▶ ARROW_BITMAP = [0x80, 0xe0, 0xf0, 0xfc, 0xff, 0xfe, 0xf8, 0xf0, 0xc0, 0x0] ARROW_ADV_W, ARROW_BOX_W, ARROW_BOX_H, ARROW_OFS_X, ARROW_OFS_Y = 165, 8, 10, 1, -1 def merge_arrow_glyph(font_path): """若字体缺 U+25B6(▶),把预提取的箭头字形合并进去(位图 + glyph_dsc + cmap)。""" import re raw = open(font_path, 'rb').read() has_bom = raw.startswith(b'\xef\xbb\xbf') src = raw.decode('utf-8-sig') if re.search(r'\.range_start\s*=\s*9654\s*,', src): print('arrow U+25B6 已存在,跳过合并') return # 当前位图总字节数(新字形 bitmap_index 的取值) bm = re.search(r'glyph_bitmap\[\]\s*=\s*\{', src) end_bm = src.find('\n};', bm.end()) body_bm = re.sub(r'/\*.*?\*/', '', src[bm.end():end_bm], flags=re.S) total = len(re.findall(r'0x[0-9a-fA-F]+', body_bm)) glyph_count = len(re.findall(r'\.bitmap_index\s*=\s*\d+', src)) # 1) 追加位图 bmp = ',\n\n /* U+25B6 "▶"(来源:Segoe UI Symbol;宋体无此字形) */\n ' + \ ', '.join('0x%02x' % b for b in ARROW_BITMAP) src = src[:end_bm] + bmp + src[end_bm:] # 2) 追加 glyph_dsc gd = re.search(r'glyph_dsc\[\]\s*=\s*\{', src) end_gd = src.find('\n};', gd.end()) entry = ',\n {.bitmap_index = %d, .adv_w = %d, .box_w = %d, .box_h = %d, .ofs_x = %d, .ofs_y = %d} /* U+25B6 */' % ( total, ARROW_ADV_W, ARROW_BOX_W, ARROW_BOX_H, ARROW_OFS_X, ARROW_OFS_Y) src = src[:end_gd] + entry + src[end_gd:] # 3) 追加 cmap(单码位映射) cm = re.search(r'cmaps\[\]\s*=\s*\{', src) end_cm = src.find('\n};', cm.end()) cmap = ',\n {\n .range_start = %d, .range_length = 1, .glyph_id_start = %d,\n' \ ' .unicode_list = NULL, .glyph_id_ofs_list = NULL, .list_length = 0, .type = LV_FONT_FMT_TXT_CMAP_FORMAT0_TINY\n }' % ( ARROW_CP_DEC, glyph_count) src = src[:end_cm] + cmap + src[end_cm:] # 4) cmap_num +1 src = re.sub(r'\.cmap_num = (\d+),', lambda m: '.cmap_num = %d,' % (int(m.group(1)) + 1), src, count=1) out = b'\xef\xbb\xbf' if has_bom else b'' open(font_path, 'wb').write(out + src.encode('utf-8')) print('已合并箭头 U+25B6:位图+%d 字节,glyph_id=%d' % (len(ARROW_BITMAP), glyph_count)) def fix_trailing_newline(path): """规范化文件末尾为恰好一个 \n(pre-commit end-of-file-fixer 要求)。 lv_font_conv 产物末尾多一个空行(`#endif` 之后 `\n\n`),统一在此收敛为单个换行, 避免每次重生成字体都触发 end-of-file-fixer 拦截提交。 """ with open(path, 'rb') as fp: data = fp.read() body = data while body.endswith(b'\r\n') or body.endswith(b'\n'): body = body[:-2] if body.endswith(b'\r\n') else body[:-1] fixed = body + b'\n' if fixed != data: with open(path, 'wb') as fp: fp.write(fixed) def main(): # Windows GBK 控制台无法打印 ▶ 等符号:降级为替换字符,避免 UnicodeEncodeError 崩溃 try: sys.stdout.reconfigure(errors='replace') except Exception: pass ap = argparse.ArgumentParser() ap.add_argument('--bpp', type=int, default=1, help='字体位深 1/2/3(默认 1)') ap.add_argument('--size', type=int, default=12, help='字号(默认 12)') ap.add_argument('--font', default=None, help='自定义字体 ttf/otf 路径(默认宋体)') args = ap.parse_args() if not os.path.exists(CHARS_FILE): sys.exit('ERROR: 请先运行 extract_cn.py 生成 ui_cn_chars.txt') chars = open(CHARS_FILE, encoding='utf-8').read().strip() cmd = resolve_lv_font_conv() font = resolve_font(args.font) cmd += ['--font', font, '--size', str(args.size), '--bpp', str(args.bpp), '-r', '0x20-0x7E', # 含 ASCII,使字体自包含 '--symbols', chars, # 本工程实际显示的汉字子集 '--no-compress', # 关闭 RLE 压缩,保证兼容 '--format', 'lvgl', '--lv-font-name', FONT_NAME, '-o', OUT] print('RUN:', ' '.join(cmd)) r = subprocess.run(cmd) if r.returncode != 0: sys.exit('FAILED rc=%d' % r.returncode) merge_arrow_glyph(OUT) # 补回宋体缺失的 ▶ 字形(幂等) fix_trailing_newline(OUT) # 规范化末尾换行(pre-commit end-of-file-fixer) print('OK %d bytes -> %s' % (os.path.getsize(OUT), OUT)) if __name__ == '__main__': main()