Skip to content

Tools: Resource Generation

Developer-facing offline tooling for generating, converting, and auditing the three categories of UI assets consumed by the firmware at runtime: fonts, images, and language packs.

These scripts run on a host PC, not on the ESP32. They produce binary artefacts that are either flashed inside the ui_resources partition or deployed via the SD-card update workflow documented in docs/ui_resources/development.md.


tools/
├── fonts/ # Font conversion tools
│ ├── font_c_to_fnt.py # LVGL .c → .fnt blob (cross-check path)
│ └── ttf_to_fnt.py # TTF/OTF → .fnt blob (primary authoring path)
├── images/ # Image conversion tools
│ └── image_c_to_bin.py # LVGL image .c → .bin (cross-check path)
├── images_converter/ # Snapshot diagnostic tools
│ ├── raw2png.py # Single ESP32 .raw snapshot → PNG
│ └── raws2pngs.py # Batch wrapper around raw2png.py
└── language_packs/ # Localisation tooling
├── audit_translations.py # Detect unused / undefined translation IDs
└── build_template.py # Generate template.lng from *_defs.inc

Three tool families, three asset kinds:

FamilyAsset kindRuntime consumer
fonts/.fnt binary blobesp3d_resources.cpp → LVGL font loader
images/ + images_converter/.bin image / PNG snapshotUI icons; screen snapshot debug
language_packs/.lng / template.lngESP3DTranslationService

Firmware / Partition

Source assets

tools/build_scripts (sibling module)

Host PC Tools (tools_resource_generation)

language_packs/

images_converter/

images/

fonts/

ttf_to_fnt.py

(FreeType + fontTools)

font_c_to_fnt.py

(lv_font_conv cross-check)

image_c_to_bin.py

(LVGL .c → .bin)

raw2png.py

(ESP32 snapshot → PNG)

raws2pngs.py

(batch wrapper)

build_template.py

(→ template.lng)

audit_translations.py

(codebase audit)

lv_font_c_parser.py

font_blob.py

generate_resources.py

(main pipeline)

package_user_resources_kit.py

TTF / OTF / WOFF

font files

lv_font_conv .c

font files

LVGL image .c

files

ESP32 .raw

snapshot files

*_defs.inc

translation macros

C++ source files

(ESP3DLabel:: usages)

.fnt blob

(ui_resources partition)

.bin image blob

template.lng

(shipped with kit)

.lng language pack

(SD card / flash)

PNG image

(debug viewing)


Produce .fnt binary blobs that are registered in the ui_resources partition and loaded at runtime by esp3d_resources.cpp. See development.md for the blob format specification and ui_resources_guide.md for the end-to-end workflow.

Both tools share the same output format via the shared library pair from tools_build_scripts.md:

Shared libRole
lv_font_c_parser.pyParses lv_font_conv-generated .c files into a ParsedFont struct
font_blob.pySerialises a ParsedFont into the binary .fnt blob

The tools locate these files automatically: first looking in tools/build_scripts/, then falling back to the same directory (flat ui_resources_kit layout produced by package_user_resources_kit.py).

Converts an existing lv_font_conv-generated .c file directly to a .fnt blob without a manifest entry or Node.js toolchain. Its primary use is as a cross-check reference against ttf_to_fnt.py.

Terminal window
python font_c_to_fnt.py --c orbitron_10.c --out orbitron_10.fnt

Process flow:

.c file

(lv_font_conv output)

parse_font_c_file()

lv_font_c_parser.py

ParsedFont

struct

build_font_blob()

font_blob.py

.fnt binary blob

Pure-Python TTF/OTF/WOFF → .fnt converter using FreeType for rasterisation and fontTools for GPOS kerning. No Node.js or lv_font_conv required. Supports merging multiple source fonts into a single output (e.g. Orbitron for text glyphs + FontAwesome for icon glyphs).

Terminal window
# Single font
python ttf_to_fnt.py --font Orbitron-Bold.ttf --range "32-126,176,192-253" \
--size 10 --bpp 4 --out orbitron_10.fnt
# Merged: icon subset from a second font in the same .fnt
python ttf_to_fnt.py --size 14 --bpp 4 \
--font Orbitron-Bold.ttf --range "32-126,176,192-253" \
--font FontAwesome.woff --range "61451,61452,61787" \
--out orbitron_14.fnt

