2019-02-02 14:04:14 +00:00
字库制作简述
---
## 1. 概述
    本文将简单讲解单色点阵字库的生成原理与方法。
    本文所述内容基于但不局限于SimpleGUI, 所有单色LCD、LED(OLED)点阵显示屏均可参照本文所述进行字库的制作和加工。
    本文中记述的内容绝大多数为原创内容,为本人的所学所知,不排除有错误和疏漏的情况,如有发现不妥,还请指正。
## 2. 字体和字号
### 2.1. 字体文件
    在计算机中, 所有显示在屏幕上的东西, 包括文字在内, 都是被像素化过后的图形, 相比基本的平面几何图形或3D图形的随机和不可预见, 文字是一种特殊的、有规律、有局限的图形如果不考虑文字的大小, 那么每一个文字在同一种字体中, 只对应一个图形。
    至于字体,就是一个文字的不同形体,再简单点儿说,就是字的写法,就如同“文”字,在宋体、黑体、楷体中的字形式完全不同的,而字体文件就是文字在同一字体下的集合文件。计算机可以通过字符编码,在字体文件中找到对应的文字图形并显示,用户就在屏幕上看到“人”可以理解的文字了。
    至于字号,表达的就是一个字符的大小了,这个量本身和字体并没有关系。
### 2.2. 光栅字体与矢量字体
    计算机在屏幕上显示图形都是一个一个的点拼凑起来的,所以,想表达一个文字最简单的办法就是记录文字在计算机上显示时候,以固定点为原点,矩形范围内文字表示需要的像素点,这就是最早的光栅字体,又称点阵字体。如果想要兼容多种不同的文字大小呢?那没办法,就得枚举一些常用的字号,针对每一种字号各取一套字体图形数据,保存在同一个文件中,这样,一个光栅字体文件就完成了,通常光栅字体都有详细标明适用的字号。
    光栅字体的优势在于数据简单直观,解析不需要计算,缺点是显示粗糙,适应性略差。
    矢量字体又称轮廓字体是现代计算机性能提高,使用者对图形显示要求提高后的产物,相比光栅字体,矢量字体中记录的不再是文字使用的像素点,而是绘制文字图形时,描述图形的矢量方程。这种字体虽然在解析时需要大量计算,但计算后的字体边缘圆滑,而且不受字号影响,可以任意缩放,如果有特殊的图形需求(加粗、倾斜等)也更容易实现。
    虽然现在的操作系统中仍然同时存在着光栅字体和矢量字体,但是针对光栅字体,基本也都做到了无极缩放,原理就是根据临近字号的光栅图形进行放大或缩小,但是这种方式元不但运算量远远超过矢量字体,而且缩放后产生形变,美观性上也略有欠缺。
    在Windows下, 通常光栅字体文件的后缀名是fon, 矢量字体的后缀名为ttf。
## 3. 编码与解码
    字符编码简称字码,指用特定的数字表示对应字符的一种索引方式。
    前文对字体文件的描述,字体文件中通常包含了该字体下大部分常用字的图形信息,那么如何索引对应的图形,就是文字编码的意义所在。
### 3.1. ASCII编码
    ASCII编码的历史基本和电子计算机的历史一样长, 全名为American Standard Code for Information Interchange, 中文译为“美国(国家)信息交换标准码”, 此种编码使用七个二进制位表达字一个字符, 最多表达128个字符, 其中可见字符96个, 控制字32个。
    后来由于ASCII不太够用, 国际标准化组织又制定了 ISO2022 标准, 它将ASCII字符集扩充为8位代码, 后续又制定了一批适用于不同地区的扩充ASCII字符集, 每种扩充ASCII字符集分别可以扩充128个字符, 这些扩充字符的编码均为高位为1的8位代码, 称为扩展ASCII码。
    拓展ASCII码现在使用并不太多, 在此不作详述。
### 3.2. ANSI编码
    为了扩充ASCII编码, 以用于显示本国的语言, 不同的国家和地区制定了不同的标准, 由此产生了针对不同国家的不同的编码标准, 称为ANSI, 全名American National Standards Institute Code, 中文译为“美国国家标准学会编码”, 又称为MBCS, 全称又称为“Muilti-Bytes Charecter Set”, 中文译为“多字节字符集”。这些标准使用2个字节来代表一个基本ASCII码以外的字符, 高位字节的高位必然为1, 用于和基本ASCII码区别。换而言之, ANSI并不是一种编码, 而是一类编码, 在上世纪90年代, 有些从国外引入的(尤其是从日本引入的)电子游戏运行时产生乱码就是因为这个原因, 虽然同为ANSI, 但是不同国家和地区使用的编码和对照的字符表完全不同。这也就催生了诸如“南极星”、“东方快车”之类的转码软件。
    中文常用的ANSI编码有GB2312、GBK、GB10801等, 各个编码的详细信息在上网搜索均有大量说明, 在此不再详述。
