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
@@ -6,11 +6,14 @@
|
||||
#include "ButtonRemapActivity.h"
|
||||
#include "ClearCacheActivity.h"
|
||||
#include "CrossPointSettings.h"
|
||||
#include "FontDownloadActivity.h"
|
||||
#include "FontSelectionActivity.h"
|
||||
#include "KOReaderSettingsActivity.h"
|
||||
#include "LanguageSelectActivity.h"
|
||||
#include "MappedInputManager.h"
|
||||
#include "OpdsServerListActivity.h"
|
||||
#include "OtaUpdateActivity.h"
|
||||
#include "SdCardFontGlobals.h"
|
||||
#include "SdFirmwareUpdateActivity.h"
|
||||
#include "SettingsList.h"
|
||||
#include "StatusBarSettingsActivity.h"
|
||||
@@ -21,16 +24,17 @@
|
||||
const StrId SettingsActivity::categoryNames[categoryCount] = {StrId::STR_CAT_DISPLAY, StrId::STR_CAT_READER,
|
||||
StrId::STR_CAT_CONTROLS, StrId::STR_CAT_SYSTEM};
|
||||
|
||||
void SettingsActivity::onEnter() {
|
||||
Activity::onEnter();
|
||||
|
||||
// Build per-category vectors from the shared settings list
|
||||
void SettingsActivity::rebuildSettingsLists() {
|
||||
displaySettings.clear();
|
||||
readerSettings.clear();
|
||||
controlsSettings.clear();
|
||||
systemSettings.clear();
|
||||
|
||||
for (const auto& setting : getSettingsList()) {
|
||||
// Pick up any fonts uploaded/deleted over the web server since the last
|
||||
// reader activity ran — otherwise the font-family picker shows stale list.
|
||||
sdFontSystem.refreshIfDirty();
|
||||
|
||||
for (auto& setting : getSettingsList(&sdFontSystem.registry())) {
|
||||
if (setting.category == StrId::STR_NONE_OPT) continue;
|
||||
if (setting.category == StrId::STR_CAT_DISPLAY) {
|
||||
displaySettings.push_back(setting);
|
||||
@@ -41,7 +45,6 @@ void SettingsActivity::onEnter() {
|
||||
} else if (setting.category == StrId::STR_CAT_SYSTEM) {
|
||||
systemSettings.push_back(setting);
|
||||
}
|
||||
// Web-only categories (KOReader Sync, OPDS Browser) are skipped for device UI
|
||||
}
|
||||
|
||||
// Append device-only ACTION items
|
||||
@@ -51,18 +54,41 @@ void SettingsActivity::onEnter() {
|
||||
systemSettings.push_back(SettingInfo::Action(StrId::STR_KOREADER_SYNC, SettingAction::KOReaderSync));
|
||||
systemSettings.push_back(SettingInfo::Action(StrId::STR_OPDS_SERVERS, SettingAction::OPDSBrowser));
|
||||
systemSettings.push_back(SettingInfo::Action(StrId::STR_CLEAR_READING_CACHE, SettingAction::ClearCache));
|
||||
systemSettings.push_back(SettingInfo::Action(StrId::STR_DOWNLOAD_FONTS, SettingAction::DownloadFonts));
|
||||
systemSettings.push_back(SettingInfo::Action(StrId::STR_CHECK_UPDATES, SettingAction::CheckForUpdates));
|
||||
systemSettings.push_back(SettingInfo::Action(StrId::STR_SD_FIRMWARE_UPDATE, SettingAction::SdFirmwareUpdate));
|
||||
systemSettings.push_back(SettingInfo::Action(StrId::STR_LANGUAGE, SettingAction::Language));
|
||||
// Insert "Download Fonts" right after the font family setting so users discover it naturally
|
||||
readerSettings.insert(readerSettings.begin() + 1,
|
||||
SettingInfo::Action(StrId::STR_DOWNLOAD_FONTS, SettingAction::DownloadFonts));
|
||||
readerSettings.push_back(SettingInfo::Action(StrId::STR_CUSTOMISE_STATUS_BAR, SettingAction::CustomiseStatusBar));
|
||||
|
||||
// Update currentSettings pointer and count for the active category
|
||||
switch (selectedCategoryIndex) {
|
||||
case 0:
|
||||
currentSettings = &displaySettings;
|
||||
break;
|
||||
case 1:
|
||||
currentSettings = &readerSettings;
|
||||
break;
|
||||
case 2:
|
||||
currentSettings = &controlsSettings;
|
||||
break;
|
||||
case 3:
|
||||
currentSettings = &systemSettings;
|
||||
break;
|
||||
}
|
||||
settingsCount = static_cast<int>(currentSettings->size());
|
||||
}
|
||||
|
||||
void SettingsActivity::onEnter() {
|
||||
Activity::onEnter();
|
||||
|
||||
// Reset selection to first category
|
||||
selectedCategoryIndex = 0;
|
||||
selectedSettingIndex = 0;
|
||||
|
||||
// Initialize with first category (Display)
|
||||
currentSettings = &displaySettings;
|
||||
settingsCount = static_cast<int>(displaySettings.size());
|
||||
rebuildSettingsLists();
|
||||
|
||||
// Trigger first update
|
||||
requestUpdate();
|
||||
@@ -159,6 +185,21 @@ void SettingsActivity::toggleCurrentSetting() {
|
||||
} else if (setting.type == SettingType::ENUM && setting.valuePtr != nullptr) {
|
||||
const uint8_t currentValue = SETTINGS.*(setting.valuePtr);
|
||||
SETTINGS.*(setting.valuePtr) = (currentValue + 1) % static_cast<uint8_t>(setting.enumValues.size());
|
||||
} else if (setting.type == SettingType::ENUM && setting.valueGetter && setting.valueSetter) {
|
||||
if (setting.nameId == StrId::STR_FONT_FAMILY) {
|
||||
// Launch font selection submenu instead of cycling
|
||||
startActivityForResult(std::make_unique<FontSelectionActivity>(renderer, mappedInput, &sdFontSystem.registry()),
|
||||
[this](const ActivityResult&) {
|
||||
SETTINGS.saveToFile();
|
||||
rebuildSettingsLists();
|
||||
});
|
||||
return;
|
||||
}
|
||||
const uint8_t totalValues = setting.enumStringValues.empty()
|
||||
? static_cast<uint8_t>(setting.enumValues.size())
|
||||
: static_cast<uint8_t>(setting.enumStringValues.size());
|
||||
const uint8_t cur = setting.valueGetter();
|
||||
setting.valueSetter((cur + 1) % totalValues);
|
||||
} else if (setting.type == SettingType::VALUE && setting.valuePtr != nullptr) {
|
||||
const int8_t currentValue = SETTINGS.*(setting.valuePtr);
|
||||
if (currentValue + setting.valueRange.step > setting.valueRange.max) {
|
||||
@@ -194,6 +235,13 @@ void SettingsActivity::toggleCurrentSetting() {
|
||||
case SettingAction::SdFirmwareUpdate:
|
||||
startActivityForResult(std::make_unique<SdFirmwareUpdateActivity>(renderer, mappedInput), resultHandler);
|
||||
break;
|
||||
case SettingAction::DownloadFonts:
|
||||
startActivityForResult(std::make_unique<FontDownloadActivity>(renderer, mappedInput),
|
||||
[this](const ActivityResult&) {
|
||||
SETTINGS.saveToFile();
|
||||
rebuildSettingsLists();
|
||||
});
|
||||
break;
|
||||
case SettingAction::Language:
|
||||
startActivityForResult(std::make_unique<LanguageSelectActivity>(renderer, mappedInput), resultHandler);
|
||||
break;
|
||||
@@ -245,6 +293,13 @@ void SettingsActivity::render(RenderLock&&) {
|
||||
} else if (setting.type == SettingType::ENUM && setting.valuePtr != nullptr) {
|
||||
const uint8_t value = SETTINGS.*(setting.valuePtr);
|
||||
valueText = I18N.get(setting.enumValues[value]);
|
||||
} else if (setting.type == SettingType::ENUM && setting.valueGetter) {
|
||||
const uint8_t value = setting.valueGetter();
|
||||
if (!setting.enumStringValues.empty() && value < setting.enumStringValues.size()) {
|
||||
valueText = setting.enumStringValues[value];
|
||||
} else if (value < setting.enumValues.size()) {
|
||||
valueText = I18N.get(setting.enumValues[value]);
|
||||
}
|
||||
} else if (setting.type == SettingType::VALUE && setting.valuePtr != nullptr) {
|
||||
valueText = std::to_string(SETTINGS.*(setting.valuePtr));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user