Key implementation details verified byte-exact against lv_font_conv:

DetailImplementation
FreeType load flagsFT_LOAD_FORCE_AUTOHINT | FT_LOAD_TARGET_LIGHT
Advance widthlinearHoriAdvance / 65536 * 16 (unhinted, 1/16 px fixed-point)
line_height / base_lineDerived from rendered ink extents, not face metrics
underline_position/thicknessDesign-unit metrics, truncated toward zero
Cmap splittingDP algorithm ported from lv_font_conv’s cmap_build_subtables.js
Kerning sourceGPOS PairPos Format 1 & 2 via fontTools (not legacy kern table)
Glyph packingTight rows, byte-aligned glyph start (stride=1, align=1)
Multi-font mergingEarlier --font args win on duplicate codepoints; glyph IDs are global, sorted by codepoint

Process flow:

Source fonts

(TTF/OTF/WOFF)

+ codepoint ranges

FreeType

rasterize_glyph()

glyph_bitmap

(packed samples)

GlyphDsc list

fontTools

gpos_kern_pairs()

kern_pairs dict

(scaled to px)

_split_cmap_blocks()

DP: byte-minimal partition

format0_tiny / format0 / sparse_tiny

CmapEntry list

ParsedFont

struct

build_font_blob()

font_blob.py

.fnt binary blob


image_c_to_bin.py — LVGL image .c → .bin

Section titled “image_c_to_bin.py — LVGL image .c → .bin”

Converts an LVGL lv_image_dsc_t .c file into the 12-byte header + raw pixel payload .bin that LVGLImage.py --ofmt BIN would produce. Use case: diff-check a committed .c icon against its source PNG without re-running the full icon pipeline.

Terminal window
python image_c_to_bin.py --c back_b.c --out back_b.bin

Binary header layout (12 bytes, little-endian):

Offset Size Field
0 1 magic = 0x19
1 1 cf (color format code, see table below)
2 2 flags (u16)
4 2 w (u16, pixels)
6 2 h (u16, pixels)
8 2 stride (u16, bytes per row)
10 2 reserved
12 ? raw pixel data (palette + pixels for indexed; pixels only otherwise)

Supported LV_COLOR_FORMAT_<NAME> values: UNKNOWN, RAW, RAW_ALPHA, L8, I1–I8, A1–A8, ARGB8888, XRGB8888, RGB565, ARGB8565, RGB565A8, RGB888.


3. Image Converter Tools (tools/images_converter/)

Section titled “3. Image Converter Tools (tools/images_converter/)”

These tools deal with screen snapshots captured by the firmware at runtime (triggered through the factory app or debug hooks) and stored on SD card as .raw files. See gfx_snapshot_begin / gfx_snapshot_end in the board-specific factory gfx.c files for the capture side.

ESP32RawConverter wraps the full conversion pipeline; main() provides the CLI.

Terminal window
python raw2png.py screen.raw # → screen.png
python raw2png.py screen.raw -o debug.png # explicit output name
python raw2png.py -d ./screenshots/ # built-in batch mode

ESP32 RAW file format:

Offset Size Field
0 4 Signature: b'E3D\x00'
4 4 Width (uint32 LE)
8 4 Height (uint32 LE)
12 4 Format (uint32 LE): 0 = RGB565, 1 = RGB888
16 4 Reserved
20 ? Pixel data
RGB565: 2 bytes/pixel → total = w × h × 2
RGB888: 4 bytes/pixel → total = w × h × 4

RGB565 → RGB888 conversion:

RGB565

uint16 numpy array

Bit-extract

R5 = px >> 11

G6 = px >> 5 & 0x3F

B5 = px & 0x1F

Bit-expand

R8 = R5<<3 | R5>>2

G8 = G6<<2 | G6>>4

B8 = B5<<3 | B5>>2

numpy stack

HxWx3 uint8

PIL Image

→ PNG

BatchRawProcessor invokes raw2png.py as a subprocess for each .raw file found in the configured input directory.

