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
+101
-3
@@ -2,18 +2,105 @@
|
||||
|
||||
#include <HalTiltSensor.h>
|
||||
#include <I18n.h>
|
||||
#include <SdCardFontRegistry.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <iterator>
|
||||
#include <vector>
|
||||
|
||||
#include "CrossPointSettings.h"
|
||||
#include "KOReaderCredentialStore.h"
|
||||
#include "activities/settings/SettingsActivity.h"
|
||||
|
||||
// Build the font family setting dynamically. When registry is non-null, SD card fonts
|
||||
// are appended after the built-in fonts. Otherwise only built-in fonts are listed.
|
||||
inline SettingInfo buildFontFamilySetting(const SdCardFontRegistry* registry) {
|
||||
// Built-in font labels (StrId)
|
||||
std::vector<StrId> enumValues = {StrId::STR_NOTO_SERIF, StrId::STR_NOTO_SANS, StrId::STR_OPEN_DYSLEXIC};
|
||||
// Runtime string labels for SD card fonts
|
||||
std::vector<std::string> enumStringValues;
|
||||
|
||||
// Reserve: first CrossPointSettings::BUILTIN_FONT_COUNT entries use StrId, rest use strings
|
||||
if (registry) {
|
||||
const auto& families = registry->getFamilies();
|
||||
enumStringValues.reserve(families.size());
|
||||
std::transform(families.begin(), families.end(), std::back_inserter(enumStringValues),
|
||||
[](const SdCardFontFamilyInfo& f) { return f.name; });
|
||||
}
|
||||
|
||||
// Capture the SD font count for the lambdas
|
||||
const int sdFontCount = static_cast<int>(enumStringValues.size());
|
||||
|
||||
// Total option count = built-in + SD card families
|
||||
// For the combined enumStringValues: we need all entries as strings (built-in names + SD names)
|
||||
// The render code checks enumStringValues first, then enumValues. So we build enumStringValues
|
||||
// with all options when SD fonts are present.
|
||||
std::vector<std::string> allStringValues;
|
||||
if (sdFontCount > 0) {
|
||||
allStringValues.push_back(I18N.get(StrId::STR_NOTO_SERIF));
|
||||
allStringValues.push_back(I18N.get(StrId::STR_NOTO_SANS));
|
||||
allStringValues.push_back(I18N.get(StrId::STR_OPEN_DYSLEXIC));
|
||||
allStringValues.insert(allStringValues.end(), enumStringValues.begin(), enumStringValues.end());
|
||||
}
|
||||
|
||||
SettingInfo s;
|
||||
s.nameId = StrId::STR_FONT_FAMILY;
|
||||
s.type = SettingType::ENUM;
|
||||
s.enumValues = std::move(enumValues);
|
||||
s.enumStringValues = std::move(allStringValues);
|
||||
s.key = "fontFamily";
|
||||
s.category = StrId::STR_CAT_READER;
|
||||
|
||||
// Capture registry families by copy for the lambdas
|
||||
std::vector<std::string> sdFamilyNames;
|
||||
if (registry) {
|
||||
const auto& families = registry->getFamilies();
|
||||
sdFamilyNames.reserve(families.size());
|
||||
std::transform(families.begin(), families.end(), std::back_inserter(sdFamilyNames),
|
||||
[](const SdCardFontFamilyInfo& f) { return f.name; });
|
||||
}
|
||||
|
||||
s.valueGetter = [sdFamilyNames]() -> uint8_t {
|
||||
// If an SD card font is selected, find its index
|
||||
if (SETTINGS.sdFontFamilyName[0] != '\0') {
|
||||
for (int i = 0; i < static_cast<int>(sdFamilyNames.size()); i++) {
|
||||
if (sdFamilyNames[i] == SETTINGS.sdFontFamilyName) {
|
||||
return static_cast<uint8_t>(CrossPointSettings::BUILTIN_FONT_COUNT + i);
|
||||
}
|
||||
}
|
||||
// SD font name not found in registry — fall through to built-in
|
||||
}
|
||||
return SETTINGS.fontFamily < CrossPointSettings::BUILTIN_FONT_COUNT ? SETTINGS.fontFamily : 0;
|
||||
};
|
||||
|
||||
s.valueSetter = [sdFamilyNames](uint8_t v) {
|
||||
if (v < CrossPointSettings::BUILTIN_FONT_COUNT) {
|
||||
SETTINGS.fontFamily = v;
|
||||
SETTINGS.sdFontFamilyName[0] = '\0';
|
||||
} else {
|
||||
int sdIdx = v - CrossPointSettings::BUILTIN_FONT_COUNT;
|
||||
if (sdIdx < static_cast<int>(sdFamilyNames.size())) {
|
||||
strncpy(SETTINGS.sdFontFamilyName, sdFamilyNames[sdIdx].c_str(), sizeof(SETTINGS.sdFontFamilyName) - 1);
|
||||
SETTINGS.sdFontFamilyName[sizeof(SETTINGS.sdFontFamilyName) - 1] = '\0';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
// Shared settings list used by both the device settings UI and the web settings API.
|
||||
// Each entry has a key (for JSON API) and category (for grouping).
|
||||
// ACTION-type entries and entries without a key are device-only.
|
||||
inline const std::vector<SettingInfo>& getSettingsList() {
|
||||
static const std::vector<SettingInfo> list = [] {
|
||||
//
|
||||
// The static list is constructed exactly once (master's optimization, #1086 +
|
||||
// #1636) so the per-entry SettingInfo cost is paid once. When an
|
||||
// SdCardFontRegistry is supplied AND has SD card fonts installed, the
|
||||
// font-family entry is replaced in a per-call copy with a registry-aware
|
||||
// version. Callers without SD fonts pay only a vector copy.
|
||||
inline std::vector<SettingInfo> getSettingsList(const SdCardFontRegistry* registry = nullptr) {
|
||||
static const std::vector<SettingInfo> baseList = [] {
|
||||
std::vector<SettingInfo> v = {
|
||||
// --- Display ---
|
||||
SettingInfo::Enum(StrId::STR_SLEEP_SCREEN, &CrossPointSettings::sleepScreen,
|
||||
@@ -40,6 +127,8 @@ inline const std::vector<SettingInfo>& getSettingsList() {
|
||||
StrId::STR_CAT_DISPLAY),
|
||||
|
||||
// --- Reader ---
|
||||
// Built-in font-family entry. Replaced per-call with a registry-aware
|
||||
// version when SD fonts are installed.
|
||||
SettingInfo::Enum(StrId::STR_FONT_FAMILY, &CrossPointSettings::fontFamily,
|
||||
{StrId::STR_NOTO_SERIF, StrId::STR_NOTO_SANS, StrId::STR_OPEN_DYSLEXIC}, "fontFamily",
|
||||
StrId::STR_CAT_READER),
|
||||
@@ -78,6 +167,7 @@ inline const std::vector<SettingInfo>& getSettingsList() {
|
||||
SettingInfo::Enum(StrId::STR_SHORT_PWR_BTN, &CrossPointSettings::shortPwrBtn,
|
||||
{StrId::STR_IGNORE, StrId::STR_SLEEP, StrId::STR_PAGE_TURN, StrId::STR_FORCE_REFRESH},
|
||||
"shortPwrBtn", StrId::STR_CAT_CONTROLS),
|
||||
|
||||
// --- System ---
|
||||
SettingInfo::Enum(StrId::STR_TIME_TO_SLEEP, &CrossPointSettings::sleepTimeout,
|
||||
{StrId::STR_MIN_1, StrId::STR_MIN_5, StrId::STR_MIN_10, StrId::STR_MIN_15, StrId::STR_MIN_30},
|
||||
@@ -149,5 +239,13 @@ inline const std::vector<SettingInfo>& getSettingsList() {
|
||||
}
|
||||
return v;
|
||||
}();
|
||||
return list;
|
||||
|
||||
std::vector<SettingInfo> v = baseList;
|
||||
if (registry && registry->getFamilyCount() > 0) {
|
||||
auto it = std::find_if(v.begin(), v.end(), [](const SettingInfo& s) { return s.nameId == StrId::STR_FONT_FAMILY; });
|
||||
if (it != v.end()) {
|
||||
*it = buildFontFamilySetting(registry);
|
||||
}
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user