FNT — Font bitmap a 1 bit
Total Annihilation include tutta una serie di file
.fntper 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.
fonts/comix.fnt disegnato come foglio di sprite a 16 colonne da kbot fnt sheet
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 --codepageusa 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 byte0x80in 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) |