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>
120 lines
3.9 KiB
C++
120 lines
3.9 KiB
C++
#include "FontCacheManager.h"
|
|
|
|
#include <FontDecompressor.h>
|
|
#include <Logging.h>
|
|
#include <SdCardFont.h>
|
|
|
|
#include <cstring>
|
|
|
|
FontCacheManager::FontCacheManager(const std::map<int, EpdFontFamily>& fontMap,
|
|
const std::map<int, SdCardFont*>& sdCardFonts)
|
|
: fontMap_(fontMap), sdCardFonts_(sdCardFonts) {}
|
|
|
|
void FontCacheManager::setFontDecompressor(FontDecompressor* d) { fontDecompressor_ = d; }
|
|
|
|
void FontCacheManager::clearCache() {
|
|
if (fontDecompressor_) fontDecompressor_->clearCache();
|
|
for (auto& [id, font] : sdCardFonts_) {
|
|
font->clearCache();
|
|
}
|
|
}
|
|
|
|
void FontCacheManager::prewarmCache(int fontId, const char* utf8Text, uint8_t styleMask) {
|
|
// SD card font prewarm path: prewarm all requested styles in one call
|
|
auto it = sdCardFonts_.find(fontId);
|
|
if (it != sdCardFonts_.end()) {
|
|
int missed = it->second->prewarm(utf8Text, styleMask);
|
|
if (missed > 0) {
|
|
LOG_DBG("FCM", "prewarmCache(SD): %d glyph(s) not found (styleMask=0x%02X)", missed, styleMask);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Standard compressed font prewarm path: loop over all requested styles
|
|
if (!fontDecompressor_ || fontMap_.count(fontId) == 0) return;
|
|
|
|
for (uint8_t i = 0; i < 4; i++) {
|
|
if (!(styleMask & (1 << i))) continue;
|
|
auto style = static_cast<EpdFontFamily::Style>(i);
|
|
const EpdFontData* data = fontMap_.at(fontId).getData(style);
|
|
if (!data || !data->groups) continue;
|
|
int missed = fontDecompressor_->prewarmCache(data, utf8Text);
|
|
if (missed > 0) {
|
|
LOG_DBG("FCM", "prewarmCache: %d glyph(s) not cached for style %d", missed, i);
|
|
}
|
|
}
|
|
}
|
|
|
|
void FontCacheManager::logStats(const char* label) {
|
|
if (fontDecompressor_) fontDecompressor_->logStats(label);
|
|
for (auto& [id, font] : sdCardFonts_) {
|
|
font->logStats(label);
|
|
}
|
|
}
|
|
|
|
void FontCacheManager::resetStats() {
|
|
if (fontDecompressor_) fontDecompressor_->resetStats();
|
|
for (auto& [id, font] : sdCardFonts_) {
|
|
font->resetStats();
|
|
}
|
|
}
|
|
|
|
bool FontCacheManager::isScanning() const { return scanMode_ == ScanMode::Scanning; }
|
|
|
|
void FontCacheManager::recordText(const char* text, int fontId, EpdFontFamily::Style style) {
|
|
scanText_ += text;
|
|
if (scanFontId_ < 0) scanFontId_ = fontId;
|
|
const uint8_t baseStyle = static_cast<uint8_t>(style) & 0x03;
|
|
const unsigned char* p = reinterpret_cast<const unsigned char*>(text);
|
|
uint32_t cpCount = 0;
|
|
while (*p) {
|
|
if ((*p & 0xC0) != 0x80) cpCount++;
|
|
p++;
|
|
}
|
|
scanStyleCounts_[baseStyle] += cpCount;
|
|
}
|
|
|
|
// --- PrewarmScope implementation ---
|
|
|
|
FontCacheManager::PrewarmScope::PrewarmScope(FontCacheManager& manager) : manager_(&manager) {
|
|
manager_->scanMode_ = ScanMode::Scanning;
|
|
manager_->clearCache();
|
|
manager_->resetStats();
|
|
manager_->scanText_.clear();
|
|
manager_->scanText_.reserve(2048); // Pre-allocate to avoid heap fragmentation from repeated concat
|
|
memset(manager_->scanStyleCounts_, 0, sizeof(manager_->scanStyleCounts_));
|
|
manager_->scanFontId_ = -1;
|
|
}
|
|
|
|
void FontCacheManager::PrewarmScope::endScanAndPrewarm() {
|
|
manager_->scanMode_ = ScanMode::None;
|
|
if (manager_->scanText_.empty()) return;
|
|
|
|
// Build style bitmask from all styles that appeared during the scan
|
|
uint8_t styleMask = 0;
|
|
for (uint8_t i = 0; i < 4; i++) {
|
|
if (manager_->scanStyleCounts_[i] > 0) styleMask |= (1 << i);
|
|
}
|
|
if (styleMask == 0) styleMask = 1; // default to regular
|
|
|
|
manager_->prewarmCache(manager_->scanFontId_, manager_->scanText_.c_str(), styleMask);
|
|
|
|
// Free scan string memory
|
|
manager_->scanText_.clear();
|
|
manager_->scanText_.shrink_to_fit();
|
|
}
|
|
|
|
FontCacheManager::PrewarmScope::~PrewarmScope() {
|
|
if (active_) {
|
|
endScanAndPrewarm(); // no-op if already called (scanText_ is empty)
|
|
manager_->clearCache();
|
|
}
|
|
}
|
|
|
|
FontCacheManager::PrewarmScope::PrewarmScope(PrewarmScope&& other) noexcept
|
|
: manager_(other.manager_), active_(other.active_) {
|
|
other.active_ = false;
|
|
}
|
|
|
|
FontCacheManager::PrewarmScope FontCacheManager::createPrewarmScope() { return PrewarmScope(*this); }
|