feat: add SD card font support with on-device download and web management
Add a complete SD card font subsystem that enables users to install and use custom fonts beyond the three built-in families. This combines the back-end firmware support (#1327) with the font configuration, build pipeline, CI distribution, and user-facing management UI (#1392). Core font system: - Custom .cpfont binary format (v4) with multi-style support (regular, bold, italic, bold-italic) packed into a single file per size - On-demand glyph loading from SD card with two-pass prewarm rendering to bulk-read glyphs per page, achieving near-flash performance for Latin text (~697ms vs ~681ms) and viable CJK rendering (~32% slower) - Persistent advance cache for layout measurement without SD I/O - Overflow ring buffer for glyph cache misses during rendering - Memory-conscious design: only advance tables kept in RAM; glyph bitmaps, kern tables, and ligatures loaded on demand from SD Font management: - On-device WiFi download from GitHub Releases with manifest-based discovery, install/update detection, and progress UI - Web interface font upload, listing, and deletion via /fonts page - Manual SD card copy to /fonts/ or /.fonts/ directories - Font selection integrated into Settings > Reader > Font Family Build pipeline: - Declarative YAML config (sd-fonts.yaml) as single source of truth for the 17-family font library (serif, sans, mono, accessibility) - Python converter (fontconvert_sdcard.py) for TTF/OTF to .cpfont with FreeType rasterization, class-based kerning, and ligature extraction - Parallel build orchestrator with variable font instance extraction - CI workflow publishing versioned + stable releases to a dedicated crosspoint-fonts repository with auto-incrementing revision tags - Centralized version constants (cpfont_version.py) shared across build tooling and CI, with firmware headers as manual sync points Additional fixes: - CJK characters no longer get hyphens inserted at line breaks - Advance table eliminates 30+ second stalls during CJK section indexing for paragraphs with >512 unique codepoints Closes #930 Co-authored-by: Zach Nelson <zach@zdnelson.com> Co-authored-by: Justin <itsthisjustin@users.noreply.github.com> Co-authored-by: jpirnay <jens@pirnay.com> Co-authored-by: mcrosson <kemonine@kemonine.info>
This commit is contained in:
committed by
Zach Nelson
co-authored by
Zach Nelson
Justin
jpirnay
mcrosson
parent
29fd29f537
commit
7993b2bb97
@@ -0,0 +1,109 @@
|
||||
#include "SdCardFontSystem.h"
|
||||
|
||||
#include <GfxRenderer.h>
|
||||
#include <Logging.h>
|
||||
|
||||
#include "CrossPointSettings.h"
|
||||
|
||||
static uint8_t fontSizeEnumFromSettings() {
|
||||
uint8_t e = SETTINGS.fontSize;
|
||||
if (e >= CrossPointSettings::FONT_SIZE_COUNT) e = 1; // default to MEDIUM
|
||||
return e;
|
||||
}
|
||||
|
||||
void SdCardFontSystem::begin(GfxRenderer& renderer) {
|
||||
registry_.discover();
|
||||
|
||||
// Register this system as the SD font ID resolver in settings.
|
||||
// Uses a static trampoline since CrossPointSettings stores a plain function pointer.
|
||||
SETTINGS.sdFontIdResolver = [](void* ctx, const char* familyName, uint8_t fontSizeEnum) -> int {
|
||||
return static_cast<SdCardFontSystem*>(ctx)->resolveFontId(familyName, fontSizeEnum);
|
||||
};
|
||||
SETTINGS.sdFontResolverCtx = this;
|
||||
|
||||
// If user has a saved SD font selection, load it
|
||||
if (SETTINGS.sdFontFamilyName[0] != '\0') {
|
||||
const auto* family = registry_.findFamily(SETTINGS.sdFontFamilyName);
|
||||
if (family) {
|
||||
if (manager_.loadFamily(*family, renderer, fontSizeEnumFromSettings())) {
|
||||
LOG_DBG("SDFS", "Loaded SD card font family: %s", SETTINGS.sdFontFamilyName);
|
||||
} else {
|
||||
LOG_ERR("SDFS", "Failed to load SD font family: %s (clearing)", SETTINGS.sdFontFamilyName);
|
||||
SETTINGS.sdFontFamilyName[0] = '\0';
|
||||
}
|
||||
} else {
|
||||
LOG_DBG("SDFS", "SD font family not found on card: %s (clearing)", SETTINGS.sdFontFamilyName);
|
||||
SETTINGS.sdFontFamilyName[0] = '\0';
|
||||
}
|
||||
}
|
||||
|
||||
LOG_DBG("SDFS", "SD font system ready (%d families discovered)", registry_.getFamilyCount());
|
||||
}
|
||||
|
||||
void SdCardFontSystem::ensureLoaded(GfxRenderer& renderer) {
|
||||
// If the web server (or another task) installed/deleted fonts, re-discover.
|
||||
// Track whether we just re-discovered so we can force a reload below even
|
||||
// when the wanted family/size still maps to the same point size — the file
|
||||
// contents on disk may have changed (e.g. user re-uploaded a new build).
|
||||
const bool registryWasDirty = registryDirty_.exchange(false, std::memory_order_acquire);
|
||||
if (registryWasDirty) {
|
||||
LOG_DBG("SDFS", "Registry dirty — re-discovering fonts");
|
||||
registry_.discover();
|
||||
}
|
||||
|
||||
const char* wantedFamily = SETTINGS.sdFontFamilyName;
|
||||
const std::string& currentFamily = manager_.currentFamilyName();
|
||||
const uint8_t sizeEnum = fontSizeEnumFromSettings();
|
||||
|
||||
if (wantedFamily[0] == '\0') {
|
||||
if (!currentFamily.empty()) {
|
||||
manager_.unloadAll(renderer);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Reload if family changed OR if the user-selected size maps to a
|
||||
// different file than what's currently loaded OR if the registry was
|
||||
// just rediscovered (file may have been replaced on disk).
|
||||
bool familyMatches = (currentFamily == wantedFamily);
|
||||
if (familyMatches) {
|
||||
const auto* family = registry_.findFamily(wantedFamily);
|
||||
if (!family) {
|
||||
LOG_DBG("SDFS", "SD font family disappeared: %s (clearing)", wantedFamily);
|
||||
manager_.unloadAll(renderer);
|
||||
SETTINGS.sdFontFamilyName[0] = '\0';
|
||||
return;
|
||||
}
|
||||
auto sizes = family->availableSizes();
|
||||
uint8_t idx = sizeEnum;
|
||||
if (idx >= sizes.size()) idx = sizes.size() - 1;
|
||||
uint8_t wantedPt = sizes.empty() ? 0 : sizes[idx];
|
||||
if (!registryWasDirty && wantedPt == manager_.currentPointSize()) return;
|
||||
LOG_DBG("SDFS", "Reloading %s: size %u -> %u (enum %u)%s", wantedFamily, manager_.currentPointSize(), wantedPt,
|
||||
sizeEnum, registryWasDirty ? " [registry dirty]" : "");
|
||||
}
|
||||
|
||||
if (!currentFamily.empty()) {
|
||||
manager_.unloadAll(renderer);
|
||||
}
|
||||
|
||||
const auto* family = registry_.findFamily(wantedFamily);
|
||||
if (family) {
|
||||
if (manager_.loadFamily(*family, renderer, sizeEnum)) {
|
||||
LOG_DBG("SDFS", "Loaded SD font family: %s", wantedFamily);
|
||||
} else {
|
||||
LOG_ERR("SDFS", "Failed to load SD font family: %s (clearing)", wantedFamily);
|
||||
SETTINGS.sdFontFamilyName[0] = '\0';
|
||||
}
|
||||
} else {
|
||||
LOG_DBG("SDFS", "SD font family not found: %s (clearing)", wantedFamily);
|
||||
SETTINGS.sdFontFamilyName[0] = '\0';
|
||||
}
|
||||
}
|
||||
|
||||
int SdCardFontSystem::resolveFontId(const char* familyName, uint8_t /*fontSizeEnum*/) const {
|
||||
// The manager loads exactly one size (closest to SETTINGS.fontSize), so the
|
||||
// enum is implicit — always return the single loaded font ID for this family.
|
||||
// ensureLoaded() must have been called with the current settings before this.
|
||||
return manager_.getFontId(familyName);
|
||||
}
|
||||
Reference in New Issue
Block a user