QMK QFF 字体格式完全解析:从二进制块结构到 QMK Painter 源码实现
2026/9/14 22:23:19 网站建设 项目流程

QMK QFF 字体格式完全解析:从二进制块结构到 QMK Painter 源码实现

【免费下载链接】qmk_firmwareOpen-source keyboard firmware for Atmel AVR and Arm USB families项目地址: https://gitcode.com/GitHub_Trending/qm/qmk_firmware

QMK 的 Quantum Painter 绘图子系统使用一种名为QFF(Quantum Font Format,量子字体格式)的自定义字体文件格式,专为资源受限的单片机环境设计。本文以 docs/quantum_painter_qff.md 的格式定义为核心骨架,结合 quantum/painter/qff.h、quantum/painter/qff.c 和 quantum/painter/qp_draw_text.c 的源码实现,完整讲解 QFF 的每个二进制块、字段的位级编码、解析校验流程,以及字体在键盘固件中的实际加载与绘制调用链。读完后你能够:理解 QFF 与 QGF 格式的复用关系、手工解读一个.qff字节的块结构、并在 QMK 中使用 Painter 字体 API 绘制文本。

一、QFF 是什么:为资源受限系统设计的字体容器

QMK 官方文档(docs/quantum_painter_qff.md)对 QFF 的定义是:

QMK uses a font format ("Quantum Font Format" - QFF) specifically for resource-constrained systems.

其核心能力有三点:

  • 支持 1、2、4、8 bit-per-pixel 的灰度(greyscale)与调色板(palette)位图:低 bpp 格式能在 OLED/TFT 面板的有限显存下渲染多灰度级别字形;
  • 内置 RLE 压缩:像素数据支持游程编码,减少 flash 占用与传输时间;
  • 块(block)化布局:整个文件由若干块串联而成,每块自带 header,解析器可以按typeid跳过不关心的部分。

所有整数字段均为**小端(little-endian)**字节序,所有结构体在 C 侧以 packed 方式定义、字段之间无填充。这一点与源码 quantum/painter/qff.h 中typedef struct PACKED qff_font_descriptor_v1_t的写法完全对应,并由STATIC_ASSERT(sizeof(qff_font_descriptor_v1_t) == ...)等编译期断言锁死大小,防止未来编译器的 ABI 变化悄悄破坏二进制兼容性。

二、文件总体结构

一个 QFF 文件由以下块按顺序组成(引自 docs/quantum_painter_qff.md 并对应 quantum/painter/qff.c 中的校验顺序):

顺序typeid是否必需
1Font descriptor block(字体描述块)0x00必需,且只能出现一次
2ASCII glyph block(ASCII 字形表)0x01可选,仅当包含 ASCII 字形时
3Unicode glyph block(Unicode 字形表)0x02可选,仅当包含 Unicode 字形时
4Font palette block(调色板块)0x03可选,仅当字体是调色板格式时
5Font data block(像素数据块)0x04必需,位于文件末尾

每块由一个header(包含typeid与后续 blob 的length)加可选的blob数据组成。块结构在 quantum/painter/qff.c 的qff_validate_stream()中得到印证:解析器先读字体描述块,再根据描述块中的has_ascii_tablenum_unicode_glyphs字段决定是否依次验证 ASCII 表块和 Unicode 表块——与文档描述的"按存在性顺序排列"严格一致。

三、块头(Block Header):5 字节的统一信封

QFF 的块头与 QGF(Quantum Graphics Format,见 docs/quantum_painter_qgf.md)完全相同。源码定义在 quantum/painter/qgf.h:

typedef struct PACKED qgf_block_header_v1_t { uint8_t type_id; // See each respective block type below. uint8_t neg_type_id; // Negated type ID, used for detecting parsing errors. uint32_t length : 24; // 24-bit blob length, allowing for block sizes of a maximum of 16MB. } qgf_block_header_v1_t; STATIC_ASSERT(sizeof(qgf_block_header_v1_t) == 5, "qgf_block_header_v1_t must be 5 bytes in v1 of QGF");

字段说明:

  • type_id:块类型标识,QFF 中取值 0x00~0x04(见第二节表格);
  • neg_type_idtype_id按位取反值,用于解析错误检测——若两个值不互为补数,说明内存或流已损坏;
  • length:24 位 blob 长度,单块最大约 16 MB(对小体积的单片机 flash 而言绰绰有余)。

校验逻辑在 quantum/painter/qgf.h 声明的qgf_validate_block_header()中统一实现,QFF 的每个块解析函数都会调用它,例如 quantum/painter/qff.c 处对字体描述块的校验。

四、字体描述块(Font Descriptor Block,typeid = 0x00)

这是 QFF 的第一个块,必须位于文件内容起始处,整个文件中至多出现一次。块头之后紧跟 20 字节的描述数据,文档给出的结构定义与 quantum/painter/qff.h 的实现一致:

typedef struct PACKED qff_font_descriptor_v1_t { qgf_block_header_v1_t header; // = { .type_id = 0x00, .neg_type_id = (~0x00), .length = 20 } uint32_t magic : 24; // constant, equal to 0x464651 ("QFF") uint8_t qff_version; // constant, equal to 0x01 uint32_t total_file_size; // total size of the entire file, starting at offset zero uint32_t neg_total_file_size; // negated value of total_file_size, used for detecting parsing errors uint8_t line_height; // glyph height in pixels bool has_ascii_table; // whether the font has an ascii table of glyphs (0x20...0x7E) uint16_t num_unicode_glyphs; // the number of glyphs in the unicode table -- no table specified if zero qp_image_format_t format : 8; // Frame format, see qp.h. uint8_t flags; // frame flags, see below. uint8_t compression_scheme; // compression scheme, see below. uint8_t transparency_index; // palette index used for transparent pixels (not yet implemented) } qff_font_descriptor_v1_t; #define QFF_MAGIC 0x464651

逐字段解读:

  • magic= 0x464651,即 ASCII 的 "QFF";
  • qff_version恒为 0x01(当前格式版本);
  • total_file_size/neg_total_file_size:文件总大小及其按位取反值,双重校验防解析错位;
  • line_height:字形行高(像素),是qp_drawtext()渲染时的行高来源;
  • has_ascii_table:是否包含 0x20~0x7E 的 ASCII 字形表;
  • num_unicode_glyphs:Unicode 字形数量,为 0 表示无 Unicode 表;
  • format/flags/compression_scheme/transparency_index:与 QGF 的 frame descriptor block(见 docs/quantum_painter_qgf.md)取值一致,唯一区别是delta标志位被 QFF 忽略——字体没有"增量帧"的概念;
  • transparency_index:调色板中用于透明像素的索引(文档注明 "not yet implemented",源码注释同样如此)。

源码中的校验流程在 quantum/painter/qff.c 的qff_read_font_descriptor():先整块读入 25 字节描述符,再依次检查块头合法性、magic/版本、文件大小取反一致性,最后把所需字段解引用输出。失败路径均通过qp_dprintf打印原因,便于用QUANTUM_PAINTER_VERBOSE一类调试选项定位坏字体。

一个可以直观对照的真实样本是 keyboards/boardsource/equals/graphics/thintel15.qff.c:该数组开头0x00, 0xFF, 0x14, 0x00, ...即块头type_id=0x00neg_type_id=0xFFlength=0x000014(24 位小端 = 20),紧接着0x51, 0x46, 0x46是小端存储的 magic "QFF"(0x464651),随后0x01是版本号——与上面的字段定义逐字节吻合。

五、ASCII 字形表(ASCII Glyph Table,typeid = 0x01)

若字体包含 ASCII 字符,该块必须紧跟在字体描述块之后。固定长度为 290 字节(5 字节块头 + 95 × 3 字节字形项)。源码定义见 quantum/painter/qff.h:

#define QFF_GLYPH_WIDTH_BITS 6 #define QFF_GLYPH_WIDTH_MASK ((1 << QFF_GLYPH_WIDTH_BITS) - 1) #define QFF_GLYPH_OFFSET_BITS 18 #define QFF_GLYPH_OFFSET_MASK (((1 << QFF_GLYPH_OFFSET_BITS) - 1) << QFF_GLYPH_WIDTH_BITS) typedef struct PACKED qff_ascii_glyph_v1_t { uint32_t value : 24; // Uses QFF_GLYPH_*_(BITS|MASK) as bitfield ordering is compiler-defined } qff_ascii_glyph_v1_t; typedef struct PACKED qff_ascii_glyph_table_v1_t { qgf_block_header_v1_t header; // = { .type_id = 0x01, .neg_type_id = (~0x01), .length = 285 } qff_ascii_glyph_v1_t glyph[95]; // 95 glyphs, 0x20..0x7E } qff_ascii_glyph_table_v1_t;

每个字形项是一个 24 位(3 字节)整型,采用位域方式打包两个信息:

位域宽度含义
低 6 位QFF_GLYPH_WIDTH_BITS= 6字形宽度(像素),最大 63
高 18 位QFF_GLYPH_OFFSET_BITS= 18该字形像素数据在 Font data block 内的字节偏移,最大约 262 KB

ASCII 表按0x20~0x7E顺序索引:第 N 项(N = code_point − 0x20)对应字符 N。渲染时源码直接按此偏移量随机访问,见 quantum/painter/qp_draw_text.c:

uint32_t glyph_info_offset = sizeof(qff_font_descriptor_v1_t) + sizeof(qgf_block_header_v1_t) + (code_point - 0x20) * sizeof(qff_ascii_glyph_v1_t); ... uint8_t glyph_width = (uint8_t)(glyph_info.value & QFF_GLYPH_WIDTH_MASK); uint32_t glyph_offset = ((glyph_info.value & QFF_GLYPH_OFFSET_MASK) >> QFF_GLYPH_WIDTH_BITS);

这也解释了为什么位宽是 6+18 的划分:宽度字段只需覆盖面板上单个字符的合理像素宽,而 18 位偏移足以覆盖整个像素数据块。

六、Unicode 字形表(Unicode Glyph Table,typeid = 0x02)

若字体包含 Unicode 字符(例如中文标点或符号字形),该块必须位于 ASCII 表之后;若字体不含 ASCII 字符,则直接跟在字体描述块之后。块长可变,每个字形条目 6 字节:

typedef struct PACKED qff_unicode_glyph_v1_t { uint32_t code_point : 24; uint32_t value : 24; // Uses QFF_GLYPH_*_(BITS|MASK) as bitfield ordering is compiler-defined } qff_unicode_glyph_v1_t; // 共 6 字节 typedef struct PACKED qff_unicode_glyph_table_v1_t { qgf_block_header_v1_t header; // length = (N * 6) qff_unicode_glyph_v1_t glyph[0]; // 变长,紧随其后的 N 个字形条目 } qff_unicode_glyph_table_v1_t;

与 ASCII 表相比,Unicode 表是稀疏映射:每个条目显式携带 24 位code_point(统一码点,最高支持 U+FFFF,足够覆盖 BMP),后 24 位的value编码规则与 ASCII 字形完全相同(低 6 位宽度、高 18 位数据偏移)。由于是线性查找,渲染端按顺序读取每个条目比较code_point,见 quantum/painter/qp_draw_text.c——这解释了文档"位于 Unicode 表的字符数即块内条目数"与源码qff_validate_unicode_descriptor()num_unicode_glyphs * 6长度校验的对应关系。

值得注意的一个细节:即使字体带完整 ASCII 表,代码点不在 0x20~0x7E 范围(或未命中 ASCII 表)时仍会走 Unicode 表查找路径,因此"仅部分 ASCII + 若干 Unicode 字形"的混合字体是被支持的。

七、调色板块(Font Palette Block,typeid = 0x03)与数据块(Font Data Block,typeid = 0x04)

QFF 对 QGF 的复用在这两个块上最彻底:

  • 调色板块与 QGF 的 frame palette block 完全相同,保留 typeid 0x03。调色板每个条目是 3 字节 HSV 三元组(见 quantum/painter/qgf.h):

    typedef struct PACKED qgf_palette_entry_v1_t { uint8_t h; // hue component: `[0,360)` degrees is mapped to `[0,255]` uint8_t. uint8_t s; // saturation component: `[0,1]` is mapped to `[0,255]` uint8_t. uint8_t v; // value component: `[0,1]` is mapped to `[0,255]` uint8_t. } qgf_palette_entry_v1_t;

    该块仅当字体为调色板格式时存在,且位于 Unicode 表(如有)或 ASCII 表之后。调色板条目数为1 << bpp。渲染时 quantum/painter/qp_draw_text.c 的qp_drawtext_prepare_font_for_render()会读入该块并调用驱动层的palette_convert把 HSV 调色板转换为面板原生像素格式;若字体不是调色板格式,则按传入的前景/背景 HSV 颜色对做qp_internal_interpolate_palette()插值生成灰度调色板。

  • 数据块是文件的最后一块,与 QGF 的 frame data block 结构相同(块头 + 原始像素 blob),只是 typeid 改为 0x04。块内像素流按照各字形的glyph_offset分散排布,整体可带 RLE 压缩(compression_scheme指定)。绘制单个字形时,quantum/painter/qp_draw_text.c 先把 RLE 输入状态复位到MARKER_BYTE,再以width × line_height为像素数通过qp_internal_appender()流式解码并写入面板视口——字体数据因此从不整体加载进 RAM,而是逐字形按需读取,这正是"资源受限系统"定位的工程落点。

八、从加载到渲染:源码中的完整调用链

把前面的块结构串起来,QMK 中一个 QFF 字体的生命周期是:

  1. 加载qp_load_font_mem()(或文件流版本)→qp_load_font_internal()在 quantum/painter/qp_draw_text.c 中分配一个字体槽位(共QUANTUM_PAINTER_NUM_FONTS个),创建流后调用qff_validate_stream()按第二节所述顺序逐块校验;可选地,当编译选项QUANTUM_PAINTER_LOAD_FONTS_TO_RAM使能时会把字体整体拷入 RAM 以加速访问,失败则回退到 flash 流;
  2. 能力检查qp_internal_bpp_capable(font->bpp)确认当前构建支持该 bpp(否则会提示检查QUANTUM_PAINTER_SUPPORTS_256_PALETTE/QUANTUM_PAINTER_SUPPORTS_NATIVE_COLORS);
  3. 测量qp_textwidth(font, str)逐 UTF-8 码点解码(decode_utf8),累加各字形宽度;
  4. 绘制qp_drawtext_recolor(device, x, y, font, str, fg_hsv…, bg_hsv…)先准备调色板并算出像素数据块起点(quantum/painter/qp_draw_text.c 中按"描述块 + ASCII 表 + Unicode 表 + 调色板"逐段累加偏移),再逐字形设置视口、定位流、按 bpp 流式解码像素。

数据块起点偏移的计算公式值得单独列出,它是理解 QFF 布局的钥匙(quantum/painter/qp_draw_text.c):

data_offset = sizeof(qff_font_descriptor_v1_t) // 25 字节描述块 + (has_ascii_table ? sizeof(qff_ascii_glyph_table_v1_t) : 0) // 290 字节 + (num_unicode_glyphs ? sizeof(qff_unicode_glyph_table_v1_t) + num_unicode_glyphs * 6 : 0) // 5 + N*6 字节 + (has_palette ? sizeof(qgf_palette_v1_t) + (1 << bpp) * 3 : 0) // 5 + (1<<bpp)*3 字节 + sizeof(qgf_block_header_v1_t) // 5 字节数据块头 + glyph_offset; // 字形自身偏移

九、字体生成与实际使用

  • 生成工具:QMK 自带的 Python 转换实现位于 lib/python/qmk/painter_qff.py,其中QFFGlyphInfo.write()((data_offset << 6) & 0xFFFFC0) | (w & 0x3F)生成 24 位字形项——与第五、六节的位域定义互为镜像,是"文档 → 生成器 → 解析器"三方一致性的直接证据;QFFFontDescriptor.write()则以小端写出 20 字节描述数据(含~total_file_size取反值)。
  • 固件侧使用:字体通常由qmk painter工具链转换为const uint8_t数组嵌入键盘源码,例如 keyboards/boardsource/equals/graphics/thintel15.qff.c 定义了 966 字节的font_thintel15,对应键盘 keyboards/boardsource/equals 的 OLED 屏;keyboards/dasky/reverb/graphics/robotomono20.qff.c、keyboards/tzarc/djinn/graphics/thintel15.qff.c 也是同类实例,可对照本文的块结构逐字节解读。
  • 调试建议:解析失败信息经qp_dprintf输出,开启 Painter 的 verbose 调试后,magic 不匹配、长度取反校验失败、Unicode 字形缺失等问题都会给出明确的十六进制对比,排障时直接对照本文第四节的字段表即可定位是哪个块损坏。

十、QFF 格式速查表

typeid块头后长度关键内容
Font descriptor0x0020magic 0x464651、version 0x01、文件总大小 + 取反值、行高、ASCII 表标志、Unicode 字形数、format/flags/compression/transparency
ASCII glyph table0x0128595 个 24 位字形项(0x20~0x7E),低 6 位宽度 + 高 18 位数据偏移
Unicode glyph table0x026 × NN 个 6 字节条目:24 位 code_point + 24 位字形信息
Palette0x033 × (1<<bpp)HSV 调色板,与 QGF 相同,仅调色板格式字体存在
Data0x04可变全部字形像素,可 RLE 压缩,位于文件末尾

QFF 的设计可以概括为一句话:用 QGF 的块信封承载字体元数据,用 24 位位域实现字形表的 O(1)/线性定位,用流式读取把内存占用压到单字形级别。对于在带 OLED/TFT 面板的键盘上绘制多灰度、可换色的文本,QMK Painter 的qp_load_font_mem/qp_drawtext/qp_drawtext_recolor就是这套格式之上的完整用户接口。

【免费下载链接】qmk_firmwareOpen-source keyboard firmware for Atmel AVR and Arm USB families项目地址: https://gitcode.com/GitHub_Trending/qm/qmk_firmware

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询