☰
LVGL多国语言字库制作全流程:从字符提取到避坑实践
2026/10/22 22:06:25 网站建设 项目流程

简介:LVGL多国语言字库制作工具是一份面向嵌入式开发者的实用资源,专门针对LVGL图形库定制字库与多语言支持。该工具可处理中、日、韩、阿拉伯文等字符集,将其转换为图形库可识别的字库格式,并支持调整字体大小、样式与颜色,生成适合项目直接集成的二进制文件。

资源包共146个文件,约32.44兆字节。其中除可执行程序与配套动态库外,还有37份C语言源码和36份头文件,便于查看字库生成与加载逻辑;21个图片文件用于界面预览,16个语言资源文件提供界面翻译;另含工程配置文件,可直接在集成开发环境中打开。整体结构清晰,适合具备基础嵌入式经验、正在为界面寻求多语言方案的开发者。

目前已有427人学习下载。借助完整工具和源码,读者可结合其中STM32平台示例,理解从字库生成到屏幕显示的完整链路,从而减少多语言适配工作量,显著缩短产品迭代周期。

1. 多国语言字库制作工具:LVGL 界面全是方块,问题不在屏幕而在字库链

我接手第一个海外项目时,设备在实验室一切正常,送到客户那边开机后中文、俄文、泰文全部变成空方块。排查了好几天主控、屏幕、驱动,最后发现根本不是屏幕的问题:LVGL 并不像 PC 那样直接读取 TTF 字体,它只认编译成 C 数组的位图字库,而当时工程里只烧了一个 ASCII 字库。所谓 lvgl 多国语言字库制作工具,就是把 TTF/OTF 字体按指定字符范围、字号、灰度打平成 LVGL 能加载的.c字库的工具链。它解决的问题很直接:让同一套 UI 在不同语言环境下能显示完整字符,同时不把整个字体文件塞进 Flash。这篇不写玄学,只讲能直接落地的流程和参数,新手能照做,熟手能避开那些容易翻车的细节。

2. lv_font_t 到底装了什么:从 C 数组到多语种渲染的原理与选型

2.1 LVGL 的字库本质上是一张位图缓存表

LVGL 渲染文字时,并不会在运行期动态解析 TTF 的曲线数据。它通过一个lv_font_t结构体指针工作,这个结构体里保存着字符映射表、字形描述符和位图数据。每次要绘制一个码点,就按码点查表,定位到字形描述符,再把这个字形对应的位图按 1/2/4/8 bpp 的位数放到屏幕上。也就是说,字库制作工具的本质是“离线把字体文件里的轮廓转成像素,并打包成可索引的 C 数组”,而不是做字体文件的格式转换。

这个设计直接决定了多语种字库的制作方式。因为 C 数组必须提前包含具体码点,所以字库文件和字符范围强相关。范围越大,固件越大。常见做法是把界面里用到的字符做成子集,而不是把 TTF 全量导入。LVGL 8 之后还支持字体回退,即同一个文本里主字库没有的码点会自动去备用字库找,这对多语种混排很有用,但前提是每个语言的字库都得先做成单独的 C 文件。

另一个容易忽略的细节是:LVGL 自带的默认字体文件里只包含 ASCII 和很少的扩展字符,做英文原型够用,一旦语言包切到俄文、泰文,必须使用自己生成的外部字库。那些默认字体并不是“不好”,而是覆盖范围固定,不适合作为多语种方案的底子。

2.2 多语种比 ASCII 多出的三件事:Unicode 范围、字体覆盖与文字整形

做 ASCII 字库只需要一段连续范围0x20-0x7F,从空格到波浪号,简单直接。但多语种要面对三件 ASCII 没有的麻烦。第一,Unicode 范围是多个离散块。西欧拉丁扩展在0x00C0-0x024F,西里尔在0x0400-0x04FF,泰文在0x0E00-0x0E7F,阿拉伯在0x0600-0x06FF,CJK 统一汉字在0x4E00-0x9FFF。这些范围不能只靠记忆,最好从 Unicode 标准表里查,或者直接用脚本从文本内容反推码点。

第二,字体覆盖不足。即使你把码点范围写进了命令,源字体本身没有这个字形,工具也只能跳过,而且很多命令行工具不会报错,只会安静地什么都不输出。比如用一款只带拉丁字符的字体生成俄文字库,出来的必然是大量空白。选择字体时要用字体查看工具逐个语言页确认,别等烧到设备上才发现缺字形。

