4、字体转换工具链:LVGL官方字体转换器、FreeType库集成、Python脚本批量转换

字体文件本身LVGL是读不懂的。你得把.ttf、.woff这些格式,转成LVGL能吃的二进制数据。说白了,就是给字体做一次「编译」。这一章我带你捋一遍市面上主流的三种转换路子,各有各的脾气。

核心结论:小项目用官方在线转换器,中等规模用离线工具,批量生产必须上Python脚本+FreeType。

4.1 LVGL官方字体转换器(Online & Offline)

LVGL官方提供了两种转换方式。在线版适合快速验证,离线版适合集成到构建流程里。

4.1.1 在线转换器(Online Converter)

地址我就不贴了,你搜「LVGL Font Converter」就能找到。它的操作很简单:上传字体文件,选好字号、字符范围、bpp(每像素位数),点一下按钮就生成.c文件。

我个人习惯在原型阶段用它。比如我想快速看看某个字体在16px下的显示效果,直接拖进去,几秒钟拿到代码,烧进去跑一下。省事。

参数 说明 我的建议
BPP(Bits Per Pixel) 每个像素用多少位表示透明度 1/2/4/8,常用4,兼顾清晰度和体积
字符范围 只转换你需要的字符 别全选,否则生成的数组能撑爆Flash
字体大小 像素单位 建议按实际显示尺寸来,不要事后缩放
注意:在线转换器有文件大小限制。我记得有一次传了个10MB的中文字体包,页面直接卡死了。大字体文件还是用离线工具吧。

4.1.2 离线转换器(Offline Converter)

离线版其实是一个Python脚本,叫lv_font_conv。它比在线版灵活得多,支持命令行参数,可以集成到Makefile或CMake里。

基本用法长这样:

lv_font_conv --font NotoSansSC-Regular.ttf \
  --size 16 \
  --bpp 4 \
  --range 0x20-0x7F \
  --range 0x4E00-0x9FFF \
  --format lvgl \
  --output my_font.c

这里--range可以指定多个区间。中文字符的Unicode范围是0x4E00到0x9FFF,但实际你只需要常用字,别一股脑全转。我在项目里一般只转GB2312一级汉字(3755个),够用。

小技巧:用--symbols参数可以手动指定字符列表,比如只转「设置」「确认」「取消」这几个词。生成的字体文件极小。

4.2 FreeType库的集成

FreeType是一个开源的字体渲染引擎。LVGL本身不依赖它,但如果你想在运行时动态加载字体,或者做更精细的渲染控制,就得把它集成进来。

为什么要用FreeType?

  • 动态加载:字体文件放在文件系统里,运行时读取,不用编译时固定死。
  • 抗锯齿:FreeType的渲染质量比LVGL内置的位图字体好。
  • 字体回退:一个字符在字体A里找不到,自动去字体B里找。

集成步骤其实不复杂:

  1. 把FreeType源码放到你的工程里,或者用包管理器安装。
  2. 在LVGL的配置文件中开启LV_USE_FREETYPE。
  3. 初始化FreeType并注册字体。

代码示例:

#include "lvgl.h"
#include "lv_freetype.h"

lv_font_t *my_font;

void font_init(void) {
    lv_freetype_init(0);  // 0表示不缓存,性能敏感场景可以设大一点
    
    my_font = lv_freetype_font_create(
        "path/to/font.ttf",  // 字体文件路径
        LV_FREETYPE_FONT_RENDER_MODE_BITMAP,  // 位图模式
        16,  // 字号
        LV_FREETYPE_FONT_STYLE_NORMAL
    );
    
    if (my_font == NULL) {
        // 处理错误
    }
}
踩坑记录:我曾经在STM32上集成FreeType,发现内存占用飙升。后来查了文档才知道,FreeType默认会为每个字体分配一个缓存池。如果你的MCU只有64KB RAM,建议把缓存设小一点,或者用LV_FREETYPE_CACHE_SIZE控制。

4.3 Python脚本批量转换

当你需要管理几十个字体文件、多种字号、多语言时,手动敲命令就太蠢了。这时候就该上Python脚本了。

我的做法是写一个batch_convert.py,遍历字体目录,自动生成所有需要的.c文件。核心逻辑如下:

import os
import subprocess

FONT_DIR = "./fonts"
OUTPUT_DIR = "./generated_fonts"
LV_FONT_CONV = "lv_font_conv"

# 定义需要转换的字体配置
FONT_CONFIGS = [
    {"name": "noto_sans_sc", "file": "NotoSansSC-Regular.ttf", "sizes": [14, 16, 20]},
    {"name": "noto_sans", "file": "NotoSans-Regular.ttf", "sizes": [12, 14, 16]},
]

def convert_font(config):
    for size in config["sizes"]:
        output_file = f"{config['name']}_{size}px.c"
        output_path = os.path.join(OUTPUT_DIR, output_file)
        
        cmd = [
            LV_FONT_CONV,
            "--font", os.path.join(FONT_DIR, config["file"]),
            "--size", str(size),
            "--bpp", "4",
            "--range", "0x20-0x7F",  # ASCII
            "--range", "0x4E00-0x9FFF",  # 中文
            "--format", "lvgl",
            "--output", output_path
        ]
        
        subprocess.run(cmd, check=True)
        print(f"Generated: {output_file}")

if __name__ == "__main__":
    os.makedirs(OUTPUT_DIR, exist_ok=True)
    for config in FONT_CONFIGS:
        convert_font(config)

这个脚本跑一次,所有字体文件就都准备好了。你只需要在代码里#include对应的.c文件就行。

进阶玩法:你可以在脚本里加入字符集合并功能。比如把英文、中文、日文合并到一个字体文件里,减少运行时切换字体的开销。我上一个项目就是这么干的,Flash占用少了30%。

4.4 三种方案对比

方案 适用场景 优点 缺点
在线转换器 原型验证、小字体 无需安装,上手快 文件大小受限,不能批量
离线转换器 中等规模项目 命令行可控,可集成构建 需要Python环境
FreeType集成 动态加载、多语言 运行时灵活,渲染质量高 内存占用大,集成复杂
Python脚本批量 大规模生产 自动化,可定制 需要维护脚本

4.5 知识体系总览

下面这张图把本章的核心脉络画出来了。你可以看到从字体源文件到LVGL可用的字体数据,中间经过了哪些环节。

字体转换工具链知识体系 字体源文件 (.ttf/.woff) 在线转换器 原型验证 小字体文件 离线转换器 命令行集成 中等规模项目 FreeType集成 动态加载 多语言回退 LVGL字体数据 (.c文件) Python脚本可自动化以上所有流程

嗯,这张图你看懂了吗?从源文件出发,三条路径最终都汇聚到LVGL的.c字体数据。而Python脚本就像一条「传送带」,可以把任意一条路径自动化起来。

我个人建议:如果你的项目还在早期,先用在线转换器快速验证UI效果。等UI定稿了,再写Python脚本批量生成正式字体文件。别一上来就搞自动化,容易陷入工具链的坑里出不来。