Alle Formatseiten ↓
Grafik und Medien

FNT – 1-Bit-Bitmap-Schriften

Aus coreprime/kbot @ 0ab97bf · Auf GitHub bearbeiten

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.

Glyphenübersicht von comix.fnt
fonts/comix.fnt, von kbot fnt sheet als Sprite-Sheet mit 16 Spalten gerendert

„kbot“ in Gelb 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 --codepage nimmt 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 Byte 0x80; 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)

Siehe auch

  • PAL – Schriften bringen keine Palette mit; beim Rendern musst du eine Vorder- und Hintergrundfarbe wählen oder die Palette von TA von Hand verwenden.
  • Glossar – 1bpp, MSB-first bit stream.