FNT——1 位位图字体
横扫千军带有一整套
.fnt文件,从屏幕上的无线电通话(comix.fnt)到单位简报画面(armbrief.fnt),无所不用。它们是小巧的、可变宽度的每像素 1 位位图字体,按 8 位字符码索引——基本上就是一张精灵图表加一张查找表。
由 kbot fnt sheet 渲染成 16 列精灵图表的 fonts/comix.fnt
kbot fnt render comix.fnt --text "kbot" --fg "#ffff00" --bg "#000000"
Tip
自己试试。
kbot fnt info fonts/comix.fnt # one-line summary
kbot fnt describe fonts/comix.fnt --list # full metadata + glyph list
kbot fnt sheet fonts/comix.fnt -o sheet.png # all glyphs as a grid
kbot fnt render fonts/comix.fnt --text "Commander" --fg "#ffff00" --bg transparent -o caption.png
kbot fnt dump fonts/comix.fnt -t ./glyphs # one PNG per character
见 CLI 的 kbot fnt 参考。
在 Go 中调用。 使用 formats/fnt:
import "github.com/coreprime/kbot/formats/fnt"
f, _ := os.Open("fonts/comix.fnt")
defer f.Close()
font, _ := fnt.LoadFromReader(f)
fmt.Println(font.Height, "px,", font.GlyphCount(), "glyphs")
if g := font.Glyphs['A']; g != nil {
fmt.Println("A is", g.Width, "px wide")
}
磁盘布局
┌─ 4-byte header ──────────┐
│ uint8 Height │ byte 0: shared by every glyph; the line height
│ uint8 (ignored) │ byte 1: not part of the height
│ int8 Baseline │ byte 2: rows the glyphs extend above the pen
│ uint8 FirstChar │ byte 3: code of the first offset-table entry
├─ (256 − FirstChar) × u16 ┤ file offsets; 0 = glyph not defined
├─ Glyph data ─────────────┤ for each defined glyph:
│ uint8 Width │ pixel width (1..255), also the pen advance
│ ⌈Width × Height/8⌉ │ 1bpp pixels, MSB-first bit stream
└──────────────────────────┘
所有零售版字体的 FirstChar = 0,因此文件头 + 偏移表是 4 + 256 × 2 = 516 字节,字形数据紧随其后。
| 字段 | 说明 |
|---|---|
Height |
仅第 0 字节。字体中所有字形共用的像素高度(零售版字体中为 9–17)。 |
Baseline |
第 2 字节,有符号。笔位于第 y 行时绘制的文字,其字形的最上一行位于 y − Baseline。零售版字体存的是 1–3。 |
FirstChar |
第 3 字节。字符 c 使用偏移表中的第 c − FirstChar 个条目;比它小的字符码没有字形。零售版字体存的是 0。 |
Offsets[] |
按字符字节索引(零售版游戏的文本使用 Windows-1252)。0 表示这个字符没有字形。非零值是从字体开头算起、指向该字形 Width 字节的偏移量。 |
字形的 Width |
1–255。宽度为 0 会让游戏前进 0 并绘制出垃圾;kbot 会跳过这样的字形并报告。 |
较早的工具把第 2–3 字节当作一个不透明的“标志”字来读;实际上它们是两个独立的字段。
读取一个字形
字形载荷是一个连续的位流,高位在前,先从左到右、再从上到下排列——也就是按行优先扫描。位数是 Width × Height,存储的字节数是 ⌈(Width × Height) / 8⌉。
def read_glyph(file, offset, height):
file.seek(offset)
width = file.read(1)[0]
if width == 0 or width > 128:
return None
n_bits = width * height
n_bytes = (n_bits + 7) // 8
bits = file.read(n_bytes)
pixels = []
for i in range(n_bits):
bit = (bits[i // 8] >> (7 - (i % 8))) & 1
pixels.append(bool(bit))
return Glyph(width, height, pixels)
没有字距调整数据,除 Width 外也没有单独的前进宽度。游戏按如下方式排版文字,kbot fnt render、MCP 的 fnt_render 工具和资源浏览器的文字预览也是如此:
- 文字的每个字节选择具有该字符码的字形(先把 UTF-8 文本转换为游戏的代码页;
kbot fnt render --codepage默认使用 Windows-1252); - 每个字形让笔前进恰好等于它的宽度,没有间隙;
- 没有字形的字符不绘制任何东西,前进 0;
- 遇到第一个 NUL 或换行符时停止绘制。
因此,在一个 13 行高、A 宽 10 像素且没有空格字形的字体中,"A A" 绘制出来是 20 像素宽。
Note
行与行之间没有填充。 每条扫描线都在同一字节中从上一条结束的地方接着开始。一个宽 5 像素、高 7 像素的字形占用 ⌈35/8⌉ = 5 个字节,末尾的 5 位被忽略。
实例解析——comix.fnt
$ kbot fnt describe fonts/comix.fnt
Height: 14 px
Baseline: 1 (glyph rows start this many rows above the pen)
First char: 0x00
Glyphs: 94 / 256 defined
Glyph width: min=3 max=13 mean=5.9
Ranges: 0x20-0x7D
解读:这是一个 14 像素高的字体,涵盖可打印 ASCII(从空格到 })。宽度从 3(i、l)到 13(M、W)不等。162 个未定义的位置大多是不可打印的控制字符;其余是游戏不显示的 Latin-1 扩展字符。
comix.fnt 中前十个字符的偏移表:
char offset width bytes meaning
0x20 0x021C 4 7 space
0x21 0x0223 3 5 '!'
0x22 0x0228 5 9 '"'
0x23 0x0231 6 11 '#'
0x24 0x023C 5 9 '$'
...
用 kbot fnt describe --list 也可以转储出同样的内容。
渲染技巧
kbot fnt render 支持:
| 参数 | 作用 |
|---|---|
--text "..." |
要渲染的字符串:只有一行;与游戏中一样,遇到第一个换行符时停止绘制。 |
--codepage NAME |
UTF-8 文本要转换到的代码页:cp1252(默认,零售版游戏所用)、cp1250、cp1251、cp437、cp850、iso-8859-1、iso-8859-15,或用 raw 原样传递字节。无法对应到字节的字符变为 ?。 |
--fg #rrggbb[aa] |
前景色。默认:白色。 |
--bg #rrggbb[aa] | transparent |
背景。默认:透明。 |
--target PATH |
输出 PNG 的路径。否则输出到标准输出。 |
与游戏中一样,字体中没有的字符不绘制任何东西,也不占空间。
坑点
Warning
Offsets[c] == 0 表示“没有字形”,而不是“字形位于偏移 0”。 偏移 0 落在文件头中间,在结构上不可能——Cavedog 借用这个哨兵值表示未定义。一律把零偏移当作缺失。
- 每个字节内的位序是高位在前(MSB-first)。 现代位图字体更常见的约定是低位在前(LSB-first);不要想当然。
- 位流在行与行之间是连续的——每条扫描线的末尾没有填充到字节边界。
- 第 2、3 字节是基线和首字符码,而不是一个标志字。首字符码不为零时,偏移表会相应变短。
- 字符是字节,而不是 Unicode 码位。
€在 Windows-1252 中是字节0x80;把码位对 256 取模会选错字形。 - 没有关于支持哪些字符的元数据。 你必须遍历偏移表才能得知字形集。
典型大小
| 项目 | 在 Cavedog fonts/*.fnt 中观察到的范围 |
|---|---|
| 文件大小 | 700 B – 4 KB |
| 文件头 + 偏移表 | 总是 516 字节 |
| 字形高度 | 9–17 像素 |
| 字形宽度 | 3–13 像素 |
| 每个字体定义的字形数 | 60–120(大多只有可打印 ASCII) |
| 每个字形的数据 | 通常 5–30 字节(1 个宽度字节 + 紧凑排列的位) |