#!/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()