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,126 @@
|
||||
#include "FontSelectionActivity.h"
|
||||
|
||||
#include <GfxRenderer.h>
|
||||
#include <I18n.h>
|
||||
|
||||
#include "CrossPointSettings.h"
|
||||
#include "MappedInputManager.h"
|
||||
#include "components/UITheme.h"
|
||||
#include "fontIds.h"
|
||||
|
||||
FontSelectionActivity::FontSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
|
||||
const SdCardFontRegistry* registry)
|
||||
: Activity("FontSelect", renderer, mappedInput), registry_(registry) {}
|
||||
|
||||
void FontSelectionActivity::onEnter() {
|
||||
Activity::onEnter();
|
||||
|
||||
// Build combined font list: built-in + SD card fonts
|
||||
fonts_.clear();
|
||||
fonts_.reserve(CrossPointSettings::BUILTIN_FONT_COUNT + (registry_ ? registry_->getFamilyCount() : 0));
|
||||
|
||||
fonts_.push_back({I18N.get(StrId::STR_NOTO_SERIF), true, 0});
|
||||
fonts_.push_back({I18N.get(StrId::STR_NOTO_SANS), true, 1});
|
||||
fonts_.push_back({I18N.get(StrId::STR_OPEN_DYSLEXIC), true, 2});
|
||||
|
||||
if (registry_) {
|
||||
const auto& families = registry_->getFamilies();
|
||||
for (int i = 0; i < static_cast<int>(families.size()); i++) {
|
||||
fonts_.push_back({families[i].name, false, static_cast<uint8_t>(CrossPointSettings::BUILTIN_FONT_COUNT + i)});
|
||||
}
|
||||
}
|
||||
|
||||
// Find current selection
|
||||
selectedIndex_ = 0;
|
||||
if (SETTINGS.sdFontFamilyName[0] != '\0' && registry_) {
|
||||
const auto& families = registry_->getFamilies();
|
||||
for (int i = 0; i < static_cast<int>(families.size()); i++) {
|
||||
if (families[i].name == SETTINGS.sdFontFamilyName) {
|
||||
selectedIndex_ = CrossPointSettings::BUILTIN_FONT_COUNT + i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
selectedIndex_ = SETTINGS.fontFamily < CrossPointSettings::BUILTIN_FONT_COUNT ? SETTINGS.fontFamily : 0;
|
||||
}
|
||||
|
||||
requestUpdate();
|
||||
}
|
||||
|
||||
void FontSelectionActivity::onExit() { Activity::onExit(); }
|
||||
|
||||
void FontSelectionActivity::loop() {
|
||||
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
|
||||
handleSelection();
|
||||
return;
|
||||
}
|
||||
|
||||
buttonNavigator_.onNextRelease([this] {
|
||||
selectedIndex_ = ButtonNavigator::nextIndex(selectedIndex_, static_cast<int>(fonts_.size()));
|
||||
requestUpdate();
|
||||
});
|
||||
|
||||
buttonNavigator_.onPreviousRelease([this] {
|
||||
selectedIndex_ = ButtonNavigator::previousIndex(selectedIndex_, static_cast<int>(fonts_.size()));
|
||||
requestUpdate();
|
||||
});
|
||||
}
|
||||
|
||||
void FontSelectionActivity::handleSelection() {
|
||||
const auto& font = fonts_[selectedIndex_];
|
||||
if (font.settingIndex < CrossPointSettings::BUILTIN_FONT_COUNT) {
|
||||
SETTINGS.fontFamily = font.settingIndex;
|
||||
SETTINGS.sdFontFamilyName[0] = '\0';
|
||||
} else if (registry_) {
|
||||
int sdIdx = font.settingIndex - CrossPointSettings::BUILTIN_FONT_COUNT;
|
||||
const auto& families = registry_->getFamilies();
|
||||
if (sdIdx < static_cast<int>(families.size())) {
|
||||
strncpy(SETTINGS.sdFontFamilyName, families[sdIdx].name.c_str(), sizeof(SETTINGS.sdFontFamilyName) - 1);
|
||||
SETTINGS.sdFontFamilyName[sizeof(SETTINGS.sdFontFamilyName) - 1] = '\0';
|
||||
}
|
||||
}
|
||||
finish();
|
||||
}
|
||||
|
||||
void FontSelectionActivity::render(RenderLock&&) {
|
||||
renderer.clearScreen();
|
||||
|
||||
const auto pageWidth = renderer.getScreenWidth();
|
||||
const auto pageHeight = renderer.getScreenHeight();
|
||||
const auto& metrics = UITheme::getInstance().getMetrics();
|
||||
|
||||
GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_FONT_FAMILY));
|
||||
|
||||
const int contentTop = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing;
|
||||
const int contentHeight = pageHeight - contentTop - metrics.buttonHintsHeight - metrics.verticalSpacing;
|
||||
|
||||
// Determine which font index is currently active (to mark as "Selected")
|
||||
int currentFontIndex = 0;
|
||||
if (SETTINGS.sdFontFamilyName[0] != '\0' && registry_) {
|
||||
const auto& families = registry_->getFamilies();
|
||||
for (int i = 0; i < static_cast<int>(families.size()); i++) {
|
||||
if (families[i].name == SETTINGS.sdFontFamilyName) {
|
||||
currentFontIndex = CrossPointSettings::BUILTIN_FONT_COUNT + i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
currentFontIndex = SETTINGS.fontFamily < CrossPointSettings::BUILTIN_FONT_COUNT ? SETTINGS.fontFamily : 0;
|
||||
}
|
||||
|
||||
GUI.drawList(
|
||||
renderer, Rect{0, contentTop, pageWidth, contentHeight}, static_cast<int>(fonts_.size()), selectedIndex_,
|
||||
[this](int index) { return fonts_[index].name; }, nullptr, nullptr,
|
||||
[this, currentFontIndex](int index) -> std::string { return index == currentFontIndex ? tr(STR_SELECTED) : ""; },
|
||||
true);
|
||||
|
||||
const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), tr(STR_DIR_UP), tr(STR_DIR_DOWN));
|
||||
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||
|
||||
renderer.displayBuffer();
|
||||
}
|
||||
Reference in New Issue
Block a user