第三,文字整形。这是最容易被忽略、也最容易翻车的一点。阿拉伯文、泰文这类复杂文字,同一个字母在词首、词中、词尾的形状不一样,甚至出现强制连写;泰文还有叠加在字母上下的符号,例如上元音、下元音。LVGL 的字库制作工具只做静态码点到位图的映射,不做上下文整形。也就是说,你往lv_label里塞一个阿拉伯字符串,它并不会自动被拆成正确的连字字形。要么保证字符串里存的已经是连字位置的字形码,要么用外部 shaping 引擎预处理生成字形序列再交给 LVGL。阿拉伯字库“能显示”不等于“能正常读”,这一点我在避坑章节还会展开。

拿泰文举例,一个泰文字符串经常由基字、上元音、下元音组成,LVGL 静态映射时会把每个组合字符画在基字中心附近,16px 小字号下就容易叠成一团黑。有些团队选择使用预组合字形,把常见音节整体映射成自定义码位,业务层再做映射,这样能绕开 shaping 引擎,但需要维护一张不小的映射表。是否要上 shaping,应该在最开始选型时决定,而不是等 UI 联调时再说。

2.3 命令行转换器与图形化配置工具怎么分工

主流做法是使用命令行工具lv_font_conv。它由社区维护,支持从 TTF/OTF 生成 LVGL 格式的 C 文件,参数清晰,适合批量、脚本化和版本管理。图形化的字体配置工具通常集成在 UI 设计软件里,适合鼠标选中字符表、拖一拖范围就预览效果,但它生成的项目文件比较重,不适合反复重新构建,也不会自动处理源文本扫描。

我自己的分工是:原型阶段用图形化工具看效果,正式固件全部走命令行工具,把生成命令写进构建脚本。这样换字体、改字号、改语言配置时,只需要改一个配置文件,不需要重新在图形界面里点一遍。

做多国语言字库前,建议先列一张清单,避免做着做着忘了哪个环节。下面的表可以直接复制到项目文档里:

项目取值示例说明
目标语言简中、繁中、俄、泰、阿拉伯决定 Unicode 范围和是否需要 shaping
源字体文件思源黑体、Noto 系列等必须包含目标语言全部字形
字号16px / 20px与 UI 实际显示字号一致,放大会有锯齿
bpp1 / 2 / 4 / 8抗锯齿位数,越大越清晰也越占内存
压缩开 / 关位图做 RLE 压缩,明显减小 Flash 占用
字符范围由源文本提取避免全量 CJK 导致固件爆炸

这张表就是后面所有命令的输入。应该在提需求时就把这些定下来,而不是生成字库之后再反复试。很多项目延期,不是因为工具难用,而是因为“先做一个看看”导致字库反复重新生成。

2.4 第一版字库怎么做:先小后大,先静态后动态

真正动手时,别一上来就想做一个覆盖所有语言的大字库。我一般先做三件事:确认目标语言里最简单的一段文本能显示,确认标点符号和数字没有丢,确认字体覆盖和 shaping 需要分开对待。把最小字符集做出来,烧到设备上看效果,再逐步扩展到完整语言包。

动态内容是另一个变量。设备上如果有用户输入、蓝牙设备名、文件列表等动态文本,字库覆盖策略必须预留常见字符。静态文案可以从语言包提取,动态内容只能靠覆盖率统计。你不能把整个 Unicode 都放进去,所以需要在产品定义阶段决定“哪些字符必须显示,哪些字符缺失时提示不支持”。这个边界越早划清楚,后期越少返工。

3. 用 lv_font_conv 跑出第一个多语种字库:最小命令与三个必调参数

3.1 最小生成命令:指定字体、范围、输出

先把环境准备说明白:lv_font_conv是 Node.js 写的命令行工具,装好后在终端直接调用。下面这条命令可以生成一个覆盖拉丁扩展、西里尔和常用汉字的字库。

lv_font_conv \ --font NotoSansSC-Regular.otf \ --size 16 \ --bpp 4 \ --range 0x20-0x7F,0x00C0-0x00FF,0x0400-0x04FF,0x4E00-0x9FA5 \ --format lvgl \ --no-compress \ --output app_font.c

这里--font指定源字体文件;--size 16决定字形高度,单位是像素;--bpp 4表示每个像素用 4 bit,即 16 级抗锯齿;--range用逗号分隔多个 Unicode 区间,这条命令里依次是 ASCII、拉丁扩展、西里尔和常用汉字;--format lvgl表示输出给 LVGL 使用的 C 数组;--output是输出文件名。

运行结束后会得到一个 C 文件,里面包含lv_font_t app_font的结构体定义和字形相关数组。注意这里加了--no-compress,是为了方便你拿原始位图对照调试;正式发布时建议去掉这个开关,让工具默认做压缩,体积会小不少。工具遇到源字体里不存在的字形会跳过,命令本身不会因为缺字形而报错,所以后面避坑部分会专门说怎么发现缺字。

