feat(UI字体): 扩字60字为页面扩充预置, 修复生成器cmap插入顺序缺陷
字库扩充(批2页面预置, task-4/道B): - ui_cn_chars.txt 119→179 字(CJK 176 + 符号 ▶/,/:),含 ASCII 共 274 字形 - +60 字: Ⅰ Ⅱ Ⅲ 段 延 投 退 跳 闸 零 频 荷 负 低 过 电 一 二 次 位 公 出 回 外 部 度 温 态 状 总 接 显 示 最 近 未 标 欠 闭 锁 长 间 障 配 道 重 载 返 年 月 分 秒 合 已 请 求 确 取 消 越 依据 = 页面补齐规划/页面扩充详细方案两篇规划中出现的页面显示串; 待拍板的厂家设置支用字(厂/家/恢/调/试)不入 - Ⅰ/Ⅱ/Ⅲ 宋体有字形(simsun face0 cmap 实测 HAS)且正常入字体,罗马数字 无需任何工具链兜底 make_font.py 修复(本次扩字暴露的真缺陷): - LVGL get_glyph_dsc_id() 线性扫描 cmaps[]、首个包含码位的 range 即返回: Ⅰ(U+2160) 使稀疏 range 起点 0x4E09→0x2160,把 U+25B6 ▶ 单码位补回条目 吞进宽 range → 模拟器箭头全变方框(菜单 658 px 差异);旧 verify_coverage 按"进了任一 range 即覆盖"判定,全绿照不出解析路径倒置 - 修复: _insert_cmap_entry() 把补回条目插到包含它的 range 之前; _cmap_lookup_gid() 可达性守卫,可达即跳合并(全角逗号走稀疏列表,不再 追加永远不可达的死条目); verify_coverage() 改按首命中语义 验证(全绿): - font-regen 四步(extract→并回 KEEP_EXTRA→make_font→diff);仓库外沙箱 dry-run + 确定性: 沙箱/仓库产物 sha256 一致(26e519db…) - 逐码位结构对账: added=60 / removed=0 / 保留字形位图字节零差异 / 度量一致(multiset 差 = 新字身) - font-charset PASS(60 未知冗余 WARN = 预置字,批2接线后自然转在用) - 模拟器菜单 8 状态 + 内容页 11 页黄金参考与旧字体逐字节一致(箭头恢复); ref/ 基线无变化,故无黄金参考更新提交 - @build 0 Error 0 Warning; A/B 同树构建: RO-data +1528 B, RW/ZI(RAM) 0 gate: only mem-budget/mem-numbers pending lead ledger backfill - pre-commit --all-files 实测: 仅这两条失败(台账受管区/内存文档数字 vs .map 漂移; 根因是 RO-data +1528 B 的 Flash 增量, RAM 无变化); 其余 38 钩子全绿(font-charset/ui-golden/cppcheck/编码规范/单测族均过) - 修复手段 ram_baseline.py --write / check_mem_numbers.py --write 均写 docs/内存管理/**(Lead 单写者域), 本分支按裁决不代跑, 请合并后回填 - 手工自查: 改动 8 文件 BOM/CR/U+FFFD 与各自原状一致(全 LF 无 BOM), 字体产物纯 ASCII, make_font.py 中文仅在注释与打印, 无字面量改动
This commit is contained in:
+99
-17
@@ -80,14 +80,95 @@ 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 _parse_cmap_entries(src):
|
||||
"""解析 cmaps[] 为 [(range_start, range_length, glyph_id_start,
|
||||
unicode_list名或None, glyph_id_ofs_list名或None, list_length, type名)],
|
||||
以及所有 static const uint8/16 数组表。"""
|
||||
arrays = {}
|
||||
for mm in re.finditer(r'static const (uint8_t|uint16_t) (\w+)\[\] = \{(.*?)\};', src, re.S):
|
||||
arrays[mm.group(2)] = [int(x, 16) for x in re.findall(r'0x[0-9a-fA-F]+', mm.group(3) if mm.group(1) == 'uint16_t' else mm.group(3))]
|
||||
cm = re.search(r'cmaps\[\]\s*=\s*\{', src)
|
||||
if not cm:
|
||||
return [], arrays, None, -1
|
||||
end_cm = src.find('\n};', cm.end())
|
||||
entries = []
|
||||
for mm in re.finditer(r'\{\s*\.range_start\s*=\s*(\d+),\s*\.range_length\s*=\s*(\d+),\s*\.glyph_id_start\s*=\s*(\d+),\s*\.unicode_list\s*=\s*([^,]+),\s*\.glyph_id_ofs_list\s*=\s*([^,]+),\s*\.list_length\s*=\s*(\d+),\s*\.type\s*=\s*(\w+)', src[cm.end():end_cm]):
|
||||
ul = mm.group(4).strip()
|
||||
ol = mm.group(5).strip()
|
||||
entries.append((int(mm.group(1)), int(mm.group(2)), int(mm.group(3)),
|
||||
None if ul == 'NULL' else ul,
|
||||
None if ol == 'NULL' else ol,
|
||||
int(mm.group(6)), mm.group(7)))
|
||||
return entries, arrays, cm, end_cm
|
||||
|
||||
|
||||
def _resolve_in_entry(e, arrays, cp):
|
||||
rs, rl, gs, ul, ol, ll, typ = e
|
||||
rcp = cp - rs
|
||||
if typ.endswith('FORMAT0_TINY'):
|
||||
return gs + rcp
|
||||
if typ.endswith('FORMAT0_FULL'):
|
||||
ofs = arrays.get(ol, [])
|
||||
if rcp >= len(ofs):
|
||||
return 0
|
||||
v = ofs[rcp]
|
||||
if v == 0 and rcp != 0:
|
||||
return 0
|
||||
return gs + v
|
||||
if typ.endswith('SPARSE_TINY'):
|
||||
lst = arrays.get(ul, [])
|
||||
try:
|
||||
i = lst.index(rcp)
|
||||
except ValueError:
|
||||
return 0
|
||||
return gs + i
|
||||
if typ.endswith('SPARSE_FULL'):
|
||||
lst = arrays.get(ul, [])
|
||||
try:
|
||||
i = lst.index(rcp)
|
||||
except ValueError:
|
||||
return 0
|
||||
ofs = arrays.get(ol, [])
|
||||
return gs + ofs[i]
|
||||
return 0
|
||||
|
||||
|
||||
def _cmap_lookup_gid(src, cp):
|
||||
"""复刻 LVGL get_glyph_dsc_id:线性扫描,第一个包含码位的 range 决定
|
||||
结果(找不到也直接返回 0,不会继续看后面的 range)。"""
|
||||
entries, arrays, cm, _end = _parse_cmap_entries(src)
|
||||
for e in entries:
|
||||
rs, rl = e[0], e[1]
|
||||
if rs <= cp < rs + rl:
|
||||
return _resolve_in_entry(e, arrays, cp)
|
||||
return 0
|
||||
|
||||
|
||||
def _insert_cmap_entry(src, cp, entry_text):
|
||||
"""把单码位 cmap 条目插到「第一个包含该码位的既有 range」之前;
|
||||
无包含者则追加到末尾。(LVGL 线性首命中语义,详见 _cmap_lookup_gid)"""
|
||||
entries, arrays, cm, end_cm = _parse_cmap_entries(src)
|
||||
block = src[cm.end():end_cm]
|
||||
pos = None
|
||||
for mm in re.finditer(r'\{\s*\.range_start\s*=\s*(\d+),\s*\.range_length\s*=\s*(\d+)', block):
|
||||
rs, rl = int(mm.group(1)), int(mm.group(2))
|
||||
if rs <= cp < rs + rl:
|
||||
pos = mm.start()
|
||||
break
|
||||
if pos is None:
|
||||
return src[:end_cm] + ',\n' + entry_text + src[end_cm:]
|
||||
blk_ins = cm.end() + pos
|
||||
return src[:blk_ins] + entry_text + ',\n ' + src[blk_ins:]
|
||||
|
||||
|
||||
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 已存在,跳过合并')
|
||||
if _cmap_lookup_gid(src, ARROW_CP_DEC) != 0:
|
||||
print('arrow U+25B6 已可经既有 cmap 解析,跳过合并')
|
||||
return
|
||||
# 当前位图总字节数(新字形 bitmap_index 的取值)
|
||||
bm = re.search(r'glyph_bitmap\[\]\s*=\s*\{', src)
|
||||
@@ -106,12 +187,10 @@ def merge_arrow_glyph(font_path):
|
||||
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 }' % (
|
||||
cmap_entry = '{\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:]
|
||||
src = _insert_cmap_entry(src, ARROW_CP_DEC, cmap_entry)
|
||||
# 4) cmap_num +1
|
||||
src = re.sub(r'\.cmap_num = (\d+),',
|
||||
lambda m: '.cmap_num = %d,' % (int(m.group(1)) + 1), src, count=1)
|
||||
@@ -321,8 +400,8 @@ def merge_extra_symbols(font_path, size, bpp):
|
||||
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))
|
||||
if _cmap_lookup_gid(src, cp) != 0:
|
||||
print('U+%04X %r 已可经既有 cmap 解析,跳过合并' % (cp, ch))
|
||||
continue
|
||||
|
||||
# 1) 单独生成仅含该符号的迷你字体(真实宋体位图/度量)
|
||||
@@ -376,13 +455,10 @@ def merge_extra_symbols(font_path, size, bpp):
|
||||
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:]
|
||||
cmap_entry = '{\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 = _insert_cmap_entry(src, sym_cp, cmap_entry)
|
||||
|
||||
src = re.sub(r'\.cmap_num = (\d+),',
|
||||
lambda m: '.cmap_num = %d,' % (int(m.group(1)) + 1), src, count=1)
|
||||
@@ -422,7 +498,13 @@ def verify_coverage(font_path):
|
||||
return False
|
||||
|
||||
chars = sorted(set(open(CHARS_FILE, encoding='utf-8').read().strip()))
|
||||
missing = [c for c in chars if not covered(ord(c))]
|
||||
# 判定必须复刻 LVGL 的「首个包含即返回」语义:宽稀疏 range 会把后面的
|
||||
# 单码位补回条目吞掉(2026-10-25 Ⅰ 段扩字实测:旧 covered() 判据下
|
||||
# coverage 全绿,实际 ▶ 上屏全是方框)。_cmap_lookup_gid 不可用时退回 covered。
|
||||
try:
|
||||
missing = [c for c in chars if _cmap_lookup_gid(src, ord(c)) == 0]
|
||||
except NameError:
|
||||
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)))
|
||||
|
||||
Reference in New Issue
Block a user