115 lines
3.6 KiB
Python
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)")
|
|
|
|
|