tools for generating glyph bitmaps for src/chars.rs

This commit is contained in:
Waveplate 2025-05-15 22:26:12 -07:00
parent 86d8167e94
commit 998d0d7a24
5 changed files with 191 additions and 0 deletions

62
utils/README.md Normal file
View File

@ -0,0 +1,62 @@
This tool converts an image containing Unicode glyphs into Rust-compatible bitmap arrays for use in the `img2irc` project.
## Workflow Overview
1. **Create input files:**
* An **image file** with glyphs rendered horizontally, without gaps. Essentially a cropped screenshot of the rendered characters with no spaces in between.
* A **text file** listing the same glyphs separated by spaces.
2. **Generate JSON bitmap data (`mkbitmap.py`).**
3. **Convert JSON to Rust source (`mkrust.py`).**
4. Replace `src/chars.rs` in `img2irc` with the generated Rust file and recompile.
---
## `mkbitmap.py`
Slices an image into bitmaps and outputs JSON.
**Arguments:**
| Argument | Description |
| ------------- | ------------------------------------------------ |
| `chars_file` | Text file containing glyphs separated by spaces. |
| `image_file` | Horizontal image of glyphs (no gaps). |
| `--icw` | Input character width (pixels). |
| `--ich` | Input character height (pixels). |
| `--threshold` | Black/white pixel threshold (0255). |
| `--obw` | Output bitmap width. |
| `--obh` | Output bitmap height. |
| `--output` | Output JSON filename. |
**Example:**
```bash
./mkbitmap.py chars.txt chars.png --icw 20 --ich 40 --threshold 128 --obw 10 --obh 20 --output chars.json
```
---
## `mkrust.py`
Converts JSON bitmap data to Rust source code.
**Arguments:**
| Argument | Description |
| ------------ | ----------------------------- |
| `input_json` | JSON file from `mkbitmap.py`. |
| `output_rs` | Output Rust source file. |
**Example:**
```bash
./mkrust.py chars.json chars.rs
```
Replace the resulting `chars.rs` in your `img2irc` project and recompile.
You can use the included `chars.txt` and `chars.png` as an example.

BIN
utils/chars.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

1
utils/chars.txt Normal file
View File

@ -0,0 +1 @@
🬀 🬁 🬂 🬃 🬄 🬅 🬆 🬇 🬈 🬉 🬊 🬋 🬌 🬍 🬎 🬏 🬐 🬑 🬒 🬓 🬔 🬕 🬖 🬗 🬘 🬙 🬚 🬛 🬜 🬝 🬞 🬟 🬠 🬡 🬢 🬣 🬤 🬥 🬦 🬧 🬨 🬩 🬪 🬫 🬬 🬭 🬮 🬯 🬰 🬱 🬲 🬳 🬴 🬵 🬶 🬷 🬸 🬹 🬺 🬻 🬼 🬼 🬽 🬾 🬿 🭀 🭁 🭂 🭃 🭄 🭅 🭆 🭇 🭈 🭉 🭊 🭋 🭌 🭍 🭎 🭏 🭐 🭑 🭒 🭓 🭔 🭕 🭖 🭗 🭘 🭙 🭚 🭛 🭜 🭝 🭞 🭟 🭠 🭡 🭢 🭣 🭤 🭥 🭦 🭧 🭨 🭩 🭪 🭫 🭬 🭭 🭮 🭯 🭶 🭷 🭸 🭹 🭺 🭻 🭼 🭽 🭾 🭿 🮀 🮁 🮂 🮃 🮄 🮅 🮆 🮇 🮈 🮉 🮊 🮋 🮗 🮚 🮛 ▀ ▁ ▂ ▃ ▄ ▅ ▆ ▇ █ ▉ ▊ ▋ ▌ ▍ ▎ ▏ ▐ ▔ ▔ ▕ ▖ ▖ ▗ ▘ ▙ ▚ ▛ ▜ ▝ ▞ ▟ 🠴 🠵 🠶 🠷 🞀 🞁 🞂 🞃 🞍 𝅛 𝅇 𝅉 𝅋 𝅍 𝅏 𝅑 𝅓 ∎ ⎖ ⏴ ⏵ ⏶ ⏷ ⏹ ⏺ │ ┃ ╸ ╷ ╸ ╹ ╻ ■ ▪ ▬ ▮ ▰ ▲ ▴ ▶ ▸ ▼ ▾ ◀ ◂ ◆ ● ◖ ◗ ◢ ◣ ◤ ◥ ❚

63
utils/mkbitmap.py Normal file
View File

