mirror of
https://github.com/wled/WLED.git
synced 2026-04-20 22:22:51 +00:00
* 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
416 lines
15 KiB
C++
416 lines
15 KiB
C++
/*
|
|
* FontManager class for managing wbf format font loading, caching, and rendering
|
|
* Supports up to 5 fonts (0-4) from flash or file system
|
|
* Caches glyphs in segment data for fast access during rendering
|
|
* Note: fontmanager relies on the Segment class for storing cached font data as well as rendering
|
|
*
|
|
* (c) 2026 by @dedehai
|
|
*/
|
|
|
|
#include "wled.h"
|
|
#include "fontmanager.h"
|
|
|
|
#include "src/font/font_tom_thumb_6px.h"
|
|
#include "src/font/font_TinyUnicode_8px.h"
|
|
#include "src/font/font_5x12.h"
|
|
#include "src/font/console_font_6x8.h"
|
|
#include "src/font/c64esque_9px.h"
|
|
|
|
// get metadata pointer
|
|
SegmentFontMetadata* FontManager::getMetadata() {
|
|
return (SegmentFontMetadata*)_segment->data;
|
|
}
|
|
|
|
void FontManager::updateFontBase() {
|
|
SegmentFontMetadata* meta = getMetadata();
|
|
// font data (header + glyph bitmaps) starts after metadata + registry
|
|
_fontBase = _segment->data + sizeof(SegmentFontMetadata) + (meta->glyphCount * sizeof(GlyphEntry));
|
|
}
|
|
|
|
// scan file system for .wbf font files, if scanAll is set, also updates availableFonts
|
|
void FontManager::getFontFileName(uint8_t fontNum, char* buffer, bool scanAll) {
|
|
SegmentFontMetadata* meta = getMetadata();
|
|
if (scanAll) {
|
|
if (!meta) return;
|
|
meta->availableFonts = 0; // reset available fonts before scanning
|
|
}
|
|
buffer[0] = '\0'; // invalidate
|
|
#ifdef CONFIG_IDF_TARGET_ESP32C3
|
|
while (!BusManager::canAllShow()) yield(); // accessing FS causes glitches due to RMT issue on C3 TODO: remove this when fixed
|
|
#endif
|
|
File rootdir = WLED_FS.open("/", "r");
|
|
File rootfile = rootdir.openNextFile();
|
|
uint8_t i = 0;
|
|
while (rootfile && i < MAX_FONTS) {
|
|
String name = rootfile.name();
|
|
if (name.endsWith(F(".wbf"))) {
|
|
if (i == fontNum) {
|
|
if (name.charAt(0) != '/') name = "/" + name; // need to add leading slash
|
|
strncpy(buffer, name.c_str(), FONT_NAME_BUFFER_SIZE - 1);
|
|
buffer[FONT_NAME_BUFFER_SIZE - 1] = '\0';
|
|
if (!scanAll) {
|
|
rootfile.close();
|
|
rootdir.close();
|
|
return;
|
|
}
|
|
}
|
|
if (scanAll)
|
|
meta->availableFonts |= (1 << i); // note: openNextFile() usually opens them in alphaetical order but there is no guarantee
|
|
i++;
|
|
}
|
|
rootfile = rootdir.openNextFile();
|
|
}
|
|
rootfile.close();
|
|
rootdir.close();
|
|
}
|
|
|
|
// scan file system for available fonts
|
|
void FontManager::scanAvailableFonts() {
|
|
char buffer[FONT_NAME_BUFFER_SIZE];
|
|
getFontFileName(0, buffer, true); // scan all fonts to update availableFonts in metadata
|
|
}
|
|
|
|
// load font by number and prepare/validate font cache, must be called before using any other FontManager functions, must not use the font if this function returns false!
|
|
bool FontManager::loadFont(uint8_t fontNum, const char* text, bool useFile) {
|
|
_segment->allocateData(sizeof(SegmentFontMetadata)); // make sure at least metadata is available, sets to 0 if segment.call==0, does nothing if already allocated
|
|
SegmentFontMetadata* meta = getMetadata();
|
|
if (!meta)
|
|
return false; // can not continue if no segment data
|
|
|
|
_fontNum = fontNum; // store font to be used
|
|
_useFileFont = useFile;
|
|
uint8_t fontRequested = fontNum | (useFile ? 0x80 : 0x00);
|
|
|
|
if (useFile) {
|
|
if (meta->lastFontNum != fontRequested) {
|
|
scanAvailableFonts(); // scan filesystem again if file font changes
|
|
}
|
|
// determine which font to actually use (with fallback): check if requested font is available - find first available font if not
|
|
if (!(meta->availableFonts & (1 << fontNum))) {
|
|
_fontNum = 0xFF; // invalidate
|
|
for (int i = 0; i < MAX_FONTS; i++) {
|
|
if (meta->availableFonts & (1 << i)) {
|
|
_fontNum = i;
|
|
break;
|
|
}
|
|
}
|
|
if (_fontNum == 0xFF) {
|
|
_fontNum = fontNum; // no custom fonts available, use flash font
|
|
_useFileFont = false;
|
|
}
|
|
}
|
|
}
|
|
meta->lastFontNum = fontRequested; // store last requested file font (only used to check file fonts)
|
|
|
|
uint8_t fontToUse = _fontNum | (_useFileFont ? 0x80 : 0x00); // highest bit indicates file vs flash
|
|
if (fontToUse != meta->cachedFontNum) {
|
|
meta->glyphCount = 0; // invalidate cache
|
|
}
|
|
cacheGlyphs(text); // prepare cache with needed glyphs
|
|
meta = getMetadata(); // reload metadata after potential cache rebuild
|
|
if (meta->glyphCount == 0) {
|
|
errorFlag = ERR_NOT_IMPL; // TODO: need a better error code if more codes are added
|
|
return false; // cache build failed (invalid font file)
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// check if all glyphs needed for the given text are present in cache, rebuild cache if not
|
|
void FontManager::cacheGlyphs(const char* text) {
|
|
if (!text) return;
|
|
|
|
SegmentFontMetadata* meta = getMetadata(); // loadFont ensures pointer is valid
|
|
// if glyphCount is 0, cache is empty - rebuild
|
|
if (meta->glyphCount == 0) {
|
|
rebuildCache(text);
|
|
return; // cache built, we are done
|
|
}
|
|
|
|
// if there is a cached font, update the pointers
|
|
updateFontBase();
|
|
FontHeader* hdr = reinterpret_cast<FontHeader*>(_fontBase);
|
|
|
|
// check if all needed glyphs for the text are present in cache
|
|
uint8_t neededCodes[MAX_CACHED_GLYPHS];
|
|
uint8_t neededCount = collectNeededCodes(text, hdr, neededCodes);
|
|
|
|
GlyphEntry* registry = (GlyphEntry*)(_segment->data + sizeof(SegmentFontMetadata));
|
|
for (uint8_t k = 0; k < neededCount; k++) {
|
|
// look up glyph in registry
|
|
bool found = false;
|
|
for (uint8_t j = 0; j < meta->glyphCount; j++) {
|
|
if (registry[j].code == neededCodes[k]) {
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!found) {
|
|
rebuildCache(text); // missing glyph - rebuild cache
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
void FontManager::rebuildCache(const char* text) {
|
|
if (!text) return;
|
|
// preserve metadata (function is only called if segment data is allocated so no null check needed)
|
|
SegmentFontMetadata savedMeta;
|
|
SegmentFontMetadata* meta = getMetadata();
|
|
meta->glyphCount = 0; // invalidates cached font
|
|
memcpy(&savedMeta, meta, sizeof(SegmentFontMetadata));
|
|
|
|
File file;
|
|
if (_useFileFont) {
|
|
// build filename from font number
|
|
char fileName[FONT_NAME_BUFFER_SIZE];
|
|
getFontFileName(_fontNum, fileName);
|
|
|
|
#ifdef CONFIG_IDF_TARGET_ESP32C3
|
|
while (!BusManager::canAllShow()) yield(); // accessing FS causes glitches due to RMT issue on C3 TODO: remove this when fixed
|
|
#endif
|
|
file = WLED_FS.open(fileName, "r");
|
|
|
|
// fallback logic - try other available fonts
|
|
if (!file) {
|
|
for (int i = 0; i < MAX_FONTS; i++) {
|
|
if (meta->availableFonts & (1 << i)) {
|
|
getFontFileName(i, fileName);
|
|
file = WLED_FS.open(fileName, "r");
|
|
if (file) {
|
|
_fontNum = i; // update to fallback font
|
|
//savedMeta.cachedFontNum = i | 0x80; // set highest bit to indicate file font
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!file) {
|
|
_useFileFont = false; // fallback straight to flash font
|
|
//savedMeta.cachedFontNum = _fontNum;
|
|
}
|
|
}
|
|
|
|
savedMeta.cachedFontNum = _fontNum | (_useFileFont ? 0x80 : 0x00); // set highest bit to indicate file font
|
|
|
|
// determine flash font to use (not used if using file font)
|
|
const uint8_t* flashFont;
|
|
switch (_fontNum) {
|
|
default:
|
|
case 0: flashFont = font_tom_thumb_6px; break;
|
|
case 1: flashFont = font_TinyUnicode_8px; break;
|
|
case 2: flashFont = console_font_6x8; break;
|
|
case 3: flashFont = c64esque_9px; break;
|
|
case 4: flashFont = font_5x12; break;
|
|
}
|
|
|
|
// read wbf font header
|
|
FontHeader hdr;
|
|
if (file) {
|
|
if (file.read((uint8_t*)&hdr, FONT_HEADER_SIZE) != FONT_HEADER_SIZE) { file.close(); return; } // header incomplete
|
|
if (hdr.magic != 0x57) { file.close(); return; } // invalid header
|
|
if (hdr.last < hdr.first) { file.close(); return; } // Invalid header
|
|
} else {
|
|
memcpy_P(&hdr, flashFont, FONT_HEADER_SIZE); // assumes built in fonts are in a valid, compatible format
|
|
}
|
|
|
|
// collect needed glyphs
|
|
uint8_t neededCodes[MAX_CACHED_GLYPHS];
|
|
uint8_t neededCount = collectNeededCodes(text, &hdr, neededCodes);
|
|
uint32_t numGlyphs = hdr.last - hdr.first + 1;
|
|
uint8_t widthTable[numGlyphs];
|
|
|
|
// read width table
|
|
if (hdr.flags & 0x01) {
|
|
if (file) {
|
|
if (file.read(widthTable, numGlyphs) != numGlyphs) { file.close(); return; } // width table starts directly after header if used
|
|
} else {
|
|
memcpy_P(widthTable, flashFont + FONT_HEADER_SIZE, numGlyphs); // assumes built in fonts are in a valid, compatible format
|
|
}
|
|
} else {
|
|
for (uint32_t k = 0; k < numGlyphs; k++) {
|
|
widthTable[k] = hdr.width; // fixed width, fill with given width from header
|
|
}
|
|
}
|
|
|
|
// calculate total size for cache: metadata + registry + header + bitmaps
|
|
size_t ramFontSize = sizeof(SegmentFontMetadata) + (neededCount * sizeof(GlyphEntry)) + FONT_HEADER_SIZE;
|
|
|
|
for (uint8_t k = 0; k < neededCount; k++) {
|
|
uint8_t code = neededCodes[k];
|
|
if (code < numGlyphs) {
|
|
uint16_t bits = widthTable[code] * hdr.height;
|
|
ramFontSize += (bits + 7) / 8; // add bitmap size for each needed glyph
|
|
}
|
|
}
|
|
|
|
if (!_segment->allocateData(ramFontSize)) {
|
|
if (file) file.close();
|
|
return;
|
|
}
|
|
|
|
// write metadata
|
|
meta = getMetadata(); // get pointer again in case segment was reallocated
|
|
memcpy(meta, &savedMeta, sizeof(SegmentFontMetadata));
|
|
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
|
|
|
|
uint8_t* dataptr = _segment->data + sizeof(SegmentFontMetadata);
|
|
|
|
// write registry (GlyphEntry array)
|
|
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);
|
|
|
|
// write font header
|
|
memcpy(dataptr, &hdr, FONT_HEADER_SIZE);
|
|
dataptr += FONT_HEADER_SIZE;
|
|
|
|
// write bitmap data to cache in registry order
|
|
uint32_t dataStart = FONT_HEADER_SIZE + ((hdr.flags & 0x01) ? numGlyphs : 0); // bitmap data in wbf font starts after header and width table (if used)
|
|
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
|
|
uint16_t bits = widthTable[glyphIdx] * hdr.height;
|
|
uint16_t bytes = (bits + 7) / 8;
|
|
// calculate file offset
|
|
uint32_t offset = dataStart;
|
|
for (uint8_t j = 0; j < glyphIdx; j++) {
|
|
uint16_t b = widthTable[j] * hdr.height;
|
|
offset += (b + 7) / 8;
|
|
}
|
|
// read from file or flash
|
|
if (file) {
|
|
file.seek(offset);
|
|
file.read(dataptr, bytes);
|
|
} else {
|
|
memcpy_P(dataptr, flashFont + offset, bytes);
|
|
}
|
|
dataptr += bytes;
|
|
}
|
|
|
|
if (file) file.close();
|
|
updateFontBase(); // set pointer to cached header/bitmaps
|
|
}
|
|
|
|
// glyph index calculator
|
|
int32_t FontManager::getGlyphIndex(uint32_t unicode, FontHeader* hdr) {
|
|
if (unicode <= LAST_ASCII_CHAR) {
|
|
if (unicode >= hdr->first && unicode <= hdr->last) return unicode - hdr->first;
|
|
} else if (hdr->firstUnicode > 0 && unicode >= hdr->firstUnicode) {
|
|
uint32_t adjusted = unicode - hdr->firstUnicode + LAST_ASCII_CHAR + 1;
|
|
if (adjusted >= hdr->first && adjusted <= hdr->last) return adjusted - hdr->first;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
// Get glyph width
|
|
uint8_t FontManager::getGlyphWidth(uint32_t unicode) {
|
|
FontHeader* hdr = reinterpret_cast<FontHeader*>(_fontBase);
|
|
int32_t idx = getGlyphIndex(unicode, hdr);
|
|
if (idx < 0) return 0;
|
|
|
|
SegmentFontMetadata* meta = (SegmentFontMetadata*)_segment->data;
|
|
GlyphEntry* registry = (GlyphEntry*)(_segment->data + sizeof(SegmentFontMetadata));
|
|
|
|
for (uint8_t k = 0; k < meta->glyphCount; k++) {
|
|
if (registry[k].code == idx) {
|
|
return registry[k].width;
|
|
}
|
|
}
|
|
return 0; // Not found in cache
|
|
}
|
|
|
|
// Get glyph bitmap
|
|
uint8_t* FontManager::getGlyphBitmap(uint32_t unicode, uint8_t& outWidth, uint8_t& outHeight) {
|
|
FontHeader* hdr = reinterpret_cast<FontHeader*>(_fontBase);
|
|
int32_t idx = getGlyphIndex(unicode, hdr);
|
|
if (idx < 0) return nullptr;
|
|
SegmentFontMetadata* meta = (SegmentFontMetadata*)_segment->data;
|
|
GlyphEntry* registry = (GlyphEntry*)(_segment->data + sizeof(SegmentFontMetadata));
|
|
|
|
uint32_t bitmapOffset = 0;
|
|
for (uint8_t k = 0; k < meta->glyphCount; k++) {
|
|
if (registry[k].code == idx) {
|
|
outWidth = registry[k].width;
|
|
outHeight = registry[k].height;
|
|
return _fontBase + FONT_HEADER_SIZE + bitmapOffset;
|
|
}
|
|
// Accumulate offset to next glyph
|
|
uint16_t bits = registry[k].width * registry[k].height;
|
|
bitmapOffset += (bits + 7) / 8;
|
|
}
|
|
return nullptr; // Glyph not found in cache
|
|
}
|
|
|
|
uint8_t FontManager::collectNeededCodes(const char* text, FontHeader* hdr, uint8_t* outCodes) {
|
|
uint8_t count = 0;
|
|
// add numbers to cache if needed (for clock use without constant re-caching)
|
|
if (_cacheNumbers) {
|
|
static const char s_nums[] PROGMEM = "0123456789:. ";
|
|
for (const char* p = s_nums; *p && count < MAX_CACHED_GLYPHS; p++) {
|
|
int32_t idx = getGlyphIndex(*p, hdr);
|
|
if (idx >= 0 && idx < 256) {
|
|
outCodes[count++] = idx;
|
|
}
|
|
}
|
|
}
|
|
// parse text
|
|
size_t i = 0, len = strlen(text);
|
|
while (i < len && count < MAX_CACHED_GLYPHS) {
|
|
uint8_t charLen;
|
|
uint32_t unicode = utf8_decode(&text[i], &charLen);
|
|
if (!charLen) break; // invalid input, stop processing
|
|
i += charLen;
|
|
int32_t idx = getGlyphIndex(unicode, hdr);
|
|
if (idx < 0) {
|
|
idx = getGlyphIndex('?', hdr);
|
|
}
|
|
if (idx >= 0 && idx < 256) {
|
|
// add if unique
|
|
bool exists = false;
|
|
for (uint8_t k = 0; k < count; k++) {
|
|
if (outCodes[k] == idx) {
|
|
exists = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!exists) outCodes[count++] = idx;
|
|
}
|
|
}
|
|
return count;
|
|
}
|
|
|
|
void FontManager::drawCharacter(uint32_t unicode, int16_t x, int16_t y, uint32_t color, uint32_t col2, int8_t rotate) {
|
|
uint8_t w, h;
|
|
const uint8_t* bitmap = getGlyphBitmap(unicode, w, h);
|
|
if (!bitmap || w == 0) return;
|
|
CRGBPalette16 grad = col2 ? CRGBPalette16(CRGB(color), CRGB(col2)) : SEGPALETTE;
|
|
uint16_t bitIndex = 0;
|
|
for (int row = 0; row < h; row++) {
|
|
CRGBW c = ColorFromPalette(grad, (row + 1) * 255 / h, 255, LINEARBLEND_NOWRAP);
|
|
for (int col = 0; col < w; col++) {
|
|
uint16_t bytePos = bitIndex >> 3;
|
|
uint8_t bitPos = 7 - (bitIndex & 7);
|
|
uint8_t byteVal = bitmap[bytePos];
|
|
if ((byteVal >> bitPos) & 1) {
|
|
int x0, y0;
|
|
switch (rotate) {
|
|
case -1: x0 = x + row; y0 = y + col; break; // 90° CW
|
|
case 1: x0 = x + (h-1) - row; y0 = y + (w-1) - col; break; // 90° CCW
|
|
case -2:
|
|
case 2: x0 = x + (w-1) - col; y0 = y + (h-1) - row; break;
|
|
default: x0 = x + col; y0 = y + row; break;
|
|
}
|
|
_segment->setPixelColorXY(x0, y0, c.color32); // bounds checking is done in setPixelColorXY
|
|
}
|
|
bitIndex++;
|
|
}
|
|
}
|
|
}
|