Files
STM32F4-Base/tools/font_gen/make_font.py
T
wandering1 508577bbf0 UI 生成文件按工程约定产出:生成器补文件头、K&R 大括号与版本守卫压平
- gen_menu.py:ui_menu_tab.h/.c 输出工程标准文件头,5 个函数定义改 K&R 大括号
- make_font.py:新增 normalize_project_conventions 后处理——工程文件头
  (替换内嵌绝对路径的 lv_font_conv 头注释)、包含收敛为 #include "lvgl.h"、
  LVGL 9.3.0 版本守卫静态压平(含守卫宏定义清理)
- 重生成 ui_font_cn_12.c / ui_menu_tab.c / ui_menu_tab.h:仅头部/大括号/守卫
  行变化,字形数据与菜单表逐字节不变
- 两文件随之满足规范检查,pre-commit 中无需再排除(配合上一提交生效)

验证:pre-commit 全钩子通过、模拟器 0 警、菜单/内容页像素回归 PASS、Keil 0 错 0 警
2026-09-13 19:38:30 +08:00

473 lines
22 KiB
Python
Raw 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
# -*- 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 集合时需要(用于抽取单字重)
后处理(本脚本自动完成,均幂等;产物可直接入库):
merge_arrow_glyph 补回宋体缺失的 ▶ 箭头字形
merge_extra_symbols 补回 lv_font_conv 静默丢弃的低码位符号(全角逗号)
verify_coverage 硬校验:ui_cn_chars.txt 全部字符必须进字体
fix_trailing_newline 规范化文件末尾为单个换行
normalize_project_conventions 工程约定规范化(文件头 / 包含路径 / 版本守卫压平)
用法:
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 re
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)
# ---------------------------------------------------------------------------
# 工程约定规范化(lv_font_conv 产物后处理最后一步,幂等)
# 1) lv_font_conv 头注释(内嵌本机绝对路径)替换为工程文件头(模块名称/作者)
# 2) LV_LVGL_H_INCLUDE_SIMPLE 双分支包含收敛为单一 #include "lvgl.h"
# (规范 2.3.5 禁止相对/绝对路径 include,如 "lvgl/lvgl.h")
# 3) 版本守卫(#if LVGL_VERSION_*/LV_VERSION_CHECK)压平:仓库 LVGL 固定 9.3.0,
# 守卫可静态求值,保留真分支、删除守卫(同时消除 cppcheck syntaxError)
# ---------------------------------------------------------------------------
LVGL_VERSION = (9, 3, 0)
def _eval_cond(cond, symbols):
"""静态求值 #if 条件(仅支持 lv_font_conv 产出的版本表达式)。
返回 True/False;无法求值时返回 None(调用方保留原块不压平)。
"""
def ver_check(m):
a, b, c = (int(x) for x in m.groups())
return '1' if LVGL_VERSION >= (a, b, c) else '0'
e = re.sub(r'LV_VERSION_CHECK\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)', ver_check, cond)
for name, val in (('LVGL_VERSION_MAJOR', LVGL_VERSION[0]),
('LVGL_VERSION_MINOR', LVGL_VERSION[1]),
('LVGL_VERSION_PATCH', LVGL_VERSION[2])):
e = re.sub(r'\b%s\b' % name, str(val), e)
for name, val in symbols.items():
e = re.sub(r'\b%s\b' % name, str(val), e)
if re.search(r'[A-Za-z_]\w*', e): # 仍有标识符 → 无法静态求值(先于 &&/|| 转换,避免误判 and/or)
return None
e = e.replace('&&', ' and ').replace('||', ' or ')
e = re.sub(r'!(?!=)', ' not ', e)
try:
return bool(eval(e, {'__builtins__': {}}))
except Exception:
return None
def _active(stack):
"""当前行是否处于"保留分支"(不可求值块视为整体保留)。"""
for cond, in_else, evalable in stack:
if evalable and in_else == cond:
return False
return True
def _flatten_guards(lines, symbols):
"""压平可静态求值的版本守卫;不可求值的块原样保留(并告警)。"""
out = []
stack = [] # [cond, in_else, evalable];cond 为 None = 不可求值
for raw in lines:
s = raw.strip()
if not s.startswith('#'):
if _active(stack):
out.append(raw)
continue
mi = re.match(r'#\s*if\s+(\S.*)$', s)
mid = re.match(r'#\s*ifdef\s+(\S+)', s)
minn = re.match(r'#\s*ifndef\s+(\S+)', s)
if mi or mid or minn:
if mid:
cond, evalable = mid.group(1) in symbols, True
elif minn:
cond, evalable = minn.group(1) not in symbols, True
else:
cond = _eval_cond(mi.group(1), symbols)
evalable = cond is not None
if not evalable:
print('WARNING: 无法静态求值,原样保留: %s' % s)
stack.append([cond, False, evalable])
if not evalable:
out.append(raw) # 不可求值块保留 #if 行
continue
if re.match(r'#\s*else\b', s):
if stack and stack[-1][2]:
stack[-1][1] = not stack[-1][1]
continue # 可求值:丢弃 #else 行
out.append(raw) # 不可求值(或无配对 #if):原样保留
continue
if re.match(r'#\s*endif\b', s):
if stack:
if stack[-1][2]:
stack.pop()
continue # 可求值:丢弃 #endif 行
stack.pop()
out.append(raw)
continue
if s.startswith('#define'):
m = re.match(r'#\s*define\s+([A-Za-z_]\w*)\s+(\S.*)$', s)
if m:
try:
symbols[m.group(1)] = int(m.group(2).strip())
except ValueError:
pass
if _active(stack):
out.append(raw)
continue
if _active(stack): # 其余指令(#include 等)
out.append(raw)
return out
def _drop_unused_guard(lines, guard_name):
"""压平后 lv_font_conv 遗留的 #define 守卫宏(如 UI_FONT_CN_12)若无人再引用则删除
(.c 文件不需要头文件保护宏)。"""
n = len(re.findall(r'\b%s\b' % re.escape(guard_name), '\n'.join(lines)))
if n != 1:
return lines
return [l for l in lines
if not re.match(r'#\s*define\s+%s\b' % re.escape(guard_name), l.strip())]
def normalize_project_conventions(font_path, size, bpp):
"""lv_font_conv 产物对齐工程约定(在 verify_coverage / fix_trailing_newline 之后调用)。"""
raw = open(font_path, 'rb').read()
has_bom = raw.startswith(b'\xef\xbb\xbf')
text = raw.decode('utf-8-sig')
eol = '\r\n' if '\r\n' in text else '\n'
lines = [l.rstrip('\r') for l in text.split('\n')]
while lines and lines[-1] == '':
lines.pop()
# 1) lv_font_conv 头注释(内嵌本机绝对路径)替换为工程文件头
if lines and lines[0].startswith('/**'):
end = None
for i in range(1, len(lines)):
if re.fullmatch(r'\s*\*+/\s*', lines[i]):
end = i
break
if end is not None:
header = [
'/*',
' * 模块名称:UI 中文位图字体(%s,自动生成)' % FONT_NAME,
' * 模块功能:中文子集 %dpx 位图字体(宋体;含 ▶ 箭头与全角逗号补回字形)' % size,
' * 适用平台:STM32F407ZGTx / PC 模拟器(共用源码)',
' * 作者:王建锋',
' * 创建日期:2026-08-30',
' * 修改记录:',
' * 2026-09-13 王建锋 工程约定规范化(文件头/包含路径/版本守卫压平),生成器同步更新',
' * 自动生成:本文件由 tools/font_gen/make_font.py(lv_font_conv --format lvgl)自动生成,请勿手改。',
' * 字符集:ui_cn_chars.txt(extract_cn.py);改字符集或字体参数后重跑 make_font.py。',
' * 生成参数:宋体 (simsun.ttc face0), size=%d, bpp=%d, --no-compress。' % (size, bpp),
' */',
]
lines = header + lines[end + 1:]
# 2) 双分支包含收敛为单一 #include "lvgl.h"
for i, l in enumerate(lines):
if l.strip() == '#ifdef LV_LVGL_H_INCLUDE_SIMPLE':
j = i
while j < len(lines) and lines[j].strip() != '#endif':
j += 1
lines[i:j + 1] = ['#include "lvgl.h"']
break
# 3) 压平版本守卫(保留真分支;本仓库 LVGL 为 9.3.0)
symbols = {}
lines = _flatten_guards(lines, symbols)
lines = _drop_unused_guard(lines, FONT_NAME.upper())
while lines and lines[-1] == '':
lines.pop()
data = eol.join(lines) + eol
open(font_path, 'wb').write((b'\xef\xbb\xbf' if has_bom else b'') + data.encode('utf-8'))
print('工程约定规范化完成:文件头/包含路径/版本守卫压平(LVGL %d.%d.%d)'
% LVGL_VERSION)
# ---------------------------------------------------------------------------
# 低码位符号补回(与箭头字形合并同一模式)
# lv_font_conv 的稀疏 cmap 从符号集中最小码位起算,低于该起点的符号被静默丢弃
# (实测:U+3001 全角逗号 < 0x4E2D 范围起点,未进字体 → 页脚","长期渲染为空白)。
# 处理:对每个此类符号单独生成迷你字体(真实宋体位图),再并入主字体
# (位图 + glyph_dsc + 单码位 cmap,幂等)。
# ---------------------------------------------------------------------------
EXTRA_SYMBOLS = [','] # U+3001 全角逗号(页脚"第 X 页,共 Y 页")
def merge_extra_symbols(font_path, size, bpp):
"""把 lv_font_conv 丢失的低码位符号从迷你字体并入主字体(逐符号、幂等)。"""
import re
for ch in EXTRA_SYMBOLS:
cp = ord(ch)
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*%d\s*,' % cp, src):
print('U+%04X %r 已存在,跳过合并' % (cp, ch))
continue
# 1) 单独生成仅含该符号的迷你字体(真实宋体位图/度量)
tmp = os.path.join(tempfile.gettempdir(), 'font_extra_%04x.c' % cp)
cmd = resolve_lv_font_conv() + ['--font', resolve_font(None),
'--size', str(size), '--bpp', str(bpp),
'--symbols', ch, '--no-compress',
'--format', 'lvgl', '--lv-font-name', 'tmp_extra',
'-o', tmp]
r = subprocess.run(cmd)
if r.returncode != 0:
sys.exit('ERROR: 生成 U+%04X 迷你字体失败' % cp)
mini = open(tmp, encoding='utf-8-sig').read()
# 2) 从迷你字体提取位图字节 / glyph_dsc 字段 / 码位
bm = re.search(r'glyph_bitmap\[\]\s*=\s*\{', mini)
end_bm = mini.find('\n};', bm.end())
body_bm = re.sub(r'/\*.*?\*/', '', mini[bm.end():end_bm], flags=re.S)
bytes_ = re.findall(r'0x[0-9a-fA-F]+', body_bm)
gd = re.search(r'glyph_dsc\[\]\s*=\s*\{', mini)
end_gd = mini.find('\n};', gd.end())
entries = re.findall(r'\{\.bitmap_index = (\d+), \.adv_w = (\d+), \.box_w = (\d+), '
r'\.box_h = (\d+), \.ofs_x = (-?\d+), \.ofs_y = (-?\d+)\}',
mini[gd.end():end_gd])
if len(entries) < 2:
sys.exit('ERROR: 无法解析 U+%04X 迷你字体 glyph_dsc' % cp)
# 取第 1 项(下标 0 是保留空字形)
bmp_ofs, adv, bw, bh, ox, oy = (int(v) for v in entries[1])
cm = re.search(r'cmaps\[\]\s*=\s*\{', mini)
m = re.search(r'\.range_start\s*=\s*(\d+)\s*,\s*\.range_length\s*=\s*(\d+)',
mini[cm.end():])
if not m or int(m.group(2)) != 1:
sys.exit('ERROR: U+%04X 迷你字体 cmap 非单码位,无法并入' % cp)
sym_cp, sym_len = int(m.group(1)), 1
# 3) 并入主字体:位图追加(偏移平移)+ glyph_dsc 追加 + 单码位 cmap 追加
body = re.search(r'glyph_bitmap\[\]\s*=\s*\{', src)
end_main_bm = src.find('\n};', body.end())
main_bytes = len(re.findall(r'0x[0-9a-fA-F]+',
re.sub(r'/\*.*?\*/', '', src[body.end():end_main_bm], flags=re.S)))
glyph_count = len(re.findall(r'\.bitmap_index\s*=\s*\d+', src))
bmp = ',\n\n /* U+%04X %r(来源:宋体迷你字体并入;lv_font_conv 丢低码位符号) */\n ' % (cp, ch) \
+ ', '.join(bytes_)
src = src[:end_main_bm] + bmp + src[end_main_bm:]
gd = re.search(r'glyph_dsc\[\]\s*=\s*\{', src)
end_main_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+%04X */' % (
main_bytes + bmp_ofs, adv, bw, bh, ox, oy, cp)
src = src[:end_main_gd] + entry + src[end_main_gd:]
cm = re.search(r'cmaps\[\]\s*=\s*\{', src)
end_main_cm = src.find('\n};', cm.end())
cmap = ',\n {\n .range_start = %d, .range_length = %d, ' \
'.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 }' % (sym_cp, sym_len, glyph_count)
src = src[:end_main_cm] + cmap + src[end_main_cm:]
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+%04X %r:%d 字节位图,glyph_id=%d' % (cp, ch, len(bytes_), glyph_count))
def verify_coverage(font_path):
"""生成后校验:ui_cn_chars.txt 的全部码位必须进字体 cmap(防静默丢字)。
lv_font_conv 对某些码位会静默丢弃(低于稀疏范围起点、或源字体缺字形),
静默缺字在 UI 上表现为空白,极难发现。此校验把"缺字"变成生成期硬错误。
须在 merge_arrow_glyph / merge_extra_symbols 之后调用(合并的符号也要计入)。
"""
import re
src = open(font_path, encoding='utf-8-sig').read()
# 稀疏 cmap 的 unicode_list_N:十六进制偏移(相对 range_start)
ulists = {}
for m in re.finditer(r'static const uint16_t (unicode_list_\d+)\[\] = \{(.*?)\};', src, re.S):
ulists[m.group(1)] = [int(x, 16) for x in re.findall(r'0x[0-9a-fA-F]+', m.group(2))]
cmaps = [] # (range_start, range_length, sparse_offsets_or_None)
for m in re.finditer(r'\.range_start = (\d+), \.range_length = (\d+), \.glyph_id_start = (\d+),\s*'
r'\.unicode_list = ([^,]+), \.glyph_id_ofs_list = ([^,]+), \.list_length = (\d+), '
r'\.type = (\w+)', src):
ul = m.group(4).strip()
cmaps.append((int(m.group(1)), int(m.group(2)), ulists.get(ul)))
def covered(cp):
for rs, rl, offs in cmaps:
if offs is None:
if rs <= cp < rs + rl:
return True
elif (cp - rs) in offs:
return True
return False
chars = sorted(set(open(CHARS_FILE, encoding='utf-8').read().strip()))
missing = [c for c in chars if not covered(ord(c))]
if missing:
sys.exit('ERROR: 字体缺字 %d 个(UI 将显示空白): %s'
% (len(missing), ' '.join('U+%04X' % ord(c) for c in missing)))
print('coverage OK: %d 个字符全部进字体' % len(chars))
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) # 补回宋体缺失的 ▶ 字形(幂等)
merge_extra_symbols(OUT, args.size, args.bpp) # 补回 lv_font_conv 丢弃的低码位符号(幂等)
verify_coverage(OUT) # 生成后校验:字符集全部码位必须进字体(防静默丢字)
fix_trailing_newline(OUT) # 规范化末尾换行(pre-commit end-of-file-fixer)
normalize_project_conventions(OUT, args.size, args.bpp) # 对齐工程约定(文件头/包含路径/版本守卫压平)
print('OK %d bytes -> %s' % (os.path.getsize(OUT), OUT))
if __name__ == '__main__':
main()