Merge pull request #147 from jpirnay/feat-fonts-sd

feat: Add sd fonts (integrate and improve upstream #1327 by adriancaruana)
This commit is contained in:
jpirnay
2026-04-28 19:09:51 +02:00
committed by GitHub
31 changed files with 3603 additions and 124 deletions
+73 -36
View File
@@ -8,8 +8,28 @@
#include <cstring>
#include <string>
#include "SdCardFontGlobals.h"
#include "fontIds.h"
// Font ID 0 is reserved as the SD card font "not found" sentinel
// (SdCardFontManager::computeFontId() never returns 0). Guard against any
// hash accidentally producing 0 — would cause silent fallback to built-in.
static_assert(BOOKERLY_12_FONT_ID != 0, "Font ID collision with sentinel");
static_assert(BOOKERLY_14_FONT_ID != 0, "Font ID collision with sentinel");
static_assert(BOOKERLY_16_FONT_ID != 0, "Font ID collision with sentinel");
static_assert(BOOKERLY_18_FONT_ID != 0, "Font ID collision with sentinel");
static_assert(NOTOSANS_12_FONT_ID != 0, "Font ID collision with sentinel");
static_assert(NOTOSANS_14_FONT_ID != 0, "Font ID collision with sentinel");
static_assert(NOTOSANS_16_FONT_ID != 0, "Font ID collision with sentinel");
static_assert(NOTOSANS_18_FONT_ID != 0, "Font ID collision with sentinel");
static_assert(OPENDYSLEXIC_8_FONT_ID != 0, "Font ID collision with sentinel");
static_assert(OPENDYSLEXIC_10_FONT_ID != 0, "Font ID collision with sentinel");
static_assert(OPENDYSLEXIC_12_FONT_ID != 0, "Font ID collision with sentinel");
static_assert(OPENDYSLEXIC_14_FONT_ID != 0, "Font ID collision with sentinel");
static_assert(UI_10_FONT_ID != 0, "Font ID collision with sentinel");
static_assert(UI_12_FONT_ID != 0, "Font ID collision with sentinel");
static_assert(SMALL_FONT_ID != 0, "Font ID collision with sentinel");
// Initialize the static instance
CrossPointSettings CrossPointSettings::instance;
@@ -244,38 +264,44 @@ bool CrossPointSettings::loadFromBinaryFile() {
}
float CrossPointSettings::getReaderLineCompression() const {
switch (fontFamily) {
case BOOKERLY:
const int effectiveFontId = getReaderFontId();
const int bookerlyId = getBuiltinReaderFontId(BOOKERLY, fontSize);
const int notosansId = getBuiltinReaderFontId(NOTOSANS, fontSize);
const int opendyslexicId = getBuiltinReaderFontId(OPENDYSLEXIC, fontSize);
if (effectiveFontId == notosansId) {
switch (lineSpacing) {
case TIGHT:
return 0.90f;
case NORMAL:
default:
return 0.95f;
case WIDE:
return 1.0f;
}
}
if (effectiveFontId == opendyslexicId) {
switch (lineSpacing) {
case TIGHT:
return 0.90f;
case NORMAL:
default:
return 0.95f;
case WIDE:
return 1.0f;
}
}
// Bookerly or any SD card font: use the Bookerly-style neutral values.
switch (lineSpacing) {
case TIGHT:
return 0.95f;
case NORMAL:
default:
switch (lineSpacing) {
case TIGHT:
return 0.95f;
case NORMAL:
default:
return 1.0f;
case WIDE:
return 1.1f;
}
case NOTOSANS:
switch (lineSpacing) {
case TIGHT:
return 0.90f;
case NORMAL:
default:
return 0.95f;
case WIDE:
return 1.0f;
}
case OPENDYSLEXIC:
switch (lineSpacing) {
case TIGHT:
return 0.90f;
case NORMAL:
default:
return 0.95f;
case WIDE:
return 1.0f;
}
return 1.0f;
case WIDE:
return 1.1f;
}
}
@@ -311,11 +337,11 @@ int CrossPointSettings::getRefreshFrequency() const {
}
}
int CrossPointSettings::getReaderFontId() const {
switch (fontFamily) {
int CrossPointSettings::getBuiltinReaderFontId(uint8_t family, uint8_t size) {
switch (family) {
case BOOKERLY:
default:
switch (fontSize) {
switch (size) {
case SMALL:
return BOOKERLY_12_FONT_ID;
case MEDIUM:
@@ -327,7 +353,7 @@ int CrossPointSettings::getReaderFontId() const {
return BOOKERLY_18_FONT_ID;
}
case NOTOSANS:
switch (fontSize) {
switch (size) {
case SMALL:
return NOTOSANS_12_FONT_ID;
case MEDIUM:
@@ -339,7 +365,7 @@ int CrossPointSettings::getReaderFontId() const {
return NOTOSANS_18_FONT_ID;
}
case OPENDYSLEXIC:
switch (fontSize) {
switch (size) {
case SMALL:
return OPENDYSLEXIC_8_FONT_ID;
case MEDIUM:
@@ -352,3 +378,14 @@ int CrossPointSettings::getReaderFontId() const {
}
}
}
int CrossPointSettings::getReaderFontId() const {
// SD card font takes priority when one is selected globally.
// resolveSdCardFontId() returns 0 if the named family isn't loaded
// (e.g. SD card removed since selection) — fall through to built-in.
if (sdFontFamilyName[0] != '\0') {
int id = resolveSdCardFontId(sdFontFamilyName, fontSize);
if (id != 0) return id;
}
return getBuiltinReaderFontId(fontFamily, fontSize);
}
+9 -1
View File
@@ -88,8 +88,9 @@ class CrossPointSettings {
FRONT_BUTTON_HARDWARE_COUNT
};
// Font family options
// Font family options (built-in fonts only; SD card fonts use sdFontFamilyName)
enum FONT_FAMILY { BOOKERLY = 0, NOTOSANS = 1, OPENDYSLEXIC = 2, FONT_FAMILY_COUNT };
static constexpr uint8_t BUILTIN_FONT_COUNT = FONT_FAMILY_COUNT;
// Font size options
enum FONT_SIZE { SMALL = 0, MEDIUM = 1, LARGE = 2, EXTRA_LARGE = 3, FONT_SIZE_COUNT };
enum LINE_COMPRESSION { TIGHT = 0, NORMAL = 1, WIDE = 2, LINE_COMPRESSION_COUNT };
@@ -211,6 +212,8 @@ class CrossPointSettings {
uint8_t frontButtonRight = FRONT_HW_RIGHT;
// Reader font settings
uint8_t fontFamily = BOOKERLY;
// SD card font family name (empty = use built-in fontFamily)
char sdFontFamilyName[32] = "";
uint8_t fontSize = MEDIUM;
uint8_t lineSpacing = NORMAL;
uint8_t paragraphAlignment = JUSTIFIED;
@@ -319,6 +322,11 @@ class CrossPointSettings {
static constexpr uint16_t getPowerButtonDuration() { return 400; }
int getReaderFontId() const;
// Pure built-in lookup (size enum + family enum -> font ID). Independent of
// SD-card font selection. Used by the per-book fontFamilyOverride path so
// an override forces back to a known built-in even when an SD font is the
// global default.
static int getBuiltinReaderFontId(uint8_t family, uint8_t size);
// If count_only is true, returns the number of settings items that would be written.
uint8_t writeSettings(FsFile& file, bool count_only = false) const;
+15
View File
@@ -189,6 +189,13 @@ bool JsonSettingsIO::saveSettings(const CrossPointSettings& s, const char* path)
doc["frontButtonLeft"] = s.frontButtonLeft;
doc["frontButtonRight"] = s.frontButtonRight;
// Font family uses a DynamicEnumCtx in SettingsList (no valuePtr) so the generic
// loop above skips it. Save manually.
doc["fontFamily"] = s.fontFamily;
if (s.sdFontFamilyName[0] != '\0') {
doc["sdFontFamilyName"] = s.sdFontFamilyName;
}
String json;
serializeJson(doc, json);
return Storage.writeFile(path, json);
@@ -268,6 +275,14 @@ bool JsonSettingsIO::loadSettings(CrossPointSettings& s, const char* json, bool*
clamp(doc["frontButtonRight"] | (uint8_t)S::FRONT_HW_RIGHT, S::FRONT_BUTTON_HARDWARE_COUNT, S::FRONT_HW_RIGHT);
CrossPointSettings::validateFrontButtonMapping(s);
// Font family uses a DynamicEnumCtx in SettingsList (no valuePtr) so the generic
// loop above skips it. Load manually.
s.fontFamily = clamp(doc["fontFamily"] | (uint8_t)CrossPointSettings::BOOKERLY,
CrossPointSettings::BUILTIN_FONT_COUNT, CrossPointSettings::BOOKERLY);
const char* sfn = doc["sdFontFamilyName"] | "";
strncpy(s.sdFontFamilyName, sfn, sizeof(s.sdFontFamilyName) - 1);
s.sdFontFamilyName[sizeof(s.sdFontFamilyName) - 1] = '\0';
LOG_DBG("CPS", "Settings loaded from file");
return true;
+38
View File
@@ -0,0 +1,38 @@
#pragma once
#include "SdCardFontSystem.h"
class GfxRenderer;
// Global SD card font system instance (defined in main.cpp).
extern SdCardFontSystem sdFontSystem;
// Ensure the correct SD card font family is loaded for current settings.
// Defined in main.cpp; call before entering the reader or after settings change.
extern void ensureSdFontLoaded();
// Resolve the SD card font ID for the given family name and font size enum.
// Returns 0 if no SD font with that family name and size is currently loaded.
// Free function (not stored as a callback in CrossPointSettings) so the linker
// can resolve it directly without runtime indirection.
int resolveSdCardFontId(const char* familyName, uint8_t fontSizeEnum);
// Trampolines used by the dynamic font-family SettingInfo. They walk
// sdFontSystem's registry on each call to translate between
// (built-in index | built-in count + SD index) and the appropriate
// CrossPointSettings field (fontFamily vs sdFontFamilyName).
// Signatures match SettingInfo::ValueGetterFn / ValueSetterFn so they can
// be wired directly into a DynamicEnum SettingInfo without further indirection.
uint8_t fontFamilyDynamicGetter(const void* ctx);
void fontFamilyDynamicSetter(void* ctx, uint8_t value);
// Returns the total number of font-family options currently available
// (BUILTIN_FONT_COUNT + number of discovered SD families). Used by the
// settings UI / web layer to enrich enumLabels and to bound cycling.
uint8_t fontFamilyOptionCount();
// Returns the localized label for option index `i`. For built-in indices
// (< BUILTIN_FONT_COUNT) this returns the I18N string; for SD indices it
// returns the family name from sdFontSystem.registry().
#include <string>
std::string fontFamilyOptionLabel(uint8_t i);
+163
View File
@@ -0,0 +1,163 @@
#include "SdCardFontSystem.h"
#include <GfxRenderer.h>
#include <I18n.h>
#include <Logging.h>
#include <climits>
#include <cstdlib>
#include <cstring>
#include "CrossPointSettings.h"
#include "SdCardFontGlobals.h"
// Free-function resolver used by CrossPointSettings::getReaderFontId().
// Resolved by the linker — no callback indirection stored in settings.
int resolveSdCardFontId(const char* familyName, uint8_t fontSizeEnum) {
return sdFontSystem.resolveFontId(familyName, fontSizeEnum);
}
// --- Font-family dynamic SettingInfo trampolines ---
//
// The font-family SettingInfo lives in a namespace-static SettingsList that is
// initialized at global-static phase, well before sdFontSystem.begin() runs.
// We therefore cannot bake the SD family list into the SettingInfo at
// construction; instead the SettingInfo holds these stateless trampolines that
// consult sdFontSystem at every call. enumLabels is enriched lazily by the
// consumers (SettingsActivity, CrossPointWebServer) before each iteration.
uint8_t fontFamilyDynamicGetter(const void* /*ctx*/) {
if (SETTINGS.sdFontFamilyName[0] != '\0') {
const auto& families = sdFontSystem.registry().getFamilies();
for (size_t i = 0; i < families.size(); i++) {
if (families[i].name == SETTINGS.sdFontFamilyName) {
return static_cast<uint8_t>(CrossPointSettings::BUILTIN_FONT_COUNT + i);
}
}
// SD family no longer present (card removed?); fall through to built-in.
}
return SETTINGS.fontFamily < CrossPointSettings::BUILTIN_FONT_COUNT ? SETTINGS.fontFamily
: CrossPointSettings::BOOKERLY;
}
void fontFamilyDynamicSetter(void* /*ctx*/, uint8_t value) {
if (value < CrossPointSettings::BUILTIN_FONT_COUNT) {
SETTINGS.fontFamily = value;
SETTINGS.sdFontFamilyName[0] = '\0';
return;
}
const auto& families = sdFontSystem.registry().getFamilies();
uint8_t sdIdx = value - CrossPointSettings::BUILTIN_FONT_COUNT;
if (sdIdx < families.size()) {
strncpy(SETTINGS.sdFontFamilyName, families[sdIdx].name.c_str(), sizeof(SETTINGS.sdFontFamilyName) - 1);
SETTINGS.sdFontFamilyName[sizeof(SETTINGS.sdFontFamilyName) - 1] = '\0';
}
}
uint8_t fontFamilyOptionCount() {
return static_cast<uint8_t>(CrossPointSettings::BUILTIN_FONT_COUNT + sdFontSystem.registry().getFamilies().size());
}
std::string fontFamilyOptionLabel(uint8_t i) {
if (i < CrossPointSettings::BUILTIN_FONT_COUNT) {
static const StrId BUILTIN_LABELS[] = {StrId::STR_BOOKERLY, StrId::STR_NOTO_SANS, StrId::STR_OPEN_DYSLEXIC};
return I18N.get(BUILTIN_LABELS[i]);
}
const auto& families = sdFontSystem.registry().getFamilies();
uint8_t sdIdx = i - CrossPointSettings::BUILTIN_FONT_COUNT;
return sdIdx < families.size() ? families[sdIdx].name : std::string();
}
// Map fontSize enum (SMALL=0, MEDIUM=1, LARGE=2, EXTRA_LARGE=3) to point sizes.
static constexpr uint8_t FONT_SIZE_TO_PT[] = {12, 14, 16, 18};
static uint8_t targetPtSizeFromSettings() {
uint8_t e = SETTINGS.fontSize;
if (e >= sizeof(FONT_SIZE_TO_PT)) e = 1; // default to MEDIUM
return FONT_SIZE_TO_PT[e];
}
void SdCardFontSystem::begin(GfxRenderer& renderer) {
registry_.discover();
// If user has a saved SD font selection, load it
if (SETTINGS.sdFontFamilyName[0] != '\0') {
const auto* family = registry_.findFamily(SETTINGS.sdFontFamilyName);
if (family) {
if (manager_.loadFamily(*family, renderer, targetPtSizeFromSettings())) {
LOG_DBG("SDFS", "Loaded SD card font family: %s", SETTINGS.sdFontFamilyName);
} else {
LOG_ERR("SDFS", "Failed to load SD font family: %s (clearing)", SETTINGS.sdFontFamilyName);
SETTINGS.sdFontFamilyName[0] = '\0';
}
} else {
LOG_DBG("SDFS", "SD font family not found on card: %s (clearing)", SETTINGS.sdFontFamilyName);
SETTINGS.sdFontFamilyName[0] = '\0';
}
}
LOG_DBG("SDFS", "SD font system ready (%d families discovered)", registry_.getFamilyCount());
}
void SdCardFontSystem::ensureLoaded(GfxRenderer& renderer) {
const char* wantedFamily = SETTINGS.sdFontFamilyName;
const std::string& currentFamily = manager_.currentFamilyName();
const uint8_t targetPt = targetPtSizeFromSettings();
if (wantedFamily[0] == '\0') {
if (!currentFamily.empty()) {
manager_.unloadAll(renderer);
}
return;
}
// Reload if family changed OR if the user-selected size changed and the
// family has a closer file than what's currently loaded.
bool familyMatches = (currentFamily == wantedFamily);
if (familyMatches) {
const auto* family = registry_.findFamily(wantedFamily);
if (!family) {
LOG_DBG("SDFS", "SD font family disappeared: %s (clearing)", wantedFamily);
manager_.unloadAll(renderer);
SETTINGS.sdFontFamilyName[0] = '\0';
return;
}
const auto* best = family->pickClosestSize(targetPt);
const uint8_t bestPt = best ? best->pointSize : 0;
if (bestPt == manager_.currentPointSize()) return; // already loaded with the right size
LOG_DBG("SDFS", "Reloading %s: size %u -> %u (target %u)", wantedFamily, manager_.currentPointSize(), bestPt,
targetPt);
}
if (!currentFamily.empty()) {
manager_.unloadAll(renderer);
}
const auto* family = registry_.findFamily(wantedFamily);
if (family) {
if (manager_.loadFamily(*family, renderer, targetPt)) {
LOG_DBG("SDFS", "Loaded SD font family: %s", wantedFamily);
} else {
LOG_ERR("SDFS", "Failed to load SD font family: %s (clearing)", wantedFamily);
SETTINGS.sdFontFamilyName[0] = '\0';
}
} else {
LOG_DBG("SDFS", "SD font family not found: %s (clearing)", wantedFamily);
SETTINGS.sdFontFamilyName[0] = '\0';
}
}
static uint8_t targetPtSizeFromEnum(uint8_t fontSizeEnum) {
if (fontSizeEnum >= sizeof(FONT_SIZE_TO_PT)) fontSizeEnum = 1; // default to MEDIUM
return FONT_SIZE_TO_PT[fontSizeEnum];
}
int SdCardFontSystem::resolveFontId(const char* familyName, uint8_t fontSizeEnum) const {
// The manager loads exactly one size for the active SD family. Resolve only
// if the requested family matches the loaded family and the requested size
// matches the loaded size. otherwise return 0 so callers can fall back.
if (!familyName || familyName[0] == '\0') return 0;
if (manager_.currentFamilyName() != familyName) return 0;
if (manager_.currentPointSize() != targetPtSizeFromEnum(fontSizeEnum)) return 0;
return manager_.getFontId(familyName);
}
+32
View File
@@ -0,0 +1,32 @@
#pragma once
#include <SdCardFontManager.h>
#include <SdCardFontRegistry.h>
class GfxRenderer;
/// Facade that owns the SD card font registry, manager, and resolver logic.
/// Hides implementation details behind a single begin() + ensureLoaded() API.
class SdCardFontSystem {
public:
SdCardFontSystem() = default;
SdCardFontSystem(const SdCardFontSystem&) = delete;
SdCardFontSystem& operator=(const SdCardFontSystem&) = delete;
/// Discover SD card fonts and load user's saved selection. Call once during setup.
void begin(GfxRenderer& renderer);
/// Ensure the correct SD font family is loaded for the current settings.
/// Call before entering the reader or after settings change.
void ensureLoaded(GfxRenderer& renderer);
/// Resolve an SD card font ID from family name + fontSize enum.
/// Returns 0 if not found. Used by CrossPointSettings::getReaderFontId().
int resolveFontId(const char* familyName, uint8_t fontSizeEnum) const;
/// Access the registry (e.g. for settings UI to enumerate available fonts).
const SdCardFontRegistry& registry() const { return registry_; }
private:
SdCardFontRegistry registry_;
SdCardFontManager manager_;
};
+8 -4
View File
@@ -6,6 +6,7 @@
#include "CrossPointSettings.h"
#include "KOReaderCredentialStore.h"
#include "SdCardFontGlobals.h"
#include "activities/settings/SettingInfo.h"
// Shared settings list used by both the device settings UI and the web settings API.
@@ -86,10 +87,13 @@ inline const std::vector<SettingInfo> list = {
SettingInfo::Enum(StrId::STR_ORIENTATION, &CrossPointSettings::orientation,
{StrId::STR_PORTRAIT, StrId::STR_LANDSCAPE_CW, StrId::STR_INVERTED, StrId::STR_LANDSCAPE_CCW},
"orientation", StrId::STR_CAT_READER),
// Font
SettingInfo::Enum(StrId::STR_FONT_FAMILY, &CrossPointSettings::fontFamily,
{StrId::STR_BOOKERLY, StrId::STR_NOTO_SANS, StrId::STR_OPEN_DYSLEXIC}, "fontFamily",
StrId::STR_CAT_READER)
// Font — DynamicEnum so SD card font families can be appended at the consumer
// side (SettingsActivity / CrossPointWebServer enrich enumLabels before
// iterating). The built-in StrIds are kept as a fallback for code paths that
// don't enrich enumLabels.
SettingInfo::DynamicEnum(StrId::STR_FONT_FAMILY,
{StrId::STR_BOOKERLY, StrId::STR_NOTO_SANS, StrId::STR_OPEN_DYSLEXIC},
fontFamilyDynamicGetter, fontFamilyDynamicSetter, "fontFamily", StrId::STR_CAT_READER)
.withSubcategory(StrId::STR_MENU_READER_FONT),
SettingInfo::Enum(StrId::STR_FONT_SIZE, &CrossPointSettings::fontSize,
{StrId::STR_SMALL, StrId::STR_MEDIUM, StrId::STR_LARGE, StrId::STR_X_LARGE}, "fontSize",
+5
View File
@@ -9,6 +9,7 @@
#include "CrossPointState.h"
#include "OpdsServerStore.h"
#include "SdCardFontGlobals.h"
#include "boot_sleep/BootActivity.h"
#include "boot_sleep/SleepActivity.h"
#include "browser/OpdsBookBrowserActivity.h"
@@ -282,6 +283,8 @@ void ActivityManager::goToBrowser() {
}
void ActivityManager::goToReader(std::string path) {
RenderLock lock;
ensureSdFontLoaded();
replaceActivity(std::make_unique<ReaderActivity>(renderer, mappedInput, std::move(path)));
}
@@ -301,6 +304,8 @@ void ActivityManager::goToKOReaderSync() {
void ActivityManager::replaceWithReader(std::string path, ReturnHint hint) {
returnHint = std::move(hint);
hasReturnHint = true;
RenderLock lock;
ensureSdFontLoaded();
replaceActivity(std::make_unique<ReaderActivity>(renderer, mappedInput, std::move(path)));
}
+106 -50
View File
@@ -30,6 +30,7 @@
#include "QrDisplayActivity.h"
#include "ReaderUtils.h"
#include "RecentBooksStore.h"
#include "SdCardFontGlobals.h"
#include "StarredPagesActivity.h"
#include "components/UITheme.h"
#include "fontIds.h"
@@ -1072,49 +1073,68 @@ uint8_t EpubReaderActivity::getEffectiveImageRendering() const {
return SETTINGS.imageRendering;
}
int EpubReaderActivity::getEffectiveReaderFontId() const {
const uint8_t fontFamily =
(bookFontFamilyOverride >= 0) ? static_cast<uint8_t>(bookFontFamilyOverride) : SETTINGS.fontFamily;
float EpubReaderActivity::getEffectiveReaderLineCompression() const {
const uint8_t fontSize = (bookFontSizeOverride >= 0) ? static_cast<uint8_t>(bookFontSizeOverride) : SETTINGS.fontSize;
switch (fontFamily) {
case CrossPointSettings::NOTOSANS:
switch (fontSize) {
case CrossPointSettings::SMALL:
return NOTOSANS_12_FONT_ID;
case CrossPointSettings::MEDIUM:
default:
return NOTOSANS_14_FONT_ID;
case CrossPointSettings::LARGE:
return NOTOSANS_16_FONT_ID;
case CrossPointSettings::EXTRA_LARGE:
return NOTOSANS_18_FONT_ID;
}
case CrossPointSettings::OPENDYSLEXIC:
switch (fontSize) {
case CrossPointSettings::SMALL:
return OPENDYSLEXIC_8_FONT_ID;
case CrossPointSettings::MEDIUM:
default:
return OPENDYSLEXIC_10_FONT_ID;
case CrossPointSettings::LARGE:
return OPENDYSLEXIC_12_FONT_ID;
case CrossPointSettings::EXTRA_LARGE:
return OPENDYSLEXIC_14_FONT_ID;
}
case CrossPointSettings::BOOKERLY:
default:
switch (fontSize) {
case CrossPointSettings::SMALL:
return BOOKERLY_12_FONT_ID;
case CrossPointSettings::MEDIUM:
default:
return BOOKERLY_14_FONT_ID;
case CrossPointSettings::LARGE:
return BOOKERLY_16_FONT_ID;
case CrossPointSettings::EXTRA_LARGE:
return BOOKERLY_18_FONT_ID;
}
const int effectiveFontId = getEffectiveReaderFontId();
const int bookerlyId = CrossPointSettings::getBuiltinReaderFontId(CrossPointSettings::BOOKERLY, fontSize);
const int notosansId = CrossPointSettings::getBuiltinReaderFontId(CrossPointSettings::NOTOSANS, fontSize);
const int opendyslexicId = CrossPointSettings::getBuiltinReaderFontId(CrossPointSettings::OPENDYSLEXIC, fontSize);
if (effectiveFontId == notosansId) {
switch (SETTINGS.lineSpacing) {
case CrossPointSettings::TIGHT:
return 0.90f;
case CrossPointSettings::NORMAL:
default:
return 0.95f;
case CrossPointSettings::WIDE:
return 1.0f;
}
}
if (effectiveFontId == opendyslexicId) {
switch (SETTINGS.lineSpacing) {
case CrossPointSettings::TIGHT:
return 0.90f;
case CrossPointSettings::NORMAL:
default:
return 0.95f;
case CrossPointSettings::WIDE:
return 1.0f;
}
}
switch (SETTINGS.lineSpacing) {
case CrossPointSettings::TIGHT:
return 0.95f;
case CrossPointSettings::NORMAL:
default:
return 1.0f;
case CrossPointSettings::WIDE:
return 1.1f;
}
}
int EpubReaderActivity::getEffectiveReaderFontId() const {
// Per-book font override: when set, force a specific BUILT-IN family even if
// an SD card font is the global default. This makes the override predictable
// ("override forces back to a known built-in") and avoids surprising users
// who set the override before they had any SD fonts.
const uint8_t fontSize = (bookFontSizeOverride >= 0) ? static_cast<uint8_t>(bookFontSizeOverride) : SETTINGS.fontSize;
if (bookFontFamilyOverride >= 0) {
return CrossPointSettings::getBuiltinReaderFontId(static_cast<uint8_t>(bookFontFamilyOverride), fontSize);
}
// No override: defer to global resolution (which honors SD card font selection).
// We synthesize a temporary lookup using the override fontSize if it's set; otherwise
// SETTINGS.getReaderFontId() is the canonical answer.
if (bookFontSizeOverride >= 0) {
if (SETTINGS.sdFontFamilyName[0] != '\0') {
const int id = resolveSdCardFontId(SETTINGS.sdFontFamilyName, fontSize);
if (id != 0) return id;
}
return CrossPointSettings::getBuiltinReaderFontId(SETTINGS.fontFamily, fontSize);
}
return SETTINGS.getReaderFontId();
}
bool EpubReaderActivity::stepPageState(const bool isForwardTurn) {
@@ -1229,7 +1249,7 @@ void EpubReaderActivity::render(RenderLock&& lock) {
section = std::make_unique<Section>(epub, currentSpineIndex, renderer);
const unsigned long sectionStart = millis();
if (!section->loadSectionFile(getEffectiveReaderFontId(), SETTINGS.getReaderLineCompression(),
if (!section->loadSectionFile(getEffectiveReaderFontId(), getEffectiveReaderLineCompression(),
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth,
viewportHeight, SETTINGS.hyphenationEnabled, embeddedStyle, imageRendering)) {
LOG_DBG("ERS", "Cache not found, building...");
@@ -1246,7 +1266,11 @@ void EpubReaderActivity::render(RenderLock&& lock) {
GUI.fillPopupProgress(renderer, popupRect, progress);
};
if (!section->createSectionFile(getEffectiveReaderFontId(), SETTINGS.getReaderLineCompression(),
// Reset cumulative SD font metadata cache so this section starts fresh.
// Pagination will rebuild only the cps it actually encounters, bounded
// by MAX_PAGE_GLYPHS per style.
renderer.clearSdCardFontAccumulation();
if (!section->createSectionFile(getEffectiveReaderFontId(), getEffectiveReaderLineCompression(),
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth,
viewportHeight, SETTINGS.hyphenationEnabled, embeddedStyle, imageRendering,
progressFn)) {
@@ -1401,14 +1425,16 @@ void EpubReaderActivity::silentIndexNextChapterIfNeeded(const uint16_t viewportW
const uint8_t imageRendering = getEffectiveImageRendering();
Section nextSection(epub, nextSpineIndex, renderer);
if (nextSection.loadSectionFile(getEffectiveReaderFontId(), SETTINGS.getReaderLineCompression(),
if (nextSection.loadSectionFile(getEffectiveReaderFontId(), getEffectiveReaderLineCompression(),
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth,
viewportHeight, SETTINGS.hyphenationEnabled, embeddedStyle, imageRendering)) {
return;
}
LOG_DBG("ERS", "Silently indexing next chapter: %d", nextSpineIndex);
if (!nextSection.createSectionFile(getEffectiveReaderFontId(), SETTINGS.getReaderLineCompression(),
// Reset cumulative SD font metadata cache for the new section.
renderer.clearSdCardFontAccumulation();
if (!nextSection.createSectionFile(getEffectiveReaderFontId(), getEffectiveReaderLineCompression(),
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth,
viewportHeight, SETTINGS.hyphenationEnabled, embeddedStyle, imageRendering)) {
LOG_ERR("ERS", "Failed silent indexing for chapter: %d", nextSpineIndex);
@@ -1759,14 +1785,44 @@ bool EpubReaderActivity::drawCurrentPageToBuffer(const std::string& filePath, Gf
}
};
const int effectiveFontId = getEffectiveFontId(effectiveFontFamily, effectiveFontSize);
const auto getEffectiveLineCompression = [&](int fontId) {
const int notosansId = CrossPointSettings::getBuiltinReaderFontId(CrossPointSettings::NOTOSANS, effectiveFontSize);
const int opendyslexicId =
CrossPointSettings::getBuiltinReaderFontId(CrossPointSettings::OPENDYSLEXIC, effectiveFontSize);
if (fontId == notosansId || fontId == opendyslexicId) {
switch (SETTINGS.lineSpacing) {
case CrossPointSettings::TIGHT:
return 0.90f;
case CrossPointSettings::NORMAL:
default:
return 0.95f;
case CrossPointSettings::WIDE:
return 1.0f;
}
}
switch (SETTINGS.lineSpacing) {
case CrossPointSettings::TIGHT:
return 0.95f;
case CrossPointSettings::NORMAL:
default:
return 1.0f;
case CrossPointSettings::WIDE:
return 1.1f;
}
};
const float effectiveLineCompression = getEffectiveLineCompression(effectiveFontId);
auto section = std::make_unique<Section>(epub, spineIndex, renderer);
if (!section->loadSectionFile(getEffectiveFontId(effectiveFontFamily, effectiveFontSize),
SETTINGS.getReaderLineCompression(), SETTINGS.extraParagraphSpacing,
SETTINGS.paragraphAlignment, viewportWidth, viewportHeight, SETTINGS.hyphenationEnabled,
SETTINGS.embeddedStyle, SETTINGS.imageRendering)) {
if (!section->loadSectionFile(getEffectiveFontId(effectiveFontFamily, effectiveFontSize), effectiveLineCompression,
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth,
viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle,
SETTINGS.imageRendering)) {
LOG_DBG("SLP", "EPUB: section cache not found for spine %d, rebuilding", spineIndex);
if (!section->createSectionFile(getEffectiveFontId(effectiveFontFamily, effectiveFontSize),
SETTINGS.getReaderLineCompression(), SETTINGS.extraParagraphSpacing,
effectiveLineCompression, SETTINGS.extraParagraphSpacing,
SETTINGS.paragraphAlignment, viewportWidth, viewportHeight,
SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle, SETTINGS.imageRendering)) {
LOG_ERR("SLP", "EPUB: failed to rebuild section cache for spine %d", spineIndex);
@@ -181,6 +181,7 @@ class EpubReaderActivity final : public Activity {
bool getEffectiveEmbeddedStyle() const;
uint8_t getEffectiveImageRendering() const;
int getEffectiveReaderFontId() const;
float getEffectiveReaderLineCompression() const;
bool stepPageState(bool isForwardTurn);
void pageTurn(bool isForwardTurn);
void runRenderBenchmark();
@@ -5,10 +5,31 @@
#include "KOReaderCredentialStore.h"
#include "MappedInputManager.h"
#include "SdCardFontGlobals.h"
#include "activities/settings/SettingsSubmenuActivity.h"
#include "components/UITheme.h"
#include "fontIds.h"
namespace {
// Returns the localized name of the family currently used as the global default
// for the reader. When the user has selected an SD card font globally, the
// override menu's "Default" label should reflect that family by name even
// though the per-book override list itself is built-in only.
std::string defaultFontFamilyLabel(const SettingInfo& item) {
if (SETTINGS.sdFontFamilyName[0] != '\0') {
return std::string(SETTINGS.sdFontFamilyName);
}
// Built-in: enumValues[0] is STR_DEFAULT_VALUE, [1..3] are the three families
// in the same order as CrossPointSettings::FONT_FAMILY (BOOKERLY, NOTOSANS,
// OPENDYSLEXIC).
const auto idx = static_cast<size_t>(SETTINGS.fontFamily + 1);
if (idx < item.enumValues.size()) {
return I18N.get(item.enumValues[idx]);
}
return {};
}
} // namespace
EpubReaderMenuActivity::EpubReaderMenuActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
const std::string& title, const int currentPage, const int totalPages,
const int bookProgressPercent, const uint8_t currentOrientation,
@@ -290,9 +311,9 @@ std::string EpubReaderMenuActivity::getItemValueString(int index) const {
}
}
if (item.nameId == StrId::STR_FONT_FAMILY && pendingFontFamilyOverride < 0) {
const auto defaultIndex = static_cast<size_t>(SETTINGS.fontFamily + 1);
if (defaultIndex < item.enumValues.size()) {
return std::string(tr(STR_DEFAULT_VALUE)) + " (" + I18N.get(item.enumValues[defaultIndex]) + ")";
const auto label = defaultFontFamilyLabel(item);
if (!label.empty()) {
return std::string(tr(STR_DEFAULT_VALUE)) + " (" + label + ")";
}
}
if (item.nameId == StrId::STR_FONT_SIZE && pendingFontSizeOverride < 0) {
@@ -324,9 +345,9 @@ void EpubReaderMenuActivity::openSubmenu(const SettingInfo& submenuEntry) {
}
}
if (item.nameId == StrId::STR_FONT_FAMILY && pendingFontFamilyOverride < 0) {
const auto valueIndex = static_cast<size_t>(SETTINGS.fontFamily + 1);
if (valueIndex < item.enumValues.size()) {
return std::string(tr(STR_DEFAULT_VALUE)) + " (" + I18N.get(item.enumValues[valueIndex]) + ")";
const auto label = defaultFontFamilyLabel(item);
if (!label.empty()) {
return std::string(tr(STR_DEFAULT_VALUE)) + " (" + label + ")";
}
}
if (item.nameId == StrId::STR_FONT_SIZE && pendingFontSizeOverride < 0) {
+19 -8
View File
@@ -6,6 +6,8 @@
#include <HalGPIO.h>
#include <Logging.h>
#include <cstring>
#include "CrossPointSettings.h"
#include "MappedInputManager.h"
#include "SettingActionDispatch.h"
@@ -66,14 +68,23 @@ void SettingsActivity::onEnter() {
setting.nameId == StrId::STR_TIMEZONE)) {
continue;
}
if (setting.category == StrId::STR_CAT_DISPLAY) {
addTo(displaySettings, lastDisplaySub, setting);
} else if (setting.category == StrId::STR_CAT_READER) {
addTo(readerSettings, lastReaderSub, setting);
} else if (setting.category == StrId::STR_CAT_CONTROLS) {
addTo(controlsSettings, lastControlsSub, setting);
} else if (setting.category == StrId::STR_CAT_SYSTEM) {
addTo(systemSettings, lastSystemSub, setting);
// Enrich the font-family entry with SD card families discovered at boot.
// The list itself is a namespace-static; we only mutate our local copy here.
SettingInfo enriched = setting;
if (setting.key && std::strcmp(setting.key, "fontFamily") == 0) {
const uint8_t n = fontFamilyOptionCount();
enriched.enumLabels.clear();
enriched.enumLabels.reserve(n);
for (uint8_t i = 0; i < n; i++) enriched.enumLabels.push_back(fontFamilyOptionLabel(i));
}
if (enriched.category == StrId::STR_CAT_DISPLAY) {
addTo(displaySettings, lastDisplaySub, enriched);
} else if (enriched.category == StrId::STR_CAT_READER) {
addTo(readerSettings, lastReaderSub, enriched);
} else if (enriched.category == StrId::STR_CAT_CONTROLS) {
addTo(controlsSettings, lastControlsSub, enriched);
} else if (enriched.category == StrId::STR_CAT_SYSTEM) {
addTo(systemSettings, lastSystemSub, enriched);
}
// Web-only categories (KOReader Sync, OPDS Browser) are skipped for device UI
}
+12 -1
View File
@@ -26,6 +26,7 @@
#include "MappedInputManager.h"
#include "OpdsServerStore.h"
#include "RecentBooksStore.h"
#include "SdCardFontSystem.h"
#include "WeatherSettingsStore.h"
#include "activities/Activity.h"
#include "activities/ActivityManager.h"
@@ -40,7 +41,8 @@ ButtonEventManager& globalButtonEvents() { return buttonEventManager; }
GfxRenderer renderer(display);
ActivityManager activityManager(renderer, mappedInputManager);
FontDecompressor fontDecompressor;
FontCacheManager fontCacheManager(renderer.getFontMap());
SdCardFontSystem sdFontSystem;
FontCacheManager fontCacheManager(renderer.getFontMap(), renderer.getSdCardFonts());
// Fonts
EpdFont bookerly14RegularFont(&bookerly_14_regular);
@@ -182,9 +184,18 @@ void setupDisplayAndFonts() {
renderer.insertFont(UI_10_FONT_ID, ui10FontFamily);
renderer.insertFont(UI_12_FONT_ID, ui12FontFamily);
renderer.insertFont(SMALL_FONT_ID, smallFontFamily);
// Discover SD card fonts (under /.crosspoint/fonts/) and load the family
// currently selected in settings (if any). Safe to call without an SD card.
sdFontSystem.begin(renderer);
LOG_DBG("MAIN", "Fonts setup");
}
// Defined here to satisfy SdCardFontGlobals.h's extern declaration. Keeps
// activity-side callers out of SdCardFontSystem internals.
void ensureSdFontLoaded() { sdFontSystem.ensureLoaded(renderer); }
void setup() {
{
esp_ota_img_states_t otaState;
+21 -3
View File
@@ -9,6 +9,7 @@
#include <esp_task_wdt.h>
#include <algorithm>
#include <cstring>
#include "CrossPointSettings.h"
#include "OpdsServerStore.h"
@@ -1224,8 +1225,21 @@ void CrossPointWebServer::handleGetSettings() const {
bool seenFirst = false;
JsonDocument doc;
for (const auto& s : settings) {
if (!s.key) continue; // Skip ACTION-only entries
for (const auto& sBase : settings) {
if (!sBase.key) continue; // Skip ACTION-only entries
// Enrich the font-family entry with current SD card families.
SettingInfo sLocal;
const SettingInfo* sPtr = &sBase;
if (std::strcmp(sBase.key, "fontFamily") == 0) {
sLocal = sBase;
const uint8_t n = fontFamilyOptionCount();
sLocal.enumLabels.clear();
sLocal.enumLabels.reserve(n);
for (uint8_t i = 0; i < n; i++) sLocal.enumLabels.push_back(fontFamilyOptionLabel(i));
sPtr = &sLocal;
}
const SettingInfo& s = *sPtr;
doc.clear();
doc["key"] = s.key;
@@ -1337,7 +1351,11 @@ void CrossPointWebServer::handlePostSettings() {
}
case SettingType::ENUM: {
const int val = doc[s.key].as<int>();
const auto count = static_cast<int>(s.enumLabels.empty() ? s.enumValues.size() : s.enumLabels.size());
// For fontFamily the enumLabels in the static list are empty by design
// (built lazily by handleGetSettings); use the dynamic option count instead.
const int count = (std::strcmp(s.key, "fontFamily") == 0)
? static_cast<int>(fontFamilyOptionCount())
: static_cast<int>(s.enumLabels.empty() ? s.enumValues.size() : s.enumLabels.size());
if (val >= 0 && val < count) {
if (s.valuePtr) {
SETTINGS.*(s.valuePtr) = static_cast<uint8_t>(val);