3.2 三个必调参数:size、bpp、range

--size最好与 UI 里的实际字号一致。LVGL 的坐标、行高都以像素为基准,你做一个 16px 的字库,把 label 的字号设成 16,行内垂直正好;字号设成 17,顶部会被截掉一点,底部留白变大。不要靠“字库做大一号再缩小”来解决问题,LVGL 不做缩放绘制,位图多大就画多大。

--bpp的取舍比较依赖场景。bpp=1 只有黑白两色,小字号下笔画发虚,但一个 16px 的字形位图只需要几十字节;bpp=2 有 4 级灰度,能看清抗锯齿轮廓;bpp=4 在普通屏上已经够用;bpp=8 适合大字号或对观感要求高的场景,内存翻倍但提升有限。做小屏设备,我一般用 bpp=2 或 4,做大字号标题才上 4。

--range是字库体积的大头。一个覆盖全部常用汉字的字库,仅 CJK 一段就包含两万多个码点,16px、bpp=4 全量生成后可能超过 1 MB,这在 MCU 场景直接不可用。所以正确做法是缩小范围到实际用到的字符。下一小节就是干这个的。

3.3 从项目源文本抽取码点:生成 range 参数

很多新手直接抄一个很大的--range,结果固件体积失控。稳妥套路是把所有要显示的语言包字符串拼成一个文本文件,然后用脚本筛出出现过的码点。下面是一段可复现的 Python 脚本:

import unicodedata def load_texts(path): with open(path, encoding="utf-8") as f: return f.read() def build_ranges(codepoints): if not codepoints: return "" codepoints = sorted(set(codepoints)) ranges = [] start = prev = codepoints[0] for cp in codepoints[1:]: if cp == prev + 1: prev = cp continue ranges.append((start, prev)) start = prev = cp ranges.append((start, prev)) return ",".join(f"0x{s:04X}-0x{e:04X}" if s != e else f"0x{s:04X}" for s, e in ranges) text = load_texts("all_strings.txt") print(build_ranges(ord(c) for c in text if not unicodedata.category(c).startswith("Z")))

这段脚本读取all_strings.txt,把所有码点排序后合并成连续区间,输出成--range需要的格式。过滤Z开头的 Unicode 分类会把空白去掉,避免把换行、空格算进字库;空格可以由 ASCII 段覆盖,不需要重复烧录。

把输出粘贴回命令里的--range,再执行一次生成,字库体积通常能缩小到全量 CJK 的十分之一以下。注意:从文本抽码点只能覆盖静态文案,如果设备上有用户输入、实时读取的设备名,那还是要预留一块常用字库范围,否则用户一输入冷僻字就变成方块。

3.4 把生成的 C 字库接进 LVGL 工程

生成.c文件后,要让它参与编译。直接把app_font.c加进工程的源文件列表,然后在需要使用字库的地方声明extern lv_font_t app_font;,再把它挂到控件的样式上。

extern lv_font_t app_font; lv_obj_t *label = lv_label_create(screen); lv_obj_set_style_text_font(label, &app_font, 0); lv_label_set_text(label, "你好,世界");

这里lv_obj_set_style_text_font(label, &app_font, 0)的最后一个0是样式选择器,表示默认状态。如果是在lv_style_set_text_font里传参,就把&app_font传给该函数。编译时还会用到--lv-include参数,让生成的 C 文件自带头文件引用:

lv_font_conv --font NotoSansSC-Regular.otf --size 16 --bpp 4 \ --range 0x20-0x7F,0x4E00-0x9FA5 --format lvgl \ --lv-include "lvgl/lv_font.h" --output app_font.c

加上--lv-include后,生成的 C 文件顶部会带一行#include,避免你手工写的头文件路径和实际工程不一致。如果你发现编译提示字体未定义,第一反应别改代码,先检查这个.c文件有没有真被编译链接,很多构建系统里新增文件不会自动加入。把构建日志翻出来,确认app_font.c出现在编译过程里,再谈 extern 和头文件的问题。

4. 从文本抽字符集并批量生成多语种字库:脚本化工作流

4.1 把字库配置写成一个 JSON 清单

多语言项目通常不止一个字库。我一般会在工程根目录放一个fonts.json,按语言和维护状态记录字号、字体文件、字符范围、输出文件名。例如:

[ { "name": "app_font_latin", "font": "Roboto-Regular.ttf", "size": 16, "bpp": 4, "ranges": "0x20-0x7F,0x00C0-0x00FF", "output": "gen/app_font_latin.c" }, { "name": "app_font_cyrillic", "font": "NotoSans-Regular.ttf", "size": 16, "bpp": 4, "ranges": "0x20-0x7F,0x0400-0x04FF", "output": "gen/app_font_cyrillic.c" } ]

这份清单的价值在于可追溯:字体来源、字号、字符覆盖、输出位置都记录在案,后续维护不用翻聊天记录和旧命令。生成脚本逐条读取并调用转换工具,如果某一个源字体缺字形,工具会输出一条警告,但执行并不停止,我会把警告统一收集进日志,等全部生成完再去处理,避免一次失败打断整个批量流程。

4.2 写批量生成脚本:循环调用转换命令

下面是一个常见的 Bash 脚本骨架:

#!/usr/bin/env bash set -euo pipefail cd "$(dirname "$0")" mkdir -p gen jq -c '.[]' fonts.json | while read -r item; do name=$(echo "$item" | jq -r '.name') font=$(echo "$item" | jq -r '.font') size=$(echo "$item" | jq -r '.size') bpp=$(echo "$item" | jq -r '.bpp') ranges=$(echo "$item" | jq -r '.ranges') output=$(echo "$item" | jq -r '.output') lv_font_conv \ --font "$font" \ --size "$size" \ --bpp "$bpp" \ --range "$ranges" \ --format lvgl \ --output "$output" done echo "all fonts generated."

set -euo pipefail保证中间任何一步失败就停止,避免把半成品字库烧进固件。jq -c把 JSON 数组逐条交给循环,每次执行一次转换命令。这里没有加--no-compress,正式产出默认带压缩。脚本还能再加一层判断:当输出文件的时间戳比源字体和配置都新时跳过转换,节省 CI 时间。

如果你不用 jq,也可以用 Python 写同样逻辑。脚本的目的不是炫技,而是让“换一个字号、换一个范围”变成只改 JSON,不碰命令。这样团队成员都能维护,不会因为某个人离开导致字库流程断掉。

4.3 用脚本比对源文本和字库,避免漏字符

生成完成后,应该再跑一个校验脚本,把源语言文件里的每一个字符和字库的码点集合做比对。这里不用重新生成,直接解析刚刚生成的 C 文件就行。下面脚本以unicode = 0xXXXX字段为准:

import re font_c = open("gen/app_font_latin.c", encoding="utf-8").read() covered = set(int(u, 16) for u in re.findall(r'unicode = 0x([0-9A-Fa-f]+)', font_c)) source = open("all_strings.txt", encoding="utf-8").read() missing = sorted({ord(c) for c in source if ord(c) not in covered}) if missing: print("MISSING:", ", ".join(f"U+{m:04X}" for m in missing)) else: print("OK")

这段代码用正则从 C 文件里取出所有已生成的码点,再和源文本做差集。不同版本的工具输出的结构字段名可能有一点差异,但unicode这个字段是字形描述表的基础字段,基本都会保留。如果你的版本输出的是U+注释格式,把正则改成匹配U+XXXX也可以。

到这里,生产和校验就是一条完整的自动化链:文本变更后重新抽码点,更新 JSON,批量生成字库,自动比对,最后交给构建系统。整个过程没有人工双击图形界面,也就没有“忘了更新字库”这种事故。

4.4 动态输入内容怎么覆盖

静态文案可以扫描语言包,动态内容不行。比如设备支持用户搜索地铁站名,那必须提前准备一张“可显示字符表”。这张表的来源可以是业务数据里的高频字符统计,也可以是产品定义的输入法字符集。设备端检测到未覆盖字符时,与其让它变成方块,不如在输入层就拦截。

常见做法是把动态字符做成一个独立的小字库,比如 16px、bpp=2,只覆盖常用几千字,和主 UI 字体分开。静态界面用大一点的 bpp=4 保证观感,用户输入区用这个小字库,这样既不拖慢主界面,也不会因为用户输入一个生僻字导致功能崩溃。多语言输入法和输入法字库要单独评估,不要和界面字库混在一起做。

5. 多语种字库避坑指南:方块、连字错乱、编译失败怎么排查

5.1 只有个别字符显示成方块

现象:界面上 90% 的文字正常,偶尔几个字是空心方块,通常出现在不常更新的弹窗或提示语里。原因:--range没有覆盖到那个码点,或者源字体缺少该字形,而工具静默跳过。解决:把那个字符的码点找出来,加进--range。调试时写一个函数打印字符串的 UTF-8 字节,转成 Unicode 码点,再对照已生成字库的 cmap。最直接的方法是重新生成一个只包含该码点的临时字库,烧进去看是否显示;如果仍显示方块,就把源字体拿到字体查看器里搜这个码点,确认字体文件本身有没有字形。很多时候不是工具坏了,是字体没有这个字。

