组合变形文字(复杂文本整形)支持方案 —— 技术评审文档
组合变形文字(阿拉伯/印度系/高棉/缅甸/蒙古等)支持方案的完整论证与决策记录
*.py / tests/* 等源码文件为工具内部实现的落点记录,源码暂未公开;不影响阅读方案本身。评审对象:Py_FontMaker 点阵字库工具链(PC 生成端 + 设备端 C 引擎 + Web 界面) 评审议题:
- 如何支持"多个字符组合后字形发生变形"的语言(阿拉伯语、印地语、高棉语等);
- Web 界面能否做成按语言勾选、可选部分或全部;
- 全部勾选是否可行,边界在哪里。
文中现状结论均以本仓库代码为准,给出文件与行号。
实施状态(2026-09-03):方案已落地——
shaping_langs.py(语言包)、shaping_arabic.py(选形表/参考实现)、shaping_prerender.py(语料簇提取/位置形/预变形合成)、font_shaper_code.py(设备端ext_font_shaper.c/h生成),CLI--langs/--corpus/--pua_base, Web 勾选界面与 API 均可用;验证见tests/test_shaping.py(10 项,含 C/Python 双端差分)。 相对本文的增强:方案甲在设备端增加了簇字典最长匹配器(生成进ext_font_shaper.c), 因此 C 组语言的动态文本(如新闻)只要语料覆盖到的簇即可正确显示,未命中簇降级为 逐码点显示——"仅固定文案"的约束放宽为"语料字典命中率"(典型新闻语料 99%+)。 且仓库内置各语言簇字典(shaping_corpus/,由 Leipzig 新闻/维基 3 万句语料 + 维基导语/Tatoeba 提炼,构建工具shaping_corpus_build.py),用户无需上传语料; PUA 默认起点相应调整为 0xE000(6400 槽)。
0. 评审结论(TL;DR)
- 可以做成按语言勾选,且可以全选。勾选粒度按"文字系统"而非国家,共约 15 个勾选项,分三个支持等级(A/B/C 组,见 §3)。
- A 组(希伯来、泰/老挝)与 B
组(阿拉伯字母系)勾选后动态文本完整可用:A
组现有管线几乎直接支持;B 组利用 Unicode 预变形区码点 + 设备端小查表(LVGL
用内建宏,裸机由工具生成约 2~3 KB 的
arabic_shaper.c),引擎与 bin 格式不动。 - C 组(印度系、高棉、缅甸、藏文、蒙古文等)推荐"方案甲:PC 端 HarfBuzz 预整形 + 簇级预渲染 + PUA 码点映射":整形复杂度全部留在 PC,设备端引擎、字库格式、cmap、缓存零改动。代价是该组语言只支持固定 UI 文案(经过预整形的字符串),不支持运行时动态拼接的该类文本。
- "全部勾选"在此前提下可行:全选 ≈ A+B 组动态可用 + C 组固定文案可用。若产品要求 C 组语言的任意动态文本(如用户输入印地语),小 MCU 上不现实(需移植 HarfBuzz,数百 KB ROM + 数十 KB RAM),仅建议大内存平台按需立项(P3,方案乙铺路)。
- 建议分三期:P1 阿拉伯系/希伯来/泰(约 1~2 人周) → P2 C 组预整形产线(约 2~3 人周) → P3 字形 ID 化 / 设备端动态整形(按需)。
1. 问题定义:什么是"组合变形"
变形文字(需 text shaping 的文字)指:最终显示的字形不是"每个码点独立查表"能得到的,字形取决于上下文。两类典型机制:
- 连写选形(阿拉伯字母系):同一字母按位置取 孤立/词首/词中/词尾 形。幸运的是这些变形字形有标准 Unicode 码点(预变形区 U+FB50–FDFF / U+FE70–FEFF),因此可以在渲染前用查表替换解决——这是 B 组能"轻量支持动态文本"的根本原因。
- 合体与重排(印度系/高棉/缅甸等):多个码点合成一个字体内部字形(GID),该字形没有任何 Unicode 码点;部分元音符号还要重排到辅音前面。按码点建的 cmap 从原理上就收录不到这些字形——这是 C 组必须引入整形环节的根本原因。
业界对第二类的标准答案只有一个:HarfBuzz
整形引擎(Android/Chrome/Firefox 同款)。本方案将其放在 PC
端(Python 绑定 uharfbuzz,pip 可装、跨平台
wheel),不上设备。
2. 现状盘点:当前管线卡在哪里
当前全链路一切以 Unicode 码点为索引,这是支持变形文字的唯一结构性障碍(也是好消息:障碍集中、边界清晰):
| 环节 | 现状 | 对变形文字的影响 |
|---|---|---|
| 栅格化 | ttf_freetype_to_xbf.py:221-231:按码点
get_char_index() 逐字渲染 |
只能渲染"有码点的字形";GID-only 的合体字形取不到 |
| XBF 格式 | 头部 FirstCode/LastCode 为 u16,密集 cmap
按码点下标(ttf_freetype_to_xbf.py:212-213) |
仅 BMP;预变形区(FB50/FE70)在 BMP 内,天然装得下 |
| 打包/cmap | xbf_to_fontlib_bin.py 码点稀疏表 +
hash;cmap_sparse_table_t.code_point(font_engine_code.py) |
同上,按码点索引 |
| kerning | (left<<16)|right
码点对二分(xbf_to_fontlib_bin.py:1197-1209) |
整形路径的位置信息由 HarfBuzz 给出,不依赖此表 |
| 设备引擎 | LVGL 回调 get_glyph_dsc(font, dsc, letter, letter_next)
逐码点(font_engine_code.py) |
LVGL 本身按码点驱动字体;这决定了"喂给 LVGL 之前就要完成变形" |
| 多语言 | excel_multi_language_to_c.py:Excel → 各语言 C
字符串表 |
现成的预整形挂接点:固定文案恰好都从这里出 |
| Web | webapp/static/index.html:82-90
参数行;webapp/server.py 表单 → 子进程 |
加"语言包"复选框卡片即可,结构现成 |
另有两个对方案有利的既有事实:
- 引擎位图属性
char_bitmap_attribute_t的x_off/y_off为int8_t、零 advance 字形已被保留(ttf_freetype_to_xbf.py:232仅跳过adv==0 && width==0),组合符号(泰文声调等)已能入库并叠画; - 字库 cmap 按字体独立,PUA 区(U+E000–F8FF,6400 个码点)在文本字库中未被占用(iconfont 是独立的 .c 字库,不同 font 对象,不冲突)。
3. 语言分组与支持等级
按设备端需要什么能力分为三组,每组对应 Web 界面上的一批复选框:
| 组 | 文字系统(→ 勾选项) | 覆盖语言/国家(举例) | 变形机制 | 动态文本 | 设备端代价 |
|---|---|---|---|---|---|
| A | 希伯来文 | 希伯来语(以色列) | 仅 RTL,不连写 | ✅ | LVGL 开 LV_USE_BIDI;裸机加 bidi-lite |
| A | 泰文 / 老挝文 | 泰语、老挝语 | 上下标符号定位 | ✅(细节瑕疵) | 零 advance 符号叠画,现状即可 |
| B | 阿拉伯字母系 | 阿拉伯语(20+ 国)、波斯语、达里/普什图语、乌尔都语、库尔德语、维吾尔语、信德语 | 位置选形 + 连字,RTL | ✅ | LVGL:内建宏;裸机:生成 arabic_shaper.c(~2‑3 KB) |
| C | 天城文、孟加拉文、古木基文、古吉拉特文、奥里亚文、泰米尔文、泰卢固文、卡纳达文、马拉雅拉姆文、僧伽罗文、藏文 | 印度各邦、尼泊尔、孟加拉国、斯里兰卡、不丹 | 辅音合体 + 元音重排 | ⚠️ 仅固定文案 | 零(方案甲) |
| C | 高棉文、缅甸文 | 柬埔寨、缅甸 | 下标辅音 / 介音变形 + 重排 | ⚠️ 仅固定文案 | 零(方案甲) |
| C | 蒙古文(传统)、满文/锡伯文 | 中国内蒙古/新疆、蒙古国 | 连写选形(无预变形码点区)+ 竖排 | ⚠️ 仅固定文案 | 零 + 应用层旋转 90° |
泰米尔文变形程度较轻(合字少),若想省预整形流程也可降级按 A 组近似处理,但会缺 க்ஷ 等合字;默认仍归 C 组走预整形,质量稳妥。
4. 总体方案
设计原则:整形复杂度全部留在 PC 端;设备端拿到的仍是"码点 → 位图"的老问题,现有引擎与格式最大限度复用。
4.1 路径①:码点直通(现状,覆盖简单文字 + A 组)
不变。希伯来只需书写方向处理:LVGL 开
LV_USE_BIDI;裸机场景由工具附带生成
bidi_lite.c(简化版:段落级 RTL + 数字/拉丁反向段,不实现完整
UAX#9,见 §8 决策点 3)。泰文/老挝文的组合符号按现状入库即可叠画。
4.2 路径②:轻量运行时整形(B 组,动态文本可用)
- 生成端:勾选阿拉伯语系后,自动把基础区(0600–06FF、0750–077F、08A0–08FF)与预变形区(FB50–FDFF、FE70–FEFF) 加入码点集合,按字体实有字形裁剪后入库。XBF/bin/引擎完全不动(码点都在 BMP 内)。
- 设备端·LVGL:开
LV_USE_BIDI+LV_USE_ARABIC_PERSIAN_CHARS,LVGL 在 label 渲染前自行把基础码点替换为预变形码点——正好落在我们字库的 cmap 上。 - 设备端·裸机:工具生成
arabic_shaper.c/h:输入码点串,查上下文选形表(每字母 4 形映射 + lam-alef 连字),输出预变形码点串,再走路径①。表体约 2 KB,纯查表无 malloc。 - 乌尔都语说明:以 Naskh 风格支持(与阿拉伯语同机制)。传统 Nastaliq 悬挂连写不可能用预变形码点表达,若产品要求 Nastaliq 观感,则乌尔都语改走 C 组预整形(见 §8 决策点 4)。
4.3 路径③:PC 预整形(C 组)—— 方案甲:簇级预渲染 + PUA 映射(推荐)
核心思想:以"字位簇(grapheme cluster)"为最小显示单元,把每个簇在 PC 端整形并整体渲染成一张位图,当作一个"伪字符"分配 PUA 码点。
新增 shaping_prerender.py,流程:
- 收集所有 C 组文案(多语言 Excel 各语言列 + Web 文本框中属于 C 组区间的文本);
uharfbuzz对每条文案整形(该语言 script/direction),得到 GID 串 + 位置,按 cluster 边界切分;- FreeType 将每个簇的 1~N 个字形按位置叠加合成一张位图(含 advance/box/偏移度量),全局去重;
- 每个唯一簇分配一个 PUA 码点(默认 U+F000 起,可配置),作为普通字形注入 XBF(复用现有 12 B 属性 + 位图的存储,RLE/位流/基线约定照常);
- 改写输出:
multi_language_*.c中该文案替换为 PUA 码点串;同时产出shaping_map.json(PUA ↔︎ 原文/GID 追溯,调试用)。
设备端零改动:lv_label_set_text() 收到
PUA 串 → 现有 cmap 查表 → 现有 Flash 读取/解压/绘制。缓存、fake
兜底、hash 查表全部照常工作。
约束(评审重点):
- 只覆盖进过预整形的固定文案;运行时动态拼出的 C 组文本(用户输入、服务器下发的未登记字符串)会落到 fake 字库兜底。数字、拉丁混排不受影响(它们不变形,走路径①)。
- 单字库 PUA 上限 6400 簇。典型产品 UI 文案去重后是数百簇量级,余量充足;超限时按语言拆分字库或升级方案乙。
- 同一簇的位图在不同上下文中偶有细微位置差(HarfBuzz 的上下文定位),簇级合并取首次出现的版本,视觉影响可忽略(簇内部相对位置是精确的)。
4.4 方案乙:字形 ID 索引 + 整形串表(P3 可选,不在本期)
当 PUA
簇方案遇到瓶颈(簇数超限、体积敏感、或要与大平台的设备端动态整形打通)时的升级路线:bin
格式 v2 增加 GID 直接下标表(与现有
cmap_continous_table_t 同构),文案表升级为
{gid, adv_fp4, dx, dy} 整形串,引擎新增约 200 行(按 GID 取形
+ 逐字形出图)。比方案甲省空间(共用字形只存一次),且 LVGL v9 的
gid.index 通道天然对齐;代价是格式与引擎双端改动、LVGL v8
label 无法直接消费。评审建议:P2
以方案甲落地,方案乙按产品需求另行立项。
5. Web 界面勾选设计
- 新增"复杂文字 / 变形语言支持"卡片,复选框按 §3 的文字系统分组排列并标注 A/B/C 徽标;默认全不勾选,行为与现状完全一致。
- 勾选项由服务端一张
LANG_PACKS配置表驱动,每项声明:码点区间集合、整形策略(none / arabic_presentation / prerender)、附加产物。勾选后区间与现有"字符范围/文本"输入取并集(沿用现状逻辑)。 - 勾选任一 C 组语言时,界面出现"上传 UI 文案(Excel/txt)"输入与明确的能力边界提示(仅固定文案)。
- CLI 与 Web
同能力:
Py_FontMaker.py --langs arabic,hebrew,devanagari,...,Web 只是配置表的可视化入口。 - 实现落点:
webapp/static/index.html(卡片)、webapp/server.py(表单字段 + 配置表)、shaping_prerender.py(新)、font_engine_code.py(追加arabic_shaper.c/bidi_lite.c模板)、excel_multi_language_to_c.py(输出前过预整形器)。
6. "全部支持"可行性评审
6.1 结论
可行,但要把"支持"拆成两个层面向产品明确承诺:
| 层面 | A 组 | B 组 | C 组 |
|---|---|---|---|
| 字库数据(字形都能生成并入库) | ✅ | ✅ | ✅ |
| 固定 UI 文案正确显示 | ✅ | ✅ | ✅(方案甲) |
| 运行时任意动态文本 | ✅ | ✅ | ❌ 小 MCU / ⚠️ 大平台可 P3 立项 |
即:全选后,固定文案在所有语言上都正确;动态文本在 A+B 组上正确。这个承诺对绝大多数嵌入式产品(UI 文案固定、动态内容是数字/时间/拉丁)是完备的。
6.2 体积量级(每字号,2 bpp,24 px,估算)
| 勾选项 | 新增字形量级 | 体积量级 |
|---|---|---|
| 希伯来 | ~90 | ~10 KB |
| 泰 + 老挝 | ~180 | ~20 KB |
| 阿拉伯系(含预变形区,按字体裁剪) | 600~900 | 60~90 KB |
| C 组单语言(取决于文案量,典型 UI 300~800 簇) | 300~800 簇 | 40~120 KB |
| 全选(9 个印度系 + 高棉/缅甸/藏/蒙 + A/B 组) | — | 粗估 0.8~1.6 MB/字号 |
外挂 SPI Flash 场景可承受;体积敏感时按目标市场勾选子集即可——这正是做成复选框的价值。
6.3 主要风险与对策
| # | 风险 | 影响 | 对策 |
|---|---|---|---|
| 1 | 字体缺目标文字字形(如用 CJK 字体勾印地语) | 生成空字库 | 生成前用 fontTools cmap 检查覆盖率,低于阈值即报错并提示换字体(如 Noto 系列) |
| 2 | PUA 与产品自用 PUA 冲突 | 显示串字 | 起始码点可配(默认 U+F000);shaping_map.json 可审计 |
| 3 | 藏文/天城合体纵向超行框,YSize 不够 |
上下裁切 | 预整形后按实测 bbox 回写 XBF 头
YSize/BaseLine;int8 偏移域校验告警 |
| 4 | 乌尔都 Nastaliq 观感争议 | 客诉 | 默认 Naskh 并在界面注明;需要 Nastaliq 时转 C 组预整形 |
| 5 | 蒙古文竖排 | 布局 | 工具只保证连写正确(横排簇串),竖排由应用层旋转 label;文档写明 |
| 6 | 簇位图不共用字形,体积偏大 | 体积 | 可接受(见 6.2);超限升级方案乙 |
| 7 | 新依赖 uharfbuzz |
构建环境 | 纯 pip wheel,Win/mac/Linux 均有预编译包,与现有 freetype-py 同级依赖 |
| 8 | LVGL 内建阿语选形覆盖面(个别连字/harakat) | 个别字符不连 | 已覆盖主流场景;要求更高时阿语也可走 C 组预整形兜底 |
7. 分期实施建议
| 期 | 内容 | 涉及文件 | 工作量估计 |
|---|---|---|---|
| P1 | Web/CLI 语言勾选框架 + LANG_PACKS 表;A
组(希伯来/泰/老挝)码点包;B 组阿拉伯预变形区入库 +
arabic_shaper.c/bidi_lite.c 模板;LVGL
配置文档 |
index.html、server.py、Py_FontMaker.py、font_engine_code.py |
1~2 人周 |
| P2 | 方案甲预整形产线:shaping_prerender.py(uharfbuzz +
簇渲染 + PUA 分配 + 去重)、Excel
产线打通、shaping_map.json、覆盖率检查、C 组全部勾选项 |
新文件 +
excel_multi_language_to_c.py、ttf_freetype_to_xbf.py(PUA
注入口) |
2~3 人周 |
| P3(可选) | 方案乙 GID 化(格式 v2 + 引擎 draw run);大平台设备端 HarfBuzz 动态整形评估 | xbf_to_fontlib_bin.py、font_engine_code.py |
立项另评 |
每期验收建议沿用现有四层验证体系(单测 / bin 逐位校验 / clang 语法 /
宿主机运行时,tests/),P2 增加"整形金样比对":同一文案
uharfbuzz 整形结果与 FreeType 渲染簇位图的哈希快照回归。
8. 待评审决策点
| # | 决策点 | 建议 |
|---|---|---|
| 1 | C 组接受"仅固定文案"的产品承诺? | 接受。动态需求走 P3 另评,不阻塞 P1/P2 |
| 2 | PUA 起始码点与分配策略 | 默认 U+F000 起、按生成批次连续分配;提供 --pua-base
覆盖 |
| 3 | 裸机 bidi 做简化版还是完整 UAX#9 | 简化版(段落 RTL + 数字/拉丁段反转);完整版收益低、成本高 |
| 4 | 乌尔都语默认风格 | Naskh(B 组);Nastaliq 需求出现时按项目转 C 组 |
| 5 | 蒙古文竖排责任边界 | 工具保证连写,竖排由应用层旋转,不做竖排布局引擎 |
| 6 | 引入 uharfbuzz 依赖 |
同意(pip wheel,跨平台,业界唯一标准整形库) |
| 7 | 泰米尔文归 C 组(默认)还是降级 A 组近似 | 默认 C 组;体积极敏感的项目可选降级开关 |
附:图目录
| 图 | 文件 | 内容 |
|---|---|---|
| 图 1 | svg/shaping_problem.svg |
逐码点渲染为何失败(阿拉伯文/天城文对照) |
| 图 2 | svg/shaping_script_tiers.svg |
语言分组与支持等级(A/B/C) |
| 图 3 | svg/shaping_arch.svg |
总体架构与设备端三条运行时路径 |
| 图 4 | svg/shaping_pua_pipeline.svg |
方案甲:簇级预渲染 + PUA 映射管线 |
| 图 5 | svg/shaping_gid_format.svg |
方案乙:GID 索引格式 v2(P3 备选) |
| 图 6 | svg/shaping_webui.svg |
Web 勾选界面与"勾选 → 生成动作"映射 |