嵌入式字库研究及Py_FontMaker工具编写
作者:刘权 · 2022-11-11 · 公众号:Bluetooth-BLE · QQ群:177341833
第一章 字体基本概念
1.1、编码字符集/字符集
这个编码其实就像每个国家的语言,假设这里有块黄金,中国人会指着说“黄金”,美国人指着说“gold”,都是指这个黄金;同理,在计算机的世界里面,有一个模型是“A”(全角字母),在中国的GBK编码里它是“0xA3C1”,而半角的“A”无论在GBK还是美国的ASCII里都是“0x41”(GBK兼容ASCII,单字节区与ASCII一致),也就是无论是在哪个国家,字模“A”是真理,永恒不变,只是每个国家根据自己的定义给字模“A”在计算机取了个不同的编号而已。
实实在在的A可以通过规定的某个二进制进行代替,就像给A取了个计算机能认知的名字,而这些二进制的集合就是编码字符集,或者字符集。
就像语言,每个国家有每个国家的语言,字符编码也是一样,例如:GBK、GB2312、BIG5、ASCII和Shift_JIS码等等!这些可以根据这个编码在字库表(就是所有符号按照某个编码规则集中存放在一个存储位置)中找到一一对应的每个字模。那么有没有全球统一一个一一映射关系的编码字符集,这个字符集包含了全球所有的符号/文字呢?这个编码字符集就是UNICODE字符集(UNICODE只规定了符号的二进制代码,却没有规定这个二进制代码应该如何传输和存储)。既然有这个全球通用的UNICODE字符集,为什么还要有各个国家自己的字符集呢?那就是存储的问题了,有些符号在某个国家永远都用不上,那么就不必要为这个符号浪费存储空间了。
文本的字节流按UTF-8规则解码得到UNICODE码点,再用码点到字库表中查找字模;如果文件实际是GBK编码却按UTF-8去解码(或反过来),得到的码点就是错的,显示出来就是乱码。

GBK就会乱码:

因为LVGL使用的都是UTF-8的编码方式,所以在使用LVGL时,需要将文件的编码方式调整为UTF-8格式,否则在LCD上显示的时候就会出现乱码。
下面使用LVGL打印
lv_label_set_text(label, "这是测试");
下面在文本文件中使用不同的字符集,左边使用的是UNICODE的UTF-8编码,右边使用的是GBK。

但是他们的二进制文件是什么样的呢?

就比较“这是测试”这个几个字的编码到底是怎么样的,可以到网站https://www.qqxiuzi.cn/zh/hanzi-gbk-bianma.php查看这几个字的GBK编码。如下编码应为“D5 E2 CA C7 B2 E2 CA D4”,与上面的Notepad++大端模式显示的二进制一样,但是实际上这个编译之后下载后并不能正常显示,因为在LVGL代码中是使用UTF-8进行解码的,所以有必要创建文件时将文件编码格式设置为UTF-8。




