#!/usr/bin/env python3 # Generate the anti-aliased bitmap fonts baked into main.py. # # Each font is one base64 blob: a small metrics table + 4-bit-alpha glyph pixels. # main.py decodes it at boot (binascii.a2b_base64) and renders text by blending the # foreground over the background per pixel via a 16-entry LUT (smooth, no upscaling). # # Re-run after changing sizes/charset: python3 pico/gen_font.py # It writes pico/_font_m.b64 + pico/_font_l.b64 and /tmp/font_verify.png (eyeball it). Then # inject the two base64 strings into main.py's FONT_M_B64 / FONT_L_B64 (replace the existing # values, or re-add the @@FONT_M@@ / @@FONT_L@@ placeholders first and substitute them in). import base64, pathlib from PIL import Image, ImageDraw, ImageFont FONT = "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf" HERE = pathlib.Path(__file__).parent ASCII = "".join(chr(c) for c in range(0x20, 0x7F)) SYMBOLS = "◀▶■" # ◀ ▶ ■ (prev / play / stop, anti-aliased) M_CHARS = ASCII + SYMBOLS L_CHARS = "0123456789 " S_CHARS = ASCII # small font for the pad-grid lane labels (CircuitPython) def build(size, chars): font = ImageFont.truetype(FONT, size) recs = [] # (cp, w, h, xoff, top, adv) pixels = bytearray() for ch in chars: cp = ord(ch) adv = round(font.getlength(ch)) bbox = font.getbbox(ch) if not bbox or bbox[2] - bbox[0] <= 0 or bbox[3] - bbox[1] <= 0: recs.append((cp, 0, 0, 0, 0, adv)); continue l, t, r, b = bbox w, h = r - l, b - t img = Image.new("L", (w, h), 0) ImageDraw.Draw(img).text((-l, -t), ch, fill=255, font=font) px = img.load() # pack 4-bit alpha, row-major, two pixels per byte (first = high nibble) nib = [] for y in range(h): for x in range(w): nib.append(px[x, y] >> 4) if len(nib) % 2: nib.append(0) for i in range(0, len(nib), 2): pixels.append((nib[i] << 4) | nib[i + 1]) recs.append((cp, w, h, l & 0xFF, t & 0xFF, adv & 0xFF)) header = bytearray([len(recs)]) for cp, w, h, xoff, top, adv in recs: header += bytes([cp >> 8, cp & 0xFF, w, h, xoff, top, adv]) return bytes(header) + bytes(pixels) # ---- reference decoder + renderer (must match main.py exactly) ---- def load_font(blob): count = blob[0]; p = 1; pixoff = 1 + count * 7; glyphs = {} for _ in range(count): cp = (blob[p] << 8) | blob[p + 1]; w = blob[p + 2]; h = blob[p + 3] xoff = blob[p + 4]; xoff = xoff - 256 if xoff > 127 else xoff top = blob[p + 5]; adv = blob[p + 6]; p += 7 glyphs[cp] = (w, h, xoff, top, adv, pixoff) pixoff += (w * h + 1) // 2 return glyphs, blob def lut(fg, bg): def unp(c): return ((c >> 11) & 0x1F, (c >> 5) & 0x3F, c & 0x1F) fr, fgc, fb = unp(fg); br, bgc, bb = unp(bg); out = [] for a in range(16): t = a * 17 r = (br * (255 - t) + fr * t) // 255 g = (bgc * (255 - t) + fgc * t) // 255 b = (bb * (255 - t) + fb * t) // 255 out.append((r << 11) | (g << 5) | b) return out def render(draw_img, font, s, x, y, fg, bg): glyphs, blob = font; L = lut(fg, bg); pen = x for ch in s: g = glyphs.get(ord(ch)) if not g: continue w, h, xoff, top, adv, off = g for j in range(h): for i in range(w): k = j * w + i; byte = blob[off + (k >> 1)] nibv = (byte >> 4) if (k & 1) == 0 else (byte & 0xF) col = L[nibv] r = (col >> 11) & 0x1F; gg = (col >> 5) & 0x3F; b = col & 0x1F draw_img.putpixel((pen + xoff + i, y + top + j), (r << 3, gg << 2, b << 3)) pen += adv return pen blob_s = build(12, S_CHARS) blob_m = build(22, M_CHARS) blob_l = build(78, L_CHARS) # base64 (baked into the MicroPython firmware, pico/main.py — FONT_M / FONT_L) (HERE / "_font_m.b64").write_text(base64.b64encode(blob_m).decode()) (HERE / "_font_l.b64").write_text(base64.b64encode(blob_l).decode()) print("FONT_M %d bytes -> %d b64" % (len(blob_m), len(base64.b64encode(blob_m)))) print("FONT_L %d bytes -> %d b64" % (len(blob_l), len(base64.b64encode(blob_l)))) # binary blobs (read at boot by the CircuitPython firmware, pico-cp/code.py) CP = HERE.parent / "pico-cp" (CP / "font_s.bin").write_bytes(blob_s) (CP / "font_m.bin").write_bytes(blob_m) (CP / "font_l.bin").write_bytes(blob_l) print("wrote pico-cp/font_{s,m,l}.bin (%d / %d / %d bytes)" % (len(blob_s), len(blob_m), len(blob_l))) # verification image on a dark bg (565 colours like the firmware) def c565(r, g, b): return ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3) BG = c565(6, 9, 14); CYAN = c565(10, 179, 247); TXT = c565(199, 208, 219); GREEN = c565(47, 224, 122) img = Image.new("RGB", (320, 220), (6, 9, 14)) fm = load_font(blob_m); fl = load_font(blob_l) render(img, fm, "PM_K-1 KIT", 12, 10, CYAN, BG) render(img, fl, "120", 150, 40, TXT, BG) render(img, fm, "BPM", 12, 70, TXT, BG) render(img, fm, "▶ RUN", 12, 120, GREEN, BG) render(img, fm, "Four-on-the-floor", 12, 150, TXT, BG) render(img, fm, "◀◀ ▶ ▶▶ - TAP +", 12, 185, TXT, BG) img.resize((640, 440), Image.NEAREST).save("/tmp/font_verify.png") print("wrote /tmp/font_verify.png")