Terminal window
python raws2pngs.py # convert raw/ → png/
python raws2pngs.py --script ../raw2png.py # custom script path
python raws2pngs.py --list # list files, no conversion
python raws2pngs.py --clean # remove all PNG output

Expected directory layout:

working_dir/
├── raw/ ← input .raw files (copied from SD card)
│ ├── snap_001.raw
│ └── snap_002.raw
├── png/ ← output PNGs (auto-created)
│ ├── snap_001.png
│ └── snap_002.png
├── raw2png.py
└── raws2pngs.py

4. Language Pack Tools (tools/language_packs/)

Section titled “4. Language Pack Tools (tools/language_packs/)”

The firmware’s translation system relies on two artefacts:

  • *_defs.inc files — define every ESP3DLabel enum value alongside its default English text via ESP3D_TR_DEF(label, id, "text") macros.
  • .lng language pack files — key-value files (l_<id>=<translated text>) loaded at runtime by ESP3DTranslationService.

For the runtime architecture see ESP3DTranslationService in Core_Platform_&_Infrastructure.md.

RangeCategorySource file
0 – 499Core UI translationsesp3d_translations_defs.inc
500 – 999CNC system translationsesp3d_system_translations_defs.inc
1000+Target firmware translationsesp3d_target_translations_defs.inc (one per target)

Note on target range: Multiple target .inc files may reuse the same 1000-range IDs with identical label/text because only one CNC target is compiled at a time. build_template.py merges all targets into the template (de-duplicating identical entries, erroring on conflicting ones) so a single .lng file can serve any firmware build.

a14 labels: template.lng and migration_map.txt were regenerated with the new labels, for example factory_reboot / confirm_factory_reboot (l_109–110), the wheel sensitivity labels (jog_wheel_sensitivity, wheel_sens_off/low/medium/high, l_111–115), the input overflow and lost-line notices (input_overflow_title, input_overflow_msg, low_memory_cmd_dropped, controller_restarted, buffer_full_lines_lost, cnc_responses_lost, cnc_lines_not_sent, l_116–122), the unit labels (unit_per_min, unit, l_561–562), probe_tool_change_hint, show_all_axes, jog_position_unit (l_563–565) and the FluidNC tool changer messages (tool_change_no_atc, tool_change_no_atc_m6_off, tool_change_reading_config, l_1007–1009).

build_template.py — Generate template.lng

Section titled “build_template.py — Generate template.lng”
Terminal window
cd tools/language_packs
python build_template.py
# → template.lng (for distribution to translators)
# → migration_map.txt (label → numeric ID lookup aid)

Process flow:

*_defs.inc files

(core + system + all targets)

parse_defs_file()

ESP3D_TR_DEF regex

skips // comments

All (label, id, text) entries

merged across files

validate_entries()

warn on ID-range gaps

generate_template()

sort by ID

de-duplicate identical target entries

error on conflicting IDs

template.lng

[translations] + [info] sections

migration_map.txt

label = l_ID

Output template.lng structure:

[translations]
# Core translations (0-499)
l_0=English
l_1=Settings
...
# System translations (500-999)
l_500=Position
...
# Target translations (1000+)
l_1000=FluidNC
...
[info]
code=<code>
name=<name>
target_slot=<1|2|3>
creator=<creator>
creation_date=<creation_date>
maintainer=<maintainer>
last_update=<last_update>
target_language=<target_language>
target_version=3.0

audit_translations.py — Codebase translation audit

Section titled “audit_translations.py — Codebase translation audit”

Detects four classes of issues by cross-referencing definition files against all C++/C/H source files under main/:

Issue classDescriptionExit
UnusedESP3D_TR_DEF entry with no ESP3DLabel::xxx in source0 (warn)
UndefinedESP3DLabel::xxx in source but missing from .inc1 (error)
Duplicate IDsSame numeric ID in two different ESP3D_TR_DEF entries1 (error)
Duplicate textsSame default English text in multiple entries (may be intentional)0 (info)
Terminal window
cd tools/language_packs
python audit_translations.py # default paths
python audit_translations.py \
--main-dir ../../main --defs-dir ../../main/display
python audit_translations.py -o report.txt # save report
python audit_translations.py -e unused.csv # export unused IDs to CSV

