Skip to content

Commit ecb3e6b

Browse files
committed
initial commit
0 parents  commit ecb3e6b

16 files changed

Lines changed: 2933 additions & 0 deletions

File tree

.github/workflows/release.yml

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
name: Release
2+
3+
on:
4+
push:
5+
tags:
6+
- "v*"
7+
workflow_dispatch:
8+
9+
permissions:
10+
contents: write
11+
12+
jobs:
13+
test:
14+
runs-on: ubuntu-latest
15+
steps:
16+
- uses: actions/checkout@v4
17+
- uses: actions/setup-go@v5
18+
with:
19+
go-version-file: go.mod
20+
- name: Run tests
21+
run: go test ./...
22+
23+
build:
24+
needs: test
25+
runs-on: ubuntu-latest
26+
strategy:
27+
fail-fast: false
28+
matrix:
29+
include:
30+
- goos: linux
31+
goarch: amd64
32+
- goos: linux
33+
goarch: arm64
34+
- goos: darwin
35+
goarch: amd64
36+
- goos: darwin
37+
goarch: arm64
38+
- goos: windows
39+
goarch: amd64
40+
- goos: windows
41+
goarch: arm64
42+
43+
steps:
44+
- uses: actions/checkout@v4
45+
46+
- uses: actions/setup-go@v5
47+
with:
48+
go-version-file: go.mod
49+
50+
- name: Build binary
51+
env:
52+
GOOS: ${{ matrix.goos }}
53+
GOARCH: ${{ matrix.goarch }}
54+
run: |
55+
version="dev-${GITHUB_SHA::7}"
56+
if [[ "${GITHUB_REF}" == refs/tags/* ]]; then
57+
version="${GITHUB_REF#refs/tags/}"
58+
fi
59+
60+
mkdir -p dist
61+
ext=""
62+
if [ "${GOOS}" = "windows" ]; then
63+
ext=".exe"
64+
fi
65+
out="dist/ra2fnt-${GOOS}-${GOARCH}${ext}"
66+
go build -trimpath -ldflags="-s -w -X main.version=${version}" -o "${out}" ./src/cmd/ra2fnt
67+
68+
- name: Upload artifact
69+
uses: actions/upload-artifact@v4
70+
with:
71+
name: ra2fnt-${{ matrix.goos }}-${{ matrix.goarch }}
72+
path: dist/*
73+
if-no-files-found: error
74+
75+
publish:
76+
if: startsWith(github.ref, 'refs/tags/')
77+
needs: build
78+
runs-on: ubuntu-latest
79+
permissions:
80+
contents: write
81+
steps:
82+
- name: Download artifacts
83+
uses: actions/download-artifact@v4
84+
with:
85+
path: dist
86+
87+
- name: Flatten artifacts
88+
run: |
89+
mkdir -p release
90+
find dist -maxdepth 3 -type f -print -exec cp {} release/ \;
91+
92+
- name: Publish GitHub release
93+
uses: softprops/action-gh-release@v2
94+
with:
95+
tag_name: ${{ github.ref_name }}
96+
name: ${{ github.ref_name }}
97+
generate_release_notes: true
98+
fail_on_unmatched_files: true
99+
files: release/*
100+
env:
101+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

.gitignore

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
.vscode/*
2+
out_font/*
3+
*.fnt
4+
/ra2fnt
5+
/ra2fnt.exe
6+
/dist/
7+
/release/
8+
/ra2fnt-linux-amd64
9+
/ra2fnt-linux-arm64
10+
/ra2fnt-darwin-amd64
11+
/ra2fnt-darwin-arm64
12+
/ra2fnt-windows-amd64.exe
13+
/ra2fnt-windows-arm64.exe

LICENSE

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
MIT License
2+
3+
Copyright (c) 2026 Belonit
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6+
7+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8+
9+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

README.md

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
# ra2fnt
2+
3+
This program was created entirely using `GPT-5.3-Codex` without writing code by hand.
4+
5+
CLI utility for converting [Westwood Unicode BitFont](https://moddingwiki.shikadi.net/wiki/Westwood_Unicode_BitFont_Format) (`game.fnt`, `fonT`), a font format used in Command & Conquer: Red Alert 2, to a PNG set and back.
6+
7+
## Build
8+
9+
```bash
10+
go build ./src/cmd/ra2fnt
11+
```
12+
13+
Local multi-platform release build scripts:
14+
15+
```bash
16+
./scripts/build-release.sh
17+
./scripts/build-release.sh v1.0.0
18+
```
19+
20+
```powershell
21+
powershell -ExecutionPolicy Bypass -File .\scripts\build-release.ps1
22+
powershell -ExecutionPolicy Bypass -File .\scripts\build-release.ps1 -Version v1.0.0
23+
```
24+
25+
```bat
26+
scripts\build-release.bat
27+
scripts\build-release.bat v1.0.0
28+
```
29+
30+
## Usage
31+
32+
Export `.fnt` to PNG set:
33+
34+
```bash
35+
./ra2fnt export -in game.fnt -out out_font
36+
```
37+
38+
Export with integer pixel scaling (for easier editing):
39+
40+
```bash
41+
./ra2fnt export -in game.fnt -out out_font --scale 3
42+
```
43+
44+
Non-interactive overwrite for scripts/CI:
45+
46+
```bash
47+
./ra2fnt export -in game.fnt -out out_font --force
48+
```
49+
50+
Create `.fnt` from PNG set:
51+
52+
```bash
53+
./ra2fnt create -in out_font -out rebuilt.fnt
54+
```
55+
56+
Create without glyph deduplication:
57+
58+
```bash
59+
./ra2fnt create -in out_font -out rebuilt.fnt --no-dedup
60+
```
61+
62+
Validate PNG set and metadata without writing `.fnt`:
63+
64+
```bash
65+
./ra2fnt validate -in out_font
66+
```
67+
68+
Show CLI version:
69+
70+
```bash
71+
./ra2fnt version
72+
./ra2fnt --version
73+
```
74+
75+
`export` and `create` show a progress bar in `stderr`.
76+
If `out` directory already exists, `export` asks for confirmation before deleting it.
77+
- Use `--force` to skip confirmation and overwrite `out`.
78+
79+
## Export format
80+
81+
`export` writes only PNG files grouped by Unicode ranges:
82+
83+
```text
84+
out_font/
85+
metadata.json
86+
0x0020-0x007F (Basic Latin)/
87+
0x0041.png
88+
0x0042.png
89+
0x0400-0x04FF (Cyrillic)/
90+
0x0451.png
91+
```
92+
93+
In this tree, only non-zero-width glyphs are shown as PNG files.
94+
Zero-width glyphs are listed only in `metadata.json` (`symbol_width`).
95+
96+
- One PNG is exported per mapped Unicode codepoint from the `.fnt` unicode table.
97+
- PNG filename is fixed-length hex codepoint: `0xXXXX.png`.
98+
- `metadata.json` stores:
99+
- `symbol_width` (only zero-width glyphs, as `0`)
100+
- `symbol_stride`
101+
- `font_height`
102+
- `ideograph_width`
103+
- `scale` (integer export scale, default `1`)
104+
- Zero-width glyphs are not exported as PNG files and are restored via `metadata.json` (`symbol_width=0`).
105+
- PNG height is `symbol_height`.
106+
- No `unicode_table.bin` or `tail.bin` is produced.
107+
108+
## Create behavior
109+
110+
`create` reconstructs a `.fnt` from PNG files in the input directory:
111+
112+
- PNG files are discovered recursively (subdirectory names are ignored by parser).
113+
- Files must be named as fixed-length hex codepoints (for example `0x0041.png`, `0x30A1.png`).
114+
- Zero-width glyphs are restored from `metadata.json` (`symbol_width=0`).
115+
- Codepoints with `symbol_width=0` must not have PNG files.
116+
- All PNG files must have the same height.
117+
- `symbol_width` contains only zero-width entries and is used to restore width `0`; otherwise width is taken from PNG width.
118+
- `symbol_stride` is taken from `metadata.json` when present; otherwise it is auto-calculated as `ceil(max_symbol_width / 8)`.
119+
- `font_height` is taken from `metadata.json`.
120+
- `ideograph_width` is taken from `metadata.json`.
121+
- `scale` is taken from `metadata.json`; when `scale > 1`, PNG dimensions are downscaled by this factor during `create` (back to normal font size).
122+
- Identical glyphs are deduplicated, so multiple codepoints can reference the same symbol index.
123+
- Use `--no-dedup` to disable deduplication.
124+
- Unicode table is rebuilt from filenames (`0xXXXX` -> symbol index in sorted codepoint order).
125+
126+
Because unicode mapping order/tail bytes are rebuilt, the resulting `.fnt` is not expected to be byte-identical to the original input file.
127+
128+
## Validate behavior
129+
130+
`validate` runs the same checks as `create` without writing output `.fnt`, and prints a summary:
131+
132+
- total codepoints
133+
- number of PNG files
134+
- number of zero-width codepoints from metadata
135+
- resulting symbol count
136+
- number of deduplicated symbols
137+
138+
## Limitations
139+
140+
- Output `.fnt` is not byte-identical to source `game.fnt`.
141+
- At least one non-zero-width PNG is required to infer `symbol_height`.
142+
- Zero-width glyphs are represented only in `metadata.json` and have no PNG files.
143+
- Unicode table order is rebuilt from sorted codepoints.

cspell.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"$schema": "https://raw.githubusercontent.com/streetsidesoftware/cspell/main/cspell.schema.json",
3+
"language": "en",
4+
"allowCompoundWords": true,
5+
"useGitignore": true,
6+
"words": []
7+
}

go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module ra2fnt
2+
3+
go 1.22

scripts/build-release.bat

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
@echo off
2+
setlocal EnableExtensions EnableDelayedExpansion
3+
4+
set "SCRIPT_DIR=%~dp0"
5+
for %%I in ("%SCRIPT_DIR%..") do set "REPO_ROOT=%%~fI"
6+
7+
set "VERSION=%~1"
8+
if "%VERSION%"=="" (
9+
for /f %%I in ('git -C "%REPO_ROOT%" rev-parse --short HEAD 2^>nul') do set "GIT_SHA=%%I"
10+
if defined GIT_SHA (
11+
set "VERSION=dev-!GIT_SHA!"
12+
) else (
13+
set "VERSION=dev"
14+
)
15+
)
16+
17+
set "OUT_DIR=%REPO_ROOT%\dist"
18+
if not exist "%OUT_DIR%" mkdir "%OUT_DIR%"
19+
20+
pushd "%REPO_ROOT%"
21+
call :build linux amd64
22+
if errorlevel 1 goto :fail
23+
call :build linux arm64
24+
if errorlevel 1 goto :fail
25+
call :build darwin amd64
26+
if errorlevel 1 goto :fail
27+
call :build darwin arm64
28+
if errorlevel 1 goto :fail
29+
call :build windows amd64
30+
if errorlevel 1 goto :fail
31+
call :build windows arm64
32+
if errorlevel 1 goto :fail
33+
popd
34+
35+
echo done: version=%VERSION%
36+
echo artifacts: %OUT_DIR%
37+
exit /b 0
38+
39+
:build
40+
set "GOOS=%~1"
41+
set "GOARCH=%~2"
42+
set "EXT="
43+
if "%GOOS%"=="windows" set "EXT=.exe"
44+
set "OUT_FILE=%OUT_DIR%\ra2fnt-%GOOS%-%GOARCH%%EXT%"
45+
46+
echo building %OUT_FILE%
47+
set "CGO_ENABLED=0"
48+
set "GOOS=%GOOS%"
49+
set "GOARCH=%GOARCH%"
50+
go build -trimpath -ldflags "-s -w -X main.version=%VERSION%" -o "%OUT_FILE%" ./src/cmd/ra2fnt
51+
if errorlevel 1 exit /b 1
52+
exit /b 0
53+
54+
:fail
55+
popd
56+
echo build failed
57+
exit /b 1

scripts/build-release.ps1

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
param(
2+
[string]$Version = ""
3+
)
4+
5+
$ErrorActionPreference = "Stop"
6+
7+
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
8+
$RepoRoot = (Resolve-Path (Join-Path $ScriptDir "..")).Path
9+
10+
if ([string]::IsNullOrWhiteSpace($Version)) {
11+
try {
12+
$sha = (git -C $RepoRoot rev-parse --short HEAD).Trim()
13+
if ([string]::IsNullOrWhiteSpace($sha)) {
14+
$Version = "dev"
15+
} else {
16+
$Version = "dev-$sha"
17+
}
18+
} catch {
19+
$Version = "dev"
20+
}
21+
}
22+
23+
$OutDir = Join-Path $RepoRoot "dist"
24+
New-Item -ItemType Directory -Force -Path $OutDir | Out-Null
25+
26+
$Targets = @(
27+
@{ GOOS = "linux"; GOARCH = "amd64" },
28+
@{ GOOS = "linux"; GOARCH = "arm64" },
29+
@{ GOOS = "darwin"; GOARCH = "amd64" },
30+
@{ GOOS = "darwin"; GOARCH = "arm64" },
31+
@{ GOOS = "windows"; GOARCH = "amd64" },
32+
@{ GOOS = "windows"; GOARCH = "arm64" }
33+
)
34+
35+
Push-Location $RepoRoot
36+
try {
37+
foreach ($target in $Targets) {
38+
$ext = if ($target.GOOS -eq "windows") { ".exe" } else { "" }
39+
$outFile = Join-Path $OutDir ("ra2fnt-{0}-{1}{2}" -f $target.GOOS, $target.GOARCH, $ext)
40+
41+
Write-Host "building $outFile"
42+
$env:GOOS = $target.GOOS
43+
$env:GOARCH = $target.GOARCH
44+
$env:CGO_ENABLED = "0"
45+
46+
go build -trimpath -ldflags "-s -w -X main.version=$Version" -o $outFile ./src/cmd/ra2fnt
47+
}
48+
} finally {
49+
Remove-Item Env:GOOS -ErrorAction SilentlyContinue
50+
Remove-Item Env:GOARCH -ErrorAction SilentlyContinue
51+
Remove-Item Env:CGO_ENABLED -ErrorAction SilentlyContinue
52+
Pop-Location
53+
}
54+
55+
Write-Host "done: version=$Version"
56+
Write-Host "artifacts: $OutDir"

0 commit comments

Comments
 (0)