FNT — однобитные растровые шрифты
Источник: coreprime/kbot @ 0ab97bf · Редактировать на GitHub
В комплекте Total Annihilation целая коллекция файлов
.fnt— на все случаи, от радиопереговоров на экране (comix.fnt) до экранов брифинга юнитов (armbrief.fnt). Это крошечные растровые шрифты переменной ширины, 1 бит на пиксель, с индексацией по 8-битному коду символа — по сути, лист спрайтов с таблицей поиска.
fonts/comix.fnt, отрисованный командой kbot fnt sheet как лист спрайтов в 16 столбцов
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
См. справку по 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 как одно непрозрачное слово «флагов»; на самом деле это два отдельных поля.
Чтение глифа
Данные глифа — это непрерывный битовый поток, начиная со старшего бита
(MSB-first), упакованный слева направо, затем сверху вниз, то есть
построчная развёртка. Число битов равно 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 строк с 10-пиксельной A и без глифа пробела
рисует "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 — это базовая линия и код первого символа, а не слово флагов. Ненулевой код первого символа укорачивает таблицу смещений.
- Символы — это байты, а не кодовые точки Юникода.
€в Windows-1252 — байт0x80; если взять кодовую точку по модулю 256, выберется не тот глиф. - Метаданных о поддерживаемых символах нет. Чтобы узнать набор глифов, придётся пройти по таблице смещений.
Типичные размеры
| Ресурс | Диапазон в fonts/*.fnt от Cavedog |
|---|---|
| Размер файла | 700 Б – 4 КБ |
| Заголовок + таблица смещений | всегда 516 байт |
| Высота глифа | 9–17 пикс. |
| Ширина глифа | 3–13 пикс. |
| Определённых глифов в шрифте | 60–120 (в большинстве только печатные ASCII) |
| Данные одного глифа | обычно 5–30 байт (1 байт ширины + упакованные биты) |