### 3.5. GB2312的编码与解码
2019-03-11 03:08:13 +00:00
   GB2312是中国大陆地区常用的中文编解码方式。GB2312编码也属于变长编码, 基本ASCII字符为0\~127, 占用一字节, 基本ASCII以外的字符均以2字节表达, 且每字节高位均为1。
2019-02-02 14:04:14 +00:00
    GB2312的非ASCII字符包括制表符、带圈数字、全角标点、全角英文字母、俄文字母、日语平假名、日语片假名、以及汉字6763个, 其中一级汉字3755个, 二级汉字3008个。其中一级汉字即我们俗称的常用字, 如果是制作嵌入式字库, 那么通常情况下汉字只包含一级汉字即可。
2019-03-11 03:09:27 +00:00
   GB2312编码表达汉字分为“页”和“码”两部分, 高位字节为“页码”, 低位字节为“字码”, 页码分为两大部分, 高四位为十六进制A的(0xAXXX)为符号, 高四位为十六进制B\~F的(0xBXXX~0xFXXX)为汉字,具体分配如下:
2019-02-02 14:04:14 +00:00
|页号|页码|表达内容|
|:-- |:-- |:-- |
2019-03-11 03:08:13 +00:00
|01\~09|A1\~A9|特殊符号,包括全角拉丁字母、日语假名、俄文西里尔字母等。|
|10\~15|AA\~AF|保留区,没有使用。|
|16\~55|B0\~D7|一级汉字。|
|56\~87|D8\~F7|二级汉字。|
|88\~94|F8\~FE|保留区,没有使用。|
2019-03-11 03:09:27 +00:00
2019-02-02 14:04:14 +00:00
    需要注意的是, GB2312的每一页都是没有用满的, 通常第一位留白, 最后一位留白, 其他的我还没有找到规律。例如GB2312编码的第一个汉字是“啊”, 编码是十六进制的B0A1, 而B0A0是空白的。
### 3.4. Unicode编码
    由于ANSI无法表达特定的编码, 在跨地域时乱码的问题几乎无可避免, 这就催生了Unicoed, 这种一个联合编码集, 意在使用一个字符集表达世界上所有语言所包含的所有书面符号(甚至是同一汉字的不同写法,如“户”和“戸”等),且每一个符号都有自己唯一的编码,这样一来,乱码的问题就迎刃而解了。
2019-12-01 15:04:02 +00:00
    Unicode码使用3字节的长度表达一个字符, 最高字节为平面(Panel)索引, 范围为0x00~0x10(十进制17), 低两字节为字符码, 表达范围为0~65535。也就是说, Unicode编码的表达范围为0x000000~0x10FFFF。
2019-02-02 14:04:14 +00:00
    其他Unicode编码的细节, 网上有详细介绍, 不作详述。
### 3.5. UTF-8的编码与解码
    UTF-8编码更准确的说, 应该属于字符串编码而非字符编码, 因为他规定的是一个Unicode字符在字符串中的表达形式。
    Unicode编码为全世界所有语言的所有字符提供了兼容的解决方案, 但是如何合理的使用才是真正的难题, 如果直接使用Unicode编码, 那么相比ASCII, 每个字符都要由1字节型变成4字节型, 高位以0补齐, 如果文本文件为纯英文, 那么内容不变的前提下, 文件体积将增加三倍。
    然而这还不是最致命的, 最致命的是文件编码的向下兼容问题。在ASCII编码下, 字符串以一字节的0x00结尾, 那么如果强制扩展到Unicode下, 所有基本ASCII字符就都变成了0x000000XX这样, 高三字节均为0x00, 在逻辑上均视为字符串终止, 这是不能接受的。
2019-03-11 03:09:27 +00:00
    于是, UTF-8编码, 提供了一种同时兼顾“节约空间”和“向下兼容”的方式来表达Unicode编码, 具体表达方式如下:
