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
@@ -3,6 +3,7 @@
|
||||
#include <FontDecompressor.h>
|
||||
#include <HalGPIO.h>
|
||||
#include <Logging.h>
|
||||
#include <SdCardFont.h>
|
||||
#include <Utf8.h>
|
||||
|
||||
#include <algorithm>
|
||||
@@ -22,9 +23,34 @@ const uint8_t* GfxRenderer::getGlyphBitmap(const EpdFontData* fontData, const Ep
|
||||
// must consume it (draw the glyph) before requesting another bitmap.
|
||||
return fd->getBitmap(fontData, glyph, glyphIndex);
|
||||
}
|
||||
// For SD card fonts, check if the glyph was loaded on demand into the overflow
|
||||
// buffer. getOverflowBitmap() returns:
|
||||
// - bitmap pointer for overflow glyphs with bitmap data
|
||||
// - nullptr for overflow glyphs without bitmap data (e.g. space: width=0, height=0)
|
||||
// - nullptr for non-overflow glyphs (normal prewarmed path)
|
||||
// We distinguish overflow-with-no-bitmap from non-overflow by checking isOverflowGlyph().
|
||||
if (fontData->glyphMissCtx) {
|
||||
auto* sdFont = SdCardFont::fromMissCtx(fontData->glyphMissCtx);
|
||||
if (sdFont->isOverflowGlyph(glyph)) {
|
||||
return sdFont->getOverflowBitmap(glyph); // may be nullptr for zero-width glyphs
|
||||
}
|
||||
}
|
||||
return &fontData->bitmap[glyph->dataOffset];
|
||||
}
|
||||
|
||||
void GfxRenderer::ensureSdCardFontReady(int fontId, const char* utf8Text, uint8_t styleMask) const {
|
||||
auto it = sdCardFonts_.find(fontId);
|
||||
if (it != sdCardFonts_.end()) {
|
||||
// Augment the persistent advance-only table for layout measurement.
|
||||
// The table survives across paragraphs/sections (capped per font), so
|
||||
// repeated indexing of the same SD font amortizes glyph-metric SD reads.
|
||||
int missed = it->second->buildAdvanceTable(utf8Text, styleMask);
|
||||
if (missed > 0) {
|
||||
LOG_DBG("GFX", "ensureSdCardFontReady: %d glyph(s) not found", missed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void GfxRenderer::begin() {
|
||||
frameBuffer = display.getFrameBuffer();
|
||||
if (!frameBuffer) {
|
||||
@@ -38,7 +64,12 @@ void GfxRenderer::begin() {
|
||||
bwBufferChunks.assign((frameBufferSize + BW_BUFFER_CHUNK_SIZE - 1) / BW_BUFFER_CHUNK_SIZE, nullptr);
|
||||
}
|
||||
|
||||
void GfxRenderer::insertFont(const int fontId, EpdFontFamily font) { fontMap.insert({fontId, font}); }
|
||||
void GfxRenderer::insertFont(const int fontId, EpdFontFamily font) {
|
||||
auto result = fontMap.insert({fontId, font});
|
||||
if (!result.second) {
|
||||
LOG_ERR("GFX", "Font ID %d already registered, ignoring duplicate", fontId);
|
||||
}
|
||||
}
|
||||
|
||||
// Translate logical (x,y) coordinates to physical panel coordinates based on current orientation
|
||||
// This should always be inlined for better performance
|
||||
@@ -1040,6 +1071,12 @@ int GfxRenderer::getScreenHeight() const {
|
||||
}
|
||||
|
||||
int GfxRenderer::getSpaceWidth(const int fontId, const EpdFontFamily::Style style) const {
|
||||
// Advance table fast-path for SD card fonts during layout
|
||||
auto sdIt = sdCardFonts_.find(fontId);
|
||||
if (sdIt != sdCardFonts_.end() && sdIt->second->hasAdvanceTable()) {
|
||||
return fp4::toPixel(sdIt->second->getAdvance(' ', static_cast<uint8_t>(style)));
|
||||
}
|
||||
|
||||
const auto fontIt = fontMap.find(fontId);
|
||||
if (fontIt == fontMap.end()) {
|
||||
LOG_ERR("GFX", "Font %d not found", fontId);
|
||||
@@ -1052,6 +1089,14 @@ int GfxRenderer::getSpaceWidth(const int fontId, const EpdFontFamily::Style styl
|
||||
|
||||
int GfxRenderer::getSpaceAdvance(const int fontId, const uint32_t leftCp, const uint32_t rightCp,
|
||||
const EpdFontFamily::Style style) const {
|
||||
// Advance table fast-path for SD card fonts during layout.
|
||||
// Kern data is not loaded during layout (consistent with previous metadataOnly behavior),
|
||||
// so we return just the space advance without kerning.
|
||||
auto sdIt = sdCardFonts_.find(fontId);
|
||||
if (sdIt != sdCardFonts_.end() && sdIt->second->hasAdvanceTable()) {
|
||||
return fp4::toPixel(sdIt->second->getAdvance(' ', static_cast<uint8_t>(style)));
|
||||
}
|
||||
|
||||
const auto fontIt = fontMap.find(fontId);
|
||||
if (fontIt == fontMap.end()) return 0;
|
||||
const auto& font = fontIt->second;
|
||||
@@ -1073,6 +1118,19 @@ int GfxRenderer::getKerning(const int fontId, const uint32_t leftCp, const uint3
|
||||
}
|
||||
|
||||
int GfxRenderer::getTextAdvanceX(const int fontId, const char* text, EpdFontFamily::Style style) const {
|
||||
// Advance table fast-path for SD card fonts during layout.
|
||||
// No kerning/ligature lookup — consistent with previous metadataOnly behavior
|
||||
// where kern/lig data was not loaded.
|
||||
auto sdIt = sdCardFonts_.find(fontId);
|
||||
if (sdIt != sdCardFonts_.end() && sdIt->second->hasAdvanceTable()) {
|
||||
int32_t widthFP = 0;
|
||||
const uint8_t styleIdx = static_cast<uint8_t>(style);
|
||||
while (uint32_t cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&text))) {
|
||||
widthFP += sdIt->second->getAdvance(cp, styleIdx);
|
||||
}
|
||||
return fp4::toPixel(widthFP);
|
||||
}
|
||||
|
||||
const auto fontIt = fontMap.find(fontId);
|
||||
if (fontIt == fontMap.end()) {
|
||||
LOG_ERR("GFX", "Font %d not found", fontId);
|
||||
|
||||
Reference in New Issue
Block a user