4、BDF 字体文件解析:BDF 文件结构、读取字符位图数据、解析属性信息

BDF(Bitmap Distribution Format)是嵌入式 GUI 领域最经典的字体格式之一。说白了,它就是一套纯文本的字体描述规范。你拿任何文本编辑器打开一个 .bdf 文件,看到的都是可读的 ASCII 字符。这跟那些二进制字体格式比起来,调试起来简直不要太爽。

我个人习惯把 BDF 文件看作三部分:文件头、字符索引、以及每个字符的位图数据。咱们这一章就把这三块彻底拆开,看看里面到底藏了什么。

核心认知:BDF 的本质是「文本化的像素矩阵」。每个字符的位图就是一张黑白点阵图,0 代表背景,1 代表前景。嵌入式系统里,我们最终要提取的就是这些 0/1 数据,然后转换成 C 语言数组。

4.1 BDF 文件整体结构

一个标准的 BDF 文件,从第一行到最后一行,遵循严格的顺序。我刚开始接触时,以为可以随便写,结果解析器直接报错。嗯,这里要注意,顺序错了就是格式错误。

下面这张图是我自己整理的 BDF 文件结构,你看一眼就能明白它的骨架:

BDF 字体文件结构 文件头(Header) STARTFONT 2.1 → FONT → SIZE → FONTBOUNDINGBOX 属性信息(Properties) FAMILY_NAME → WEIGHT_NAME → COPYRIGHT → FONT_ASCENT / FONT_DESCENT 字符索引(Chars) CHARS → ENCODING → SWIDTH → DWIDTH → BBX → BITMAP 位图数据(Bitmap Data) 十六进制行 → ENDCHAR → 下一个字符 ENDFONT

你看,整个文件就像一条流水线。从上往下依次是:文件头、属性、字符索引、位图数据,最后以 ENDFONT 收尾。每个部分都有固定的关键字作为标记。

4.2 文件头解析

文件头是 BDF 文件的身份证。它告诉解析器:这是什么版本、字体叫什么、字号多大、整体边界框是多少。

一个典型的文件头长这样:

STARTFONT 2.1
FONT -Adobe-Courier-Bold-R-Normal--24-240-75-75-M-150-ISO10646-1
SIZE 24 75 75
FONTBOUNDINGBOX 24 34 0 -7

我来逐行解释一下:

  • STARTFONT 2.1:版本号。目前主流是 2.1,2.0 也有但很少见了。
  • FONT:字体的 XLFD 名称。这串东西看着复杂,其实它包含了字体家族、风格、像素大小、分辨率等信息。解析时我们通常只提取家族名和像素大小。
  • SIZE 24 75 75:第一个数字是点阵大小(24pt),后面两个是水平和垂直分辨率(75 dpi)。
  • FONTBOUNDINGBOX 24 34 0 -7:这是全局边界框。四个数字分别代表:宽度、高度、X 偏移、Y 偏移。说白了,就是整个字体中最大的字符占多大地方。

我的经验:解析 FONTBOUNDINGBOX 时,Y 偏移通常是负数。这是因为 BDF 的坐标系原点在左下角,而字符的基线在底部。我曾经踩过这个坑,直接把负数当正数用,结果字符全部跑偏了。

4.3 属性信息解析

属性信息位于文件头之后,以 STARTPROPERTIES 开头,以 ENDPROPERTIES 结束。这里面存放了字体的元数据。

常见的属性字段:

属性关键字 含义 解析建议
FAMILY_NAME 字体家族名称 直接提取字符串
WEIGHT_NAME 字重(Bold/Medium) 用于区分粗细
COPYRIGHT 版权信息 可选,可忽略
FONT_ASCENT 字体上升高度(基线以上) 必须解析,用于排版
FONT_DESCENT 字体下降高度(基线以下) 必须解析,用于排版
DEFAULT_CHAR 缺省字符编码 找不到字符时使用

举个例子:

STARTPROPERTIES 20
FAMILY_NAME "Courier"
WEIGHT_NAME "Bold"
FONT_ASCENT 18
FONT_DESCENT 6
DEFAULT_CHAR 0
ENDPROPERTIES

这里 FONT_ASCENT 和 FONT_DESCENT 特别重要。它们决定了字符在垂直方向上的位置。你想想看,如果这两个值解析错了,中文字符和英文字符混排时,上下会错位得很明显。

注意:有些 BDF 文件没有 STARTPROPERTIES 段,而是直接把属性写在文件头后面。这种情况多见于老版本 BDF。解析时要做兼容处理,先检查有没有 STARTPROPERTIES 关键字。

4.4 字符索引与位图数据

这是 BDF 解析的核心。每个字符以 STARTCHAR 开头,以 ENDCHAR 结束。中间包含了字符的编码、宽度、边界框和位图数据。

一个完整的字符定义:

STARTCHAR A
ENCODING 65
SWIDTH 600 0
DWIDTH 12 0
BBX 12 18 0 -3
BITMAP
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
ENDCHAR