5.2 英文和数字正常,中文全变方块

现象:纯英文界面无问题,切到中文后所有汉字都消失。原因:你用的源字体是拉丁字体,没有 CJK 字形,中文码点全被跳过。解决:换一个支持 CJK 的字体文件重新生成。注意不要只看字体名字带“CN”或“SC”就认为支持,最好在字体工具里直接搜“中”和“文”两个字确认。生成命令里明明写了0x4E00-0x9FA5,但源字体缺字形时工具不会停下来,这也是很多人踩坑之后才发现的原因。选字体时我会直接看字体文件里汉字部首的覆盖统计,而不是看宣传文字。

5.3 阿拉伯文/泰文顺序错乱、连字丢失

现象:阿拉伯文字母像被打散,字符位置完全不对,甚至从右往左的排版变成左往右。原因:LVGL 默认不做双向排版和复杂文本整形,字库工具只能静态映射码点,无法判断字母在词中的位置。同一个阿拉伯字母,词首、词中、词尾和独立形式的字形码是不同的。解决思路分两层。第一层,在字库工具里把阿拉伯字母的四种形式所对应的字形码都包含进来;第二层,在业务层用外部 shaping 引擎把字符串转换成带位置信息的字形序列,再交给 LVGL。泰文也类似,需要处理上方元音、下方辅音等叠置符号。这不是换一个字库就能解决的,必须在软件设计阶段就给语言模块加 shaping 步骤,否则测试时看着每个字母能显示,用户一读就发现是乱的。

5.4 编译报错 undefined reference toapp_font

现象:代码里写了extern lv_font_t app_font;,链接时仍然报未定义。原因:多数情况是.c文件没有加进构建系统,或者输出文件名与 extern 不一致。解决:先打开构建脚本,确认gen/app_font.c在源文件列表里;再搜索生成文件里的字体变量名,不同参数下变量名可能与文件名不一致,以结构体名字为准。还有同学把lv_font_t app_font和lv_font_t *app_font混在一起,extern 声明成指针,导致链接器找的是另一个符号。记住,生成的变量是结构体,不是指针,使用时要传&app_font。如果构建系统做了文件分类,把.c文件放进了“工具链源码”目录但没被识别,也会出现这个问题。

5.5 字库文件太大,Flash 塞不下

现象:固件编译完超出片内 Flash 容量,裁掉日志功能才勉强放下。原因:字符范围太大、bpp 太高,或者没有开压缩。解决:开压缩,改小 bpp,严格限制字符范围。做 CJK 时不要整个0x4E00-0x9FA5全量包含,先按文案提取。还可以把字库放进外部 Flash,运行时加载,但加载路径、内存池和缓存管理都要额外设计,适合产品功能稳定之后作为优化项。一个项目里多个字号千万不要各做一份全量字库,应该让 UI 统一字号,或者把常用小字号做成共享字库,大字号只覆盖标题文本。

6. 进阶验证:把字库覆盖检查做进 CI,再验证阿拉伯语连字

当字库由脚本批量生成后,最怕的不是命令写错,而是有人改了一行文案,新增了一个生僻字,CI 却不知道。我会把第 4 章的校验脚本做成一个独立命令,在每次构建前先执行:扫描所有源语言文件,比对已提交的字库 C 文件,一旦发现缺失码点,直接让构建失败,而不是等测试人员肉眼去找。这一步成本很低,收益很大,值得立刻做。

另一个进阶技巧是针对阿拉伯语等复杂文字。在接入 shaping 引擎后,不能只看最终字符串,还要在模拟器里传入三种上下文:独立形式、词首、词中、词尾。同一个字母至少对应 4 个不同字形码,字库范围必须把这些字形码全部覆盖。验证方法是在 LVGL 模拟器里放三行内容,一行是独立单词,一行是双连写,一行是带前后缀的完整词,截屏后逐项对照标准形状。测试用例直接编进语言模块的自动化回归里,比每次手工输入可靠得多。

如果产品有动态字符串,建议把用户可能输入的常用字也做成一个输入字库。比如设备名、蓝牙名、地名搜索,这部分可以提前收集典型数据做词频统计,再做低频覆盖,冷僻字缺失时提示“暂不支持”,而不是显示方块。到这里,多语言字库的整个链路就算闭环了。

我自己的教训是早期做某项目 X 时,阿拉伯文测试只看“能显示”,没验证连字,结果海外样机拿过去没法读。后来在流程里加了 shaping 用例,这个坑再也没踩过。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询