Усі сторінки форматів ↓
Графіка й медіа

FNT — 1-бітні растрові шрифти

Джерело: 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

Див. довідник 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 як одне непрозоре слово «прапорців»; насправді це два окремі поля.


Читання гліфа

Дані гліфа — це неперервний бітовий потік від старшого біта (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. Інакше — stdout.

Символ, якого немає у шрифті, нічого не малює й не займає місця, як і в грі.


Підводні камені

Warning

Offsets[c] == 0 означає «гліфа немає», а не «гліф за зміщенням 0». Зміщення 0 потрапляє в середину заголовка й структурно неможливе — Cavedog використовує це сторожове значення для позначення невизначеного гліфа. Завжди вважайте нульові зміщення відсутніми.

  • Біти в кожному байті йдуть від старшого (MSB first). У сучасних растрових шрифтах поширеніша домовленість — від молодшого (LSB first); не робіть припущень.
  • Бітовий потік між рядками неперервний — у кінці кожного рядка розгортки немає доповнення до межі байта.
  • Байти 2 і 3 — це базова лінія й код першого символу, а не слово прапорців. Ненульовий код першого символу скорочує таблицю зміщень.
  • Символи — це байти, а не кодові точки Unicode. € — це байт 0x80 у Windows-1252; якщо взяти кодову точку за модулем 256, буде обрано не той гліф.
  • Немає метаданих про те, які символи підтримуються. Щоб з'ясувати набір гліфів, доведеться пройти таблицею зміщень.

Типові розміри

Ресурс Діапазон, що трапляється в fonts/*.fnt від Cavedog
Розмір файлу 700 Б – 4 КБ
Заголовок + таблиця зміщень завжди 516 байтів
Висота гліфа 9–17 px
Ширина гліфа 3–13 px
Визначених гліфів у шрифті 60–120 (здебільшого лише друковані символи ASCII)
Дані одного гліфа зазвичай 5–30 байтів (1 байт ширини + упаковані біти)

Див. також

  • PAL — шрифти не містять палітри; рендерер потребує, щоб ви обрали колір переднього плану й тла або вручну скористалися палітрою TA.
  • Глосарій — 1bpp, MSB-first bit stream.