better font rendering and start of z80 access
This commit is contained in:
115
tools/generate_fonts.py
Normal file
115
tools/generate_fonts.py
Normal file
@@ -0,0 +1,115 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user