
QMK QFF 字体格式完全解析从二进制块结构到 QMK Painter 源码实现【免费下载链接】qmk_firmwareOpen-source keyboard firmware for Atmel AVR and Arm USB families项目地址: https://gitcode.com/GitHub_Trending/qm/qmk_firmwareQMK 的 Quantum Painter 绘图子系统使用一种名为QFFQuantum 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 blockASCII 字形表0x01可选仅当包含 ASCII 字形时3Unicode glyph blockUnicode 字形表0x02可选仅当包含 Unicode 字形时4Font palette block调色板块0x03可选仅当字体是调色板格式时5Font data block像素数据块0x04必需位于文件末尾每块由一个header包含typeid与后续 blob 的length加可选的blob数据组成。块结构在 quantum/painter/qff.c 的qff_validate_stream()中得到印证解析器先读字体描述块再根据描述块中的has_ascii_table与num_unicode_glyphs字段决定是否依次验证 ASCII 表块和 Unicode 表块——与文档描述的按存在性顺序排列严格一致。三、块头Block Header5 字节的统一信封QFF 的块头与 QGFQuantum Graphics Format见 docs/quantum_painter_qgf.md完全相同。源码定义在 quantum/painter/qgf.htypedef 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按位取反值用于解析错误检测——若两个值不互为补数说明内存或流已损坏length24 位 blob 长度单块最大约 16 MB对小体积的单片机 flash 而言绰绰有余。校验逻辑在 quantum/painter/qgf.h 声明的qgf_validate_block_header()中统一实现QFF 的每个块解析函数都会调用它例如 quantum/painter/qff.c 处对字体描述块的校验。四、字体描述块Font Descriptor Blocktypeid 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 的 QFFqff_version恒为 0x01当前格式版本total_file_size/neg_total_file_size文件总大小及其按位取反值双重校验防解析错位line_height字形行高像素是qp_drawtext()渲染时的行高来源has_ascii_table是否包含 0x20~0x7E 的 ASCII 字形表num_unicode_glyphsUnicode 字形数量为 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_id0x00、neg_type_id0xFF、length0x00001424 位小端 20紧接着0x51, 0x46, 0x46是小端存储的 magic QFF0x464651随后0x01是版本号——与上面的字段定义逐字节吻合。五、ASCII 字形表ASCII Glyph Tabletypeid 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 KBASCII 表按0x20~0x7E顺序索引第 N 项N code_point − 0x20对应字符 N。渲染时源码直接按此偏移量随机访问见 quantum/painter/qp_draw_text.cuint32_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);这也解释了为什么位宽是 618 的划分宽度字段只需覆盖面板上单个字符的合理像素宽而 18 位偏移足以覆盖整个像素数据块。六、Unicode 字形表Unicode Glyph Tabletypeid 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统一码点最高支持 UFFFF足够覆盖 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 Blocktypeid 0x03与数据块Font Data Blocktypeid 0x04QFF 对 QGF 的复用在这两个块上最彻底调色板块与 QGF 的 frame palette block 完全相同保留 typeid 0x03。调色板每个条目是 3 字节 HSV 三元组见 quantum/painter/qgf.htypedef 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 字体的生命周期是加载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 流能力检查qp_internal_bpp_capable(font-bpp)确认当前构建支持该 bpp否则会提示检查QUANTUM_PAINTER_SUPPORTS_256_PALETTE/QUANTUM_PAINTER_SUPPORTS_NATIVE_COLORS测量qp_textwidth(font, str)逐 UTF-8 码点解码decode_utf8累加各字形宽度绘制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.cdata_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 (1bpp)*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/transparencyASCII glyph table0x0128595 个 24 位字形项0x20~0x7E低 6 位宽度 高 18 位数据偏移Unicode glyph table0x026 × NN 个 6 字节条目24 位 code_point 24 位字形信息Palette0x033 × (1bpp)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),仅供参考