@ -0,0 +1,63 @@
#!/usr/bin/env python3
import argparse
import json
from PIL import Image
def main():
p = argparse.ArgumentParser(
description="Slice a row-image of glyphs into bitmaps and output JSON mapping."
)
p.add_argument('chars_file', help='Space-delimited glyphs text file (UTF-8)')
p.add_argument('image_file', help='Image with glyphs in one row, no gaps')
p.add_argument('--icw', type=int, required=True, help='Input char width (px)')
p.add_argument('--ich', type=int, required=True, help='Input char height (px)')
p.add_argument('--threshold', type=int, required=True, help='B/W threshold (0255)')
p.add_argument('--obw', type=int, required=True, help='Output bitmap width')
p.add_argument('--obh', type=int, required=True, help='Output bitmap height')
p.add_argument('--output', required=True, help='Output JSON filename')
args = p.parse_args()
# 1) Read glyphs
with open(args.chars_file, 'r', encoding='utf-8') as f:
glyphs = f.read().strip().split(' ')
# 2) Load image & sanity-check
im = Image.open(args.image_file).convert('L')
w, h = im.size
if h != args.ich:
raise ValueError(f"Image height {h} ≠ ich {args.ich}")
count = w // args.icw
if count * args.icw != w:
raise ValueError(f"Image width {w} not a multiple of icw {args.icw}")
if len(glyphs) != count:
raise ValueError(f"{len(glyphs)} glyphs but {count} blocks in image")
# 3) Build mapping glyph → grid
mapping = {}
for i, glyph in enumerate(glyphs):
left = i * args.icw
block = im.crop((left, 0, left + args.icw, args.ich))
block = block.resize((args.obw, args.obh), Image.LANCZOS)
data = list(block.getdata())
bits = [1 if pix > args.threshold else 0 for pix in data]
grid = [bits[row*args.obw:(row+1)*args.obw] for row in range(args.obh)]
mapping[glyph] = grid
# 4) Write JSON manually so rows stay inline
with open(args.output, 'w', encoding='utf-8') as out:
out.write('{\n')
total = len(mapping)
for idx, (glyph, grid) in enumerate(mapping.items()):
# properly quote the glyph
key = json.dumps(glyph, ensure_ascii=False)
out.write(f' {key}: [\n')
for ridx, row in enumerate(grid):
row_str = ','.join(str(bit) for bit in row)
comma = ',' if ridx < len(grid) - 1 else ''
out.write(f' [{row_str}]{comma}\n')
out.write(' ]')
out.write(',\n' if idx < total - 1 else '\n')
out.write('}\n')
if __name__ == '__main__':
main()

65
utils/mkrust.py Normal file
View File

@ -0,0 +1,65 @@
#!/usr/bin/env python3
import json
import unicodedata
import argparse
import sys
def get_glyph_name(ch):
"""
Return the official Unicode name of ch, or its codepoint if unnamed.
"""
try:
return unicodedata.name(ch)
except ValueError:
# Fallback to codepoint string
return f"U+{ord(ch):04X}"
def format_rust_array(matrix):
"""
Given a 2D list of ints, return a Rust-style nested array string.
"""
lines = []
for row in matrix:
# join numbers with commas, no spaces after commas to match example
line = "[" + ",".join(str(v) for v in row) + "],"
lines.append(" " + line)
return "\n".join(lines)
def main():
parser = argparse.ArgumentParser(
description="Convert JSON glyph bitmaps to a Rust source file."
)
parser.add_argument("input_json", help="Path to input JSON file")
parser.add_argument("output_rs", help="Path to write Rust source file")
args = parser.parse_args()
# Load JSON
with open(args.input_json, "r", encoding="utf-8") as f:
data = json.load(f)
if not data:
print("No glyphs found in JSON.", file=sys.stderr)
sys.exit(1)
# Determine dimensions from first glyph
first_matrix = next(iter(data.values()))
rows = len(first_matrix)
cols = len(first_matrix[0]) if rows > 0 else 0
# Begin emitting Rust code
with open(args.output_rs, "w", encoding="utf-8") as out:
out.write("// This file was generated by mkrust.py\n")
out.write(f"pub const GLYPH_BITMAPS: &[(char, [[u8; {cols}]; {rows}])] = &[\n")
for ch, matrix in data.items():
name = get_glyph_name(ch)
# comment with the Unicode name
out.write(f" // {name}\n")
out.write(f" ('{ch}', [\n")
out.write(format_rust_array(matrix) + "\n")
out.write(" ]),\n\n")
out.write("];\n")
if __name__ == "__main__":
main()