Skip to content

Extend scrolling text FX with custom fonts and international UTF-8 character support - #5372

Merged
DedeHai merged 39 commits into
wled:mainfrom
DedeHai:cusomFonts
Mar 29, 2026
Merged

Extend scrolling text FX with custom fonts and international UTF-8 character support#5372
DedeHai merged 39 commits into
wled:mainfrom
DedeHai:cusomFonts

Conversation

@DedeHai

@DedeHai DedeHai commented Feb 15, 2026

Copy link
Copy Markdown
Collaborator
  • change font format to a tightly bit packed format with a header
  • added UTF-8 to unicode conversion functions (2, 3 and 4 byte codes supported)
  • added support to read font files from file system (any ".wbs" font, up to 5 fonts)
  • added checkmark to use custom fonts - fallback to internal fonts if no fonts are found (backwads compatible)
  • replaced 4 of the 5 built-in fonts with nicer looking ones (same font height for backwards compatibility), made all of them per-glyph width

in the current state, this only uses 1k of extra flash (thanks to optimized font bit packing)

This is accompanied with a web based tool to generate wbs fonts from true type fonts as well as from bitmap based bdf fonts. It's a very versatile and easy to use tool: load a font, move sliders until it looks like you want it to, choose extended char range or set it to any unicode offset you like. Choose the range of glyphs to be exportet, remove unwanted glyphs and even edit glyph pixels before export. I also made a "showcase" tool that loads a wbf font file and exports the glyphs as a PNG so users can share their fonts with a nice preview on discourse/reddit etc.

Currently the tool is not yet downloadable in the PixelForge but available here: https://github.com/DedeHai/WLED-Tools/tree/main/WLED-FontFactory

Comparison of the old vs. new fonts:
6x3 Font:
image

5x8 Font:
image

6x8 Font (same font, just variable width):
image

7x9 Font:
image

5x12 Font:
image

Fixes #5101 #3337 #3332

Summary by CodeRabbit

  • New Features
    • Custom font selection for 2D scrolling text with several built‑in bitmap fonts.
    • UTF‑8 support enabling international characters.
    • Dynamic font loading from storage and glyph caching for smoother rendering.
    • Enhanced scrolling text: date/time tokens, rotation handling, and automatic centering when text fits.

@coderabbitai

This comment was marked as outdated.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

🤖 Fix all issues with AI agents
In `@wled00/FX_2Dfcn.cpp`:
- Around line 969-970: Check that fileHdr.last >= fileHdr.first before computing
numGlyphs to avoid underflow; if not, handle the error (return/fail) instead of
proceeding. Change the numGlyphs calculation to use a wider type (e.g., size_t
or uint16_t) and allocate the width table safely (use std::vector<uint8_t> or
heap allocation) rather than a VLA: compute numGlyphs = (size_t)fileHdr.last -
(size_t)fileHdr.first + 1 after the validation, resize the vector to numGlyphs,
then call file.read(widthTable.data(), numGlyphs). Ensure early exit or error
logging if validation fails to prevent reading an incorrect/huge amount of data.
- Around line 1010-1015: The loop copies widths from widthTable into registry
but widthTable may be partially uninitialized if file.read(widthTable,
numGlyphs) returned fewer bytes; check the return value of file.read and ensure
it equals numGlyphs before using widthTable, or otherwise handle the short read
by adjusting numGlyphs/neededCount, zero-filling the remaining widthTable
entries, or aborting loading; also validate that each `code` from neededCodes is
within the bounds of widthTable before doing `registry[k].width =
widthTable[code]` (and continue to set `registry[k].height = fileHdr.height`),
so update the loading logic that populates widthTable and the loop that uses
`neededCount`, `neededCodes`, `registry`, and `widthTable` to guard against
truncated files.
- Around line 783-795: The cache-clear branch in FX_2Dfcn.cpp (when fontToUse !=
meta->cachedFontNum) reallocates SegmentFontMetadata via _segment->allocateData
but does not reset meta->glyphCount, leaving stale non-zero values that prevent
prepare() from detecting an empty cache; after reassigning meta = getMetadata()
and before setting meta->availableFonts/meta->cachedFontNum/meta->fontsScanned,
explicitly set meta->glyphCount = 0 so prepare()'s if (meta->glyphCount == 0)
correctly triggers a rebuild of the glyph cache.

