FNT — tipos de letra bitmap de 1 bit
Fonte: coreprime/kbot @ 0ab97bf · Editar no GitHub
O Total Annihilation traz uma coleção de ficheiros
.fntpara 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.
fonts/comix.fnt renderizado como folha de sprites de 16 colunas pelo kbot fnt sheet
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 --codepageusa 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 byte0x80em 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) |