所有格式页面 ↓
图形与媒体

FNT——1 位位图字体

来自 coreprime/kbot @ 0ab97bf · 在 GitHub 上编辑

横扫千军带有一整套 .fnt 文件,从屏幕上的无线电通话(comix.fnt)到单位简报画面(armbrief.fnt),无所不用。它们是小巧的、可变宽度的每像素 1 位位图字体,按 8 位字符码索引——基本上就是一张精灵图表加一张查找表。

comix.fnt 的字形表
由 kbot fnt sheet 渲染成 16 列精灵图表的 fonts/comix.fnt

以黄色渲染的“kbot”
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 个宽度字节 + 紧凑排列的位)

另见

  • PAL——字体不带调色板;渲染器需要你选择前景/背景色,或者手动使用 TA 调色板。
  • 术语表——1bpp、高位在前的位流(MSB-first bit stream)。