Todas as páginas de formatos ↓
Gráficos e multimédia

FNT — tipos de letra bitmap de 1 bit

Fonte: coreprime/kbot @ 0ab97bf · Editar no GitHub

O Total Annihilation traz uma coleção de ficheiros .fnt para tudo, desde as conversas de rádio no ecrã (comix.fnt) até aos ecrãs de briefing das unidades (armbrief.fnt). São tipos de letra bitmap minúsculos, de largura variável e 1 bit por píxel, indexados por código de carácter de 8 bits: no fundo, uma folha de sprites com uma tabela de consulta.

Folha de glifos de comix.fnt
fonts/comix.fnt renderizado como folha de sprites de 16 colunas pelo kbot fnt sheet

"kbot" renderizado a amarelo
kbot fnt render comix.fnt --text "kbot" --fg "#ffff00" --bg "#000000"

Tip

Experimente por si.

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

Veja a referência de kbot fnt da linha de comandos.

A partir de Go. Use 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")
}

Disposição no 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
└──────────────────────────┘

Todos os tipos de letra comerciais têm FirstChar = 0, por isso o cabeçalho mais a tabela de deslocamentos ocupam 4 + 256 × 2 = 516 bytes, e os dados dos glifos começam logo a seguir.

Campo Notas
Height Só o byte 0. Altura em píxeis partilhada por todos os glifos do tipo de letra (9–17 nos tipos de letra comerciais).
Baseline Byte 2, com sinal. Um texto desenhado com a caneta na linha y coloca a linha superior dos glifos em y − Baseline. Os tipos de letra comerciais guardam 1–3.
FirstChar Byte 3. O carácter c usa a entrada c − FirstChar da tabela de deslocamentos; os códigos abaixo dele não têm glifo. Os tipos de letra comerciais guardam 0.
Offsets[] Indexado pelo byte do carácter (o texto do jogo comercial está em Windows-1252). 0 significa não há glifo para este carácter. Os valores diferentes de zero são deslocamentos a partir do início do tipo de letra, que apontam para o byte Width do glifo.
Width do glifo 1–255. Com uma largura de 0, o jogo avança 0 e desenha lixo; o kbot salta esses glifos e assinala-os.

As ferramentas mais antigas leem os bytes 2–3 como uma única palavra opaca de "flags"; são, na verdade, dois campos separados.


Ler um glifo

O payload do glifo é um fluxo de bits contínuo, MSB primeiro, arrumado da esquerda para a direita e depois de cima para baixo, ou seja, linha a linha. O número de bits é Width × Height e o número de bytes guardados é ⌈(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)

Não há dados de kerning nem uma largura de avanço separada de Width. O jogo dispõe o texto desta forma, e o kbot fnt render, a ferramenta MCP fnt_render e a pré-visualização de texto do explorador de recursos fazem o mesmo:

  • cada byte do texto escolhe o glifo com esse código (converta primeiro o texto UTF-8 para a página de código do jogo; o kbot fnt render --codepage usa Windows-1252 por omissão);
  • um glifo avança a caneta exatamente a sua largura, sem intervalo;
  • um carácter sem glifo não desenha nada e avança 0;
  • o desenho para no primeiro NUL ou mudança de linha.

Assim, um tipo de letra de 13 linhas com um A de 10 píxeis e sem glifo para o espaço desenha "A A" com 20 píxeis de largura.

Note

Não há preenchimento entre linhas. Cada linha de varrimento continua no ponto do mesmo byte onde a anterior acabou. Um glifo com 5 píxeis de largura e 7 de altura usa ⌈35/8⌉ = 5 bytes, e os 5 bits finais são ignorados.


Exemplo comentado — 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

Como ler isto: é um tipo de letra com 14 píxeis de altura que cobre o ASCII imprimível (do espaço até }). As larguras vão de 3 (i, l) a 13 (M, W). As 162 posições sem glifo são sobretudo caracteres de controlo não imprimíveis; as restantes são os extras do Latin-1 que o jogo não mostra.

A tabela de deslocamentos dos primeiros dez 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     '$'
...

Pode obter o mesmo com kbot fnt describe --list.


Dicas de renderização

O kbot fnt render suporta:

Opção Efeito
--text "..." A cadeia a renderizar: uma linha; o desenho para na primeira mudança de linha, como no jogo.
--codepage NAME Página de código para a qual o texto UTF-8 é convertido: cp1252 (por omissão, a do jogo comercial), cp1250, cp1251, cp437, cp850, iso-8859-1, iso-8859-15, ou raw para passar os bytes tal como estão. Os caracteres sem byte correspondente passam a ?.
--fg #rrggbb[aa] Cor do primeiro plano. Por omissão: branco.
--bg #rrggbb[aa] | transparent Fundo. Por omissão: transparente.
--target PATH Caminho do PNG de saída. Caso contrário, a saída padrão.

Um carácter que não exista no tipo de letra não desenha nada e não ocupa espaço, como no jogo.


Armadilhas

Warning

Offsets[c] == 0 significa "sem glifo", e não "glifo no deslocamento 0". O deslocamento 0 cai a meio do cabeçalho e é estruturalmente impossível: a Cavedog reaproveita a sentinela para dizer indefinido. Trate sempre os deslocamentos zero como em falta.

  • A ordem dos bits é MSB primeiro dentro de cada byte. LSB primeiro é a convenção mais comum nos tipos de letra bitmap modernos; não parta do princípio contrário.
  • O fluxo de bits é contínuo entre linhas: não há preenchimento até ao limite de um byte no fim de cada linha de varrimento.
  • Os bytes 2 e 3 são a linha de base e o código do primeiro carácter, e não uma palavra de flags. Um código de primeiro carácter diferente de zero encurta a tabela de deslocamentos.
  • Os caracteres são bytes, não pontos de código Unicode. € é o byte 0x80 em Windows-1252; tomar um ponto de código módulo 256 escolhe o glifo errado.
  • Não há metadados sobre os caracteres suportados. Tem de percorrer a tabela de deslocamentos para descobrir o conjunto de glifos.

Tamanhos típicos

Recurso Intervalo observado nos fonts/*.fnt da Cavedog
Tamanho do ficheiro 700 B – 4 KB
Cabeçalho + tabela de deslocamentos sempre 516 bytes
Altura dos glifos 9–17 px
Largura dos glifos 3–13 px
Glifos definidos por tipo de letra 60–120 (na maioria, só ASCII imprimível)
Dados por glifo normalmente 5–30 bytes (1 byte de largura + bits compactados)

Ver também

  • PAL — os tipos de letra não trazem paleta; o renderizador precisa que escolha uma cor de primeiro plano e de fundo, ou que use a paleta do TA manualmente.
  • Glossário — 1bpp, fluxo de bits MSB primeiro.