FNT — 1비트 비트맵 글꼴
Total Annihilation에는 화면의 무전 대화(
comix.fnt)부터 유닛 브리핑 화면(armbrief.fnt)까지 온갖 곳에 쓰이는.fnt파일이 여럿 들어 있습니다. 8비트 문자 코드로 인덱싱하는, 작고 가변 폭인 픽셀당 1비트 비트맵 글꼴입니다 — 사실상 조회 테이블이 딸린 스프라이트 시트입니다.
kbot fnt sheet로 16열 스프라이트 시트로 렌더링한 fonts/comix.fnt
kbot fnt render comix.fnt --text "kbot" --fg "#ffff00" --bg "#000000"
Tip
직접 해 보세요.
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
CLI kbot fnt 레퍼런스를 참고하세요.
Go에서 쓰기. 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")
}
디스크상 구조
┌─ 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
└──────────────────────────┘
모든 정식판 글꼴은 FirstChar = 0이므로 헤더 + 오프셋 테이블은
4 + 256 × 2 = 516바이트이고, 글리프 데이터는 그 바로 뒤에서 시작합니다.
| 필드 | 비고 |
|---|---|
Height |
바이트 0만 해당. 글꼴의 모든 글리프가 함께 쓰는 픽셀 높이(정식판 글꼴에서는 9–17). |
Baseline |
바이트 2, 부호 있음. 펜을 y행에 두고 텍스트를 그리면 글리프의 맨 윗행이 y − Baseline에 옵니다. 정식판 글꼴은 1–3을 저장합니다. |
FirstChar |
바이트 3. 문자 c는 오프셋 테이블 항목 c − FirstChar를 쓰며, 그보다 작은 코드에는 글리프가 없습니다. 정식판 글꼴은 0을 저장합니다. |
Offsets[] |
문자 바이트로 인덱싱합니다(정식판 게임의 텍스트는 Windows-1252). 0은 이 문자에 대한 글리프 없음을 뜻합니다. 0이 아닌 값은 글꼴 시작 기준의 오프셋으로, 글리프의 Width 바이트를 가리킵니다. |
글리프 Width |
1–255. 너비가 0이면 게임은 펜을 0만큼 움직이고 쓰레기를 그립니다. kbot은 그런 글리프를 건너뛰고 보고합니다. |
예전 도구들은 바이트 2–3을 하나의 불투명한 "플래그" 워드로 읽었지만, 이 둘은 별개의 필드입니다.
글리프 읽기
글리프 페이로드는 끊기지 않는 비트 스트림으로, MSB 우선이며 왼쪽에서
오른쪽으로, 그다음 위에서 아래로 채워집니다 — 즉 행 우선 스캔입니다. 비트 수는
Width × Height이고, 저장된 바이트 수는 ⌈(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)
커닝 데이터는 없으며, Width와 별개인 전진 폭도 없습니다. 게임은 텍스트를
다음과 같이 배치하며, kbot fnt render, MCP fnt_render 도구, 에셋 탐색기의
텍스트 미리 보기도 똑같이 합니다.
- 텍스트의 각 바이트가 그 코드의 글리프를 고릅니다(UTF-8 텍스트는 먼저 게임의
코드 페이지로 변환하세요.
kbot fnt render --codepage의 기본값은 Windows-1252입니다). - 글리프는 펜을 정확히 자기 너비만큼 간격 없이 움직입니다.
- 글리프가 없는 문자는 아무것도 그리지 않고 0만큼 움직입니다.
- 그리기는 첫 NUL이나 줄바꿈에서 멈춥니다.
따라서 A의 너비가 10픽셀이고 공백 글리프가 없는 13행짜리 글꼴은 "A A"를
너비 20픽셀로 그립니다.
Note
행 사이에 패딩이 없습니다. 각 스캔 라인은 같은 바이트 안에서 앞 라인이 끝난
곳에서 바로 이어집니다. 너비 5픽셀, 높이 7픽셀인 글리프는 ⌈35/8⌉ = 5바이트를
쓰며, 뒤쪽 5비트는 무시됩니다.
실제 예제 — 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
읽는 법: 출력 가능한 ASCII(공백부터 }까지)를 다루는 높이 14픽셀 글꼴입니다.
너비는 3(i, l)부터 13(M, W)까지 다양합니다. 정의되지 않은 슬롯 162개는
대부분 출력할 수 없는 제어 문자이고, 나머지는 게임이 표시하지 않는 Latin-1 추가
문자입니다.
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 '$'
...
kbot fnt describe --list로 같은 내용을 덤프할 수 있습니다.
렌더링 팁
kbot fnt render가 지원하는 플래그:
| 플래그 | 효과 |
|---|---|
--text "..." |
렌더링할 문자열: 한 줄이며, 게임과 마찬가지로 첫 줄바꿈에서 그리기를 멈춥니다. |
--codepage NAME |
UTF-8 텍스트를 변환할 코드 페이지: cp1252(기본값, 정식판 게임의 것), cp1250, cp1251, cp437, cp850, iso-8859-1, iso-8859-15, 또는 바이트를 그대로 넘기는 raw. 대응하는 바이트가 없는 문자는 ?가 됩니다. |
--fg #rrggbb[aa] |
전경색. 기본값: 흰색. |
--bg #rrggbb[aa] | transparent |
배경. 기본값: 투명. |
--target PATH |
출력 PNG 경로. 없으면 표준 출력. |
글꼴에 없는 문자는 게임과 마찬가지로 아무것도 그리지 않고 공간도 차지하지 않습니다.
주의할 점
Warning
Offsets[c] == 0은 "오프셋 0에 있는 글리프"가 아니라 "글리프 없음"을
뜻합니다. 오프셋 0은 헤더 한가운데에 해당하므로 구조상 불가능합니다 —
Cavedog은 이 감시값을 정의되지 않음의 뜻으로 다시 씁니다. 0인 오프셋은 항상
없는 것으로 취급하세요.
- 비트 순서는 바이트마다 MSB 우선입니다. 요즘 비트맵 글꼴에서는 LSB 우선이 더 흔한 관례이니 지레짐작하지 마세요.
- 비트 스트림은 행 사이에서 끊기지 않고 이어집니다 — 각 스캔 라인 끝에 바이트 경계까지 채우는 패딩이 없습니다.
- 바이트 2와 3은 플래그 워드가 아니라 기준선과 첫 문자 코드입니다. 첫 문자 코드가 0이 아니면 오프셋 테이블이 짧아집니다.
- 문자는 유니코드 코드 포인트가 아니라 바이트입니다.
€는 Windows-1252에서 바이트0x80입니다. 코드 포인트를 256으로 나눈 나머지를 쓰면 엉뚱한 글리프를 고르게 됩니다. - 어떤 문자를 지원하는지에 대한 메타데이터가 없습니다. 글리프 세트를 알아내려면 오프셋 테이블을 훑어야 합니다.
일반적인 크기
| 에셋 | Cavedog fonts/*.fnt에서 관찰된 범위 |
|---|---|
| 파일 크기 | 700 B – 4 KB |
| 헤더 + 오프셋 테이블 | 항상 516바이트 |
| 글리프 높이 | 9–17 px |
| 글리프 너비 | 3–13 px |
| 글꼴당 정의된 글리프 | 60–120(대부분 출력 가능한 ASCII뿐) |
| 글리프당 데이터 | 보통 5–30바이트(너비 바이트 1개 + 채워 넣은 비트) |