Все страницы форматов ↓
Графика и медиа

FNT — однобитные растровые шрифты

Источник: coreprime/kbot @ 0ab97bf · Редактировать на GitHub

В комплекте Total Annihilation целая коллекция файлов .fnt — на все случаи, от радиопереговоров на экране (comix.fnt) до экранов брифинга юнитов (armbrief.fnt). Это крошечные растровые шрифты переменной ширины, 1 бит на пиксель, с индексацией по 8-битному коду символа — по сути, лист спрайтов с таблицей поиска.

Лист глифов comix.fnt
fonts/comix.fnt, отрисованный командой kbot fnt sheet как лист спрайтов в 16 столбцов

Слово «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

См. справку по 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 байт ширины + упакованные биты)

См. также

  • PAL — шрифты не несут палитры; движку отрисовки нужно указать цвета переднего плана и фона или вручную воспользоваться палитрой TA.
  • Глоссарий — 1bpp (1 бит на пиксель), MSB-first bit stream (битовый поток от старшего бита).