2019-02-02 14:04:14 +00:00
|Unicode编码(HEX)|UTF-8 字节流(BIN)|
|:-- |:-- |
2019-03-11 03:16:23 +00:00
|000000\~00007F|0XXXXXXX|
|000080\~0007FF|110XXXXX 10XXXXXX|
|000800\~00FFFF|1110XXXX 10XXXXXX 10XXXXXX|
|010000\~10FFFF|11110XXX 10XXXXXX 10XXXXXX 10XXXXXX|
2019-02-02 14:04:14 +00:00
    由上可见, UTF-8提供了一种变长编码格式, 忽略Unicode中高位字节为0x00的字节。
    在字符串中的解码方式为从第一个字节起, 如果字节最高位为0, 那么此字节表示一个基本ASCII字符, 编码为当前字节的值, 如果最高位为1, 那么从最高位向低, 遇到第一个0为止1的个数(至少为两个)表达字符占用的字节数(包括当前字节), 后续字节均为10开头, 那么取当前字节高位起第一个0之后的所有位以及后续特定数目字节的低六位(忽略开头的10), 拼接在一起, 即当前字符对应的Unicode码。
    依此规则, UTF-8格式的字符串就可以被逐一解析为Unicode编码, 并最终索引到对应字符。
## 4. 字库制作
### 4.1. 标准ASCII字库
    基本ASCII字库是最简单的, 总共包含96个可见字符, 编码自32(十六进制0x20)起, 至127止, 只需要对所有的可见字符取模即可, 字库数据数据占用的空间也不会很大。
2019-03-11 03:16:23 +00:00
   基本ASCII字库的编码方式有两种, 第一种是0\~31空白, 字符编码直接作为索引。另一种是去除0\~31的空白, 直接从32号开始, 字符编码作为索引时首先减去32。
   市面上成熟的字库芯片或带有字库芯片的显示屏通常情况下, 使用第一种方案, 同时在0\~31的位置上安排一些半角的特殊字符以充分利用空间或用于显示特殊控制字。
2019-02-02 14:04:14 +00:00
### 4.2. GB2312的中文字库
    前文详述了GB2312编码的编码方式和特征, GB2312编码的字符索引也比较容易计算了, 但是如果真的制作字库的话, 还存在一些问题。
    首先, 如果制作GB2312字库, 那么有必要包含基本ASCII字符。但是基本ASCII字符都是半角字符, 而GB2312中均为全角字符, 每一个GB2312字符使用的数据量均为ASCII字符的两倍。其次, GB2312中存在大量的空白页, 如果直接使用和计算索引, 那么字库文件势必造成大量不必要的存储空间浪费。
    综上, 个人总结出了一套制作GB2312字库的, 个人认为比较理想的方式。
    首先, 对字符的编码进行判断, 将字符分为三个区块进行编码, 分别为ASCII块, GB2312符号块和GB2312汉字块, 如果还要细分, 汉字块还可以分为一级汉字和二级汉字两块。
    GB2312中符号编码自A1A0至A9FF, 假设每个页除首尾两个位置外都具有有效字符, 那么在符号域, 字符的索引计算方式如下:
```
PB为GB2312的高位字节, 页码, CB为GB2312的低位字节, 字码, 则有:
字符索引 = (PB-0xA1)*94 + (CB-0xA1)
```
    同理,汉字的索引计算方式为:
```
PB为GB2312的高位字节, 页码, CB为GB2312的低位字节, 字码, 则有:
字符索引 = (CH-0xB0)*94 + (CL-0xA1)
```
    常数94的来源为, 每个页有96个字符位, 去除首尾各一个余白字符, 每页有94个有效字符。
    通过这样的方式, 就将AA~AF保留区的数据空间节省了出来。
    那么下一个问题就是中英文混编, 为了方便在连续的存储器空间中索引字符数据, 最简单的办法就是在索引字库数据时汉字的每半个字符当作一个字符来算。计算上只需要将计算出来的字符索引乘以2即可, 如下:
```
BH为GB2312的高位字节, BL为GB2312的低位字节, 字码, 则有:
字符数据索引 = 符号字库数据起始索引 + (((PB-0xA1)*94 + (CB-0xA1))*2)
```
    同理,汉字也可以做如下处理:
```
BH为GB2312的高位字节, BL为GB2312的低位字节, 字码, 则有:
字符数据索引 = 汉字字库数据起始索引 + (((CH-0xB0)*94 + (CL-0xA1))*2)
```
    这样一来, 就可以得到一个整齐的索引, 在读取数据时, 可以通过统一的偏移量来计算Flash的空间地址, 使读取算法更加简单高效。
