FNT – 1-Bit-Bitmap-Schriften
Total Annihilation liefert eine ganze Reihe von
.fnt-Dateien aus, für alles vom Funkverkehr auf dem Bildschirm (comix.fnt) bis zu den Briefing-Bildschirmen der Einheiten (armbrief.fnt). Es sind winzige Bitmap-Schriften mit variabler Breite und 1 Bit pro Pixel, indiziert über einen 8-Bit-Zeichencode – im Grunde ein Sprite-Sheet mit Nachschlagetabelle.
fonts/comix.fnt, von kbot fnt sheet als Sprite-Sheet mit 16 Spalten gerendert
kbot fnt render comix.fnt --text "kbot" --fg "#ffff00" --bg "#000000"
Tip
Probier es selbst.
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
Siehe die kbot fnt-Referenz der CLI.
Aus Go heraus. Verwende 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")
}
Aufbau auf der Platte
┌─ 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
└──────────────────────────┘
Jede Schrift der Verkaufsversion hat FirstChar = 0, Header und
Offset-Tabelle sind also 4 + 256 × 2 = 516 Byte groß, und die
Glyphendaten beginnen unmittelbar danach.
| Feld | Anmerkungen |
|---|---|
Height |
Nur Byte 0. Pixelhöhe, die alle Glyphen der Schrift gemeinsam haben (9–17 bei den Schriften der Verkaufsversion). |
Baseline |
Byte 2, vorzeichenbehaftet. Text, der mit dem Stift in Zeile y gezeichnet wird, setzt die oberste Zeile der Glyphen auf y − Baseline. Die Schriften der Verkaufsversion speichern 1–3. |
FirstChar |
Byte 3. Das Zeichen c verwendet den Eintrag c − FirstChar der Offset-Tabelle; Codes darunter haben keinen Glyph. Die Schriften der Verkaufsversion speichern 0. |
Offsets[] |
Indiziert über das Zeichenbyte (der Text des Spiels der Verkaufsversion ist Windows-1252). 0 bedeutet kein Glyph für dieses Zeichen. Werte ungleich null sind Offsets ab dem Anfang der Schrift, die auf das Width-Byte des Glyphs zeigen. |
Width des Glyphs |
1–255. Eine Breite von 0 lässt das Spiel um 0 weiterrücken und Müll zeichnen; kbot überspringt solche Glyphen und meldet sie. |
Ältere Werkzeuge lesen die Bytes 2–3 als ein undurchsichtiges „Flags“-Wort; es sind zwei getrennte Felder.
Einen Glyph lesen
Die Nutzdaten eines Glyphs sind ein durchgehender Bitstrom, MSB
zuerst, von links nach rechts und dann von oben nach unten gepackt – also
zeilenweise abgetastet. Die Anzahl der Bits ist Width × Height, die
Anzahl der gespeicherten Bytes ⌈(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)
Es gibt keine Kerning-Daten und keine Vorschubbreite getrennt von Width.
Das Spiel setzt Text so, und kbot fnt render, das MCP-Werkzeug
fnt_render und die Textvorschau des Asset-Explorers machen es genauso:
- jedes Byte des Texts wählt den Glyph mit diesem Code (wandle
UTF-8-Text zuerst in die Codepage des Spiels um;
kbot fnt render --codepagenimmt standardmäßig Windows-1252); - ein Glyph rückt den Stift um genau seine Breite vor, ohne Abstand;
- ein Zeichen ohne Glyph zeichnet nichts und rückt um 0 vor;
- das Zeichnen endet beim ersten NUL oder Zeilenumbruch.
Eine Schrift mit 13 Zeilen, einem A von 10 Pixeln Breite und ohne
Leerzeichen-Glyph zeichnet "A A" deshalb 20 Pixel breit.
Note
Zwischen den Zeilen gibt es keine Auffüllung. Jede Scanzeile setzt
dort im selben Byte fort, wo die vorige aufgehört hat. Ein Glyph von
5 Pixeln Breite und 7 Pixeln Höhe belegt ⌈35/8⌉ = 5 Byte, wobei die
5 überzähligen Bits ignoriert werden.
Beispiel im Detail – 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
So liest man das: Es ist eine Schrift von 14 Pixeln Höhe, die druckbares
ASCII abdeckt (Leerzeichen bis }). Die Breiten reichen von 3 (i, l)
bis 13 (M, W). Die 162 undefinierten Plätze sind überwiegend nicht
druckbare Steuerzeichen; der Rest sind Latin-1-Zusatzzeichen, die das
Spiel nicht anzeigt.
Die Offset-Tabelle für die ersten zehn Zeichen von 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 '$'
...
Dasselbe kannst du mit kbot fnt describe --list ausgeben.
Tipps zum Rendern
kbot fnt render unterstützt:
| Flag | Wirkung |
|---|---|
--text "..." |
Der zu rendernde String: eine Zeile; das Zeichnen endet wie im Spiel beim ersten Zeilenumbruch. |
--codepage NAME |
Codepage, in die der UTF-8-Text umgewandelt wird: cp1252 (Standard, die des Spiels der Verkaufsversion), cp1250, cp1251, cp437, cp850, iso-8859-1, iso-8859-15 oder raw, um die Bytes unverändert durchzureichen. Zeichen ohne passendes Byte werden zu ?. |
--fg #rrggbb[aa] |
Vordergrundfarbe. Standard: weiß. |
--bg #rrggbb[aa] | transparent |
Hintergrund. Standard: transparent. |
--target PATH |
Pfad der PNG-Ausgabe. Sonst Standardausgabe. |
Ein Zeichen, das in der Schrift fehlt, zeichnet nichts und nimmt keinen Platz ein, wie im Spiel.
Stolperfallen
Warning
Offsets[c] == 0 bedeutet „kein Glyph“, nicht „Glyph bei Offset
0“. Offset 0 liegt mitten im Header und ist strukturell unmöglich –
Cavedog verwendet diesen Sentinel-Wert für undefiniert. Behandle
Offsets von null immer als fehlend.
- Die Bitreihenfolge ist MSB zuerst innerhalb jedes Bytes. LSB zuerst ist bei modernen Bitmap-Schriften die häufigere Konvention; geh nicht einfach davon aus.
- Der Bitstrom läuft zwischen den Zeilen durch – am Ende jeder Scanzeile wird nicht auf eine Bytegrenze aufgefüllt.
- Die Bytes 2 und 3 sind die Grundlinie und der Code des ersten Zeichens, kein Flags-Wort. Ein erster Zeichencode ungleich null verkürzt die Offset-Tabelle.
- Zeichen sind Bytes, keine Unicode-Codepoints.
€ist in Windows-1252 das Byte0x80; einen Codepoint modulo 256 zu nehmen, wählt den falschen Glyph. - Keine Metadaten darüber, welche Zeichen unterstützt werden. Du musst die Offset-Tabelle durchlaufen, um den Glyphensatz zu ermitteln.
Typische Größen
| Asset | Beobachteter Bereich in Cavedogs fonts/*.fnt |
|---|---|
| Dateigröße | 700 B – 4 KB |
| Header + Offset-Tabelle | immer 516 Byte |
| Glyphenhöhe | 9–17 px |
| Glyphenbreite | 3–13 px |
| Definierte Glyphen pro Schrift | 60–120 (meist nur druckbares ASCII) |
| Daten pro Glyph | typischerweise 5–30 Byte (1 Breitenbyte + gepackte Bits) |