FNT: fontes bitmap de 1 bit
Fonte: coreprime/kbot @ 0ab97bf · Editar no GitHub
O Total Annihilation vem com uma coleção de arquivos
.fntpara tudo, das mensagens de rádio na tela (comix.fnt) às telas de briefing das unidades (armbrief.fnt). São fontes bitmap minúsculas, de largura variável, com 1 bit por pixel, indexadas por código de caractere de 8 bits: no fundo, uma folha de sprites com uma tabela de consulta.
fonts/comix.fnt renderizada como folha de sprites de 16 colunas pelo kbot fnt sheet
kbot fnt render comix.fnt --text "kbot" --fg "#ffff00" --bg "#000000"
Tip
Experimente você mesmo.
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 CLI.
Em 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")
}
Estrutura 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
└──────────────────────────┘
Todas as fontes da versão comercial têm FirstChar = 0, então o cabeçalho + a tabela
de offsets ocupam 4 + 256 × 2 = 516 bytes, e os dados dos glifos começam
logo depois.
| Campo | Observações |
|---|---|
Height |
Só o byte 0. Altura em pixels comum a todos os glifos da fonte (de 9 a 17 nas fontes da versão comercial). |
Baseline |
Byte 2, com sinal. Um texto desenhado com a caneta na linha y põe a linha de cima dos glifos em y − Baseline. As fontes da versão comercial guardam de 1 a 3. |
FirstChar |
Byte 3. O caractere c usa a entrada c − FirstChar da tabela de offsets; os códigos abaixo dele não têm glifo. As fontes da versão comercial guardam 0. |
Offsets[] |
Indexada pelo byte do caractere (o texto do jogo comercial é Windows-1252). 0 significa nenhum glifo para este caractere. Os valores diferentes de zero são offsets contados do início da fonte e apontam para o byte Width do glifo. |
Width do glifo |
De 1 a 255. Com largura 0, o jogo avança 0 e desenha lixo; o kbot pula esses glifos e os informa. |
Ferramentas mais antigas liam os bytes 2–3 como uma única palavra opaca de "flags"; na verdade são dois campos separados.
Como ler um glifo
Os dados do glifo são um fluxo de bits contínuo, MSB primeiro,
empacotado da esquerda para a direita e depois de cima para baixo, ou seja,
varredura por linhas. 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 largura de avanço separada de Width. O jogo
compõe o texto assim, 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 aquele código (converta antes o
texto UTF-8 para a página de código do jogo; o padrão de
kbot fnt render --codepageé Windows-1252); - um glifo avança a caneta exatamente pela sua largura, sem espaço extra;
- um caractere sem glifo não desenha nada e avança 0;
- o desenho para no primeiro NUL ou quebra de linha.
Uma fonte de 13 linhas com um A de 10 pixels e sem glifo de espaço desenha,
portanto, "A A" com 20 pixels de largura.
Note
Não há preenchimento entre as linhas. Cada linha de varredura continua
de onde a anterior terminou, no mesmo byte. Um glifo de 5 pixels 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 isso: é uma fonte de 14 pixels 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 não definidas são, na maioria, caracteres de controle
não imprimíveis; o resto são extras do Latin-1 que o jogo não exibe.
A tabela de offsets dos dez primeiros 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 '$'
...
O mesmo conteúdo sai com kbot fnt describe --list.
Dicas de renderização
O kbot fnt render aceita:
| Opção | Efeito |
|---|---|
--text "..." |
O texto a renderizar: uma linha; o desenho para na primeira quebra de linha, como no jogo. |
--codepage NAME |
Página de código para a qual o texto UTF-8 é convertido: cp1252 (padrão, a do jogo comercial), cp1250, cp1251, cp437, cp850, iso-8859-1, iso-8859-15, ou raw para passar os bytes sem conversão. Os caracteres sem byte correspondente viram ?. |
--fg #rrggbb[aa] |
Cor do primeiro plano. Padrão: branco. |
--bg #rrggbb[aa] | transparent |
Fundo. Padrão: transparente. |
--target PATH |
Caminho do PNG de saída. Sem ele, a saída vai para stdout. |
Um caractere que não existe na fonte não desenha nada e não ocupa espaço, como no jogo.
Armadilhas
Warning
Offsets[c] == 0 significa "sem glifo", não "glifo no offset 0". O
offset 0 cai no meio do cabeçalho e é estruturalmente impossível; a
Cavedog reaproveita esse valor sentinela para dizer não definido. Trate
sempre os offsets zero como ausentes.
- A ordem dos bits é MSB primeiro dentro de cada byte. LSB primeiro é a convenção mais comum nas fontes bitmap modernas; não presuma.
- O fluxo de bits é contínuo entre as linhas: não há preenchimento até o limite de um byte no fim de cada linha de varredura.
- Os bytes 2 e 3 são a linha de base e o código do primeiro caractere, e não uma palavra de flags. Um código de primeiro caractere diferente de zero encurta a tabela de offsets.
- 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 quais caracteres são suportados. É preciso percorrer a tabela de offsets para descobrir o conjunto de glifos.
Tamanhos típicos
| Recurso | Faixa observada nos fonts/*.fnt da Cavedog |
|---|---|
| Tamanho do arquivo | 700 B – 4 KB |
| Cabeçalho + tabela de offsets | sempre 516 bytes |
| Altura do glifo | 9–17 px |
| Largura do glifo | 3–13 px |
| Glifos definidos por fonte | 60–120 (a maioria só com ASCII imprimível) |
| Dados por glifo | em geral de 5 a 30 bytes (1 byte de largura + bits empacotados) |