Pure-MicroPython loader and renderer for Picovector PPF (pixel) fonts,
drawing onto any framebuf.FrameBuffer.
Optional per-format viper
blitters accelerate the common linear buffer layouts.
Get some cool icon fonts from here: https://gadgetoid.github.io/iconfont-ppf/
Or steal some from Badgeware: https://github.com/pimoroni/tufty2350/tree/main/romfs/fonts (preview them under pixel fonts here: https://try.badgewa.re/fonts.html).
Above: the bundled petme128, the Badgeware Manticore font, and the
boardgames icon font from iconfont-ppf, all rendered by this library.
With mpremote:
mpremote mip install github:gadgetoid/fbppf
Or freeze it into a firmware build by adding this to a board manifest:
include("/path/to/fbppf/manifest.py")ppf and ppf_viper install to the device's library path; import ppf then
works directly.
import framebuf
from ppf import PPFFont, Canvas
font = PPFFont("ark.ppf")
# Any framebuf.FrameBuffer works (portable path, every pixel format):
fb = framebuf.FrameBuffer(buf, 240, 135, framebuf.RGB565)
font.text(fb, "Hello", 4, 4, 0xFFFF) # col, then optional scale
font.text(fb, "Big", 4, 20, 0xF800, scale=2)
w, h = font.measure("Hello", scale=2)
# A Canvas carries the buffer geometry viper needs. On a build with a native
# emitter and a supported format it renders through the viper fast path;
# otherwise it transparently falls back to the portable path.
canvas = Canvas(240, 135, framebuf.RGB565)
font.text(canvas, "Fast", 4, 4, 0xFFFF)
# canvas.fb is the FrameBuffer; canvas.buffer is the backing bytearray.text() returns the x cursor after the last glyph. \n returns to the start
column and drops one line; \r is ignored. The space glyph advances
width // 3 pixels (times scale), matching the reference renderer.
Viper blitters exist for RGB565, GS8 and MONO_VLSB — the linear formats
where a pixel address is a cheap function of (x, y). Other formats
(GS4_HMSB, MONO_HLSB, ...) use the portable path, which coalesces
horizontal runs into fill_rect calls and clips via framebuf. The portable
path is always used on desktop MicroPython (no native emitter for x86-64 /
aarch64) and whenever a plain FrameBuffer is passed.
All multi-byte fields are big-endian.
| Offset | Type | Field |
|---|---|---|
| 0 | char[4] |
"ppf!" marker |
| 4 | u16 |
flags (unused) |
| 6 | u32 |
glyph count |
| 10 | u16 |
width (maximum glyph width; sets the bitmap row stride) |
| 12 | u16 |
height |
| 14 | char[32] |
name, nul-padded |
| 46 | glyph table | glyph count records of u32 codepoint, u16 width, sorted by codepoint |
| ... | bitmaps | glyph count bitmaps, each ((width + 7) // 8) * height bytes |
Each bitmap is packed row-major, MSB-first, at the font's maximum-width stride;
a glyph uses only its own width columns. Glyphs are looked up by codepoint
with a binary search.
cd tests
micropython test_ppf.py [font.ppf]
micropython test_viper_algo.py [font.ppf]
The viper blitters cannot execute on a desktop build (no aarch64/x86-64 native
emitter), so test_viper_algo.py validates their index math and unsigned clip
logic against the portable renderer. Compilation for real targets is checked
with mpy-cross:
mpy-cross -march=armv6m ppf_viper.py # RP2040
mpy-cross -march=armv7emsp ppf_viper.py # RP2350 (Arm)
mpy-cross -march=rv32imc ppf_viper.py # RP2350 (RISC-V)
MIT; see LICENSE.
examples/petme128.ppf is generated by tools/petme128_to_ppf.py from
MicroPython's built-in font_petme128_8x8.h and remains under its MIT license.