####   参考SimpleGUI读取GB2312字库索引的函数实现:
```c++
SGUI_SIZE SGUI_Text_GetCharacterTableIndex(SGUI_UINT16 uiCharacterCode)
{
/*----------------------------------*/
/* Variable Declaration */
/*----------------------------------*/
SGUI_UINT16 uiCharacterCodeHighByte;
SGUI_UINT16 uiCharacterCodeLowByte;
SGUI_SIZE uiFontTableIndex;
/*----------------------------------*/
/* Initialize */
/*----------------------------------*/
uiCharacterCodeHighByte = (uiCharacterCode >> 8) & 0x00FF;
uiCharacterCodeLowByte = uiCharacterCode & 0x00FF;
/*----------------------------------*/
/* Process */
/*----------------------------------*/
// ASCII code.
if((0 == uiCharacterCodeHighByte) & & (uiCharacterCodeLowByte < 128 ) )
{
uiFontTableIndex = (uiCharacterCodeLowByte + FONT_LIB_OFFSET_ASCII);
}
// GB2312 punctuation
else if((0xAA > uiCharacterCodeHighByte) & & (0xA0 < uiCharacterCodeHighByte ) )
{
uiFontTableIndex = ((((uiCharacterCodeHighByte-0xA1)*94 + (uiCharacterCodeLowByte-0xA1))*2) + FONT_LIB_OFFSET_GB2312_SYMBOL);
}
// GB2312 level one character.
else if((0xF8 > uiCharacterCodeHighByte) & & (0xAF < uiCharacterCodeHighByte ) )
{
uiFontTableIndex = ((((uiCharacterCodeHighByte-0xB0)*94 + (uiCharacterCodeLowByte-0xA1))*2) + FONT_LIB_OFFSET_GB2312_CHARL1);
}
// Other to return full width space.
else
{
uiFontTableIndex = FONT_LIB_OFFSET_GB2312_SYMBOL; // Full-size space.
}
return uiFontTableIndex;
}
```
### 4.3. 定制字库
    中文支持固然好, 但是对静态存储资源的消耗也相当可观, 以SimpleGUI默认的12像素字体为例, 每个半角字符需要消耗6*2字节, 全角字符需要消耗12*2字节, 那么整个GB2312字库对Flash的消耗为:
```
(6*2)*96+(12*2)*3755+(12*2)*682 = 53820 Bytes
```
。
2019-03-11 03:16:23 +00:00
    53KB的Flash消耗对于单片机来说还是非常可观的, 这还仅仅包含了一级汉字, 如果再加上二级汉字, 那么还需要额外的7.2KB空间,如果还需要其他不同大小的字体,空间资源需求还要成倍增加,所以这种时候,通常就要使用外部字库了。
2019-02-02 14:04:14 +00:00
    但事实上,很多时候,界面上的文字都是既定好的,不需要动态的变化,这也就决定了整个系统中可能仅仅就需要使用少数的几个字符。这时候如果还要去设计外部字库显然对软硬件成本都是一种浪费,精简字库消耗才是正途。
    通过前文的说明,字符和字符串的编码,说到底不过就是从字符编码到字库索引上的一种算法表达,精简字库,说到底也是从这一方面着手,删除掉字库中用不到的文字和符号,重新定义编码和索引,字库的精简就完成了。
    基于以上思想,精简字库首先要列出整个目标系统中所有可能用到的文字,然后进行去重,提炼出目标系统中用到的所有汉字。然后给这些汉字进行重新编码,简而言之就是进行简单排序,然后重新编号,这个编号就是新规定的字符编码。最后,用这个新的编码重新去对字符串进行编码,这样就完成了字库的精简了。
    举个例子,假设我们要编写一个万年历,要求显示时间和二十四节气,那么可以预知,要显示的文字包括数字、半角冒号、大写数字、还有二十四节气的名字。需要用到的文字如下:
```
:1234567890
一二三四五六七八九十廿卅
年月日时分秒
立春
雨水
惊蛰
春分
清明
谷雨
立夏
小满
芒种
夏至
小暑
大暑
立秋
处暑
白露
秋分
寒露
霜降
立冬
小雪
大雪
冬至
小寒
大寒
```
    基本上要用的文字资源就这些了,然后对这些文字进行提炼,删除掉重复的文字,得到如下资源:
```
0123456789:
一七三九二五八六冬分十卅四处夏大寒小年廿惊日时明春暑月水清满白秋种秒立至芒蛰谷降雨雪霜露
```
    可以看到, 删除掉重复文字后, 内容明显少了很多, 按照上述资源, 对每一个文字进行重新编号, 也就是编码, 同时根据前文保持步进一致以方便数据寻址的原则, 汉字等全角字符全部间隔一个编号, 例如上述字符“0”编号为0x00, 字符“:”编号为0x0A, 字符“一”编号为0x800B, 而字符七则编号为“0x800D”, 中间跳过一个编码0x800C, 此处为了区别半角与全角字符, 所有非ASCII字符全部以0x80开头, 占两字节。
    通过以上的编码操作, 我们就可以再万年历系统中, 对需要的字符串给出新的编码定义, 例如立春, 标准的GB2312编码应为:
```
0x33,0x02,0x20,0x26
```
    而依照我们自定义的新编码应为:
```
0x80,0x4F,0x80,0x3B
```
    以此类推, 其他23个节气的新编码分别为:
```
0x80,0x5B,0x80,0x41
0x80,0x33,0x80,0x55
0x80,0x3B,0x80,0x1D
0x80,0x43,0x80,0x39
0x80,0x57,0x80,0x5B
0x80,0x4F,0x80,0x27
0x80,0x2D,0x80,0x45
0x80,0x53,0x80,0x4B
0x80,0x27,0x80,0x51
0x80,0x2D,0x80,0x3D
0x80,0x29,0x80,0x3D
0x80,0x4F,0x80,0x49
0x80,0x25,0x80,0x3D
0x80,0x47,0x80,0x61
0x80,0x49,0x80,0x1D
0x80,0x2B,0x80,0x61
0x80,0x5F,0x80,0x59
0x80,0x4F,0x80,0x1B
0x80,0x2D,0x80,0x5D
0x80,0x29,0x80,0x5D
0x80,0x1B,0x80,0x51
0x80,0x2D,0x80,0x2B
0x80,0x29,0x80,0x2B
```
    反映到C语言代码中为:
```c++
{"\x80\x4F\x80\x3B"}, // 立春
{"\x80\x5B\x80\x41"}, // 雨水
{"\x80\x33\x80\x55"}, // 惊蛰
{"\x80\x3B\x80\x1D"}, // 春分
{"\x80\x43\x80\x39"}, // 清明
{"\x80\x57\x80\x5B"}, // 谷雨
{"\x80\x4F\x80\x27"}, // 立夏
{"\x80\x2D\x80\x45"}, // 小满
{"\x80\x53\x80\x4B"}, // 芒种
{"\x80\x27\x80\x51"}, // 夏至
{"\x80\x2D\x80\x3D"}, // 小暑
{"\x80\x29\x80\x3D"}, // 大暑
{"\x80\x4F\x80\x49"}, // 立秋
{"\x80\x25\x80\x3D"}, // 处暑
{"\x80\x47\x80\x61"}, // 白露
{"\x80\x49\x80\x1D"}, // 秋分
{"\x80\x2B\x80\x61"}, // 寒露
{"\x80\x5F\x80\x59"}, // 霜降
{"\x80\x4F\x80\x1B"}, // 立冬
{"\x80\x2D\x80\x5D"}, // 小雪
{"\x80\x29\x80\x5D"}, // 大雪
{"\x80\x1B\x80\x51"}, // 冬至
{"\x80\x2D\x80\x2B"}, // 小寒
{"\x80\x29\x80\x2B"}, // 大寒
```
    至此, 经过统计字库大小由143文字精简至99文字( 全角字符一个算两文字) , 实现了预期的精简字库的效果。
    同时, 为了方便这个文字提取与重编码的操作, 我还编写了一个小工具MinimumFontLib, 您可以访问码云上[MinimumFontLib的托管页面](https://gitee.com/Polarix/MinimumFontLib)获取该工具的可执行文件与源码,欢迎您试用和反馈。
### 5. 联系开发者
2019-12-01 15:04:02 +00:00
    首先, 感谢您对SimpleGUI的赏识与支持。
2019-02-02 14:04:14 +00:00
    虽然最早仅仅作为一套GUI接口库使用, 但我最终希望SimpleGUI能够为您提供一套完整的单色屏GUI及交互设计解决方案, 如果您有新的需求、提议亦或想法, 欢迎在以下地址留言, 或加入[QQ交流群799501887](https://jq.qq.com/?_wv=1027& k=5ahGPvK)留言交流。
>SimpleGUI@开源中国: https://www.oschina.net/p/simplegui
>SimpleGUI@码云: https://gitee.com/Polarix/simplegui
2019-03-06 13:44:21 +00:00
    本人并不是全职的开源开发者,依然有工作及家庭的琐碎事务要处理,所以对于大家的需求和疑问反馈的可能并不及时,多有怠慢,敬请谅解。
2019-02-02 14:04:14 +00:00
    最后,再次感谢您的支持。