In `@wled00/FX.cpp`:
- Around line 6455-6485: The centering math is wrong: SEGENV.aux0 was computed
as the left-origin centered offset but drawX is using cols - SEGENV.aux0 which
places text at the far right; change the drawX calculation in FX.cpp so it uses
SEGENV.aux0 as the left-origin base (e.g., baseX = SEGENV.aux0) and compute
drawX = baseX + currentXOffset (still using advance = (rotate == 1 || rotate ==
-1) ? letterHeight : glyphWidth), ensuring this branch (when totalTextWidth <=
cols / non-scrolling case) uses the left-origin base rather than cols - aux0 so
glyphs are centered correctly.

In `@wled00/FX.h`:
- Around line 1145-1149: Update the memory layout comment to match the constant
FONT_HEADER_SIZE by changing "[11-byte font header]" to "[12-byte font header]";
locate the comment near the cached fonts section in FX.h and ensure it exactly
reflects the value of FONT_HEADER_SIZE (defined earlier) so the documentation
and the constant remain consistent.
- Around line 1095-1100: The readUInt32LE function can invoke signed-integer
overflow because readByte() (uint8_t) promotes to signed int before left shifts;
cast each readByte(...) to uint32_t (e.g., (uint32_t)readByte(offset + N))
before shifting and ORing so all shifts operate on unsigned 32-bit values,
ensuring defined behavior in readUInt32LE.
- Around line 814-815: Update the two non-2D drawCharacter stub signatures to
use the same Unicode type as the 2D variant and FontManager: change the first
parameter from "unsigned char chr" to "uint32_t unicode" in both inline stubs
(the two empty drawCharacter overloads currently declared), so their signatures
match the 2D drawCharacter (uint32_t unicode) and FontManager::drawCharacter in
FX_2Dfcn.cpp; keep the rest of the parameter list the same and do not modify the
empty bodies.

In `@wled00/util.cpp`:
- Around line 165-196: Fix the typos in the UTF-8 comment block above
utf8_decode: change "onversion" to "conversion" and remove the stray trailing
'c' so "U+10FFFFc" becomes "U+10FFFF"; these edits live in the comment
immediately preceding the UTF8_LEN macro/utf8_decode function and do not require
code changes.
- Around line 184-188: Replace the expensive strlen(s) call and the lax
continuation handling by iterating up to n bytes from s and validating each
continuation byte prefix instead of scanning the whole string; specifically, in
the branch that currently does "if (strlen(s) < n) { *len = 1; return '?'; }"
check for early NUL by verifying s[i] != '\0' for each i in [0,n-1] and also
validate each continuation byte with (s[i] & 0xC0) == 0x80, and if any check
fails set *len = 1 and return '?' (preserving behavior of n, *len and the
returned default char).
🧹 Nitpick comments (4)
wled00/util.cpp (1)

174-196: Indentation uses 4 spaces; the rest of util.cpp uses 2 spaces.

The function body uses 4-space base indentation, which is inconsistent with the file's established 2-space convention. As per coding guidelines, wled00/**/*.cpp should use 2-space indentation.

wled00/FX.cpp (1)

6394-6397: Avoid reloading/preparing fonts every frame.

loadFont() + prepare() are called on every frame (Line 6395-6396). If these hit filesystem or re-parse glyphs, this will tank FPS and I/O. Consider gating on SEGENV.call == 0 or when fontNum/useCustomFont/text changes (the unused init flag could drive this).

wled00/FX_2Dfcn.cpp (2)

1024-1040: Repeated O(code) offset recomputation for each glyph.

For every needed glyph, lines 1031–1034 re-scan widthTable from index 0 to code-1 to compute the file offset. With up to 64 needed glyphs and codes up to ~255, this is ~16K iterations — tolerable for a one-time cache build but easy to optimize with a single prefix-sum pass.