The scanner intentionally skips:

  • Lines starting with //, /*, or * (comments)
  • Lines starting with #define (macro definition sites)
  • Labels unknown_index and label (enum sentinels)
  • Lines containing ESP3D_TR_DEF (definition sites, not usage sites)

Audit data-flow:

*_defs.inc files

parse_all_definitions()

find_defs_files() walk

definitions dict

label → TranslationDef

main/ source tree

.cpp .c .h .inc

scan_directory_for_usage()

ESP3DLabel::\\w+ regex

skip comments & defines

usages dict

label → list[TranslationUsage]

find_unused_translations()

defined_labels − used_labels

find_undefined_usages()

used_labels − defined_labels

find_duplicate_ids()

group by numeric ID

find_duplicate_texts()

group by normalised text

generate_report()

Console / .txt file

exit 0 or 1


Firmware source

External Python libs

tools_build_scripts

tools_resource_generation

ttf_to_fnt.py

font_c_to_fnt.py

image_c_to_bin.py

raw2png.py

raws2pngs.py

build_template.py

audit_translations.py

lv_font_c_parser.py

font_blob.py

freetype-py

fontTools

Pillow

numpy

*_defs.inc

(translation macros)

main/ C++ source

(ESP3DLabel:: usages)


Integration with the Main Resource Pipeline

Section titled “Integration with the Main Resource Pipeline”

These tools are authoring and debugging aids, not part of the automated build pipeline. The relationship to the main pipeline in tools_build_scripts.md:

Firmware(ui_resources partition)generate_resources.py(build_scripts)font_c_to_fnt.pyttf_to_fnt.pyDeveloperFirmware(ui_resources partition)generate_resources.py(build_scripts)font_c_to_fnt.pyttf_to_fnt.pyDeveloperFont authoring and verification workflowdiff both .fnt files to verify byte-exact matchProduction build (automated, CI)reads resources_config.pyuses lv_font_c_parser + font_blobfor all configured fonts/imagespython ttf_to_fnt.py --font F.ttf ...orbitron_10.fnt (candidate)python font_c_to_fnt.py --c orbitron_10.corbitron_10.fnt (reference from .c)python generate_resources.py --variant ...ui_resources.bin (flashed to device)

For language packs:

SD card / flashTranslatorbuild_template.pyaudit_translations.pyDeveloperSD card / flashTranslatorbuild_template.pyaudit_translations.pyDeveloperFix errors in *_defs.inc or source, commitpython audit_translations.pyReport: unused / undefined IDspython build_template.pytemplate.lng + migration_map.txtDistribute template.lngFill in translations → fr.lngDeploy via SD-card update workflow
Terminal window
# TTF → .fnt (primary authoring path; no Node.js required)
python tools/fonts/ttf_to_fnt.py \
--font Orbitron-Bold.ttf --range "32-126,176,192-253" \
--size 10 --bpp 4 --out orbitron_10.fnt
# LVGL .c → .fnt (cross-check / reference, no Node.js required)
python tools/fonts/font_c_to_fnt.py \
--c resources/fonts/res_320_240/orbitron_10.c \
--out orbitron_10_ref.fnt
Terminal window
# LVGL image .c → .bin (cross-check against source PNG)
python tools/images/image_c_to_bin.py --c resources/icons/back_b.c
Terminal window
# Single file
python tools/images_converter/raw2png.py snap_001.raw
# Batch (run from images_converter/; expects a raw/ sub-directory)
python tools/images_converter/raws2pngs.py
Terminal window
cd tools/language_packs
python build_template.py # generate template.lng
python audit_translations.py # audit the whole codebase
python audit_translations.py -o audit_report.txt # save report to file
python audit_translations.py -e unused.csv # export unused IDs to CSV

DocumentTopic
development.md.fnt / .bin binary blob format, ui_resources partition layout, full build pipeline
ui_resources_guide.mdEnd-to-end guide: add/modify icons and fonts, regenerate and flash the partition
tools_build_scripts.mdgenerate_resources.py, resources_config.py, package_user_resources_kit.py
theme_palette.mdColour token system (separate resource type in the same partition)
ui_style_guide.mdLVGL style system; where generated icons and fonts are applied
Core_Platform_&_Infrastructure.mdESP3DTranslationService runtime (language pack consumer)