← 返回生成页

嵌入式字库研究及Py_FontMaker工具编写

作者:刘权 · 2022-11-11 · 公众号:Bluetooth-BLE · QQ群:177341833

编者注(2026-09 整理):本文是 Py_FontMaker 的原始研究与开发记录, 完整保留了从字库原理、XBF/Font_Lib.bin 格式设计到工具实现的全过程。文中工具用法对应 2022 年版本, 当前版本的使用方式请以 完全教程使用文档 为准; 原文中的概念性笔误已修订,重要修订处以「📝 编者注」标出。

第一章 字体基本概念

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的传输和存储的一种实现方式。

1.4、UTF-8 的编码规则

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

/**
 * 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;
}
/**
 * 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的灰度字体。

制作字库的步骤:

  1. 确定字体(华为、OPPO和阿里等都有商业免费的字体),字号(西文一般用磅pt,方块字一般用号数,指字的实际大小,没有上下留白,嵌入式一般直接用像素px)、字重(粗细程度,如Regular/Medium/Bold,见第十四章)和样式(正体、斜体等)

  2. 将字体中的“样子”显示在电脑显示上

  3. 读取每个像素RGB的值

  4. 根据灰度位数将RGB值计算成Alpha值

  5. 组成字节后存储到bin

2.2、Freetype

FreeType 是一个免费提供的字体渲染软件库,是一种软件字体引擎,旨在实现小型、高效、高度可定制和便携,同时能够生成高质量的输出(字形图像)。它可以用于图形库、显示服务器、字体转换工具、文本图像生成工具以及许多其他产品。这里不具体介绍这个,可以到官网(https://freetype.org/freetype2/docs/)上看,我主要是参考字形指标(Glyph Metrics)的一些概念,在制作字模时需要用到,主要是了解下图中的参数:

2.3、免费商用字体下载地址

点击下载 HarmonyOS Sans

进入source-han-sans:

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

第三章 FontCvtST.exe使用

先了解这个工具的目的是为后面写python工具时,借鉴bin文件的存放格式,为了兼容这个工具,python写的工具生成的xbf文件完全和这个工具生成的xbf格式一致,只是本文写的工具会把bpp也放在xbf中,而FontCvtST工具在xbf中并没有体现bpp信息。

在嵌入式中,要显示一个字符,需要经过:

  1. 将字符转为代码中使用的编码,如UTF-8转UNICODE;

  2. 通过编码找到cmap(character maps)(二分法\hash\直接计算),找到bitmap的偏移地址和大小;

  3. 通过偏移找到存储在字库表中字符属性描述

  4. 同时也获取到字符的bitmap;

  5. 如果有压缩还需将bitmap进行解压;

  6. 将灰度转为RGB;

  7. 将转换后的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

在非连续字库中,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;

字体还有一些参数,例如基线baseline(LVGL中的baseline是相对左下角计算的,如上图中的baseline,而实际上一般是从左上角开始计算,也就是两种计算方式之和应该是Line_size - 1),字占空间高line_size。在字模提取时,实际只提取box_w*box_h的面积内的像素点,然后通过上面测参数计算出显示的坐标进行显示。

python中如何计算这些参数呢?。

在python中,上图中的(xMin,xMax,yMin,yMax,ascent),这个5个参数可以通过接口获取, ascent的值就是baseline的值,这样就可以计算出LVGL中使用的参数了。例如:

= baseline – off_y – y_Size

= baseline – (baseline - yMax) – (yMax - yMin)

=yMin

在python中提取像素时,A点和B点坐标就是遍历的条件控制参数,所以需要利用上面的参数计算出A和B的坐标:

本文代码和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计算得到

字母A

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;

上面的元素中,

4.3、非LVGL使用字库

通过上面的了解,实际上这个问题很好理解了,同样也是上面的那两个结构体,也就说LVGL中的这个方案同样也适用在非LVGL中的使用,只是在生成字库时,字的属性描述可以按照正常的以左上角为基点进行计算的参数,本文中的工具也支持非LVGL的字库生成。

第五章 字模点阵/RGB/Gray关系

这个单独起章节,因为觉得重要,搞懂这些为后面制作字库做准备。思考几个问题:

  1. 字库根据字体文件生成原理?

  2. 字库存储的是点阵模型,就是这个点是亮还是灭,但是LCD的每个点是RGB,如何转?

  3. 2bits和4bits灰度是什么意思?如何在生成字库时将字体文件中字形的RGB转为灰度?

  4. 灰度字库中的点如何转为RGB

5.1、字库根据字体文件生成原理

原理步骤如下:

  1. 通过工具对字库文件进行解析后显示到显示器或者某个表面;

  2. 通过读取这个字的相关属性可以得到第四章中要用到的属性;

  3. 通过读取显示界面上字的每一个像素点的RGB,然后将RGB转成灰度后进行保存,就可以得到字库了。

后文会用python进行字库制作,并进行验证。

5.2、RGB如何转为灰度值

这里要搞明白什么是灰度,灰度可以认为就是亮度,也就是色彩的深浅程度。所谓灰度色,就是指纯白、纯黑以及两者中的一系列从黑到白的过渡色。在RGB模式中三原色光各有256个级别,灰度是在像素的RGB数值相等的情况下形成的。而RGB数值相等的排列组合是256个,灰度的数量就是256级。其中除了纯白和纯黑以外,还有254种中间过渡色。除了256级的表示方法外,灰度也可通过百分比表示,范围从0%到100%。注意这个百分比是以纯黑为基准的百分比。与RGB正好相反,百分比越高颜色越偏黑,百分比越低颜色越偏白。

什么是透明度,透明度也叫alpha通道,而透明度简单理解就是背景色和前景色进行混色时,谁显示多,谁显示少的问题,例如:

📝 编者注:注意本文的alpha定义与常规图形学惯例(0=全透明)相反——这里alpha表示背景色占比(0=全前景色),仅为本文示例代码的自定约定,与后文5.3节代码一致。

上面的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进行安装,代码中导入了如下库:

自己调试一下,如果报错就安装哪个库。例如:

主要使用的是pygame中的Font库。

6.5、Pygame的Font库生成字库步骤

  1. 使用pygame.font.Font创建一个字体对象;

  2. 定义一个显示表面,大小为字符的大小;

  3. 将字显示到表面;

  4. 计算字体的实际有效坐标:

可以得到以下参数:

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

  1. 从A坐标一直到B坐标开始读取像素点;

for y in range(y的范围):

for x in range(A_x, BytesPerLine * 8 / bpp):

  1. 将读取到的点转为灰度;

gray = int((299*(color[0]) + 587*(color[1]) + 114*(color[2]) + 500)/1000)

  1. 将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
📝 编者注:本章描述的是早期基于pygame渲染的实现(生成全字库约20分钟)。当前版本已改为FreeType直连渲染器(ttf_freetype_to_xbf),全字库生成仅需约1分钟,且支持精确像素字号、kerning提取与多TTF fallback链,用法见完全教程

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、验证工程代码和下载脚本

6.9、将py文件变为exe文件

  1. pip install pipenv

  2. pipenv shell

  3. 在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

  1. pip install pyinstaller

  2. 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信息其实就是从第六章种的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信息可以直接从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

  1. 将需要的字号使用对应的字体文件,使用第6章的py生成全字库备用,例如HarmonyOS_Sans_SC_Medium_16_2Bpp.xbf和HarmonyOS_Sans_SC_Medium_16_4Bpp.xbf这个两个都是全字库;

  2. 将需要的多语言文字放到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的格式

  3. 将所有的txt文件名称去掉后缀放到另一个all_font_type_to_generate.txt中,py可以读取这个文件来生成所有的字库;

  4. 上面的工作准备好了,那就开始写py代码,将txt和xbf转为Font_Lib.bin即可。

如下图:

font_out文件夹中生成的就可以直接拿到工程里面进行编译,接口提供了和第四章中提到的两个结构体一样,同时根据参数会生成支持LVGL的还是非LVGL的字库和访问通用代码,这个代码会在下章中进行说明。

7.3、多语言合并

步骤:

  1. 将txt中的多语言信息通过python中的字典去重,得到unicode的列表;

  2. 读取xbf文件,填补字体信息写到font_lib中,将cmap添加codepoint以及重新计算font_lib.bin中的bitmap偏移地址赋值给cmap的偏移值,存储到font_lib.bin中;

  3. 如果是非连续的需要生成一个.c文件,里面包含有一个映射表的数组,同时还要包含一个font_info_t结构体的变量,用来给用户描述这个字体的信息;如果是全字库就只需要包含font_info_t结构体。

  4. 读取bitmap文件,将字符属性压缩到6个字节,重新计算地址后将数据写到font_lib.bin中;

  5. 轮询完文件中所有的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、工程中使用字库

📝 编者注:工具新版已把集成方式升级为「用户注册读接口」——生成的 ext_font_app.c 不再内置需要移植的 font_get_data() 模板,用户在自己的代码里实现读函数并在 app_font_init() 时传入注册即可,生成代码零修改;早期版本函数名 app_ext_font_init() / app_type_get_font() 现为 app_font_init(读函数, 日志回调) / app_get_font()。下文步骤已按新版修订。

字库使用步骤:

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;
}

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)
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);
}
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

只是这里有几个条件:

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使用步骤:

📝 编者注:本章记录的是2022年Windows版exe的参数。当前版本在此基础上新增了--bitstream(字形内连续位流)、--baseline(top/bottom基线坐标系)、--kern(字距)、--langs(阿拉伯/印度系等变形文字语言包)、--lvexport(LVGL官方格式导出)等参数,并提供网页版一键生成,完整参数以 --help 与线上使用文档为准。

9.1、Py_FontMaker的demo使用

生成demo的步骤如下:

  1. Py_FontMaker.exe直接回车,可以生成font_out、font_type、font_xbf和UI_resource文件夹以及demo相关文件

  2. 输入Py_FontMaker.exe --LIB True 可以生成demo外部字库(包含ICON和EMOJI)以及Fake字库

  3. 输入Py_FontMaker.exe --INNER True 可以生成内部字库

  4. 输入Py_FontMaker.exe --ICON True 可以生成内部图标字库

  5. 输入Py_FontMaker.exe --EMOJI True 可以生成内部emoji字库

  6. 输入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的图片资源。

下面对某些文件进行说明

这个文件是最终生成多语言时的输入的文件,这个文件的内容如下:

也就是font_type目录下的:

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

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

内部图标字体,和icon_font_32_8Bpp.txt一样的

内部emoji字体文件,和emoji_font_32_8Bpp.txt一样

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

再看一下目录结构:

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

这个命令生成内部图标字库。

这个命令生成内部emoji字库。

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

9.2、Py_FontMaker.exe的6大功能

Py_FontMaker.exe会在根目录下生成4个文件夹:

上面6个功能后必须紧跟 True才能使用,其余的各个参数前面表格有说明。在下面的各个功能里面会更进一步说明。

9.3、功能一 -> --XBF True生成xbf文件

这个是生成xbf文件的,具体需要的参数如下:

9.4、功能二 -> --LIB True生成字库和通用访问代码

这个是生成Font_Lib.bin以及使用的通用文件的,具体需要的参数如下:

9.5、功能三 -> --INNER True内部字库生成

这个是生成内部字库以及更新部分通用文件的命令,具体需要的参数如下:

内部字体是同时支持LVGL和非LVGL,Hash和RLE,最终取决于用户是否使能这3个宏。

9.6、功能四 -> --ICON True内部图标字库生成

这个是生成内部图标字库以及更新部分通用文件的命令,具体需要的参数如下:

9.7、功能五 -> --EMOJI True内部emoji字库生成

这个是生成内部emoji字库以及更新部分通用文件的命令,具体需要的参数如下:

Emoji的大小是图片资源本身的大小,本来python可以设置图片大小的,但是一设置后,图片的灰度图就会出问题,所以没有使用python修改大小的功能。

9.8、功能六 -> --LANG True将多语言excel转为.c和.h

第十章 GUI_Py_FontMaker使用说明

命令行使用,很多人还是不习惯,所以用remi做了个界面的,本来是当做web程序的,但是感觉好多东西都没有处理好,所以就当做单机版本了。本来网页还长得挺好看的,但是生成exe就有变样了,整个界面如下:

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

第十一章 ICON图标字体

11.1、直接使用iconfont的图标

ICON图标字体是从https://www.iconfont.cn/这里进行制作,步骤如下:

  1. 首先得注册一个账号

  2. 搜索你需要的图标,例如SD

  1. 加入购物车

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

  1. 查看项目

  1. 下载项目

  1. 查看下载内容

将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账号,点击上传的云朵图标

上传后如下,点击提交

等待审核

  1. 查看我上传的图标

点击加入购物车,后面的步骤就和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文件,这个文件的规则如下:

13.2、Language.c和Language.h

这两个文件主要是用于对多语言的引用,language.h中主要有三个宏:

这个宏是得到key在多语言key结构体类型中的偏移,也就是key在结构体中的引用地址,最终会调用:

#define LANG_OFFSET_IN_TYPE(type, key) ((unsigned int)(&(((type *)0)->key)))

这个宏是根据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;
}

设置语言类型宏

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)

unsigned int key_ofs = APP_GET_KEY_OFFSET(key_ebook)

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查看汉字和韩语点阵字体的字高是什么情况:

运气真好,合并完成。

微信公众号
微信公众号
QQ群 BLE5.4 学习讨论 177341833
QQ群 177341833
QQ群 BLE 开发学习 498676838
QQ群 498676838