1.2、字库表
实实在在的符号字模的数据库,例如字母A的点阵存储,这个点阵就是这个字库表的某个元素。字库表存储了编码字符集中能显示的所有字符,计算机就是根据二进制数从字库表中找到字符然后显示给用户的,相当于一个存储字符的数据库。无论是什么样的编码字符集,实际上字库表是不用变的,所有在生成点阵字库时,无论使用UNICODE、ASCII、GB2312还是GBK制作的点阵,字母A的点阵始终不会改变,始终是A,只是应该通过什么样的方式得到存储A的位置。
编码字符集和实际存储数值(字库)之间的转换关系,就是把字符集中的字符编码为指定(字库)集合中的某一对象,以便在计算机中存储和通过网络传递。
1.3、字符集编码(编码方式)
这里编码方式是对UNICODE而言的,因为UNICODE只规定了符号的二进制代码,却没有规定这个二进制代码应该如何传输和存储,所以一般使用UTF-8可变长字符编码方式对UNICODE进行编码后进行传输。
有几个注意点:
“文章采用的是UTF-8编码方式”,准确的说法是“文章采用的是基于UNICODE编码字符集的UTF-8的编码方案”。
“UNICODE编码每个字符占几个字节”,答案应该是:第一.UNICODE不是编码!第二.每个字符具体占多少字节是要看编码方案!
也就是UTF-8是UNICODE的传输和存储的一种实现方式。
1.4、UTF-8 的编码规则
对于单字节的符号,字节的第一位设为0,后面7位为这个符号的 Unicode 码。因此对于英语字母,UTF-8 编码和 ASCII 码是相同的。
对于n字节的符号(n > 1),第一个字节的前n位都设为1,第n + 1位设为0,后面字节的前两位一律设为10。剩下的没有提及的二进制位,全部为这个符号的 Unicode 码。
Unicode符号范围 | UTF-8编码方式
(十六进制) | (二进制)
--------------------------------+---------------------------------------------
0000 0000-0000 007F | 0xxxxxxx
0000 0080-0000 07FF | 110xxxxx 10xxxxxx
0000 0800-0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx
0001 0000-0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
LVGL源码中的unicode转utf8
/**
* Convert an Unicode letter to UTF-8.
* @param letter_uni an Unicode letter
* @return UTF-8 coded character in Little Endian to be compatible with C chars (e.g. 'Á', 'Ű')
*/
static uint32_t lv_txt_unicode_to_utf8(uint32_t letter_uni)
{
if(letter_uni < 128) return letter_uni;
uint8_t bytes[4];
if(letter_uni < 0x0800) {
bytes[0] = ((letter_uni >> 6) & 0x1F) | 0xC0;
bytes[1] = ((letter_uni >> 0) & 0x3F) | 0x80;
bytes[2] = 0;
bytes[3] = 0;
}
else if(letter_uni < 0x010000) {
bytes[0] = ((letter_uni >> 12) & 0x0F) | 0xE0;
bytes[1] = ((letter_uni >> 6) & 0x3F) | 0x80;
bytes[2] = ((letter_uni >> 0) & 0x3F) | 0x80;
bytes[3] = 0;
}
else if(letter_uni < 0x110000) {
bytes[0] = ((letter_uni >> 18) & 0x07) | 0xF0;
bytes[1] = ((letter_uni >> 12) & 0x3F) | 0x80;
bytes[2] = ((letter_uni >> 6) & 0x3F) | 0x80;
bytes[3] = ((letter_uni >> 0) & 0x3F) | 0x80;
}
uint32_t * res_p = (uint32_t *)bytes;
return *res_p;
}
LVGL源码中的utf8转unicode
/**
* Decode an UTF-8 character from a string.
* @param txt pointer to '\0' terminated string
* @param i start byte index in 'txt' where to start.
* After call it will point to the next UTF-8 char in 'txt'.
* NULL to use txt[0] as index
* @return the decoded Unicode character or 0 on invalid UTF-8 code
*/
static uint32_t lv_txt_utf8_next(const char * txt, uint32_t * i)
{
/**
* Unicode to UTF-8
* 00000000 00000000 00000000 0xxxxxxx -> 0xxxxxxx
* 00000000 00000000 00000yyy yyxxxxxx -> 110yyyyy 10xxxxxx
* 00000000 00000000 zzzzyyyy yyxxxxxx -> 1110zzzz 10yyyyyy 10xxxxxx
* 00000000 000wwwzz zzzzyyyy yyxxxxxx -> 11110www 10zzzzzz 10yyyyyy 10xxxxxx
*/
uint32_t result = 0;
/*Dummy 'i' pointer is required*/
uint32_t i_tmp = 0;
if(i == NULL) i = &i_tmp;
/*Normal ASCII*/
if(LV_IS_ASCII(txt[*i])) {
result = txt[*i];
(*i)++;
}
/*Real UTF-8 decode*/
else {
/*2 bytes UTF-8 code*/
if(LV_IS_2BYTES_UTF8_CODE(txt[*i])) {
result = (uint32_t)(txt[*i] & 0x1F) << 6;
(*i)++;
if(LV_IS_INVALID_UTF8_CODE(txt[*i])) return 0;
result += (txt[*i] & 0x3F);
(*i)++;
}
/*3 bytes UTF-8 code*/
else if(LV_IS_3BYTES_UTF8_CODE(txt[*i])) {
result = (uint32_t)(txt[*i] & 0x0F) << 12;
(*i)++;
if(LV_IS_INVALID_UTF8_CODE(txt[*i])) return 0;
result += (uint32_t)(txt[*i] & 0x3F) << 6;
(*i)++;
if(LV_IS_INVALID_UTF8_CODE(txt[*i])) return 0;
result += (txt[*i] & 0x3F);
(*i)++;
}
/*4 bytes UTF-8 code*/
else if(LV_IS_4BYTES_UTF8_CODE(txt[*i])) {
result = (uint32_t)(txt[*i] & 0x07) << 18;
(*i)++;
if(LV_IS_INVALID_UTF8_CODE(txt[*i])) return 0;
result += (uint32_t)(txt[*i] & 0x3F) << 12;
(*i)++;
if(LV_IS_INVALID_UTF8_CODE(txt[*i])) return 0;
result += (uint32_t)(txt[*i] & 0x3F) << 6;
(*i)++;
if(LV_IS_INVALID_UTF8_CODE(txt[*i])) return 0;
result += txt[*i] & 0x3F;
(*i)++;
}
else {
(*i)++; /*Not UTF-8 char. Go the next.*/
}
}
return result;
}
第二章 什么是字体
2.1、说明一下
啥是字体呢,其实字体就是电脑上使用的字库,只是它是矢量的,也就是它里面存放的就是每个符号的“样子”,就是描述计算机中可以显示或者可以使用的字体的文件,一般后缀为.otf和.ttf。
我们之所以要用它,是因为要将字符的“样子”显示后,通过读取“样子”的像素点来获取“样子”的点阵,即RGB来确定这个点是该显示黑还是显示白,当然这里还要涉及到RGB和灰度的转化问题,后文在制作工具时会讲,一般为了圆滑,字库制作时都会使用2bits或者4bits的灰度字体。
制作字库的步骤:
确定字体(华为、OPPO和阿里等都有商业免费的字体),字号(西文一般用磅pt,方块字一般用号数,指字的实际大小,没有上下留白,嵌入式一般直接用像素px)、字重(粗细程度,如Regular/Medium/Bold,见第十四章)和样式(正体、斜体等)
将字体中的“样子”显示在电脑显示上
读取每个像素RGB的值
根据灰度位数将RGB值计算成Alpha值
组成字节后存储到bin
2.2、Freetype
FreeType 是一个免费提供的字体渲染软件库,是一种软件字体引擎,旨在实现小型、高效、高度可定制和便携,同时能够生成高质量的输出(字形图像)。它可以用于图形库、显示服务器、字体转换工具、文本图像生成工具以及许多其他产品。这里不具体介绍这个,可以到官网(https://freetype.org/freetype2/docs/)上看,我主要是参考字形指标(Glyph Metrics)的一些概念,在制作字模时需要用到,主要是了解下图中的参数:

2.3、免费商用字体下载地址
OPPO Sans字体官网:https://www.coloros.com/index/newsDetail?id=72,点击下载地址


点击下载 HarmonyOS
Sans

Adobe Fonts商用思源字体放在GitHub:https://github.com/adobe-fonts

进入source-han-sans:

点击Latest release 找到需要下载的TTF或者OTF即可


第三章 FontCvtST.exe使用
先了解这个工具的目的是为后面写python工具时,借鉴bin文件的存放格式,为了兼容这个工具,python写的工具生成的xbf文件完全和这个工具生成的xbf格式一致,只是本文写的工具会把bpp也放在xbf中,而FontCvtST工具在xbf中并没有体现bpp信息。
在嵌入式中,要显示一个字符,需要经过:
将字符转为代码中使用的编码,如UTF-8转UNICODE;
通过编码找到cmap(character maps)(二分法\hash\直接计算),找到bitmap的偏移地址和大小;
通过偏移找到存储在字库表中字符属性描述
同时也获取到字符的bitmap;
如果有压缩还需将bitmap进行解压;
将灰度转为RGB;
将转换后的RGB点阵送给显示帧buf,送显;
这个工具是用来生成字库的,上面的步骤B中的cmap表的好坏决定了代码的查找复杂度,查找方法有多种,例如:连续cmap就直接计算偏移地址,非连续的可以使用二分法查找和hash表查找。
如果存储足够大,那么可以直接将UNICODE的全字库生成后放到存储的位置,完全不用查找,直接使用编码进行地址偏移就可以得到字模,但是嵌入式一般存储是不够的,所以需要想办法减少空间,同时对于穿戴产品,也不希望字库太大带来升级的速度问题。
3.1、FontCvtST.exe生成xbf文件方法
这个工具如下面的附件,直接右键后保存到文件就可以了,同时后文中的Py_FontMaker.exe工具中也包含了这个FontCvtST工具,会生成在font_xbf文件夹中:
(原docx此处内嵌FontCvtST.exe附件;工具已随Py_FontMaker一同分发,生成在font_xbf文件夹中,无需单独下载。)
这里xbf其实就是bin文件,使用方法如下。





3.2、FontCvtST.exe命令行使用说明

Valid command line options:
-exit Finish application after job
-create <FONTNAME>,<STYLE>,<HEIGHT>,<TYPE>,<ENCODING>[,<METHOD>] Create font
- FONTNAME:
Name of font to be used
- STYLE:
THIN - Creates a thin font
THIN_ITALIC - Creates a thin italic font
EXTRALIGHT - Creates an extra light font
EXTRALIGHT_ITALIC - Creates an extra light italic font
LIGHT - Creates a light font
LIGHT_ITALIC - Creates a light italic font
REGULAR - Creates a regular font
REGULAR_ITALIC - Creates a regular italic font
MEDIUM - Creates a medium font
MEDIUM_ITALIC - Creates a medium italic font
SEMIBOLD - Creates a semi bold font
SEMIBOLD_ITALIC - Creates a semi bold italic font
BOLD - Creates a bold font
BOLD_ITALIC - Creates a bold italic font
EXTRABOLD - Creates an extra bold font
EXTRABOLD_ITALIC - Creates an extra bold italic font
HEAVY - Creates a heavy font
HEAVY_ITALIC - Creates a heavy italic font
- HEIGHT:
Height in pixels of font to be created
- TYPE:
STD - Standard 1bpp font
AA2 - Antialiased font (2bpp)
AA4 - Antialiased font (4bpp)
EXT - Extended font
EXT_FRM - Extended framed font
EXT_AA2 - Extended antialiased font (2bpp)
EXT_AA4 - Extended antialiased font (4bpp)
- ENCODING:
UC16 - 16 bit Unicode encoding
ISO8859 - 8 bit ASCII + ISO8859
JIS - Shift JIS
- METHOD:
OS - Antialiasing of operating system, default is 'OS'
INTERNAL - Internal antialiasing method
-edit <ACTION>,<DETAIL>[,<CNT>] Equivalent to the 'Edit' menu
- ACTION:
DEL - Deletes pixels
INS - Inserts pixels
- DETAIL:
TOP - ...from top
BOTTOM - ...from bottom
- CNT:
Number of operations, needs to be >0. Default is 1
-enable <FIRST-LAST>,<STATE> Enables or disables the given range of characters
- FIRST-LAST:
Hexadecimal values separated by a '-' defining the range of characters
- STATE:
0 - Disables the given range
1 - Enables the given range
-merge <FILENAME> Merges the given file to the current content
-readpattern <FILENAME> Reads a pattern file
-saveas <FILENAME>,<TYPE> Saves the font data as 'C', 'SIF' or 'XBF' file font
- FILENAME:
File name including extension
- TYPE:
C - "C" file
SIF - System independent font file
XBF - External binary font file
-nomessages Suppress messages in command line mode
-? Shows this box
例如:
我写的工具,最终通过GUI方式和命令行方式生成XBF,当然还有更多的功能,详细后面会讲。
3.3、FontCvtST.exe生成的xbf文件的数据格式
Xbf文件的组成格式如下:
| 段名称 | 结构体 | 字节数 |
| Xbf头 | xbf_font_head_t | 18字节 |
| Cmap | xbf_cmap_table_t | 每个字符的cmap占6字节,且cmap是连续的,即从FirstCode到LastCode,中间空缺某个字就填充6字节的0x00 |
| Bitmap | 字属性xbf_bitmap_attribute_t | 12字节 |
| 字模 | x字节 |
这里通过Cmap中间层过度到bitmap,这个工具生成的camp是连续的,所以也可以直接通过计算固定偏移来间接得到bitmap的地址,其实在ttf文件中也是有cmap(character code mapping)字符到字形的映射。
我在代码中结构体(python中是使用的字典,key是下面结构体的元素)如:
typedef struct
{
char m_magic[4];
uint16_t m_YSize;
uint16_t m_YDist;
uint16_t m_BaseLine;
uint16_t m_lHight; //这个没有用到,所以Py_FontMaker.exe工具中写了bpp的值和以像素为单位的字大小
uint16_t m_cHight;
uint16_t m_FirstCode;
uint16_t m_LastCode;
}xbf_font_head_t;
typedef struct
{
uint32_t offset;
uint16_t size;
}xbf_cmap_table_t;
typedef struct
{
uint16_t m_XDist;
uint16_t m_XSize;
uint16_t m_YSize;
int16_t m_XOffset;
int16_t m_YOffset;
uint16_t m_BytesPerLine;
} xbf_bitmap_attribute_t;
3.4、含3个字母ABC 的xbf文件

3.5、含非连续9个字母ABC,EFG,QRE的xbf文件

可以看出Cmap是连续的,bitmap是非连续的。
3.6、Xbf头

.m_magic = "GUXI",
.m_YSize = 28, //字模的最大高度
.m_YDist = 28, //字体高度和垂直上下间隙和,这里就是m_YSize
.m_BaseLine = 22, //基线是从左上角向下数的,这个要注意
.m_lHight = 12, //小写字母的实际高,这个参数实际没有用
.m_cHight = 15, //大写字母的实际高,这个参数实际没有用
.m_FirstCode = 0x0041,
.m_uLastCode = 0x0043,
3.7、Cmap

字母A:在xbf中的偏移:0x00000024,长度是0x48字节;
字母B:在xbf中的偏移:0x0000006C,长度是0x39字节;
字母C:在xbf中的偏移:0x000000a5,长度是0x39字节;
在非连续字库中,cmap在最小和最大编码之间是连续的,没有的字符使用6字节的0进行代替,这样还是占用了空间,所以有一种牺牲时间换取空间的方法,就是自己再写一个工具将这个xbf文件中的cmap的信息还附带上UNICODE编码,例如:
typedef struct
{
uint16_t codePoint;
uint32_t offset;
uint16_t size;
}xbf_cmap_table_t;
只是每次找字符时需要在cmap中搜索,当然这个cmap需要放在xip(eXecute In Place)的存储器中编译使用查找算法,本文中的工具,会使用二分法查找cmap,同时也支持hash查找。
3.8、Bitmap

.m_XDist = 0x0d; // 13占用显示宽度,控制下一个字描绘的位置
.m_XSize = 0x0e; // 14 实际字符数据所占宽度
.m_YSize = 0x0f; // 15 实际字符数据所占高度, <= 字体高度
.m_XOffset = 0; // 0 字符偏移x,从右上角为原点计算
.m_YOffset = 0x07; // 7 字符偏移y,从右上角为原点计算
.m_BytesPerLine = 0x04; // 4bytes 实际字符数据所占宽度,这里4*8共32bits,但是有2bits灰度,所以共占用的bits是4*8/2 = 16bits


第四章 LVGL如何显示字
4.1、LVGL字模的属性(字符描述)
LVGL每个字的属性如下:
/** Describes the properties of a glyph.*/
typedef struct {
uint16_t adv_w; /**< The glyph needs this space. Draw the next glyph after this width.*/
uint16_t box_w; /**< Width of the glyph's bounding box*/
uint16_t box_h; /**< Height of the glyph's bounding box*/
int16_t ofs_x; /**< x offset of the bounding box*/
int16_t ofs_y; /**< y offset of the bounding box*/
uint8_t bpp; /**< Bit-per-pixel: 1, 2, 4, 8*/
} lv_font_glyph_dsc_t;

adv_w:图像占用的空间,包含了左右留空的像素,这个保证了;每个字之间不会叠加在一起;
box_w:这个才是实际的图像宽度,实际占用的像素点(本身不必是8的整数倍);但存储时每一行要按字节对齐补零,所以按行字节数反推出的像素数可能比字形实际像素宽glyph_width略大,这里不得不引出另一个属性:
BytesPerLine = (glyph_width * bpp +7)/8,这个参数就是每一行所占用的字节数,通过这个参数就可以计算出box_w:
box_w = BytesPerLine * 8 /bpp = (glyph_width * bpp +7)/bpp;
box_h:这是字形的实际高度;
ofs_x:从左上角到字形实际开始的x轴起点;
ofs_y:这个在LVGL中实际上是图中baseline基线的向下到有效字形的y轴最底端,但是最后在代码中还是计算成了图中的ofs_y’,即左上角向下偏移的像素点,和FontCvtST.exe工具一致,只是名称是Y-Position;
bpp:每像素位数(bit per pixel),如1/2/4/8,决定灰度级数,显示时按灰度混色成RGB;
字体还有一些参数,例如基线baseline(LVGL中的baseline是相对左下角计算的,如上图中的baseline,而实际上一般是从左上角开始计算,也就是两种计算方式之和应该是Line_size - 1),字占空间高line_size。在字模提取时,实际只提取box_w*box_h的面积内的像素点,然后通过上面测参数计算出显示的坐标进行显示。
python中如何计算这些参数呢?。

在python中,上图中的(xMin,xMax,yMin,yMax,ascent),这个5个参数可以通过接口获取, ascent的值就是baseline的值,这样就可以计算出LVGL中使用的参数了。例如:
lvgl_baseline = line_size - baseline + 1
lvgl_off_y = line_size - off_y + 1 - ySize – lvgl_baseline
= baseline – off_y – y_Size
= baseline – (baseline - yMax) – (yMax - yMin)
=yMin
在python中提取像素时,A点和B点坐标就是遍历的条件控制参数,所以需要利用上面的参数计算出A和B的坐标:
A坐标为(xMin, baseline - yMax)
B坐标为(xMax, baseline + yMin)
本文代码和FontCvtST工具生成的字属性的名称对应关系如下表:
| Mycode | FontCvtST | LVGL |
| line_height | xbf文件头的第5和6个字节组成的16bits | line_height |
| m_BaseLine | xbf文件头的第9和10个字节组成的16bits | base_line = line_height - m_BaseLine + 1 |
| m_Bpp | xbf文件中是没有体现灰度的 | bpp |
| m_XDist | Cursor dist | adv_w |
| m_XSize | Width | box_w = m_BytesPerLine * 8 / m_Bpp |
| m_YSize | Height | box_h |
| m_XOffset | X-Position | ofs_x |
| m_YOffset | Y-Position | LVGL使用的是yMin |
| m_BytesPerLine | xbf文件bitmap中STRU_XBF_SPARSE_BITMAP_INFO结构体中的第6个字段,实际(glyph_width * bpp +7)/8计算得到 |



4.2、LVGL使用属性绘字
lvgl\src\draw\sw\lv_draw_sw_letter.c中有函数lv_draw_sw_letter:
int32_t pos_x = pos_p->x + g.ofs_x;
int32_t pos_y = pos_p->y + (dsc->font->line_height - dsc->font->base_line) - g.box_h - g.ofs_y;
在LVGL中g.ofs_y的值其实就是下图中的yMin的值,这里的y偏移的位置实际上就计算成了上图中的Y-Position。上面的计算就得到了A坐标的值,也就是LCD上显示该字形的起始坐标点。

lv_draw_sw_letter这个函数中会调用lv_font_get_glyph_dsc(在lvgl\src\font\lv_font.c中),原型如下:
bool lv_font_get_glyph_dsc(const lv_font_t * font_p, lv_font_glyph_dsc_t * dsc_out, uint32_t letter, uint32_t letter_next)
实际上是调用第一个参数font_p中的函数指针get_glyph_dsc来获取字符描述,然后进行上面的计算。接着就是获取bitmap的函数lv_font_get_glyph_bitmap,这个里面其实也是font_p->get_glyph_bitmap中获取真正的字符的位图信息,而这个font_p的结构体是通过lv_obj_set_style_text_font接口传递进来的一个结构体指针lv_font_t,这个结构体如下:
/** Describe the properties of a font*/
typedef struct _lv_font_t {
/** Get a glyph's descriptor from a font*/
bool (*get_glyph_dsc)(const struct _lv_font_t *, lv_font_glyph_dsc_t *, uint32_t letter, uint32_t letter_next);
/** Get a glyph's bitmap from a font*/
const uint8_t * (*get_glyph_bitmap)(const struct _lv_font_t *, uint32_t);
/*Pointer to the font in a font pack (must have the same line height)*/
lv_coord_t line_height; /**< The real line height where any text fits*/
lv_coord_t base_line; /**< Base line measured from the top of the line_height*/
uint8_t subpx : 2; /**< An element of `lv_font_subpx_t`*/
int8_t underline_position; /**< Distance between the top of the underline and base line (< 0 means below the base line)*/
int8_t underline_thickness; /**< Thickness of the underline*/
const void * dsc; /**< Store implementation specific or run_time data or caching here*/
#if LV_USE_USER_DATA
void * user_data; /**< Custom user data for font.*/
#endif
} lv_font_t;
上面的元素中,
line_height和上节中的line_size是同一个意思;
base_line和上文中的lvgl_baseline是同一个意思;
get_glyph_dsc函数:获取上文中字形的属性或者字形的描述接口;
get_glyph_bitmap函数:获取字形的灰度点阵,这里并没有传入长度,返回的为灰度点阵的指针,实际上长度是通过获取的属性描述进行计算的,例如总字节数= BytesPerLine * box_h,就得到了点阵的总长度。
user_data:这个其实就是一个指针,源码中有这样的代码:typedef void * lv_font_user_data_t; 说明user_data就是一个指针类型的数据,那里面就可以放很多东西了,函数、结构体和数据等等,那么LVGL怎么用呢?看函数get_glyph_dsc和get_glyph_bitmap接口的入口参数都有入参struct _lv_font_struct *,也就是说这个user_data指针同样也传入到了这两个函数中。get_glyph_dsc和get_glyph_bitmap这两个函数的作用就是获取glyph的描述和bitmap,每个字库如果查找cmap各有各的方法,怎么统一呢?那就可以通过user_data传一个结构体指针进去,这个结构中有包含例如查找cmap方法,访问外部flash的功能函数以及字库的具体信息,例如bpp,字库是否连续,cmap在外部flash的偏移地址,字库的起始和结束unicode码等等,都可以通过user_data指针传入到get_glyph_dsc和get_glyph_bitmap这两个函数中,最终从flash中获取字形的属性和bitmap给到LVGL中应用。
4.3、非LVGL使用字库
通过上面的了解,实际上这个问题很好理解了,同样也是上面的那两个结构体,也就说LVGL中的这个方案同样也适用在非LVGL中的使用,只是在生成字库时,字的属性描述可以按照正常的以左上角为基点进行计算的参数,本文中的工具也支持非LVGL的字库生成。
第五章 字模点阵/RGB/Gray关系
这个单独起章节,因为觉得重要,搞懂这些为后面制作字库做准备。思考几个问题:
字库根据字体文件生成原理?
字库存储的是点阵模型,就是这个点是亮还是灭,但是LCD的每个点是RGB,如何转?
2bits和4bits灰度是什么意思?如何在生成字库时将字体文件中字形的RGB转为灰度?
灰度字库中的点如何转为RGB
5.1、字库根据字体文件生成原理
原理步骤如下:
通过工具对字库文件进行解析后显示到显示器或者某个表面;
通过读取这个字的相关属性可以得到第四章中要用到的属性;
通过读取显示界面上字的每一个像素点的RGB,然后将RGB转成灰度后进行保存,就可以得到字库了。
后文会用python进行字库制作,并进行验证。
5.2、RGB如何转为灰度值
这里要搞明白什么是灰度,灰度可以认为就是亮度,也就是色彩的深浅程度。所谓灰度色,就是指纯白、纯黑以及两者中的一系列从黑到白的过渡色。在RGB模式中三原色光各有256个级别,灰度是在像素的RGB数值相等的情况下形成的。而RGB数值相等的排列组合是256个,灰度的数量就是256级。其中除了纯白和纯黑以外,还有254种中间过渡色。除了256级的表示方法外,灰度也可通过百分比表示,范围从0%到100%。注意这个百分比是以纯黑为基准的百分比。与RGB正好相反,百分比越高颜色越偏黑,百分比越低颜色越偏白。
什么是透明度,透明度也叫alpha通道,而透明度简单理解就是背景色和前景色进行混色时,谁显示多,谁显示少的问题,例如:

上面的A是用黑色显示在白色的背景板上,本应该全部显示纯黑色的标号4,但是为了平滑,不那么有棱角,所以就出现了1-3的标号的混色,这个混色就是黑色和白色的混色,而谁占比更多些,这个时候所谓的透明度在起作用,alpha的值为0表示不透明,也就是全是前景色,就是黑色。那么RGB如何转为灰度呢?我制作的工具中的转换公式如下:
gray = (299*R + 587*G + 114*B + 500)/1000
这里生成的gray的值就是灰度了,这个是RGB888,24位深的8bits的灰度值,如果2bit灰度,那就取gray的最高2bits,如果是4bits的灰度值就取gray值的最高4bits。
5.3、灰度如何转为RGB
这个转就更加容易了,把灰度值转为显示百分比,假设2bits的灰度,可以将100分为3份,当gray = 0时,alpha = 0,当gray = 1时,alpha = 33,当gray =2时,alpha = 66,当gray = 3时,alpha = 100,直接将前景色和背景色的RGB值单独取出后乘以各自的占比然后再组合成RGB值即可。一般手表都使用RGB565,所有取出背景色和前景色的RGB就是移位:
uint8_t bgr = (bgcolor >> 11) & 0x1F;
uint8_t bgg = (bgcolor >> 5) & 0x3F;
uint8_t bgb = bgcolor & 0x1F;
uint8_t fgr = (fgcolor >> 11)& 0x1F;
uint8_t fgg = (fgcolor >> 5)& 0x3F;
uint8_t fgb = fgcolor & 0x1F;
......
uint8_t newfgr = ((bgr * alpha + (fgr * (100 - alpha))) / 100) & 0x1F; uint8_t newfgg = ((bgg * alpha + (fgg * (100 - alpha))) / 100) & 0x3F;
uint8_t newfgb = ((bgb * alpha + (fgb * (100 - alpha))) / 100) & 0x1F;
......
uint16_t RGB = newfgr << 11 | newfgg << 5 | newfgb;
第六章 python全字库XBF生成工具
6.1、全字库组成格式
为了兼容FontCvtST.exe工具生成的字库一样的,这里python生成的字库也同样采用FontCvtST.exe格式,即:
| 段名称 | 结构体 | 字节数 |
| Xbf头 | xbf_font_head_t | 18字节 |
| Cmap | xbf_cmap_table_t | 每个字6字节,且cmap是连续的,即从FirstCode到LastCode,中间空缺某个字就填充6字节的0x00 |
| Bitmap | 字属性xbf_bitmap_attribute_t | 12字节 |
| 字模 | x字节,这个字模是字的有效大小 |

注:glyph字模的点阵像素是如上图中的有效字体的像素点阵。只是从字库中读取点阵后显示时,需要用到字的属性去还原字的文字进行显示。
6.2、Python编译环境
我是用的window+SVC+python,这里不详细写环境安装,使用的是Python 3.9.7版本,为了不依赖系统环境的python,这里将python
直接放在了源码目录下:

因为使用的VSC,所以可以通过settings.json来控制当前终端的环境变量:
{
"terminal.integrated.profiles.windows": {
"PowerShell": {
"source": "PowerShell",
//"path": "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe",
"args": []
}
},
"terminal.integrated.defaultProfile.windows": "PowerShell",
"terminal.integrated.env.windows":{
"PATH":".\\Python39;.\\Python39\\Scripts"
},
"debug.allowBreakpointsEverywhere": true,
"workbench.colorCustomizations": {
"terminal.background": "#181818",
"terminal.foreground": "#4a6d50",
"terminalCursor.background": "#D8D8D8",
"terminalCursor.foreground": "#D8D8D8",
"terminal.ansiBlack": "#181818",
"terminal.ansiBlue": "#7CAFC2",
"terminal.ansiBrightBlack": "#585858",
"terminal.ansiBrightBlue": "#7CAFC2",
"terminal.ansiBrightCyan": "#86C1B9",
"terminal.ansiBrightGreen": "#A1B56C",
"terminal.ansiBrightMagenta": "#BA8BAF",
"terminal.ansiBrightRed": "#AB4642",
"terminal.ansiBrightWhite": "#F8F8F8",
"terminal.ansiBrightYellow": "#F7CA88",
"terminal.ansiCyan": "#86C1B9",
"terminal.ansiGreen": "#A1B56C",
"terminal.ansiMagenta": "#BA8BAF",
"terminal.ansiRed": "#AB4642",
"terminal.ansiWhite": "#D8D8D8",
"terminal.ansiYellow": "#F7CA88",
"terminal.integrated.cursorBlinking": true,
"terminal.integrated.lineHeight": 1.6,
"terminal.integrated.letterSpacing": 0.1,
"terminal.integrated.fontSize": 30, //字体大小设置
"terminal.integrated.fontFamily": "Lucida Console", //字体设置
},
"cygwin32.root": "",
"cygwin64.root": "",
"mingw32.provider": "",
"mingw64.provider": "",
"msys2.root": "",
}
新建一个终端后如下命令查看环境变量:
echo $env:Path //查看当前终端环境变量
gcm python //查看python路径

6.3、pip使用异常
因为python是放在源码目录下,代码可能在不同的路径下,如果需要保证pip能用,就必须修改pip的引用路径。一般情况下python安装时pip是指定在某个目录引用python.exe的,例如“c:\users\john\appdata\local\programs\python\python39\python.exe”,这样就会出现如下错误:

这个时候需要修改pip.exe中的引用路径,我使用的比较暴力的方法,直接修改pip.exe中的引用路径,首先找到python目录下所有pip.exe相关的文件,如下:

使用notepad++打开这3个文件,查找python,然后直接替换路径:

这样再查看pip的命令:

pip命令路径:

6.4、pip安装库
这里使用到了多个python的库,需要使用pip进行安装,代码中导入了如下库:
import os, glob, shutil
import argparse
import pygame
import sys
自己调试一下,如果报错就安装哪个库。例如:
pip install pygame
pip install fontTools
主要使用的是pygame中的Font库。
6.5、Pygame的Font库生成字库步骤
使用pygame.font.Font创建一个字体对象;
定义一个显示表面,大小为字符的大小;
将字显示到表面;
计算字体的实际有效坐标:
font.get_ascent()得到ascent,它是基线到字体轮廓最高点的距离,其实就是左上角到基线的距离,如下图中的baseline;
font.get_height()获取整个字的高,不是实际字高,是总高度;
font.metrics(char)接口可以获取char的(xMin, xMax, yMin, yMax, advance);
可以得到以下参数:
Baseline = font.get_ascent()
Linesize = font.get_height()
A_x = xMin if xMin > 0 else 0
A_y = ascent - yMax if ascent >= yMax else 0
width = xMax - xMin
height = yMax - yMin
B_x = xMax
B_y = A_y + yMax - yMin
= ascent - yMax + yMax - yMin
= ascent - yMin if ascent >= yMax else yMax - yMin
所以x的范围 = (xMin或0 , xMin或0 + width)
y的范围 =((ascent - yMax)或 0, (ascent - yMax)或 0 + height)
Off_x = xMin
Off_y = ascent - yMax
BytesPerLine = (width *bpp + 7)/8
从A坐标一直到B坐标开始读取像素点;
for y in range(y的范围):
for x in range(A_x, BytesPerLine * 8 / bpp):
将读取到的点转为灰度;
gray = int((299*(color[0]) + 587*(color[1]) + 114*(color[2]) + 500)/1000)
将gray放到字节中存储;
注意:A坐标点开始读,从左向右开始读到的像素点得到的gray的值,组成字节时,第一个读到的点是放到字节的最高位,例如如果是2Bpp的灰度值,从左到右读取的4个点分别为Gray0,Gray1,Gray2和Gray3,那么组成的字节为:
byte = Gray0 << 6 |Gray1 << 4 |Gray2 << 2 |Gray3 << 0

还有注意事项:需判断所选择的字体文件中,那些字是有真正的有效的cmap和glyph,然后再在有效的glyph中生成全字库xbf文件。
6.6、使用
命令:
--min : unicode的最小值
--max : unicode的最大值
--ttf : ttf 文件的路径
--bpp : 灰度位数
--height :字体高度(像素为单位)
使用:
python .\ttf_conver_FontCvtST_fontlib_bin.py --min 0x5199 --max 0x5199 --ttf HarmonyOS_Sans_SC_Regular.ttf --bpp 2 --height 32

生成一个“写”字。bin文件如下:

解析如下:
m_pName = “John”
m_YSize = 0x1F
m_YDist = 0x1F
m_BaseLine = 0x19
m_bpp = 2 这里我放的灰度值
m_px = 0x20 这里放的是字高度
m_FirstCode = 0x5199
m_LastCode = 0x5199
offset = 0x18
size = 0x8A
m_XDist = 0x1A
m_XSize = 0x17
m_YSize = 0x15
m_i16XOffset = 0x01
m_i16YOffset = 0x06
m_BytesPerLine = 0x06
Python制作的这个工具在生成全字库xbf时,时间比较久,一个全字库可能要20分钟左右,所以我是写了一个脚本,晚上一次性让电脑生成许多的xbf文件。
6.7、裸机验证
为了验证字库生成的是否正确,我又写了亮屏的程序,如果把字的有效部分解析出RGB然后送到屏幕上面显示,那么就可以查看字是否生成的OK。
从winhex中复制出bitmap的数据:
unsigned char data[138] = {
0x1A, 0x00, 0x17, 0x00, 0x15, 0x00, 0x01, 0x00, 0x06, 0x00, 0x06, 0x00, 0x0F, 0xFF, 0xFF, 0xFF,
0xFF, 0xF4, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xF4, 0x0F, 0x01, 0x00, 0x00, 0x00, 0xF4, 0x0F, 0x0F,
0x00, 0x00, 0x00, 0xF4, 0x0F, 0x0F, 0x00, 0x00, 0x00, 0xF4, 0x0A, 0x0F, 0xFF, 0xFF, 0xFC, 0x60,
0x00, 0x1F, 0xFF, 0xFF, 0xFD, 0x00, 0x00, 0x2E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2D, 0x00, 0x00,
0x00, 0x00, 0x00, 0x3D, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xFF, 0xFF, 0xFF, 0xC0, 0x00, 0x3F,
0xFF, 0xFF, 0xFF, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x03, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x03, 0xC0,
0x00, 0x00, 0x00, 0x00, 0x03, 0xC0, 0x3F, 0xFF, 0xFF, 0xFF, 0xC3, 0xC0, 0x3F, 0xFF, 0xFF, 0xFF,
0xC7, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x0B, 0x80, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x40, 0x00, 0x00,
0x00, 0xBF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x7F, 0xF8, 0x00
};
非LVGL解析显示的函数如下。
void oled_go(int x, int y)
{
uint32_t err_code;
nrf_gpio_cfg_output(LCD_BL_EN);
nrf_gpio_pin_set(LCD_BL_EN);
memset(lcd_buffer, 0xff , sizeof(lcd_buffer));
const uint8_t offsetmask[] = {6, 4, 2, 0};
bitmap_attribute.adv_w = (data[1]<< 8) | data[0];
bitmap_attribute.box_w = (data[3]<< 8) | data[2];
bitmap_attribute.box_h = (data[5]<< 8) | data[4];
bitmap_attribute.ofs_x = (data[7]<< 8) | data[6];
bitmap_attribute.ofs_y = (data[9]<< 8) | data[8];
bitmap_attribute.BytesPerLine = (data[11]<< 8) | data[10];
uint8_t * font = &data[12];
uint8_t byte, gray_color;
uint16_t bgcolor = 0, fgcolor = 0xffff;
uint8_t bgr = (bgcolor >> 11) & 0x1F;
uint8_t bgg = (bgcolor >> 5) & 0x3F;
uint8_t bgb = bgcolor & 0x1F;
uint8_t fgr = (fgcolor >> 11)& 0x1F;
uint8_t fgg = (fgcolor >> 5)& 0x3F;
uint8_t fgb = fgcolor & 0x1F;
uint8_t bppEvtBytes = 8/xbf_font_head.m_bpp;
uint16_t ypos = 0, xpos = 0;
uint8_t bits = 0;
bool loop = true;
while(loop)
{
byte = *font++;
for(bits = 0; bits < bppEvtBytes; bits++) //bit2
{
if (xpos < bitmap_attribute.box_w)
{
gray_color = (byte >> offsetmask[bits]) & 0x3;
uint8_t alpha = 0;
if(gray_color == 0)
{
alpha = 0;
}
else if(gray_color ==1)
{
alpha = 33;
}
else if(gray_color ==2)
{
alpha = 66;
}
else if(gray_color ==3)
{
alpha = 100;
}
uint8_t newfgr = ((bgr * alpha + (fgr * (100 - alpha))) / 100) & 0x1F;
uint8_t newfgg = ((bgg * alpha + (fgg * (100 - alpha))) / 100) & 0x3F;
uint8_t newfgb = ((bgb * alpha + (fgb * (100 - alpha))) / 100) & 0x1F;
lcd_buffer[x + xpos][y + ypos] = newfgr << 11 | newfgg << 5 | newfgb;
}
xpos++;
if(xpos == bitmap_attribute.BytesPerLine * 8 / 2)
{
xpos = 0;
ypos++;
if(ypos == bitmap_attribute.box_h )
{
loop = false;
}
}
}
}//while(loop)
lcd_ie3130b_put_gfx(0, 0, LCD_IE3130B_WIDTH, LCD_IE3130B_HEIGHT, (uint8_t *)lcd_buffer);
}
显示结果如下(板子是否熟悉啊):

6.8、验证工程代码和下载脚本
验证代码
验证代码工程代码放码云:https://gitee.com/woowill/ZJ_RT_Thread_LVGL_Font_Nordic.git

下载验证
使用的是ZJ曹孟德开发板验证,首先是将Font_lib.bin下载到板子上的norflash,这里采用jlink进行下载。首先了解一下Jlink的SPI下载功能的引脚定义:


接线如下:


使用图形界面下载如下:


下载脚本
为方便验证,配合我自己搞的JLink板子,可以非常方便的使用脚本进行下载,下载工具路径如下图:

下载脚本如下,输入1将下载支持LVGL的验证bin;输入2将下载裸机字库验证bin;输入3是擦除nrf52840,因为输入4之前需要先将nrf52840擦除,避免SPI引脚冲突。

输入4之前需要将jlink的接线改为上面提到的一样,输入之后就会下载Font_lib.bin到norflash中,如下图:

显示效果
LVGL显示效果

逻辑显示效果,代码就是上节中写的,icon和emoji是8bpp的,但是Gray转RGB时并没有做到256级灰度,所以就会显示如下:

6.9、将py文件变为exe文件
pip install pipenv
pipenv shell
在pipenv的环境下运行py看有没有依赖库需要安装,我这里需要安装
No module named 'pygame' ---> pip install pygame
No module named 'fontTools' ---> pip install fontTools
本工具一起安装了如下包:
pip install pygame
pip install fontTools
pip install pillow
pip install numpy
pip install cv2
pip install opencv-python
pip install remi
pip install xlwings
pip install pyinstaller
pyinstaller -F xxx.py 生成exe文件
-F生成的有命令框,-Fw生成的没有命令框

第七章 多语言和多字号策略
在手表应用中一定是会有多语言和多字号的字库需求,那么上面可以单独制作特定的字号字库,那么如果多语言,不连续的怎么办呢?一般情况下,手表分为多语言字库和一个全字库,多语言字库一般会有多个字号,全字库一般用于消息通知。接下来这里就是要根据上面章节生成的全字库xbf文件,来生成多语言字库,并最后将所有的字库都合成一个bin,并且能将使用同一个访问策略访问所有的字库。
7.1、Font_Lib.bin数据格式设计
设计思路和上面字库制作一样,如下:
| 名称 | 字节/偏移 | 说明 |
| Bin头信息 | 256字节 | |
| 字体信息集合 | 256-4096字节 | |
| 1号字体cmap信息 | 从0x1000地址开始 | |
| 1号字体bitmap信息 | ||
| 2号字体cmap信息 | ||
| 2号字体bitmap信息 | ||
| ...... | ||
| n号字体cmap信息 | ||
| n号字体bitmap信息 |
头部信息
共占有20字节,从Font_bin的0字节开始
typedef struct
{
uint8_t m_Magic[8]; //固定为John_LIB
uint32_t m_Version; //每次生成会有随机数作为版本
uint32_t m_FontNum; //文件共有多少个字体
uint32_t m_FontOffset;//第一个字体偏移地址0x1000
} font_lib_head_t;
字体信息集合
在font_lib文件的0x100地址开始,存放字体信息的集合,每个字体信息包含如下:
typedef struct
{
char m_FontName[44];
uint8_t m_Bpp;
uint8_t m_CmapContinued; // 1 continous 0 sparse
uint16_t m_YSize;
uint16_t m_YDist;
uint16_t m_BaseLine;
uint16_t m_lHight;
uint16_t m_cHight;
uint16_t m_FirstCode;
uint16_t m_LastCode;
uint32_t m_CmapOffset;
uint16_t m_CodePointsNum;
cmap_sparse_table_t* m_pCmaptable; // if m_CmapContinued = 0 this vilue will be set
}font_info_t;
这个结构体固定大小为64字节,可以存放这个结构体的空间是0x1000-0x100 = 0xF00 = 3840字节,也就是可以存放字体最多是3840/64 = 60种字体,当然代码都是自己设计,那么想怎么改就怎么改。
Cmap信息
Cmap信息其实就是从第六章种的xbf文件种直接拷贝过来了,只是由于多语言的文字一般是非连续的,所以需要添加code码到font_lib中,同时需要将映射表创建一个.c文件,放到c目的是编译到XIP中,方便遍历查找。所以这里分为两个结构体:
// For sparse glyph cmap table
typedef struct
{
uint16_t m_CodePoint;
uint32_t m_Offset;
uint16_t m_Size;
}cmap_sparse_table_t;
// For continous glyph cmap table
typedef struct
{
uint32_t m_uOffset;
uint16_t m_uSize;
}cmap_continous_table_t;
在生成过程中就会判断字库是不是连续,如果连续,就使用 cmap_continous_table_t结构体中的两个变量,其实就是更新xbf文件中的偏移地址后就可以直接存到font_lib.bin中。如果非连续的,那么就需要将对应的codepoint一同存到font_lib.bin中,同时还需要将这个存到一个.c文件中形成一个映射数组,例如:
static const cmap_sparse_table_t font_lib_CvtST_HarmonyOS_24_2BPP_cmap[] = {
{.m_CodePoint = 0x9, .m_Offset = 0x0, .m_Size = 0},
{.m_CodePoint = 0xa, .m_Offset = 0x0, .m_Size = 0},
{.m_CodePoint = 0x20, .m_Offset = 0x4038, .m_Size = 0},
{.m_CodePoint = 0x21, .m_Offset = 0x4038, .m_Size = 20},
{.m_CodePoint = 0x22, .m_Offset = 0x404c, .m_Size = 16},
{.m_CodePoint = 0x23, .m_Offset = 0x405c, .m_Size = 48},
{.m_CodePoint = 0x24, .m_Offset = 0x408c, .m_Size = 63},
{.m_CodePoint = 0x25, .m_Offset = 0x40cb, .m_Size = 62},
{.m_CodePoint = 0x26, .m_Offset = 0x4109, .m_Size = 66},
{.m_CodePoint = 0x27, .m_Offset = 0x414b, .m_Size = 11},
{.m_CodePoint = 0x28, .m_Offset = 0x4156, .m_Size = 42},
{.m_CodePoint = 0x29, .m_Offset = 0x4180, .m_Size = 42},
{.m_CodePoint = 0x2a, .m_Offset = 0x41aa, .m_Size = 20},
};
Bitmap信息
Bitmap信息可以直接从xbf中复制过来,但是因为每个字符占用的属性描述共12字节,嵌入式中实际上每个属性只需要1个字节就足够了,所以在生成时,会将属性缩减为共6个字节,修改后的结构:
typedef struct
{
uint8_t m_XDist;
uint8_t m_XSize;
uint8_t m_YSize;
int8_t m_XOffset;
int8_t m_YOffset;
uint8_t m_BytesPerLine;
} char_bitmap_attribute_t;
7.2、如何将多语言多字体联合生成bin
将需要的字号使用对应的字体文件,使用第6章的py生成全字库备用,例如HarmonyOS_Sans_SC_Medium_16_2Bpp.xbf和HarmonyOS_Sans_SC_Medium_16_4Bpp.xbf这个两个都是全字库;
将需要的多语言文字放到HarmonyOS_Sans_SC_Medium_16_4Bpp.txt中,名字和上面的xbf相同,在py中好对应,如果需要HarmonyOS_Sans_SC_Medium_16_2Bpp.xbf的全字库,那么创建一个HarmonyOS_Sans_SC_Medium_16_2Bpp.txt文件,这个文件是空的,什么都不写,py中判断空就知道是全字库了。同时注意,py中是用utf-8打开的文件,所以txt文件需要保存为utf-8的格式;
将所有的txt文件名称去掉后缀放到另一个all_font_type_to_generate.txt中,py可以读取这个文件来生成所有的字库;
上面的工作准备好了,那就开始写py代码,将txt和xbf转为Font_Lib.bin即可。
如下图:

font_out文件夹中生成的就可以直接拿到工程里面进行编译,接口提供了和第四章中提到的两个结构体一样,同时根据参数会生成支持LVGL的还是非LVGL的字库和访问通用代码,这个代码会在下章中进行说明。
Font_Lib.h:公共的结构体,第四章中提到的两个结构体,我在这里进行了重新命名,以满足LVGL和非LVGL使用;
Font_Lib.bin:最终生成的字库bin文件;
ext_font_engine:用于真正的获取属性和bitmap的实现,这个文件中还实现了二分法查找,hash查找cmap以及RLE(run-length-encoding)的解码bitmap等;
ext_font_app:用户传入flash访问接口,多字体初始化统一接口;
fake_font_24_2Bpp:这个是一个假字库,当没有字库,或者字库没有某个字时会使用这个里面的字,目前这个文件中的数字和字母以及“口”字,口”字用来填充所有的显示不了的字符;
cmap_HarmonyOS_Sans_SC_Medium_xx_xBpp.c:所有字号的字体信息和cmap,cmap一般是要放到XIP的,编译遍历,这里支持二分法和hash;
HarmonyOS_Sans_SC_Medium_16_2Bpp.c:这个工具生成的内部字体,bitmap也在这个文件。
7.3、多语言合并
步骤:
将txt中的多语言信息通过python中的字典去重,得到unicode的列表;
读取xbf文件,填补字体信息写到font_lib中,将cmap添加codepoint以及重新计算font_lib.bin中的bitmap偏移地址赋值给cmap的偏移值,存储到font_lib.bin中;
如果是非连续的需要生成一个.c文件,里面包含有一个映射表的数组,同时还要包含一个font_info_t结构体的变量,用来给用户描述这个字体的信息;如果是全字库就只需要包含font_info_t结构体。
读取bitmap文件,将字符属性压缩到6个字节,重新计算地址后将数据写到font_lib.bin中;
轮询完文件中所有的txt即可。
7.4、查看生成的font_lib.bin
字库头信息:

版本号:0xB300A5A8
字体类型个数:2
第一个字库的偏移地址:0x1000

const font_info_t font_lib_CvtST_HarmonyOS_24_2BPP_font_info = {
.m_pFontName = "font_lib_CvtST_HarmonyOS_24_2BPP",
.m_Bpp = 2,
.m_CmapContinued = 0,
.m_YSize = 23,
.m_YDist = 23,
.m_BaseLine = 18,
.m_lHight = 0,
.m_cHight = 0,
.m_FirstCode = 0x9,
.m_LastCode = 0xff5e,
.m_CmapOffset = 0x1000,
};
const font_info_t ffont_lib_CvtST_HarmonyOS_24_2BPP_font_info = {
.m_pFontName = "ffont_lib_CvtST_HarmonyOS_24_2BPP",
.m_Bpp = 2,
.m_CmapContinued = 1,
.m_YSize = 23,
.m_YDist = 23,
.m_BaseLine = 18,
.m_lHight = 0,
.m_cHight = 0,
.m_FirstCode = 0x0,
.m_LastCode = 0xffff,
.m_CmapOffset = 0x23f60,
};
非连续的Cmap是有codepoint的,当然这个已经写到.c中去了,直接通过.c中的表进行查找,找到bitmap

到地址0x23f60地址去看看连续的cmap,我查找一下“#”这个字符的地址,“#”的unicode的0x23,那么它的地址应该是0x23*6 + 0x23f60 = 0x24032:

Offset = 0x83f84,大小是0x30,那就继续读出来:

unsigned char data[48] = {
0x0D, 0x0B, 0x0E, 0x01, 0x05, 0x03, 0x00, 0xE0, 0x70, 0x00, 0xD0, 0xB0, 0x01, 0xC0, 0xE0, 0x02,
0xC0, 0xD0, 0x3F, 0xFF, 0xFC, 0x17, 0x96, 0xD4, 0x07, 0x03, 0x80, 0x0B, 0x03, 0x40, 0x0E, 0x07,
0x00, 0xFF, 0xFF, 0xF0, 0xAE, 0xAF, 0xA0, 0x2C, 0x0D, 0x00, 0x28, 0x1C, 0x00, 0x38, 0x1C, 0x00
};

第八章 访问font_lib.bin通用代码
在第四章中其实已经讲了,主要就是两个接口get_glyph_dsc函数和get_glyph_bitmap函数,本章结合Py_FontMaker工具生成的通用代码文件进行说明,包括LVGL和非LVGL的字属性描述差异,cmap的hash表方案,字形bitmap的RLE压缩和解压方案。Py_FontMaker会生成如下文件,在7.2节中已经讲了这些文件的作用,本章通过源码进行说明。

8.1、工程中使用字库
字库使用步骤:
在你自己的代码里实现外部flash的读函数(生成的.c/.h一行都不用改),例如:
static uint32_t my_flash_read(uint32_t offset, uint8_t *pbuf, uint32_t size)
{
/* 从 Font_Lib.bin 烧写起始地址 + offset 处读 size 字节到 pbuf,
返回实际读到的字节数(成功即 size, 失败返回 0) */
#define EXT_FONT_FLASH_START_ADDR 0x00
uint32_t err_code = bsp_qspi_flash_read(pbuf, size, EXT_FONT_FLASH_START_ADDR + offset);
return (err_code == 0) ? size : 0;
}
字形bitmap的静态buffer大小MAX_STATIC_BITMAP_BUF_SIZE,早期版本需按最大字号手工评估 (例如 #define MAX_STATIC_BITMAP_BUF_SIZE (28 * 28 * 4 / 8)),新版工具在生成时已按全部字形 逐个统计出真实最大值自动写入Font_Lib.h,无需手改;
包含头文件ext_font_app.h;在使用字体前,把读函数注册给引擎:调用 app_font_init(my_flash_read, printf);
通过调用ext_font_app.c中的public_font_t* app_get_font(FONT_TYPE font_type)函数,可以获取到需要使用的字体指针,入口参数是字库中的字体枚举; public_font_t的结构体其实就是LVGL的lv_font_t结构体,只是重新命名而已,如果是在LVGL中使用, 那就可以强转为lv_font_t指针,如果是非LVGL中,就可以直接进行元素获取使用,可以获取到字的描述和字的bitmap。
8.2、LVGL和非LVGL中font_t结构体
LVGL中直接包含lvgl.h头文件,然后重命名lv_font_glyph_dsc_t和lv_font_t结构体。非LVGL就定义如下结构体。
#if LVGL_FONT_ENABLE
#include "lvgl.h"
#define public_font_t lv_font_t
#define glyph_dsc_t lv_font_glyph_dsc_t
#else
/** Describes the properties of a glyph.*/
typedef struct
{
uint16_t adv_w;
uint16_t box_w;
uint16_t box_h;
int16_t ofs_x;
int16_t ofs_y;
uint8_t bpp;
} glyph_dsc_t;
/*Describe the properties of a font*/
typedef struct _public_font_struct
{
/** Get a glyph's descriptor from a font*/
bool (*get_glyph_dsc)(const struct _public_font_struct *, glyph_dsc_t *, uint32_t letter, uint32_t letter_next);
/** Get a glyph's bitmap from a font*/
const uint8_t * (*get_glyph_bitmap)(const struct _public_font_struct *, uint32_t);
/*Pointer to the font in a font pack (must have the same line height)*/
int16_t line_height;
int16_t base_line;
void * user_data;
} public_font_t;
#endif
下面讲一下LVGL和非LVGL中应用时的差别:
📝 编者注:下方示例基于LVGL V6的旧式style API(lv_style_copy等),仅作历史记录;当前引擎自动适配LVGL v8/v9,新版写法为lv_obj_set_style_text_font(label, (lv_font_t*)app_get_font(...), 0)。
LVGL中使用(LVGL V6.0.2)
void lv_font_test_start(FONTTYPE_ENUM font)
{
lv_obj_t* scr = lv_scr_act();
lv_obj_t* label1 = lv_label_create(scr,NULL);
static lv_style_t style1;
lv_style_copy(&style1,&lv_style_plain_color);
style1.text.font = (lv_font_t *)app_type_get_font(e_HarmonyOS_Sans_SC_Medium_24_2Bpp);
lv_label_set_style(label1,LV_LABEL_STYLE_MAIN,&style1);
lv_label_set_text(label1,"A");
lv_label_set_body_draw(label1,true);
lv_obj_align(label1,NULL,LV_ALIGN_CENTER,0,0);
}
非LVGL中
public_font_t *my_font = NULL;
app_ext_font_init();
my_font = app_type_get_font(e_HarmonyOS_Sans_SC_Medium_28_4Bpp);
8.3、LVGL和非LVGL的字属性
使用Py_FontMaker工具可以输入参数确定是否支持LVGL,支持或者不支持LVGL的差别在于字的属性描述的不同,实际上是下面结构体中的Yoffset不同。
typedef struct
{
uint8_t m_XDist;
uint8_t m_XSize;
uint8_t m_YSize;
int8_t m_XOffset;
int8_t m_YOffset;
uint8_t m_BytesPerLine;
LVGL中的m_YOffset = y_Min,而非LVGL中m_YOffset为左上角到字型起点的像素点数量。
在Font_Lib.bin中始终支持的是非LVGL的字属性描述,只是在ext_font_engine.c文件中会根据是否支持LVGL的宏进行重新计算y_off以及baseline。
但是在内部字体生成时,避免重新计算,所以在bitmap中有两种计算好了的字属性描述,通过LVGL的宏进行控制编译那一个,例如:
/*(!)*/
#if LVGL_FONT_ENABLE
0x04, 0x04, 0x0f, 0x01, 0x00, 0x01, //dsc : unicode = 0x0021
#else
0x04, 0x04, 0x0f, 0x01, 0x05, 0x01, //dsc : unicode = 0x0021
#endif
0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0,
0xf0, 0xf0, 0xf0, 0x00, 0xa0, 0xf0, 0xb0,
8.4、cmap支持hash
为了让cmap更快速的进行查找,在生成cmap时,也同样支持二分法查找的顺序排放表和支持hash查找的hash表。
hash生成的Python代码如下:
def hash_func(self, key, size):
return key % size
def re_hash(self, old_hash, size):
return (old_hash + 1) % size
def put(self, key, value):
start_slot = self.hash_func(key, len(self.slots))
if self.slots[start_slot] is None:
""" 没有冲突 """
self.slots[start_slot] = key
self.data[start_slot] = value
else:
""" 存在冲突 """
if self.slots[start_slot] == key:
""" 已存在,而非冲突 """
self.data[start_slot] = value
else:
""" 存在冲突 """
next_slot = self.re_hash(start_slot, len(self.slots))
while self.slots[next_slot] is not None and self.slots[next_slot] != key:
""" 当没有找到空槽或者找到相同key的时候,执行循环 """
next_slot = self.re_hash(next_slot, len(self.slots))
if next_slot == start_slot:
# raise IndexError("hash表已满")
print("hash表已满")
return
# 走到这边说明已经找到了空槽或者找到了相同的key
if self.slots[next_slot] is None:
# 说明找到了空槽
self.slots[next_slot] = key
self.data[next_slot] = value
else:
# 说明已存在,而非冲突
self.data[next_slot] = value
ext_font_engine.c中的hash查找函数如下:
static uint16_t hash_func(uint16_t key, uint16_t size)
{
return key % size;
}
static uint16_t re_hash(uint16_t old_hash, uint16_t size)
{
return (old_hash + 1) % size;
}
int hash_unicode_get_index(const cmap_sparse_table_t* p_cmap_hash_table, uint16_t cmap_size, uint16_t unicode)
{
uint16_t start_slot = hash_func(unicode, cmap_size); // get hash start_slot
uint16_t position = start_slot;
while (p_cmap_hash_table[position].code_point != 0)
{
// No empty slot is found, no target is found, and the loop continues without ending
if (p_cmap_hash_table[position].code_point == unicode)
{
return position; //Find the target
}
else
{
position = re_hash(position, cmap_size); // Keep looking
if (position == start_slot)
{
// That means they looked around, didn't find it, that means there's no target
return -1;
}
}
}
return -1;
}
8.5、RLE的压缩和解压
为了节约存储空间,那么可以采用RLE的压缩方式进行压缩,这个理论很好理解,就第一个字节为数据重复次数,后面紧接着是重复的字节,这里进行了改进,将不重复数据的个数也使用一个字节开头表示,后面紧接着就是不重复的数据,只是这个代表重复或者不重复数据个数的这个字节的最高bit用来表示是重复还是非重复数据,最高bit为1表示是重复的数据,如果是0表示不重复的数据个数,举个例子吧:
原始数据:0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 1, 1, 1, 1, 1, 1, 1, 1
RLE数据:0x86, 0, 0x5, 1, 2, 3, 4, 5, 0x88, 1
0x86🡪 最高位为1,说明后面跟的是连续的数据,次数为6次,也就是6个0,和原始数据一样;
0x05🡪 最高位为0,说明后面跟的是非连续的数据,共有5个非连续的数据,1,2,3,4,5,和原始数据一样;
0x88🡪 最高位为1,说明后面跟的是连续的数据,次数为8次,也就是8个1,同样和原始数据一样
只是这里有几个条件:
连续3个以及以上才算连续,因为2个连续压缩并没有实际产生压缩率;
同一个数据最大的重复次数为127,因为只有7bits表示可以压缩的次数,同样非连续的也只能表示127个,再多那就要重新开始计算。
Python中的RLE的压缩源码如下:
def is_repetition_start(self, src, srcLeft):
if srcLeft < 3:
return 0
if (src[0] == src[1]) and (src[1] == src[2]):
# print("repetition_start")
return 1
return 0
def get_repetition_count(self, src, src_size):
repeated_byte = src[0]
length = 1
while length < src_size and length < 0x7f and src[length] == repeated_byte:
length += 1
return length
def get_nonRepetition_count(self, src, src_size):
if src_size < 3 :
return src_size
length = 2
a = src[0]
b = src[1]
while length < src_size and length < 0x7f and ((a != b) or (b != src[length])):
a = b
b = src[length]
# print("next = %d"% b)
length += 1
return length - 2 if (a == b) else length
def Rle_Encode(self, inbuf, inSize):
src = inbuf
encSize = 0
srcLeft = inSize
onuBufSize = 1024*10
outbuf = [None] * onuBufSize
while srcLeft > 0 :
count = 0
if self.is_repetition_start(src, srcLeft):
if((encSize + 2) > onuBufSize):
return outbuf, -1
count = self.get_repetition_count(src, srcLeft)
# print("get_repetition_count = %d"% count)
outbuf[encSize] = count | 0x80
encSize += 1
outbuf[encSize] = src[0]
encSize += 1
src = src[count:]
srcLeft -= count
else:
count = self.get_nonRepetition_count(src, srcLeft)
# print("get_nonRepetition_count = %d" % count)
if (encSize + count + 1) > onuBufSize:
return outbuf, -1
outbuf[encSize] = count
encSize +=1
for i in range(0, count):
outbuf[encSize]= src[i]
encSize +=1
srcLeft -= count
src = src[count:]
return outbuf, encSize
ext_font_engine.c中的RLE解压函数如下:
int RLE_glyph_bitmap_decode(uint8_t *inbuf,uint16_t in_size, uint8_t *outbuf,uint16_t out_size)
{
uint8_t *src = inbuf;
uint16_t i = 0;
uint16_t decode_size = 0;
uint8_t count = 0;
uint8_t sign = 0;
while(src < (inbuf + in_size))
{
sign = *src++;
count = sign & 0x7F;
if((decode_size + count) > out_size)
{
return -1;
}
if((sign & 0x80) == 0x80)
{
for(i = 0; i < count; i++)
{
outbuf[decode_size++] = *src;
}
src ++;
}
else
{
for(i = 0; i < count; i++)
{
outbuf[decode_size++] = *src++;
}
}
}
return decode_size;
}
第九章 Py_FontMaker使用说明
Py_FontMaker.exe通过cmd打开输入-h就可以查看帮助信息:

工具默认是支持LVGL、Hash查表、RLE压缩和cache字符缓存的。支持的命令如下:
| 命令 | 描述 | 使用者 | 默认 |
| -h, | 显示此帮助消息并退出 | ||
| --XBF | 功能项-> ttf 生成 XBF | False | |
| --LIB | 功能项-> xbf生成font_lib | False | |
| --INNER | 功能项-> 产生内部字库 | False | |
| --ICON | 功能项-> 产生图标字库 | False | |
| --EMOJI | 功能项-> 产生emoji字库 | False | |
| --LANG | 功能项-> 将多语言Excel转为.c和.h | False | |
| --min | 参数-> unicode最小值 | XBF使用 | 0x00 |
| --max | 参数-> unicode最大值 | XBF使用 | 0xffff |
| --ttf | 参数-> 字体文件路径 | XBF,LIB(fake)和INNER | .\font_xbf\TTF_file\HarmonyOS_Sans_SC_Medium.ttf |
| --bpp | 参数-> 灰度位 | XBF,LIB(fake)和INNER | 2 |
| --height | 参数-> 字符像素高度 | XBF,LIB(fake)和INNER | 24 |
| --lib_file | 参数-> 生成Font_Lib时的字库类型文件 | LIB使用 | .\font_type\all_font_type_to_generate.txt |
| --fake_file | 参数-> 生成Fake font字符的txt文件 | LIB(fake) | .\font_type\FAKE_FONT_CODE_POINT.txt |
| --inner_file | 参数-> 生成内部字库字符的txt文件 | INNER使用 | .\font_type\INTERIOR_FONT_CODE_POINT.txt |
| --icon_file | 参数-> 生成图标字库字符的txt文件 | ICON 使用 | .\font_type\ICON_FONT_CODE_POINT.txt |
| --emoji_file | 参数-> 生成emoji字库字符的txt文件 | EMOJI使用 | .\font_type\EMOJI_FONT_CODE_POINT.txt |
| --emoimgpath | 参数-> 生成emoji的图片资源路径 | EMOJI使用 | .\UI_resource\emoji\32 |
| --lvgl | 参数-> 支持LVGL的字库 | LIB使用 | True |
| --hash | 参数-> cmap支持hash查找 | LIB使用 | True |
| --rle | 参数-> 支持Run Length Encode压缩 | LIB使用 | True |
| --cache | 参数-> 字库支持字符描述缓存 | LIB使用 | True |
当什么命令都不输入直接回车,就会有demo使用步骤:

9.1、Py_FontMaker的demo使用
生成demo的步骤如下:
Py_FontMaker.exe直接回车,可以生成font_out、font_type、font_xbf和UI_resource文件夹以及demo相关文件
输入Py_FontMaker.exe --LIB True 可以生成demo外部字库(包含ICON和EMOJI)以及Fake字库
输入Py_FontMaker.exe --INNER True 可以生成内部字库
输入Py_FontMaker.exe --ICON True 可以生成内部图标字库
输入Py_FontMaker.exe --EMOJI True 可以生成内部emoji字库
输入Py_FontMaker.exe --LANG True 可以将 multi_language_table.xlsx 多语言生成.c和.h
下面按照上面的步骤生成字库
直接回车可以在根目录下生成如下红框中的文件:

会生成3个文件夹,font_out、font_type、font_xbf和UI_resource。font_out是最终输出的源代码和bin文件,multi_language文件夹最终生成多语言的.c和.h文件;font_type是字库类型的txt文档,全都使用utf-8格式保存的,multi_language为对语言excel的文件路径,里面会默认生成multi_language_table.xlsx文件;font_xbf是生成的xbf文件,在里面还有一个二级目录是TTF_file,里面工具生成了一个华为的字体文件,font_xbf\TTF_file\my_iconfont目录下放的是icon文件,是https://www.iconfont.cn/生成的文件;\UI_resource\emoji里面是emoji的图片资源。
下面对某些文件进行说明
all_font_type_to_generate.txt
这个文件是最终生成多语言时的输入的文件,这个文件的内容如下:

也就是font_type目录下的:
HarmonyOS_Sans_SC_Medium_24_2Bpp.txt(这个是多语言文件,所有多语言字符全部写在这个文件,python会去重和排序的)和
fHarmonyOS_Sans_SC_Medium_24_2Bpp.txt(这是一个空文件,表示是全字库)
icon_font_32_8Bpp为图标字体unicode文件,unicode单个code一行且以”;”结尾

emoji_font_32_8Bpp为emoji的字体unicode文件,以”;”结尾,unicode可以大于0xffff

FAKE_FONT_CODE_POINT.txt
这个文件是没有任何字库或者字库里面没有相应的文字时对使用这个默认的字库,这个文件的内容如下:

INTERIOR_FONT_CODE_POINT.txt
这个文件是内部字体的demo文件,分为两个部分,一个部分是可以输入unicode的编码范围,另一个部分是输入需要的生成字库的文字,如下(下面的###注释千万不要动):

ICON_FONT_CODE_POINT.txt
内部图标字体,和icon_font_32_8Bpp.txt一样的
emoji_font_32_8Bpp.txt
内部emoji字体文件,和emoji_font_32_8Bpp.txt一样
multi_language_table.xlsx对语言翻译表

FontCvtST.exe
这个工具也直接生成了,省得到处找,FontCvtST工具可以生成xbf文件,Py_FontMaker也可以生成xbf文件,只是Py_FontMaker的速度比较慢,生成后的xbf都可以被Py_FontMaker进行处理成Font_Lib.bin。
Py_FontMaker.exe --LIB True

再看一下目录结构:

cmap_*Bpp.c 这个就是字库的cmap文件
ext_font_app.c 字体定义和初始化以及外部flash访问接口在这里实现
ext_font_app.h 用户使用外部字库是需要包含这个头文件
ext_font_engine.c字符描述、bitmap获取都在这里,hash、rle和cache都在这个文件
ext_font_engine.h 供ext_font_app.c调用
fake_font_24_2Bpp.c 默认字库
Font_Lib.bin 烧入到外部flash的字库bin文件
Font_Lib.h 字库结构体、LVGL使能、hash使能、cache使能和RLE使能宏定义。
emoji_font_def.h 引用emoji的宏,emoji并没有像文字一样的字符,所以需要直接调用UTF-8的码,如下:

icon_font_def.h引用ICON的宏,ICON并没有像文字一样的字符,所以需要直接调用UTF-8的码,如下:

Py_FontMaker.exe --INNER True
这个操作会在font_out文件夹中生成HarmonyOS_Sans_SC_Medium_24_2Bpp.c文件,同时还会修改ext_font_app.c和ext_font_app.h以及Font_Lib.h文件,将字库声明到相应文件以及在相应的数据结构体中进行调用,方便用户使用统一接口获取到字库。

Py_FontMaker.exe --ICON True
这个命令生成内部图标字库。

Py_FontMaker.exe --EMOJI True
这个命令生成内部emoji字库。

Py_FontMaker.exe --LANG True
这个命令将font_type\multi_language\multi_language_table.xlsx生成对应的.c和.h在目录font_out\multi_language中,使用见13章:

9.2、Py_FontMaker.exe的6大功能
--XBF:输入相应参数可以产生和工具FontCvtST.exe一样的xbf文件,只是这个命令生成一样全字库时非常慢,要将近20分钟,这个问题是变量ttf字符太多导致,我一般是晚上执行一个脚本,生成一大堆的xbf,反正只需要生成一次,当然这个命令也同样能生成ICON的xbf文件,但是FontCvtST.exe生成不了;
--LIB:这个就是生成Font_lib.bin以及使用这个bin的通用代码的;
--INNER:这个是生成内部字库;
--ICON:生成内部图标字库;
--EMOJI:生成emoji字库;
--LANG:将多语言excel转为.c和.h。
Py_FontMaker.exe会在根目录下生成4个文件夹:
font_xbf:最终生成的xbf文件会在这里,同时ttf文件也放在这个文件夹,icon也会生成在这个文件夹;
font_type:这个是多语言字库的txt的文件
font_out:最终输出的Font_Lib.bin已经使用他的代码文件。
UI_resource:这个文件夹存放的是emoji的图片资源
上面6个功能后必须紧跟 True才能使用,其余的各个参数前面表格有说明。在下面的各个功能里面会更进一步说明。
9.3、功能一 -> --XBF True生成xbf文件
这个是生成xbf文件的,具体需要的参数如下:
--min:最小的code码,默认为0x00,其实xbf文件都是全字库;
--max:最大的code码,默认为0xffff;
--ttf:字体文件的路径,默认路径是.\font_xbf\TTF_file\HarmonyOS_Sans_SC_Medium.ttf文件的;
--bpp:灰度位深,支持1bit、2bits和4bits;
--height:字体高度,单位是像素;
9.4、功能二 -> --LIB True生成字库和通用访问代码
这个是生成Font_Lib.bin以及使用的通用文件的,具体需要的参数如下:
--lib_file:是生成Font_Lib.bin时,需要输入的各个字体的汇总txt文件,默认是.\font_type\all_font_type_to_generate.txt文件;
--lvgl:后跟True或者False,是否是支持LVGL的字库,默认为True
--hash:后跟True或者False,cmap是否是支持Hash查表,默认为True;
--rle:后跟True或者False,bitmap是否是支持RLE压缩,默认为True;
--cache:后跟True或者False,用来是否启用字符属性描述的缓存,默认是开启的,且默认是100字符的缓存;
--fake_file:生成默认fake字体时,需要输入fake字体需要支持字符的txt文件,默认是.\font_type\FAKE_FONT_CODE_POINT.txt文件;
--ttf:生成fake字库需要的ttf文件,默认是.\font_xbf\TTF_file\HarmonyOS_Sans_SC_Medium.ttf;
--bpp:生成fake字库的灰度;
--height:生成fake字库时的像素高度;
9.5、功能三 -> --INNER True内部字库生成
这个是生成内部字库以及更新部分通用文件的命令,具体需要的参数如下:
--inte_file:生成内部字体时,需要输入内部字体需要支持字符的txt文件,默认是.\font_type\INTERIOR_FONT_CODE_POINT.txt文件;
--ttf:生成内部字库需要的ttf文件,默认是.\font_xbf\TTF_file\HarmonyOS_Sans_SC_Medium.ttf;
--bpp:生成内部字库的灰度;
--height:生成内部字库时的像素高度;
内部字体是同时支持LVGL和非LVGL,Hash和RLE,最终取决于用户是否使能这3个宏。
9.6、功能四 -> --ICON True内部图标字库生成
这个是生成内部图标字库以及更新部分通用文件的命令,具体需要的参数如下:
--icon_file:生成内部图标字体时,需要输入内部图标字体需要支持字符的txt文件,默认是.\font_type\ICON_FONT_CODE_POINT.txt文件;
--ttf:生成内部图标字库需要的ttf文件,默认是.\font_xbf\TTF_file\my_iconfont\iconfont.ttf;
--bpp:生成内部图标字库的灰度;
--height:生成内部图标字库时的像素高度;
9.7、功能五 -> --EMOJI True内部emoji字库生成
这个是生成内部emoji字库以及更新部分通用文件的命令,具体需要的参数如下:
--emoji_file:生成内部emoji字体时,需要输入内部图标字体需要支持字符的txt文件,默认是.\font_type\EMOJI_FONT_CODE_POINT.txt文件;
--emoimgpath:生成内部emoji字库需要的图片资源路径文件,默认是.\UI_resource\emoji\32,且每张图片资源名称都是以“emoji_u”开头,因为找图片时会匹配这个名字;
--bpp:生成内部emoji字库的灰度,一般是8bpp;
Emoji的大小是图片资源本身的大小,本来python可以设置图片大小的,但是一设置后,图片的灰度图就会出问题,所以没有使用python修改大小的功能。
9.8、功能六 -> --LANG True将多语言excel转为.c和.h
--lang_excel 选择多语言翻译excel的表格路径,生成的.c和.h在font_out\multi_language目录中。
第十章 GUI_Py_FontMaker使用说明
命令行使用,很多人还是不习惯,所以用remi做了个界面的,本来是当做web程序的,但是感觉好多东西都没有处理好,所以就当做单机版本了。本来网页还长得挺好看的,但是生成exe就有变样了,整个界面如下:

左上角可以选择功能,选择输出路径,就是最终生成代码的目录,这个和命令行是一样的,只是有个界面点一点,每个界面都截个图吧!






第十一章 ICON图标字体
11.1、直接使用iconfont的图标
ICON图标字体是从https://www.iconfont.cn/这里进行制作,步骤如下:
首先得注册一个账号
搜索你需要的图标,例如SD

加入购物车

将购物车中的图标移到项目中


查看项目

下载项目

查看下载内容

将iconfont.json和iconfont.ttf文件复制粘贴到目录\font_xbf\TTF_file\my_iconfon下面,iconfont.json是用来生成icon_font_def.h的,所以图标的名字不要用中文,有中文的话,需要自己进行修改;iconfont.ttf就是图标字体文件了。
这个ttf也是可以安装后然后通过FontCvtST.exe工具进行查看的,例如:

注意,如果生成bin库,需要通过命令或者GUI_Py_FontMaker将ttf转为xbf,如果是生成内部icon字库,可以直接使用ttf。
11.2、添加figma中的img
在制作相册表盘时,希望某些图标随着定义的颜色变化,如果用切图,那么太占用空间,这个时候icon字库是非常有用的,那么如何将自己的figma中的icon生成ttf呢?
A、将figma中的img下载成SVG格式的文件

下载图片的时候点击如下的大小下载,也就是只保留icon大小的像素。

B、登入iconfont账号,点击上传的云朵图标


上传后如下,点击提交

等待审核

查看我上传的图标

点击加入购物车,后面的步骤就和11.1里面的一样了。
11.3、LVGL使用图标字体
Py_FontMaker工具生成字库时会将iconfont.json中的Unicode码生成一个头文件表:

调用如下:

第十二章 Emoji字库
Emoji实际上是图片生成的灰度点阵,按照字库的形式进行存储的,为了表现更好的Emoji表情,灰度最好选8Bits。Emoji的unicode编码其实也是有规定的,可以查看google的关于emoji的仓库,https://github.com/googlefonts/noto-emoji这个里面有emoji的字库文件ttf,例如:

可惜的是,python的pygame好像无法解析上面的TTF文件,实际上上面的ttf文件与仓库中的图片资源是对应的。

可以通过http://blog.luckly-mjw.cn/tool-show/iconfont-preview/index.html来查看上面的ttf文件里面的字符,例如(unicode编码大于了0xffff):

而本工具其实是使用的图片资源生成的emoji字库。
12.1、Emoji的图片资源来源和unicode编码定义
Emoji的图片资源就是从google的项目中获取的,https://github.com/googlefonts/noto-emoji/tree/main/png/32,这个里面的名称都是以emoji_u+unicode码命名的,这样的命名方式便于生成emoji的引用头文件。
Emoji的unicode的编码官网是http://www.unicode.org/reports/tr51/proposed.html,在这个官网中可以找到关于unicode的编码连接:https://unicode.org/emoji/charts/full-emoji-list.html。

这里google的0x1F600是露齿微笑,和上上图中的google中的0x1F600的图标表情一样,都是露齿微笑,其他的APP中只是表情表现形式是不一样的,这个就是每个公司去自己设计的了。主要想说明的是,无论那家公司的emoji,它的unicode编码是已经固定好了的,例如微信的emoji复制到钉钉的显示效果:
微信:
钉钉:
将这个信息拷贝到Notepad++:
将文档使用UTF8编码,保留第一个图标,看看二进制是个什么情况:

这个是UTF-8编码(UTF-8是字节流,本身没有大小端问题,按字节顺序依次存放),即F0 9F 98 81这4个字节:
11110000 10011111 10011000 10000001
取出各字节的有效位拼接:000 + 011111 + 011000 + 000001 = 0b0001 1111 0110 0000 0001
即:UNICODE码为0x1f601和上面的网页一致
12.2、Py_FontMaker的Emoji生成
这里主要说明,emoji的生成是根据图片生成的,demo的32*32像素的图片是来自google的emoji的项目中的图片文件夹,引用emoji的方法是使用emoji_font_def.h文件中的utf-8的宏,例如:
lv_label_set_text(label, EMOJI_SYMBOL_0x1F600);
第十三章 多语言Excel表生成代码
嵌入式中难免存在多语言的问题,一般产品会给出多语言的Excel表格,里面各个词条翻译成的各国语言的字符串,在项目中就得根据设置的语言进行相应的词条进行显示,Py_FontMaker同样支持将多语言excel表转为代码和引用接口。
13.1、多语言excel表的规范
demo会在font_type\multi_language目录下生成一个multi_language_table.xlsx文件,这个文件的规则如下:

A列:为引用的key,类似一个键值对,字符项为key,而后面的翻译词条为value;
B列:在工具转换成代码时,这个描述项并没有使用,可以作为注释使用,但是必须保留在excel中;
C列:这个是第一个语言的起始列,所以前面的A和B列一定要保留在excel中,代码中并没有做出判断,而是直接默认C列就是第一个语言翻译词条开始;
D列之后:都是每个支持的语言词条翻译;
C1-*1:支持的语言的第一行en_us(English)的规则:en_us最终会形成lang_en_us.c文件,en表示英语,us表示美国,而括号里面的“English”会是language_info_t中locale元素的描述;
F2:像这种没有翻译的空缺,在形成代码时的字符串也同样是””进行代替。
13.2、Language.c和Language.h
这两个文件主要是用于对多语言的引用,language.h中主要有三个宏:
#define APP_GET_KEY_OFFSET(key)
这个宏是得到key在多语言key结构体类型中的偏移,也就是key在结构体中的引用地址,最终会调用:
#define LANG_OFFSET_IN_TYPE(type, key) ((unsigned int)(&(((type *)0)->key)))
#define APP_GET_STR_BY_KEY_OFS(key_ofs)
这个宏是根据key在多语言key结构体中的元素偏移得到对应的翻译字符串,实际上调用的是,源码如下;
const char *app_get_lang_string(unsigned int offset, char *default_str)
{
if (offset >= sizeof(LANGUAGE_TRANSLATION)) {
return "no keyname";
}
const char *lang_str = ((LANG_TRANSLA_ITEM_t *)((unsigned int)language_pack[lang_type]->translation + offset))->lang_string;
return (*lang_str) ? lang_str : default_str;
}
#define APP_SET_LANG_TYPE(lang)
设置语言类型宏
13.3、各国语言词条翻译.c文件
在language.h中有结构体:
typedef struct {
const char *lang_string;
} LANG_TRANSLA_ITEM_t;
而LANGUAGE_TRANSLATION_t中就是LANG_TRANSLA_ITEM_t根据excel表中的key组成的key结构体。
每个.c文件包含有LANGUAGE_TRANSLATION_t的key值对应的翻译和language_info_t语言信息描述结构体:
typedef struct {
const char *locale;
const LANGUAGE_TRANSLATION_t *translation;
} language_info_t;
例如lang_zh_cn.c中的内容如下:


13.4、多语言词条引用方法
引用步骤:
设定使用的语言
APP_SET_LANG_TYPE(LANGUAGE_ZH_CN)
得到key的在结构体中的偏移
unsigned int key_ofs = APP_GET_KEY_OFFSET(key_ebook)
根据key的偏移得到字符串
char *str = APP_GET_STR_BY_KEY_OFS(key_ofs)
获取当前使用的哪种语言
APP_GET_LANG_TYPE()实际调用的是函数app_get_lang_type()
第十四章 TTF文件修改(多字重和合并)
有时候TTF文件中有多个字重(Weight),字重指字体的粗细程度,例如:淡体 Thin/Hairline、特细 Extra-Light/Ultra-Light、细体 Light、次细 Demi-Light、标准 Regular/Normal/Book/Plain、适中 Medium、次粗 Demi-Bold/Semi-Bold、粗体 Bold、特粗 Extra-bold/Extra、浓体 Black/Heavy和特浓 Extra-Black/Ultra-Black等等。手表中一般会用到标准 Regular/Normal或者适中 Medium,根据支持国家语言的不同,就需要所有这些国家的字体ttf文件,可以是多个ttf文件,但是在软件上就需要根据设定的语言进行选择不同的ttf生成的字库文件,比较麻烦,所以可以将两个不同的ttf文件进行合并成一个ttf,本文因为要用到韩语,但是HarmonyOS_Sans_SC_Regular.ttf字体中并没有韩语,所以找到开源免费商用的思源字体,在2.3节中有下载地址。
14.1、用到的工具
FontCreator这个工具可以将包含有多种字重的ttf只留下自己想要的字重,例如只保留Regular,同时多个ttf文件合并时,因为设计不一样,导出字体参数不一样,这样直接合并会存在问题,所以可以看一下同一个字的字体宽度是否一样,如果不一样看之间相差多大,计算出倍数,然后通过python将其中的ttf所有属性进行修改,再通过另一个工具FontForge进行合并。
FontCreator是一款收费的软件,不过用的比较少,有试用期,所以直接到官网下载安装即可:https://www.high-logic.com/font-editor/fontcreator/download-confirmation

下载直接安装即可。
FontForge则是开源的一款软件,免费使用,可以将两个ttf进行合并,下载地址在GitHub:https://github.com/fontforge/fontforge,也可以到官网:https://fontforge.org/en-US/

14.2、如何查看ttf中有多种字重
查看SourceHanSansKR-VF.ttf字体中支持多少个字重,这个ttf在2.3节中有下载说明,通过如下方式查看ttf字体中有哪些字重,直接双击ttf文件就可以通过windows中默认程序打开ttf文件,如果有如下图中的“下一个”,说明有多个字重。

还有一种方法是,点击上面的安装后,可以到C:\Windows\Fonts中查看这个字体文件:

14.3,FontCreator将多字重的ttf保留一个Regular字重
打开FontCreator工具后直接将SourceHanSansKR-VF.ttf拖到这个工具中,会看到如下界面,共有24964个字符:

点击工具中“Font”中的“Properties”

点击“Properties”弹出如下界面

点击“Instances(7)”,7表示7个实例,7个字重:

双击Regular就能将字体字符展示窗口切换到Regular的粗细,如下对比:

这里将其他6个字重全部删除,然后将默认的字重设置为Regular

切到“Master(2)”如下图:

现在默认的是ExtraLight字重,点击其他字重,然后点击右键,就可以设置成默认的了,但是这里我们需要Regular字重的数据,点击上面的“+”号可以将Regular的是数据添加进来。

上面这个过程需要一点时间,完成后会有如下显示:

将上面的wght 503.51改名为Regular并设置为默认的字重,其他两个删除掉:

再切换到“Axes(1)”,实际上Default的值本来是250的,因为Default本来是ExtraLight:

删除Axis Values中的值,然后从实例中添加:


导出字库:

选择要导出的位置即可:


14.4、查看两个ttf的字符A的属性
将上面生成的SourceHanSansKRVFRegular-VF.ttf和HarmonyOS_Sans_SC_Regular.ttf都拖到FontCreator中,可以同时打开字符A看具体的数据:

通过对比,很幸运,这两个ttf设计的参数都差不多,那么这里直接进行合并。
14.5、FontForge工具合并两个ttf
这里是把字库SourceHanSansKRVFRegular-VF.ttf合并到字库HarmonyOS_Sans_SC_Regular.ttf中,所以先通过FontForge打开HarmonyOS_Sans_SC_Regular.ttf文件,点击“Element”选择Merge Fonts,这时会弹出一个窗口,再次选择SourceHanSansKRVFRegular-VF.ttf字体,经过一段时间就会合并完成。

如果某个字符是合并过来的会显示蓝色:

选择File中的Generate Fonts就可以导出新的ttf了


导出如下:

通过FontCreator打开可以看到韩语了且字符为44906个:

进行安装后通过FontCvtST.exe查看汉字和韩语点阵字体的字高是什么情况:


运气真好,合并完成。