Todas las páginas de formatos ↓
Gráficos y multimedia

FNT: fuentes de mapa de bits de 1 bit

Fuente: coreprime/kbot @ 0ab97bf · Editar en GitHub

Total Annihilation distribuye una buena colección de archivos .fnt para todo, desde los mensajes de radio en pantalla (comix.fnt) hasta las pantallas de instrucciones de las misiones (armbrief.fnt). Son fuentes de mapa de bits diminutas, de ancho variable, de 1 bit por píxel e indexadas por un código de carácter de 8 bits: en el fondo, una hoja de sprites con una tabla de consulta.

Hoja de glifos de comix.fnt
fonts/comix.fnt renderizada como hoja de sprites de 16 columnas con kbot fnt sheet

«kbot» renderizado en amarillo
kbot fnt render comix.fnt --text "kbot" --fg "#ffff00" --bg "#000000"

Tip

Pruébalo tú mismo.

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 la referencia de kbot fnt de la CLI.

Desde 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")
}

Estructura en 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
└──────────────────────────┘

Todas las fuentes de la versión comercial tienen FirstChar = 0, así que su cabecera y su tabla de desplazamientos ocupan 4 + 256 × 2 = 516 bytes, y los datos de los glifos empiezan justo después.

Campo Notas
Height Solo el byte 0. Altura en píxeles que comparten todos los glifos de la fuente (de 9 a 17 en las fuentes de la versión comercial).
Baseline Byte 2, con signo. El texto dibujado con la pluma en la fila y coloca la fila superior de los glifos en y − Baseline. Las fuentes de la versión comercial guardan de 1 a 3.
FirstChar Byte 3. El carácter c usa la entrada c − FirstChar de la tabla de desplazamientos; los códigos inferiores no tienen glifo. Las fuentes de la versión comercial guardan 0.
Offsets[] Indexada por el byte del carácter (el texto del juego comercial está en Windows-1252). 0 significa no hay glifo para este carácter. Los valores distintos de cero son desplazamientos desde el principio de la fuente que apuntan al byte Width del glifo.
Width del glifo 1–255. Con un ancho de 0, el juego avanza 0 y dibuja basura; kbot se salta esos glifos y lo indica.

Las herramientas antiguas leían los bytes 2–3 como una única palabra opaca de «flags»; son dos campos distintos.


Leer un glifo

Los datos del glifo son un flujo de bits continuo, con el MSB primero, empaquetado de izquierda a derecha y después de arriba abajo, es decir, un barrido por filas. El número de bits es Width × Height y el número de bytes guardados es ⌈(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)

No hay datos de kerning ni un avance distinto de Width. El juego compone el texto así, y kbot fnt render, la herramienta MCP fnt_render y la vista previa de texto del explorador de recursos hacen lo mismo:

  • cada byte del texto selecciona el glifo con ese código (convierte primero el texto UTF-8 a la página de códigos del juego; kbot fnt render --codepage usa Windows-1252 por defecto);
  • un glifo avanza la pluma exactamente su ancho, sin separación;
  • un carácter sin glifo no dibuja nada y avanza 0;
  • el dibujo se detiene en el primer NUL o salto de línea.

Por tanto, una fuente de 13 filas con una A de 10 píxeles y sin glifo de espacio dibuja "A A" con 20 píxeles de ancho.

Note

No hay relleno entre filas. Cada línea de barrido continúa en el mismo byte donde terminó la anterior. Un glifo de 5 píxeles de ancho y 7 de alto usa ⌈35/8⌉ = 5 bytes, y los 5 bits finales se ignoran.


Ejemplo desarrollado: 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

Cómo leerlo: es una fuente de 14 píxeles de alto que cubre el ASCII imprimible (del espacio a }). Los anchos van de 3 (i, l) a 13 (M, W). Las 162 posiciones sin definir son casi todas caracteres de control no imprimibles; el resto son caracteres adicionales de Latin-1 que el juego no muestra.

La tabla de desplazamientos de los diez primeros caracteres de 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     '$'
...

Puedes volcar lo mismo con kbot fnt describe --list.


Consejos de renderizado

kbot fnt render admite:

Opción Efecto
--text "..." La cadena que se renderiza: una línea; el dibujo se detiene en el primer salto de línea, como en el juego.
--codepage NAME Página de códigos a la que se convierte el texto UTF-8: cp1252 (por defecto, la del juego comercial), cp1250, cp1251, cp437, cp850, iso-8859-1, iso-8859-15, o raw para pasar los bytes tal cual. Los caracteres que no tienen byte pasan a ser ?.
--fg #rrggbb[aa] Color del primer plano. Por defecto: blanco.
--bg #rrggbb[aa] | transparent Fondo. Por defecto: transparente.
--target PATH Ruta del PNG de salida. Si no, la salida estándar.

Un carácter que no está en la fuente no dibuja nada ni ocupa espacio, como en el juego.


Escollos

Warning

Offsets[c] == 0 significa «sin glifo», no «glifo en el desplazamiento 0». El desplazamiento 0 cae en mitad de la cabecera y es estructuralmente imposible: Cavedog reutiliza el valor centinela para indicar sin definir. Trata siempre los desplazamientos cero como ausentes.

  • El orden de los bits es MSB primero dentro de cada byte. En las fuentes de mapa de bits modernas es más habitual LSB primero; no lo des por hecho.
  • El flujo de bits es continuo entre filas: no hay relleno hasta el límite de byte al final de cada línea de barrido.
  • Los bytes 2 y 3 son la línea base y el código del primer carácter, no una palabra de flags. Un código de primer carácter distinto de cero acorta la tabla de desplazamientos.
  • Los caracteres son bytes, no puntos de código Unicode. € es el byte 0x80 en Windows-1252; tomar un punto de código módulo 256 elige el glifo equivocado.
  • No hay metadatos sobre qué caracteres se admiten. Tienes que recorrer la tabla de desplazamientos para descubrir el conjunto de glifos.

Tamaños habituales

Recurso Rango observado en los fonts/*.fnt de Cavedog
Tamaño del archivo 700 B – 4 KB
Cabecera + tabla de desplazamientos siempre 516 bytes
Altura del glifo 9–17 px
Ancho del glifo 3–13 px
Glifos definidos por fuente 60–120 (la mayoría solo ASCII imprimible)
Datos por glifo normalmente 5–30 bytes (1 byte de ancho + bits empaquetados)

Véase también

  • PAL: las fuentes no llevan paleta; el renderizador necesita que elijas un color de primer plano y de fondo o que uses a mano la paleta de TA.
  • Glosario: 1bpp, MSB-first bit stream.