Release v2.0.1 - 26 June 2026
Please visit the main project's home page here: GitHub
Please the LICENSE file for details of licensing.
This project is a font rendering engine written in pure C for low-to-moderate resolution displays, such as typical 480p or 720p LCD panels, coupled with a lower-power CPU such as an ESP32 or a small ARM processor.
Portable Type Engine works as a standalone add-on for LVGL 9.4 and newer.
It provides runtime-scalable lv_font_t fonts without modifying or patching
LVGL, and supports the Arduino IDE, PlatformIO, and CMake. One compact generated
font can be rendered at multiple sizes or resized while the application runs.
See the LVGL add-on guide, the ESPHome and LVGL
guide, and the examples.
-
High-quality embedded display output: designed for greyscale and color displays, with anti-aliasing that improves readability, particularly at small text sizes.
-
Runtime font scaling: render one compact font definition at many sizes instead of storing a separate font copy for every size.
-
Fast rendering without a persistent glyph cache: text is rasterized on demand without the large RAM cache commonly used by scalable font engines.
-
Extremely low RAM footprint: font data remains in compact, read-only storage, with typical engine working memory around 0.5 KiB for the bundled Roboto font. See RAM usage for the full breakdown and comparison.
PTE includes an ESPHome external component that exposes runtime-scalable fonts
as ordinary IDs accepted by LVGL's text_font option. It bundles regular,
bold, italic, and bold-italic Roboto, so the normal setup is YAML-only:
external_components:
- source:
type: git
url: https://github.com/matt123p/portable-type-engine.git
ref: main
path: src/esphome
components: [pte_font]
pte_font:
- id: ui_font
font: roboto_regular
lvgl:
widgets:
- label:
text: "PTE at 24 pixels"
text_font: ui_font_24Custom fonts can be generated automatically from TTF/OTF files:
pte_font:
- id: ui_custom
font_file: fonts/MyFont.ttf
ranges:
- "0x20-0x7e" # ASCII printable
- "0xa0-0xff" # Latin-1 supplement
symbols: "€£→✓" # Additional symbolsOne pte_font entry automatically creates suffixed IDs from size 6 through
75% of the generated font's sample size. The bundled 128-pixel fonts therefore
provide ui_font_6 through ui_font_96. Only IDs referenced by the validated
ESPHome configuration are emitted into the firmware; unused sizes consume no
font-object RAM or descriptor allocation. Bundled face source files are also
compiled only when at least one ID from that face is used.
Python, Pillow, FontTools, and local TTF/OTF files are required only when building a custom font; they are not needed for the bundled fonts. See the ESPHome and LVGL guide for configuration, bundled font names, and automatic or manual custom-font generation.
A complete ESPHome/LVGL dashboard example is included in
examples/esphome/example_480p_simple.yaml.
It shows PTE's bundled Roboto faces, an automatically generated Material Design
icon font, a 480x480 background image, and LVGL labels that reference multiple
runtime-generated sizes such as ui_font_46, ui_font_14, and
ui_icon_font_75.
It has the following features:
- Standalone LVGL 9.4+ integration with no changes required to LVGL itself
- Extremely low RAM footprint, suitable for memory-constrained embedded systems
- Very small compact C code, with a single dependency of stdlib
- Can render the font at any size at run time from a single font definition
- High quality font output - Characters are rendered to sub-pixel placement and with full anti-aliasing
- A simple python tool is included to convert any TrueType or OpenType font to a C for inclusion in your project
- Compact font definitions, each font is compressed using run-length encoding.
- Unicode glyph tables with UTF-8 text input.
PTE preprocesses TTF and OTF files on a development machine. The embedded
device therefore does not need a TrueType parser, outline data structures, or a
persistent rendered-glyph cache. The generated glyph, kerning, and compressed
bitmap arrays are declared const, so embedded toolchains can keep them in
flash/ROM rather than copying them into RAM.
For the standalone renderer, each generated font has a 36-byte base descriptor
on a typical 32-bit target, and each active size uses a 20-byte pte_font
handle (24 bytes on a typical 64-bit target). While a glyph is being rendered,
PTE allocates one scanline accumulator whose approximate size is:
4 * (encoded glyph width + sub-pixel correction + 1) bytes
This allocation is released immediately after the scanline is rendered. For the bundled Roboto font, encoded at 128 pixels, the widest glyph is 118 pixels; the accumulator is therefore about 0.5 KiB. There is no allocation proportional to the number of characters in the font or the amount of text already drawn. Allocator bookkeeping and application/display buffers are not included in these figures.
When used with LVGL, PTE also has one lv_font_t and a small adapter descriptor
per active size. LVGL supplies an A8 output buffer for the current glyph, which
is approximately rendered width * rendered height bytes plus stride and draw
buffer metadata. That glyph buffer is part of LVGL's rendering pipeline rather
than a persistent PTE cache.
LVGL's Tiny TTF parses the original TTF at runtime and maintains glyph-metric,
rendered-bitmap, and kerning caches. For example, LVGL 9.6 defaults to 128 glyph
entries and 256 kerning entries; defaults can differ between LVGL versions.
Every cached A8 glyph additionally consumes roughly width * height bytes. The
exact total depends on font size, characters used, allocator overhead, and cache
configuration, but it can grow to tens of kilobytes as rendered glyphs fill the
cache.
Tiny TTF can reduce its glyph cache with the _ex creation functions, including
setting the glyph cache size to zero, and it can stream a font from a filesystem
when file support is enabled. Those options trade retained RAM for more runtime
parsing and rasterization work. FreeType and other runtime scalable-font engines
make similar trade-offs and vary considerably with their selected modules and
cache settings.
PTE instead pays the conversion cost before deployment and keeps runtime memory bounded by the current glyph. This makes it a strong fit when predictable, often sub-kilobyte engine working memory matters more than loading arbitrary font files at runtime.
Glyph coverage is blended into the destination pixels to produce smooth edges and readable text, particularly at small sizes on lower-resolution displays.
Glyphs are positioned using fractional pixel coordinates rather than being forced onto whole-pixel boundaries. Anti-aliasing distributes each glyph's coverage across the neighbouring pixels to compensate for that fractional position. This preserves the font's exact advances, kerning, and intended text spacing even when a character does not begin on a pixel boundary.
How to use:
- Convert a font file to a C file (or use
examples/fonts/roboto_regular.c) - Include the font file and the rendering engine (
src/pte/pte.c) in your project - Implement the
hw_blendPixelcallback used by the engine to draw each pixel. - Call
pte_drawText(),pte_drawTextRect()orpte_measureText()to render text on to your display.
Example usage
pte_font f = pte_getFont(get_Roboto_Regular(), 40);
y = f.m_baseline;
pte_drawText(&f, 5, y, 0, "Example text", -1, 0);
y += f.m_line_height;This repository is also an Arduino and PlatformIO library. Add it alongside
LVGL, include lv_pte.h, and create an LVGL font directly from any font emitted
by the converter:
#include <lv_pte.h>
pte_base_font * get_Roboto_Regular(void);
lv_font_t * font = lv_pte_create(get_Roboto_Regular(), 24);
lv_obj_set_style_text_font(label, font, 0);No LVGL source changes or lv_conf.h option are required. See the add-on guide
for PlatformIO and Arduino installation, resizing, lifetime, and testing.

