Files

115 lines
3.6 KiB
Python

#!/usr/bin/env python3
"""
Generate a VLW font file compatible with LovyanGFX / M5GFX.
VLW format (all big-endian int32), verified against lgfx_fonts.cpp:
Header (6 x int32):
[0] glyph_count
[1] vlw_encoder_version (write 8, matches Processing default)
[2] font_size (points)
[3] reserved / discard
[4] ascent
[5] descent
Per glyph (7 x int32), at offset 24:
[0] codepoint
[1] height
[2] width
[3] xAdvance
[4] dY (y delta from baseline: ascent_px - bitmap_top)
[5] gdX (x delta from cursor: bitmap_left / lsb)
[6] reserved (0)
Bitmaps (at offset 24 + gCount*28):
width * height bytes each, 1 byte per pixel (0=off, 255=on)
"""
import struct
import math
import freetype
FONT_PATH = "/usr/share/fonts/Unifont/Unifont.ttf"
SIZE_PT = 24
DPI = 96
GLYPHS = "ʘ⌐■★()>_<Ox "
OUT = "unifont24.vlw"
OUT_H = "../src/unifont24_vlw.h"
face = freetype.Face(FONT_PATH)
face.set_char_size(SIZE_PT * 64, 0, DPI, 0)
metrics_face = face.size
ascent_px = math.ceil(metrics_face.ascender / 64)
descent_px = math.ceil(-metrics_face.descender / 64) # positive
glyphs_data = [] # (codepoint, height, width, xAdvance, dY, gdX, pixels)
for ch in GLYPHS:
cp = ord(ch)
# Grayscale render — gives 1 byte per pixel output
face.load_char(cp, freetype.FT_LOAD_RENDER)
slot = face.glyph
bitmap = slot.bitmap
w = bitmap.width
h = bitmap.rows
gdX = slot.bitmap_left # x offset from cursor
dY = slot.bitmap_top # y delta from baseline (above baseline = positive)
xAdv = math.ceil(slot.advance.x / 64)
# Grayscale pitch may be wider than width — trim to width bytes per row
# FreeType renders top-to-bottom in FT_PIXEL_MODE_GRAY — no reversal needed
pitch = bitmap.pitch
raw = bytes(bitmap.buffer)
pixels = bytearray()
for row in range(h):
pixels += raw[row * pitch: row * pitch + w]
glyphs_data.append((cp, h, w, xAdv, dY, gdX, bytes(pixels)))
print(f" U+{cp:04X} '{ch}' adv={xAdv} {w}x{h} gdX={gdX} dY={dY}")
# MUST be sorted ascending by codepoint — getUnicodeIndex uses lower_bound
glyphs_data.sort(key=lambda g: g[0])
with open(OUT, "wb") as f:
# Header — 6 int32s
f.write(struct.pack(">iiiiii",
len(glyphs_data), # [0] glyph count
8, # [1] vlw encoder version
SIZE_PT, # [2] font size in points
0, # [3] reserved
ascent_px, # [4] ascent
descent_px, # [5] descent
))
# Glyph records
for cp, h, w, xAdv, dY, gdX, _ in glyphs_data:
f.write(struct.pack(">iiiiiii", cp, h, w, xAdv, dY, gdX, 0))
# Bitmaps (1 byte per pixel)
for _, _, _, _, _, _, pixels in glyphs_data:
f.write(pixels)
print(f"\nWrote '{OUT}' ({len(glyphs_data)} glyphs, size={SIZE_PT}pt @ {DPI}dpi)")
print(f"Ascent={ascent_px} Descent={descent_px}")
# C header for embedding in firmware flash
vlw_bytes = open(OUT, "rb").read()
with open(OUT_H, "w") as h_file:
h_file.write("#pragma once\n")
h_file.write("#include <stdint.h>\n\n")
h_file.write(f"// Auto-generated by gen_vlw.py — {len(glyphs_data)} glyphs, {SIZE_PT}pt\n")
h_file.write(f"// Usage: M5.Display.loadFont(unifont24_vlw);\n\n")
h_file.write(f"static const uint8_t unifont24_vlw[] = {{\n")
for i, b in enumerate(vlw_bytes):
if i % 16 == 0:
h_file.write(" ")
h_file.write(f"0x{b:02X},")
if i % 16 == 15:
h_file.write("\n")
else:
h_file.write(" ")
h_file.write("\n};\n")
print(f"Wrote '{OUT_H}' ({len(vlw_bytes)} bytes)")