Sketch: pre-compute cumulative offsets
+  // Pre-compute cumulative byte offsets for each glyph
+  uint32_t cumOffset[numGlyphs + 1];
+  cumOffset[0] = 0;
+  for (uint8_t j = 0; j < numGlyphs; j++) {
+    uint16_t b = widthTable[j] * fileHdr.height;
+    cumOffset[j + 1] = cumOffset[j] + (b + 7) / 8;
+  }
+
   // Write bitmap data in registry order
   for (uint8_t k = 0; k < neededCount; k++) {
     uint8_t code = neededCodes[k];
     uint16_t bits = widthTable[code] * fileHdr.height;
     uint16_t bytes = (bits + 7) / 8;
     
     // Calculate file offset
-    uint32_t offset = fileDataStart;
-    for (uint8_t j = 0; j < code; j++) {
-      uint16_t b = widthTable[j] * fileHdr.height;
-      offset += (b + 7) / 8;
-    }
+    uint32_t offset = fileDataStart + cumOffset[code];
     
     // Read from file
     file.seek(offset);

647-701: Pointer returned from getGlyphBitmap is only used as an offset base — document this contract.

The returned const uint8_t* is never dereferenced directly; drawCharacter computes (bitmap - _fontBase) + bytePos and reads through _reader. This works correctly but is a non-obvious contract — a future caller might dereference the pointer directly, which would fail for PROGMEM data. A brief comment would prevent misuse.

Comment thread wled00/FX_2Dfcn.cpp Outdated
Comment thread wled00/FX_2Dfcn.cpp Outdated
Comment thread wled00/FX.cpp Outdated
Comment thread wled00/FX.h Outdated
Comment thread wled00/FX.h Outdated
Comment thread wled00/FX.h Outdated
Comment thread wled00/util.cpp
Comment thread wled00/util.cpp Outdated
@DedeHai

DedeHai commented Feb 15, 2026

Copy link
Copy Markdown
Collaborator Author

The Font Factory tool:
image


Font pixel editor mode:

image

Built-in international chars support:

image

@DedeHai

DedeHai commented Feb 16, 2026

Copy link
Copy Markdown
Collaborator Author

@coderabbitai please review the fontmanager and make suggesions for code consolidation and maintainability.

@coderabbitai

This comment was marked as outdated.

coderabbitai[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
wled00/FX.cpp (1)

6340-6341: ⚠️ Potential issue | 🟠 Major

Cast to unsigned char before std::toupper to avoid undefined behavior on non-ASCII bytes.

At line 6340, std::toupper(SEGMENT.name[i + j]) invokes undefined behavior when char is signed and contains byte values >127. Since segment names are user-provided with no ASCII restriction enforced, non-ASCII bytes from UTF-8 or extended ASCII are possible, breaking token parsing for international text.

Proposed fix
-          token[j] = std::toupper(SEGMENT.name[i + j]);
+          token[j] = static_cast<char>(
+            std::toupper(static_cast<unsigned char>(SEGMENT.name[i + j]))
+          );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wled00/FX.cpp` around lines 6340 - 6341, The call to std::toupper on
SEGMENT.name[i + j] can invoke UB for negative-char values; change the call to
cast the input to unsigned char and cast the result back to char when assigning
to token[j], e.g. use std::toupper(static_cast<unsigned char>(SEGMENT.name[i +
j])) and assign the returned int converted to char so token[j] gets a defined
uppercase value; update the occurrence in the parsing loop where token[j] =
std::toupper(SEGMENT.name[i + j]) (and any similar uses) accordingly.
♻️ Duplicate comments (1)
wled00/FX_2Dfcn.cpp (1)

782-795: ⚠️ Potential issue | 🔴 Critical

Keep registry compact and set glyphCount to actual written entries.

Line 790 skips invalid codes, but Line 782 still sets glyphCount = neededCount. That can leave uninitialized registry slots while later lookups iterate all glyphCount entries. The bitmap loop also still indexes by original neededCodes on Line 804.

💡 Proposed fix
-  meta->glyphCount = neededCount; // glyph count is used to determine if cache is valid. If file is corrupted, ram cache is still large enough to not cause crashes
+  meta->glyphCount = 0; // set after writing only valid entries

@@
-  GlyphEntry* registry = (GlyphEntry*)dataptr;
-  for (uint8_t k = 0; k < neededCount; k++) {
-    uint8_t code = neededCodes[k];
-    if (code >= numGlyphs) continue; // skip invalid codes (safety check if anything is corrupted)
-    registry[k].code = code;
-    registry[k].width = widthTable[code];
-    registry[k].height = hdr.height;
-  }
-  dataptr += neededCount * sizeof(GlyphEntry);
+  GlyphEntry* registry = (GlyphEntry*)dataptr;
+  uint8_t validCodes[MAX_CACHED_GLYPHS];
+  uint8_t validCount = 0;
+  for (uint8_t k = 0; k < neededCount; k++) {
+    uint8_t code = neededCodes[k];
+    if (code >= numGlyphs) continue;
+    registry[validCount].code = code;
+    registry[validCount].width = widthTable[code];
+    registry[validCount].height = hdr.height;
+    validCodes[validCount++] = code;
+  }
+  meta->glyphCount = validCount;
+  dataptr += meta->glyphCount * sizeof(GlyphEntry);

@@
-  for (uint8_t k = 0; k < neededCount; k++) {
-    uint8_t glyphIdx = neededCodes[k]; // neededCodes contais index of the glyph in the font, not the raw unicode value
+  for (uint8_t k = 0; k < meta->glyphCount; k++) {
+    uint8_t glyphIdx = validCodes[k];

Also applies to: 803-806

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wled00/FX_2Dfcn.cpp` around lines 782 - 795, The code sets meta->glyphCount =
neededCount but skips invalid codes when populating the GlyphEntry array,
leaving holes and causing later lookups to read uninitialized entries; change
the logic in the block that fills registry (and the following bitmap loop) to
count and write only valid entries: maintain an actualWritten counter, for each
valid neededCodes[k] write registry[actualWritten] (setting
registry[actualWritten].code/width/height) and increment actualWritten, then set
meta->glyphCount = actualWritten and advance dataptr by actualWritten *
sizeof(GlyphEntry); update the bitmap-copy loop to iterate 0..meta->glyphCount
and use registry[i].code (not neededCodes) when indexing widthTable/bitmap
source so only written entries are processed.
🧹 Nitpick comments (1)
wled00/FX.h (1)

428-428: Consider removing friend class FontManager once access points are stabilized.

This increases coupling between Segment and FontManager. A narrow accessor/mutator surface on Segment would keep boundaries cleaner and make future FontManager refactors safer.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wled00/FX.h` at line 428, Currently Segment exposes protected state to
FontManager via "friend class FontManager"; remove this friendship and instead
add minimal, well-named accessors/mutators on class Segment for each specific
member FontManager reads or writes (e.g., getFontIndex(), setFontIndex(...),
getGlyphData(...), isMonospace(), etc.) and update FontManager to use those
methods. Keep the API surface as narrow as possible (only the exact
getters/setters FontManager needs), preserve existing semantics and
const-correctness, and run compile to resolve remaining access errors and adjust
tests/usages accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@wled00/FX_2Dfcn.cpp`:
- Around line 745-747: The computation of numGlyphs can overflow for 256 glyphs
because it’s declared uint8_t; change its type to uint16_t (use uint16_t
numGlyphs = hdr.last - hdr.first + 1) and replace the stack VLA
widthTable[numGlyphs] with a properly sized container (e.g.,
std::vector<uint8_t> widthTable(numGlyphs) or allocate widthTable = new
uint8_t[numGlyphs]) so accesses to widthTable (used later where
hdr.first/hdr.last are referenced) are valid for sizes up to 256; ensure any new
allocation is freed or vector is used to avoid leaks and update any widthTable
indexing accordingly.

In `@wled00/FX.h`:
- Around line 1092-1096: GlyphEntry is 3 bytes which can cause _fontBase =
_segment->data + 4 + (glyphCount * 3) to be unaligned for subsequent FontHeader
access; modify the GlyphEntry struct (the one with fields code, width, height)
by adding a uint8_t reserved (or padding) field so the struct becomes 4 bytes,
ensuring _fontBase is 4-byte aligned for safe access to FontHeader::firstUnicode
and similar uint32_t fields; if any code assumes the old size (serializations or
memcpy), update those spots to account for the new 4-byte GlyphEntry size.
- Line 779: Add the missing 2D overload declaration for drawCharacter that
accepts packed uint32_t colors so the CRGB wrapper can forward to it: declare
drawCharacter(uint32_t unicode, int16_t x, int16_t y, const uint8_t* fontData,
File &fontFile, uint32_t color, uint32_t color2 = 0, int8_t rotate = 0) const in
the 2D section (matching the signature your inline CRGB wrapper forwards to
using RGBW32), and update the non-2D stub variants of drawCharacter to be const
(make the stub methods that currently lack const include the trailing const) so
their signatures match the 2D API surface and const call sites when
WLED_DISABLE_2D is enabled.

---

Outside diff comments:
In `@wled00/FX.cpp`:
- Around line 6340-6341: The call to std::toupper on SEGMENT.name[i + j] can
invoke UB for negative-char values; change the call to cast the input to
unsigned char and cast the result back to char when assigning to token[j], e.g.
use std::toupper(static_cast<unsigned char>(SEGMENT.name[i + j])) and assign the
returned int converted to char so token[j] gets a defined uppercase value;
update the occurrence in the parsing loop where token[j] =
std::toupper(SEGMENT.name[i + j]) (and any similar uses) accordingly.

---

Duplicate comments:
In `@wled00/FX_2Dfcn.cpp`:
- Around line 782-795: The code sets meta->glyphCount = neededCount but skips
invalid codes when populating the GlyphEntry array, leaving holes and causing
later lookups to read uninitialized entries; change the logic in the block that
fills registry (and the following bitmap loop) to count and write only valid
entries: maintain an actualWritten counter, for each valid neededCodes[k] write
registry[actualWritten] (setting registry[actualWritten].code/width/height) and
increment actualWritten, then set meta->glyphCount = actualWritten and advance
dataptr by actualWritten * sizeof(GlyphEntry); update the bitmap-copy loop to
iterate 0..meta->glyphCount and use registry[i].code (not neededCodes) when
indexing widthTable/bitmap source so only written entries are processed.

---

Nitpick comments:
In `@wled00/FX.h`:
- Line 428: Currently Segment exposes protected state to FontManager via "friend
class FontManager"; remove this friendship and instead add minimal, well-named
accessors/mutators on class Segment for each specific member FontManager reads
or writes (e.g., getFontIndex(), setFontIndex(...), getGlyphData(...),
isMonospace(), etc.) and update FontManager to use those methods. Keep the API
surface as narrow as possible (only the exact getters/setters FontManager
needs), preserve existing semantics and const-correctness, and run compile to
resolve remaining access errors and adjust tests/usages accordingly.

ℹ️ Review info

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d355ab4 and 4970c41.

📒 Files selected for processing (3)
  • wled00/FX.cpp
  • wled00/FX.h
  • wled00/FX_2Dfcn.cpp

Comment thread wled00/FX_2Dfcn.cpp Outdated
Comment thread wled00/FX.h Outdated
Comment thread wled00/FX.h Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (2)
wled00/FX.h (1)

779-779: ⚠️ Potential issue | 🔴 Critical

Declare the forwarded drawCharacter(uint32_t,...,uint32_t,...) const overload explicitly.

Line 779 forwards to a uint32-color overload that is not declared in the 2D branch, and the non-2D stubs at Lines 814-815 should be const to keep signatures aligned.

💡 Proposed fix
 `#ifndef` WLED_DISABLE_2D
+    void drawCharacter(uint32_t unicode, int16_t x, int16_t y, const uint8_t* fontData, File &fontFile, uint32_t color, uint32_t col2 = 0, int8_t rotate = 0) const;
     inline void drawCharacter(uint32_t unicode, int16_t x, int16_t y, const uint8_t* fontData, File &fontFile, CRGB c, CRGB c2 = CRGB::Black, int8_t rotate = 0) const { drawCharacter(unicode, x, y, fontData, fontFile, RGBW32(c.r,c.g,c.b,0), RGBW32(c2.r,c2.g,c2.b,0), rotate); } // automatic inline
 `#else`
-    inline void drawCharacter(uint32_t unicode, int16_t x, int16_t y, const uint8_t* fontData, File &fontFile, uint32_t color, uint32_t col2 = 0, int8_t rotate = 0) {}
-    inline void drawCharacter(uint32_t unicode, int16_t x, int16_t y, const uint8_t* fontData, File &fontFile, CRGB c, CRGB c2 = CRGB::Black, int8_t rotate = 0) {}
+    inline void drawCharacter(uint32_t unicode, int16_t x, int16_t y, const uint8_t* fontData, File &fontFile, uint32_t color, uint32_t col2 = 0, int8_t rotate = 0) const {}
+    inline void drawCharacter(uint32_t unicode, int16_t x, int16_t y, const uint8_t* fontData, File &fontFile, CRGB c, CRGB c2 = CRGB::Black, int8_t rotate = 0) const {}
 `#endif`
#!/bin/bash
# Verify declaration/signature mismatch in Segment::drawCharacter overloads.
rg -nP 'drawCharacter\s*\(\s*uint32_t\s+unicode.*fontFile.*(uint32_t\s+color|CRGB\s+c)' wled00/FX.h -C2

Also applies to: 814-815

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wled00/FX.h` at line 779, Declare the missing const uint32_t-color overload
of drawCharacter so the 2D branch explicitly exposes drawCharacter(uint32_t
unicode, int16_t x, int16_t y, const uint8_t* fontData, File &fontFile, uint32_t
color, uint32_t color2, int8_t rotate = 0) const (matching the forwarded call on
the inline at drawCharacter(..., CRGB c, CRGB c2 ...)), and make the non-2D stub
overloads (the drawCharacter stubs around lines 814-815) const as well so their
signatures align with the 2D declarations; update the declaration list in FX.h
to include the uint32_t-color prototype and add const to the stub method
declarations (same method name drawCharacter) to eliminate the mismatch.
wled00/FX_2Dfcn.cpp (1)

781-783: ⚠️ Potential issue | 🔴 Critical

Prevent numGlyphs truncation for full-range fonts.

Line 781 computes glyph count into uint8_t; for first=0, last=255, the value wraps from 256 to 0. That invalidates width-table handling and can cascade into bad bitmap indexing.

💡 Proposed fix
-  uint8_t numGlyphs = hdr.last - hdr.first + 1;
-  uint8_t widthTable[numGlyphs];
+  const uint16_t numGlyphs = static_cast<uint16_t>(hdr.last) - static_cast<uint16_t>(hdr.first) + 1U;
+  uint8_t widthTable[256];
@@
-    for (uint8_t k = 0; k < numGlyphs; k++) {
+    for (uint16_t k = 0; k < numGlyphs; k++) {
       widthTable[k] = hdr.width; // fixed width, fill with given width from header
     }
#!/bin/bash
# Verify the truncating declaration is present and demonstrate wrap behavior.
rg -n 'numGlyphs\s*=\s*hdr\.last\s*-\s*hdr\.first\s*\+\s*1' wled00/FX_2Dfcn.cpp
python3 - << 'PY'
first, last = 0, 255
numGlyphs_uint8 = (last - first + 1) & 0xFF
print("Simulated uint8_t numGlyphs for first=0,last=255:", numGlyphs_uint8)  # expected problematic 0
PY

Also applies to: 792-793

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wled00/FX_2Dfcn.cpp` around lines 781 - 783, The glyph count is stored in a
uint8_t (numGlyphs) which overflows for full-range fonts (hdr.first=0,
hdr.last=255); change numGlyphs to a wider integer (e.g., size_t or uint16_t)
and allocate widthTable using that type/size (avoid stack-array with uint8_t
length when it can be >=256 — use dynamic allocation or a
std::vector<uint8_t>/new[] sized by numGlyphs). Update any loops, indices, and
uses that assume uint8_t (e.g., widthTable accesses and any counters iterating
over numGlyphs) to use the wider type to prevent truncation and out-of-bounds
bitmap indexing; also apply the same change to the other occurrences around the
width table handling (the subsequent width-table and glyph indexing code).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@wled00/FX_2Dfcn.cpp`:
- Around line 781-783: The glyph count is stored in a uint8_t (numGlyphs) which
overflows for full-range fonts (hdr.first=0, hdr.last=255); change numGlyphs to
a wider integer (e.g., size_t or uint16_t) and allocate widthTable using that
type/size (avoid stack-array with uint8_t length when it can be >=256 — use
dynamic allocation or a std::vector<uint8_t>/new[] sized by numGlyphs). Update
any loops, indices, and uses that assume uint8_t (e.g., widthTable accesses and
any counters iterating over numGlyphs) to use the wider type to prevent
truncation and out-of-bounds bitmap indexing; also apply the same change to the
other occurrences around the width table handling (the subsequent width-table
and glyph indexing code).

In `@wled00/FX.h`:
- Line 779: Declare the missing const uint32_t-color overload of drawCharacter
so the 2D branch explicitly exposes drawCharacter(uint32_t unicode, int16_t x,
int16_t y, const uint8_t* fontData, File &fontFile, uint32_t color, uint32_t
color2, int8_t rotate = 0) const (matching the forwarded call on the inline at
drawCharacter(..., CRGB c, CRGB c2 ...)), and make the non-2D stub overloads
(the drawCharacter stubs around lines 814-815) const as well so their signatures
align with the 2D declarations; update the declaration list in FX.h to include
the uint32_t-color prototype and add const to the stub method declarations (same
method name drawCharacter) to eliminate the mismatch.

ℹ️ Review info

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4970c41 and 087c06b.

📒 Files selected for processing (3)
  • wled00/FX.h
  • wled00/FX_2Dfcn.cpp
  • wled00/src/font/font_5x12.h

@DedeHai

DedeHai commented Feb 27, 2026

Copy link
Copy Markdown
Collaborator Author

@softhack007 I think the code is mostly finished now. Feel free to give it a go. The font factory UI tool still needs integration into pixel forge though.
I now added support for any file naming, when selecting custom font, it scans the file system for any .wbf file present - this leads to a brief pause of the FX that can not be avoided except if we would be storing the actual file names, wasting precious ram. IMHO the pause (~0.5s if not too many files are in the FS) is an acceptable compromise.

For testing you can use any TTF font you like, I added some font packs that are public domain here: https://github.com/DedeHai/WLED-Tools/tree/main/WLED-FontFactory/public%20domain%20fonts

the folder with bdf fonts also contains some fonts with cyrillic and greek glyphs.

@softhack007 softhack007 modified the milestone: 16.1 Mar 9, 2026
@DedeHai

DedeHai commented Mar 18, 2026

Copy link
Copy Markdown
Collaborator Author

@willmmiles I checked your request regarding "make the rendering universal" but in the current implementation that is not feasible: the fontmanager is closely entangled with the segment class as it uses the segment's allocatedata() function to store persistent data. To make it more universal (similar to the particle system) a lot of code would need to go into the scrolling text FX, like calculating required buffer size and allocating enough data.
I also moved the fontmanager into its own files, what is still to be implemented is integration into pixelforge and adding a button there to download the old fonts.

@DedeHai

DedeHai commented Mar 25, 2026

Copy link
Copy Markdown
Collaborator Author

now that users can download the classic wled fonts at the click of a button I need to rethink the default fonts, then this is ready to ship.

@DedeHai
DedeHai merged commit f6a43f4 into wled:main Mar 29, 2026
24 checks passed
@netmindz netmindz modified the milestones: 16.1, 16.0.0 beta Mar 29, 2026
Bunton33 added a commit to ApolloAutomation/WLED-M1 that referenced this pull request Jul 15, 2026
… file removed

Three upstream PR texts (AP sleep, force-scroll, freeze-thaw) rewritten
after the adversarial verify pass: honest testing sections (no isolated
setSleep A/B exists; before-metrics were already clean), corrected font
math (58px default vs 56px at max font), fixed hunk headers, wled#5372 is
already in v16.0.1, UI freeze icon acknowledged, preset frz-storage fact
added, cross-API scope caveat added. The pixelpaint freeze repro capture
committed at session open turned out to be blank (probe never joined the
AP); removed, re-capture planned for the b8 gate. Release draft + hosting
split staged in apollo/RELEASE_DRAFT.md; publish only on Justin's go.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
aenertia pushed a commit to aenertia/WLED that referenced this pull request Aug 18, 2026
…aracter support (wled#5372)

* new font format: *.wbf with a 12byte header, bit packed data and support for variable char width
* add support to load custom fonts from file system
* UTF-8 to unicode support functions
* support for any unicode offset in char 128-255 enables many international chars
* update built-in fonts with similar but nicer ones
* update pixelforge scrolling text tool with a preview and support for custom fonts
* accompanied by Font Factory tool (pixelforge) to easily create custom fonts from various formats
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Cyrilyc font 6x8, and special character support for scrolling text

3 participants