关键字段解析:

  • ENCODING:字符的 Unicode 编码。65 就是大写字母 'A'。
  • SWIDTH:缩放宽度,单位是 1/1000 像素。嵌入式系统里基本不用。
  • DWIDTH:设备宽度,单位是像素。这个值决定了字符在水平方向上的步进距离。
  • BBX:字符的边界框。四个数字:宽度、高度、X 偏移、Y 偏移。注意,这个偏移是相对于字符原点的。
  • BITMAP:后面跟着的就是位图数据。每行是一个十六进制数,代表一行的像素点阵。

位图数据的解析逻辑是这样的:

假设 BBX 的宽度是 12,那么每行需要 12 个 bit。12 个 bit 用十六进制表示,需要 2 个字节(16 bit),高位在前。比如 0x0F 表示二进制 0000 1111,对应像素就是 4 个白点、4 个黑点。

我写了一个简单的解析伪代码,你看一下:

// 假设 bbx_width = 12, bbx_height = 18
// 每行字节数 = (bbx_width + 7) / 8 = 2
for (int row = 0; row < bbx_height; row++) {
    uint16_t line_data = hex_to_uint16(bitmap_lines[row]);
    for (int col = 0; col < bbx_width; col++) {
        // 从高位开始取 bit
        int bit = (line_data >> (15 - col)) & 0x01;
        pixel_buffer[row][col] = bit;
    }
}

关键点:位图数据是「高位在前」的。也就是说,十六进制数的最高位对应像素的最左边。这个顺序搞反了,字符就会左右镜像。我在项目中遇到过这个问题,调试了半天才发现是字节序搞错了。

4.5 实战:提取字符位图到 C 数组

嵌入式系统里,我们最终要把 BDF 的位图数据转换成 C 语言的 const 数组。这样就能直接烧录到 Flash 里,运行时直接读取。

转换步骤:

  1. 解析 BBX,得到字符的宽度和高度。
  2. 读取 BITMAP 后面的每一行十六进制数据。
  3. 按行逐 bit 提取,生成一个二维数组。
  4. 为了节省空间,通常按字节打包。比如宽度 12 的字符,每行用 2 个字节存储。

最终生成的 C 代码示例:

// 字符 'A' (编码 0x41)
// 宽度: 12, 高度: 18
const uint8_t font_char_A[18][2] = {
    {0x00, 0x00},
    {0x00, 0x00},
    {0x00, 0x00},
    {0x00, 0x00},
    {0x00, 0x00},
    {0x00, 0x00},
    {0x00, 0x00},
    {0x00, 0x00},
    {0x00, 0x00},
    {0x00, 0x00},
    {0x00, 0x00},
    {0x00, 0x00},
    {0x00, 0x00},
    {0x00, 0x00},
    {0x00, 0x00},
    {0x00, 0x00},
    {0x00, 0x00},
    {0x00, 0x00}
};

我的建议:不要直接用手工转换。写一个 Python 脚本,批量解析整个 BDF 文件,自动生成 .h 和 .c 文件。我自己的工具链里,这个脚本大概 200 行,能处理 95% 的 BDF 文件。遇到不标准的文件,再手动微调。

4.6 常见坑与避坑指南

解析 BDF 文件时,有几个坑我反复踩过,今天一并告诉你:

  • 坑一:BBX 偏移为负。Y 偏移通常是负数,表示字符基线以下的部分。解析位图时,要把这个偏移考虑进去,否则字符会上下错位。
  • 坑二:DWIDTH 不等于 BBX 宽度。DWIDTH 是字符的步进宽度,BBX 是实际像素宽度。两者可能不同。比如小写字母 'i',BBX 可能只有 4 像素,但 DWIDTH 是 8 像素,因为后面要留空白。
  • 坑三:位图行数不等于 BBX 高度。有些 BDF 文件会在位图数据前后加空行。解析时要以 BITMAP 和 ENDCHAR 为界,中间的行数必须等于 BBX 高度,否则就是格式错误。
  • 坑四:编码重复。同一个 ENCODING 出现两次,以最后一次为准。解析时要做覆盖处理。

曾经我遇到过一个坑:某个第三方提供的 BDF 字体,FONTBOUNDINGBOX 的宽度是 20,但实际字符的 BBX 宽度最大只有 18。结果我按 20 分配缓冲区,浪费了内存。后来我改成动态计算,每个字符按自己的 BBX 分配空间,内存占用直接降了 15%。

4.7 本章小结

BDF 文件解析,说白了就是三件事:读头、读属性、读位图。头决定全局参数,属性提供元数据,位图才是真正的像素数据。嵌入式系统里,我们最终要的是那个 0/1 矩阵,其他都是辅助信息。

我个人习惯在解析完成后,做一个可视化验证——把位图数据打印成字符画,用 '#' 表示前景,'.' 表示背景。一眼就能看出解析对不对。这个方法我用了很多年,从来没失手过。