116 lines
4.3 KiB
Python
116 lines
4.3 KiB
Python
#!/usr/bin/env python3
|
|
"""Generate a separate XBM file for every supported PNG font atlas."""
|
|
|
|
import re
|
|
from pathlib import Path
|
|
|
|
from PIL import Image
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
FONT_DIR = ROOT / "font"
|
|
|
|
|
|
def make_full_ascii_atlas(source: Image.Image, name: str) -> Image.Image:
|
|
"""Return a 16x16 atlas whose character slots match byte values 0..255."""
|
|
if name == "IBM_BIOS_8x8":
|
|
if source.size != (177, 67):
|
|
raise ValueError(f"{name}: expected 177x67, got {source.size}")
|
|
|
|
# Printable ASCII in 11x11 gridded cells with a centered 8x8 glyph.
|
|
atlas = Image.new("L", (128, 128), 255)
|
|
for glyph in range(96):
|
|
source_x = (glyph % 16) * 11 + 2
|
|
source_y = (glyph // 16) * 11 + 2
|
|
character = 32 + glyph
|
|
dest_x = (character % 16) * 8
|
|
dest_y = (character // 16) * 8
|
|
atlas.paste(source.crop((source_x, source_y, source_x + 8, source_y + 8)),
|
|
(dest_x, dest_y))
|
|
return atlas
|
|
|
|
if name == "IBM_VGA_8x16":
|
|
if source.size != (128, 90):
|
|
raise ValueError(f"{name}: expected 128x90, got {source.size}")
|
|
|
|
# The source contains printable ASCII 32..127 in 16 columns and six
|
|
# rows of 8x15 glyphs. Store them in 8x16 cells, padding the bottom.
|
|
atlas = Image.new("L", (128, 256), 255)
|
|
for glyph in range(96):
|
|
source_x = (glyph % 16) * 8
|
|
source_y = (glyph // 16) * 15
|
|
character = 32 + glyph
|
|
dest_x = (character % 16) * 8
|
|
dest_y = (character // 16) * 16
|
|
atlas.paste(source.crop((source_x, source_y, source_x + 8, source_y + 15)),
|
|
(dest_x, dest_y))
|
|
return atlas
|
|
|
|
if name == "ucs_6x13":
|
|
if source.size != (145, 97):
|
|
raise ValueError(f"{name}: expected 145x97, got {source.size}")
|
|
|
|
# Printable ASCII 32..127 is arranged in a 16x6 grid. Each 9x16
|
|
# source cell has a one-pixel grid line and one pixel of inner padding
|
|
# around its 6x13 glyph.
|
|
atlas = Image.new("L", (96, 208), 255)
|
|
for glyph in range(96):
|
|
source_x = (glyph % 16) * 9 + 2
|
|
source_y = (glyph // 16) * 16 + 2
|
|
character = 32 + glyph
|
|
dest_x = (character % 16) * 6
|
|
dest_y = (character // 16) * 13
|
|
atlas.paste(source.crop((source_x, source_y, source_x + 6, source_y + 13)),
|
|
(dest_x, dest_y))
|
|
return atlas
|
|
|
|
# The MBF images are already complete 16x16 character atlases.
|
|
if source.width % 16 or source.height % 16:
|
|
raise ValueError(f"{name}: atlas dimensions must be divisible by 16")
|
|
return source
|
|
|
|
|
|
def write_xbm(image: Image.Image, destination: Path) -> None:
|
|
image = image.convert("L")
|
|
row_bytes = (image.width + 7) // 8
|
|
data = []
|
|
for y in range(image.height):
|
|
for byte_x in range(row_bytes):
|
|
value = 0
|
|
for bit in range(8):
|
|
x = byte_x * 8 + bit
|
|
# Keep the existing renderer convention: white/background is
|
|
# one, while black/glyph pixels are zero. XBM is LSB-first.
|
|
if x >= image.width or image.getpixel((x, y)) >= 128:
|
|
value |= 1 << bit
|
|
data.append(value)
|
|
|
|
symbol = re.sub(r"[^a-zA-Z0-9_]", "_", destination.stem)
|
|
lines = [
|
|
f"#define font_width {image.width}",
|
|
f"#define font_height {image.height}",
|
|
f"static const unsigned char font_bits[] = {{",
|
|
]
|
|
for offset in range(0, len(data), 12):
|
|
values = ", ".join(f"0x{value:02X}" for value in data[offset:offset + 12])
|
|
suffix = "," if offset + 12 < len(data) else ""
|
|
lines.append(f" {values}{suffix}")
|
|
lines.extend(["};", ""])
|
|
destination.write_text("\n".join(lines), encoding="ascii")
|
|
print(f"generated {destination.relative_to(ROOT)} ({symbol})")
|
|
|
|
|
|
def main() -> None:
|
|
sources = sorted(FONT_DIR.glob("*.png"))
|
|
if not sources:
|
|
raise SystemExit("no PNG font atlases found")
|
|
|
|
for source_path in sources:
|
|
source = Image.open(source_path).convert("L")
|
|
atlas = make_full_ascii_atlas(source, source_path.stem)
|
|
write_xbm(atlas, source_path.with_suffix(".xbm"))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|