Tutte le pagine dei formati ↓
Grafica e media

FNT — Font bitmap a 1 bit

Fonte: coreprime/kbot @ 0ab97bf · Modifica su GitHub

Total Annihilation include tutta una serie di file .fnt per qualsiasi cosa, dalle comunicazioni radio a schermo (comix.fnt) alle schermate di briefing delle unità (armbrief.fnt). Sono piccoli font bitmap a larghezza variabile, a 1 bit per pixel, indicizzati per codice di carattere a 8 bit: in sostanza un foglio di sprite con una tabella di ricerca.

Foglio dei glifi di comix.fnt
fonts/comix.fnt disegnato come foglio di sprite a 16 colonne da kbot fnt sheet

"kbot" disegnato in giallo
kbot fnt render comix.fnt --text "kbot" --fg "#ffff00" --bg "#000000"

Tip

Provalo tu stesso.

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

Consulta il riferimento CLI di kbot fnt.

Da Go. Usa 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")
}

Struttura su disco

┌─ 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
└──────────────────────────┘

Ogni font originale ha FirstChar = 0, quindi la sua intestazione più la tabella degli offset occupano 4 + 256 × 2 = 516 byte e i dati dei glifi cominciano subito dopo.

Campo Note
Height Solo il byte 0. Altezza in pixel condivisa da tutti i glifi del font (da 9 a 17 nei font originali).
Baseline Byte 2, con segno. Il testo disegnato con la penna alla riga y mette la riga superiore dei glifi a y − Baseline. I font originali memorizzano da 1 a 3.
FirstChar Byte 3. Il carattere c usa la voce c − FirstChar della tabella degli offset; i codici inferiori non hanno glifo. I font originali memorizzano 0.
Offsets[] Indicizzata per byte del carattere (il testo del gioco originale è in Windows-1252). 0 significa nessun glifo per questo carattere. I valori diversi da zero sono offset dall'inizio del font che puntano al byte Width del glifo.
Width del glifo Da 1 a 255. Con una larghezza 0 il gioco avanza di 0 e disegna spazzatura; kbot salta questi glifi e li segnala.

Gli strumenti più vecchi leggono i byte 2–3 come un'unica parola opaca di "flag"; sono invece due campi separati.


Leggere un glifo

Il payload del glifo è un flusso di bit continuo, MSB-first, impacchettato da sinistra a destra e poi dall'alto in basso, cioè una scansione per righe. Il numero di bit è Width × Height e il numero di byte memorizzati è ⌈(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)

Non ci sono dati di crenatura (kerning) né una larghezza di avanzamento distinta da Width. Il gioco dispone il testo così, e kbot fnt render, lo strumento MCP fnt_render e l'anteprima del testo dell'esploratore di asset fanno lo stesso:

  • ogni byte del testo seleziona il glifo con quel codice (converti prima il testo UTF-8 nella codepage del gioco; kbot fnt render --codepage usa Windows-1252 come predefinita);
  • un glifo fa avanzare la penna di esattamente la sua larghezza, senza spazio in mezzo;
  • un carattere senza glifo non disegna nulla e avanza di 0;
  • il disegno si ferma al primo NUL o a capo.

Un font di 13 righe con una A larga 10 pixel e senza glifo per lo spazio disegna quindi "A A" largo 20 pixel.

Note

Non c'è riempimento tra le righe. Ogni riga di scansione continua da dove era finita la precedente, nello stesso byte. Un glifo largo 5 pixel e alto 7 usa ⌈35/8⌉ = 5 byte, e i 5 bit finali vengono ignorati.


Esempio svolto — 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

Come leggerlo: è un font alto 14 pixel che copre l'ASCII stampabile (dallo spazio a }). Le larghezze vanno da 3 (i, l) a 13 (M, W). I 162 slot non definiti sono per lo più caratteri di controllo non stampabili; il resto sono caratteri aggiuntivi Latin-1 che il gioco non mostra.

La tabella degli offset per i primi dieci caratteri di 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     '$'
...

Puoi ottenere lo stesso elenco con kbot fnt describe --list.


Consigli per il rendering

kbot fnt render supporta:

Opzione Effetto
--text "..." La stringa da disegnare: una sola riga; il disegno si ferma al primo a capo, come nel gioco.
--codepage NAME La codepage in cui viene convertito il testo UTF-8: cp1252 (predefinita, quella del gioco originale), cp1250, cp1251, cp437, cp850, iso-8859-1, iso-8859-15, oppure raw per passare i byte così come sono. I caratteri senza un byte corrispondente diventano ?.
--fg #rrggbb[aa] Colore di primo piano. Predefinito: bianco.
--bg #rrggbb[aa] | transparent Sfondo. Predefinito: trasparente.
--target PATH Percorso del PNG di output. Altrimenti stdout.

Un carattere che non è presente nel font non disegna nulla e non occupa spazio, come nel gioco.


Insidie

Warning

Offsets[c] == 0 significa "nessun glifo", non "glifo all'offset 0". L'offset 0 cade a metà dell'intestazione ed è strutturalmente impossibile: Cavedog riusa il valore sentinella per indicare non definito. Tratta sempre gli offset a zero come mancanti.

  • L'ordine dei bit è MSB-first all'interno di ciascun byte. Nei font bitmap moderni la convenzione più comune è LSB-first; non darlo per scontato.
  • Il flusso di bit è continuo tra una riga e l'altra: non c'è riempimento fino al confine di byte alla fine di ogni riga di scansione.
  • I byte 2 e 3 sono la linea di base e il codice del primo carattere, non una parola di flag. Un codice del primo carattere diverso da zero accorcia la tabella degli offset.
  • I caratteri sono byte, non code point Unicode. € è il byte 0x80 in Windows-1252; prendere un code point modulo 256 sceglie il glifo sbagliato.
  • Nessun metadato su quali caratteri siano supportati. Per scoprire l'insieme dei glifi devi scorrere la tabella degli offset.

Dimensioni tipiche

Elemento Intervallo osservato in fonts/*.fnt di Cavedog
Dimensione del file 700 B – 4 KB
Intestazione + tabella degli offset sempre 516 byte
Altezza dei glifi 9–17 px
Larghezza dei glifi 3–13 px
Glifi definiti per font 60–120 (per lo più solo ASCII stampabile)
Dati per glifo di solito 5–30 byte (1 byte di larghezza + bit impacchettati)

Vedi anche

  • PAL: i font non contengono una palette; per il rendering devi scegliere un colore di primo piano e di sfondo, oppure usare a mano la palette di TA.
  • Glossario: 1bpp, flusso di bit MSB-first.