3、点阵字体原理:像素矩阵、字模提取算法、BDF 格式解析
点阵字体,说白了就是用一个二维的像素网格来描绘每个字符。你想想看,一个「中」字,在 16×16 的格子里,哪些点涂黑、哪些点留白,这就是最原始的字体数据。我在做嵌入式 GUI 的初期,就是靠这种最笨的办法让屏幕显示汉字的。
这一节,我会带你从像素矩阵的底层结构讲起,再到如何从标准字体文件中提取字模,最后解析一种非常通用的点阵字体格式——BDF。嗯,这些都是我实际项目中反复用到的技能。
3.1 像素矩阵:点阵字体的最小单元
每个字符本质上是一个 位图。比如一个 8×8 的点阵,就是 8 行、每行 8 个像素点。每个像素点要么亮(1),要么灭(0)。
我个人习惯把这种矩阵叫做「字模位图」。它的大小直接决定了字体的清晰度和存储开销。
| 点阵尺寸 | 每字字节数 | 适用场景 |
|---|---|---|
| 8×8 | 8 字节 | 极简状态显示(如 LED 点阵) |
| 12×12 | 18 字节 | 小屏菜单、数字时钟 |
| 16×16 | 32 字节 | 嵌入式 GUI 常用汉字 |
| 24×24 | 72 字节 | 高分辨率屏幕、标题字体 |
关键点:点阵尺寸越大,字形越平滑,但存储空间也成倍增长。在资源受限的 MCU 上,16×16 是一个比较折中的选择。
3.2 字模提取算法:从像素到字节
字模提取,就是把像素矩阵转换成计算机能存储的字节序列。常见的扫描方式有两种:逐行扫描和逐列扫描。
我曾经在一个项目里,因为搞错了扫描方向,导致所有汉字显示都是镜像的。嗯,那会儿排查了整整一个下午。
3.2.1 逐行扫描(水平扫描)
这是最直观的方式。从左上角开始,从左到右扫描每一行,每 8 个像素组成一个字节。如果一行不足 8 个像素,就补零。
举个例子,一个 8×8 的「A」字模:
// 假设像素矩阵(1 表示黑点,0 表示白点)
行0: 0 0 1 1 1 1 0 0 → 0x3C
行1: 0 1 0 0 0 0 1 0 → 0x42
行2: 1 0 0 0 0 0 0 1 → 0x81
行3: 1 0 0 0 0 0 0 1 → 0x81
行4: 1 1 1 1 1 1 1 1 → 0xFF
行5: 1 0 0 0 0 0 0 1 → 0x81
行6: 1 0 0 0 0 0 0 1 → 0x81
行7: 1 0 0 0 0 0 0 1 → 0x81
// 最终字模数组
uint8_t font_A[8] = {0x3C, 0x42, 0x81, 0x81, 0xFF, 0x81, 0x81, 0x81};
3.2.2 逐列扫描(垂直扫描)
这种方式在部分 LCD 驱动芯片中更常见。从左上角开始,从上到下扫描每一列。
同样的「A」字,逐列扫描的结果完全不同:
// 逐列扫描结果
列0: 0 0 1 1 1 1 1 1 → 0x3F
列1: 0 1 0 0 0 0 0 0 → 0x40
列2: 1 0 0 0 1 0 0 0 → 0x88
列3: 1 0 0 0 1 0 0 0 → 0x88
列4: 1 0 0 0 1 0 0 0 → 0x88
列5: 1 0 0 0 1 0 0 0 → 0x88
列6: 0 1 0 0 0 0 0 0 → 0x40
列7: 0 0 1 1 1 1 1 1 → 0x3F
注意:提取字模前,一定要确认你的显示驱动芯片期望的扫描方向。搞反了,字就是镜像或者旋转的。
3.3 BDF 格式解析:工业标准的点阵字体
BDF(Bitmap Distribution Format)是 Adobe 提出的一种纯文本格式,用来描述点阵字体。它的最大好处是——你可以直接用文本编辑器打开看。
我建议你手头准备一个 BDF 文件,边看边学。下面是一个典型的 BDF 字符定义片段:
STARTCHAR A
ENCODING 65
SWIDTH 1000 0
DWIDTH 8 0
BBX 8 8 0 -1
BITMAP
3C
42
81
81
FF
81
81
81
ENDCHAR
我们来逐行拆解一下:
- STARTCHAR A:字符名称,通常就是字符本身。
- ENCODING 65:字符的 Unicode 编码,65 对应 ASCII 的 'A'。
- SWIDTH 1000 0:在 PostScript 坐标系中的缩放宽度,嵌入式一般用不到。
- DWIDTH 8 0:设备宽度,表示显示这个字符后,光标向右移动 8 个像素。
- BBX 8 8 0 -1:字符的实际边界框(Bounding Box)。参数依次为:宽度、高度、X 偏移、Y 偏移。这里的 -1 表示字符底部比基线低 1 个像素。
- BITMAP ... ENDCHAR:字模数据,每行一个字节的十六进制表示。
小技巧:BBX 的偏移参数经常被忽略,但它决定了字符之间的间距和对齐。我曾经因为没处理 Y 偏移,导致中文和英文在同一行时上下错位。
3.4 知识体系总览
下面这张图,把点阵字体的核心逻辑串起来了。从像素矩阵到字模提取,再到 BDF 格式解析,每一步都有明确的输入和输出。
3.5 实战:手写一个简单的 BDF 解析器
理解了 BDF 的结构后,我们可以写一个轻量级的解析函数。这个函数只提取我们最关心的字段:编码、宽度、高度、偏移和字模数据。
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
typedef struct {
unsigned int encoding;
int dwidth;
int bbw, bbh, bbx, bby;
unsigned char *bitmap;
} BDF_Char;
int parse_bdf_char(FILE *fp, BDF_Char *ch) {
char line[256];
int bitmap_lines = 0;
int line_count = 0;
while (fgets(line, sizeof(line), fp)) {
if (strncmp(line, "ENCODING", 8) == 0) {
sscanf(line, "ENCODING %u", &ch->encoding);
} else if (strncmp(line, "DWIDTH", 6) == 0) {
sscanf(line, "DWIDTH %d", &ch->dwidth);
} else if (strncmp(line, "BBX", 3) == 0) {
sscanf(line, "BBX %d %d %d %d",
&ch->bbw, &ch->bbh, &ch->bbx, &ch->bby);
bitmap_lines = ch->bbh;
ch->bitmap = (unsigned char*)malloc(bitmap_lines);
} else if (strncmp(line, "BITMAP", 6) == 0) {
for (int i = 0; i < bitmap_lines; i++) {
fgets(line, sizeof(line), fp);
ch->bitmap[i] = (unsigned char)strtol(line, NULL, 16);
}
return 1; // 解析成功
}
}
return 0; // 未找到完整字符
}
提示:实际项目中,BDF 文件可能包含数百个字符。建议先解析整个文件的全局属性(如 FONTBOUNDINGBOX),再逐个解析字符。这样可以提前分配好内存。
3.6 避坑指南
最后,分享几个我踩过的坑:
- 字节序问题:有些 BDF 文件使用大端字节序,有些使用小端。解析时要注意。
- BBX 偏移为负:部分字符(如 'g'、'p')的 Y 偏移是负数,表示字符下沉到基线以下。显示时要做偏移处理。
- 空字符处理:空格字符通常没有字模数据,但 DWIDTH 不能为 0,否则排版会乱。
- 内存泄漏:解析完一个字符后,记得释放 bitmap 内存,或者用池化管理。