From c6fe67d48d8b65f5dfdefae31f06d0bc9826f267 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ph=E1=BA=A1m=20B=C3=ACnh=20An?= <111893501+brianhuster@users.noreply.github.com> Date: Tue, 18 Aug 2026 16:16:34 +0700 Subject: [PATCH 01/77] fix(ruby): don't flush while in a ruby group (#3102) --- lib/Epub/Epub/ParsedText.cpp | 3 ++- lib/Epub/Epub/Section.cpp | 2 +- lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/Epub/Epub/ParsedText.cpp b/lib/Epub/Epub/ParsedText.cpp index b9bdedb4bb..5135cfe428 100644 --- a/lib/Epub/Epub/ParsedText.cpp +++ b/lib/Epub/Epub/ParsedText.cpp @@ -632,7 +632,8 @@ void ParsedText::setRubyGroupAt(size_t startIndex, size_t count, const std::stri rubyTexts[idx] = ""; wordStyles[idx] = static_cast(static_cast(wordStyles[idx]) | EpdFontFamily::RUBY_CONTINUE); - wordContinues[idx] = true; // Prevent page breaker from splitting the Group Ruby! + wordContinues[idx] = true; // Prevent page breaker from splitting the Group Ruby! + wordNoSpaceBefore[idx] = false; // Ensure allowsBreak returns false! } } diff --git a/lib/Epub/Epub/Section.cpp b/lib/Epub/Epub/Section.cpp index 352cd1db06..251e56bd63 100644 --- a/lib/Epub/Epub/Section.cpp +++ b/lib/Epub/Epub/Section.cpp @@ -40,7 +40,7 @@ namespace { // v39: Image top margin is clamped so a full-viewport-height image cannot // overflow the page bottom; older caches can hold placements that panels // with no bottom inset refuse to draw. -constexpr uint8_t SECTION_FILE_VERSION = 39; +constexpr uint8_t SECTION_FILE_VERSION = 40; // Written into the version field while a build is in progress; patched to // SECTION_FILE_VERSION only when the build is finalized. An abandoned / // crash-interrupted .bin therefore carries version 0, which loadSectionFile rejects diff --git a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp index c9a3c2bd25..bf3a94c771 100644 --- a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp +++ b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp @@ -1337,7 +1337,7 @@ void XMLCALL ChapterHtmlSlimParser::characterData(void* userData, const XML_Char const size_t blockWordCount = self->currentTextBlock->size(); const size_t softFlushThreshold = self->embeddedStyle ? TEXT_BLOCK_SOFT_FLUSH_WORDS_WITH_CSS : TEXT_BLOCK_SOFT_FLUSH_WORDS; - if (blockWordCount > softFlushThreshold) { + if (blockWordCount > softFlushThreshold && !self->inRuby) { LOG_DBG("EHP", "Text block soft flush (%u words)", static_cast(blockWordCount)); const int horizontalInset = self->currentTextBlock->getBlockStyle().totalHorizontalInset(); const uint16_t effectiveWidth = (horizontalInset < self->viewportWidth) From 27f964b3e7ea73d65933946a13ea8ceb127ad3a7 Mon Sep 17 00:00:00 2001 From: Sung-jin Brian Hong Date: Wed, 19 Aug 2026 00:58:11 +0900 Subject: [PATCH 02/77] fix: stop negative font IDs from stealing scan slots (#3107) --- lib/GfxRenderer/FontCacheManager.cpp | 11 +++++++---- lib/GfxRenderer/FontCacheManager.h | 6 +++++- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/lib/GfxRenderer/FontCacheManager.cpp b/lib/GfxRenderer/FontCacheManager.cpp index f3714ba651..9bb2b9091c 100644 --- a/lib/GfxRenderer/FontCacheManager.cpp +++ b/lib/GfxRenderer/FontCacheManager.cpp @@ -73,12 +73,14 @@ void FontCacheManager::recordText(const char* text, int fontId, EpdFontFamily::S ScanEntry* entry = nullptr; for (auto& e : scanEntries_) { - if (e.fontId == fontId) { + if (e.used && e.fontId == fontId) { entry = &e; break; } - if (e.fontId < 0) { + if (!e.used) { + e.used = true; e.fontId = fontId; + e.text.clear(); // Entry 0 typically accumulates the page body; later entries hold short // furniture strings (status bar, headers). e.text.reserve(&e == &scanEntries_[0] ? 2048 : 256); @@ -96,7 +98,8 @@ void FontCacheManager::recordText(const char* text, int fontId, EpdFontFamily::S void FontCacheManager::resetScanEntries() { for (auto& e : scanEntries_) { - e.fontId = -1; + e.used = false; + e.fontId = 0; e.styleMask = 0; e.text.clear(); e.text.shrink_to_fit(); @@ -116,7 +119,7 @@ void FontCacheManager::PrewarmScope::endScanAndPrewarm() { manager_->scanMode_ = ScanMode::None; for (auto& e : manager_->scanEntries_) { - if (e.fontId < 0 || e.text.empty()) continue; + if (!e.used || e.text.empty()) continue; manager_->prewarmCache(e.fontId, e.text.c_str(), e.styleMask != 0 ? e.styleMask : 1); } manager_->resetScanEntries(); diff --git a/lib/GfxRenderer/FontCacheManager.h b/lib/GfxRenderer/FontCacheManager.h index 4206141dc0..68d6dbb6d8 100644 --- a/lib/GfxRenderer/FontCacheManager.h +++ b/lib/GfxRenderer/FontCacheManager.h @@ -65,7 +65,11 @@ class FontCacheManager { // (they fall back to the per-string prewarm in GfxRenderer). static constexpr uint8_t MAX_SCAN_FONTS = 4; struct ScanEntry { - int fontId = -1; + // Occupancy is tracked separately: font ids are FNV hashes cast to int + // (SdCardFontManager::computeFontId, src/fontIds.h) and are routinely + // negative, so no id value can serve as the "free slot" sentinel. + bool used = false; + int fontId = 0; std::string text; uint8_t styleMask = 0; }; From eef205044f6cdc0898e93e6213c51a082e20d3d2 Mon Sep 17 00:00:00 2001 From: Thiago Kenji Okada Date: Tue, 18 Aug 2026 19:09:01 +0100 Subject: [PATCH 03/77] fix: clear sleep image after wake (#3009) ## Summary * **What is the goal of this PR?** Fix the sleep-screen image remaining visible underneath the Home screen after a splashless wake. * **What changes are included?** * Pass a one-shot clean-initial-refresh option to HomeActivity for splashless wakes without a retained Quick Resume frame. * Use `HALF_REFRESH` for that first Home render instead of issuing a second post-render refresh. * Preserve Quick Resume and normal Home navigation behavior. Fix https://github.com/crosspoint-reader/crosspoint-reader/issues/3002. ## Scope Check - [x] I have read SCOPE.md and ROADMAP.md. - [x] This PR is **not** a new built-in theme. - [x] This PR is **not** a new external network connector. - [x] This PR is **not** an interactive app, writing tool, RSS/news/browser, media playback, or PDF feature. - [x] The stock firmware does not already handle this well, and no other popular CrossPoint fork already does. - [x] This PR does not touch `freeink-sdk/`, `lib/hal/`, the bootloader, OTA, or recovery code. ## Additional Context Tested in Xteink X4: - Custom sleep image + waking up from main menu + Double checked that we are only refreshing once (the first fix I did the screen flashed twice) - Custom sleep image + waking up from book (already working, just checking for regressions) - Quick sleep + waking up from book (not really related to the changes here but just to make sure there were no regressions) --- ### AI Usage Did you use AI tools to help write this code? **YES** --- src/activities/ActivityManager.cpp | 4 ++-- src/activities/ActivityManager.h | 2 +- src/activities/home/HomeActivity.cpp | 3 ++- src/activities/home/HomeActivity.h | 7 +++++-- src/main.cpp | 7 ++++++- 5 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/activities/ActivityManager.cpp b/src/activities/ActivityManager.cpp index dd037230f3..badffa1ff3 100644 --- a/src/activities/ActivityManager.cpp +++ b/src/activities/ActivityManager.cpp @@ -256,7 +256,7 @@ void ActivityManager::goToFullScreenMessage(std::string message, EpdFontFamily:: replaceActivity(std::make_unique(renderer, mappedInput, std::move(message), style)); } -void ActivityManager::goHome(HomeMenuItem initialMenuItem) { +void ActivityManager::goHome(HomeMenuItem initialMenuItem, bool cleanInitialRefresh) { if (initialMenuItem == HomeMenuItem::NONE && currentActivity) { const auto& activityName = currentActivity->name; if (activityName == "FileBrowser") { @@ -271,7 +271,7 @@ void ActivityManager::goHome(HomeMenuItem initialMenuItem) { initialMenuItem = HomeMenuItem::SETTINGS_MENU; } } - replaceActivity(std::make_unique(renderer, mappedInput, initialMenuItem)); + replaceActivity(std::make_unique(renderer, mappedInput, initialMenuItem, cleanInitialRefresh)); } void ActivityManager::goToCrashReport() { replaceActivity(std::make_unique(renderer, mappedInput)); } diff --git a/src/activities/ActivityManager.h b/src/activities/ActivityManager.h index 8d3c49e7f3..bfba4b9068 100644 --- a/src/activities/ActivityManager.h +++ b/src/activities/ActivityManager.h @@ -91,7 +91,7 @@ class ActivityManager { void goToBoot(); void goToFullScreenMessage(std::string message, EpdFontFamily::Style style = EpdFontFamily::REGULAR); void goToCrashReport(); - void goHome(HomeMenuItem initialMenuItem = HomeMenuItem::NONE); + void goHome(HomeMenuItem initialMenuItem = HomeMenuItem::NONE, bool cleanInitialRefresh = false); // This will move current activity to stack instead of deleting it void pushActivity(std::unique_ptr&& activity); diff --git a/src/activities/home/HomeActivity.cpp b/src/activities/home/HomeActivity.cpp index 38123c1a37..228b2e6055 100644 --- a/src/activities/home/HomeActivity.cpp +++ b/src/activities/home/HomeActivity.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -335,7 +336,7 @@ void HomeActivity::render(RenderLock&&) { tr(STR_DIR_DOWN)); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); - renderer.displayBuffer(); + renderer.displayBuffer(cleanInitialRefresh && !firstRenderDone ? HalDisplay::HALF_REFRESH : HalDisplay::FAST_REFRESH); if (!firstRenderDone) { firstRenderDone = true; diff --git a/src/activities/home/HomeActivity.h b/src/activities/home/HomeActivity.h index 1b565b0cdd..4ea540e57f 100644 --- a/src/activities/home/HomeActivity.h +++ b/src/activities/home/HomeActivity.h @@ -29,6 +29,7 @@ class HomeActivity final : public Activity { int coverRectH = 0; std::vector recentBooks; const HomeMenuItem initialMenuItem; + const bool cleanInitialRefresh; // Convert HomeMenuItem to menu index (used in onEnter) static int menuItemToIndex(HomeMenuItem item, bool hasOpdsUrl) { @@ -71,8 +72,10 @@ class HomeActivity final : public Activity { public: explicit HomeActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, - HomeMenuItem initialMenuItemValue = HomeMenuItem::NONE) - : Activity("Home", renderer, mappedInput), initialMenuItem(initialMenuItemValue) {} + HomeMenuItem initialMenuItemValue = HomeMenuItem::NONE, bool cleanInitialRefresh = false) + : Activity("Home", renderer, mappedInput), + initialMenuItem(initialMenuItemValue), + cleanInitialRefresh(cleanInitialRefresh) {} void onEnter() override; void onExit() override; void loop() override; diff --git a/src/main.cpp b/src/main.cpp index 517ebd6ccb..1b94e319d6 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -464,6 +464,7 @@ void setup() { : isSleepWake && !APP_STATE.showBootScreen ? BootResume::SplashlessWake : BootResume::Splash; bool allowFastInitialReaderRefresh = false; + bool needsWakeRefresh = false; setupDisplayAndFonts(resume != BootResume::Splash); @@ -494,6 +495,10 @@ void setup() { } else { renderer.displayBuffer(HalDisplay::HALF_REFRESH); } + } else { + // The first Home/Reader paint is followed by an explicit clean refresh + // because the panel still physically shows the sleep image. + needsWakeRefresh = true; } break; case BootResume::Splash: @@ -523,7 +528,7 @@ void setup() { mappedInputManager.isPressed(MappedInputManager::Button::Back) || APP_STATE.readerActivityLoadCount > 0) { // Boot to home screen if no book is open, last sleep was not from reader, back button is held, or reader activity // crashed (indicated by readerActivityLoadCount > 0) - activityManager.goHome(); + activityManager.goHome(HomeMenuItem::NONE, needsWakeRefresh); } else { // Clear app state to avoid getting into a boot loop if the epub doesn't load const auto path = APP_STATE.openEpubPath; From a7a776a992ad5fa102eb262b28ae0b8ee34d8d8f Mon Sep 17 00:00:00 2001 From: Uri Tauber Date: Wed, 19 Aug 2026 15:27:33 +0300 Subject: [PATCH 04/77] fix: add `'` to the static HTML entity table (#3122) --- lib/Epub/Epub/htmlEntities.cpp | 102 ++++++++++++++++----------------- 1 file changed, 51 insertions(+), 51 deletions(-) diff --git a/lib/Epub/Epub/htmlEntities.cpp b/lib/Epub/Epub/htmlEntities.cpp index a798d0e251..a3b4794062 100644 --- a/lib/Epub/Epub/htmlEntities.cpp +++ b/lib/Epub/Epub/htmlEntities.cpp @@ -13,57 +13,57 @@ struct EntityPair { // Sorted lexicographically by key to allow binary search. static constexpr EntityPair ENTITY_LOOKUP[] = { - {"Æ", "Æ"}, {"Á", "Á"}, {"Â", "Â"}, {"À", "À"}, {"Α", "Α"}, - {"Å", "Å"}, {"Ã", "Ã"}, {"Ä", "Ä"}, {"Β", "Β"}, {"Ç", "Ç"}, - {"Χ", "Χ"}, {"‡", "‡"}, {"Δ", "Δ"}, {"Ð", "Ð"}, {"É", "É"}, - {"Ê", "Ê"}, {"È", "È"}, {"Ε", "Ε"}, {"Η", "Η"}, {"Ë", "Ë"}, - {"Γ", "Γ"}, {"Í", "Í"}, {"Î", "Î"}, {"Ì", "Ì"}, {"Ι", "Ι"}, - {"Ï", "Ï"}, {"Κ", "Κ"}, {"Λ", "Λ"}, {"Μ", "Μ"}, {"Ñ", "Ñ"}, - {"Ν", "Ν"}, {"Œ", "Œ"}, {"Ó", "Ó"}, {"Ô", "Ô"}, {"Ò", "Ò"}, - {"Ω", "Ω"}, {"Ο", "Ο"}, {"Ø", "Ø"}, {"Õ", "Õ"}, {"Ö", "Ö"}, - {"Φ", "Φ"}, {"Π", "Π"}, {"″", "″"}, {"Ψ", "Ψ"}, {"Ρ", "Ρ"}, - {"Š", "Š"}, {"Σ", "Σ"}, {"Þ", "Þ"}, {"Τ", "Τ"}, {"Θ", "Θ"}, - {"Ú", "Ú"}, {"Û", "Û"}, {"Ù", "Ù"}, {"Υ", "Υ"}, {"Ü", "Ü"}, - {"Ξ", "Ξ"}, {"Ý", "Ý"}, {"Ÿ", "Ÿ"}, {"Ζ", "Ζ"}, {"á", "á"}, - {"â", "â"}, {"´", "´"}, {"æ", "æ"}, {"à", "à"}, {"ℵ", "ℵ"}, - {"α", "α"}, {"&", "&"}, {"∧", "∧"}, {"∠", "∠"}, {"å", "å"}, - {"≈", "≈"}, {"ã", "ã"}, {"ä", "ä"}, {"„", "„"}, {"β", "β"}, - {"¦", "¦"}, {"•", "•"}, {"∩", "∩"}, {"ç", "ç"}, {"¸", "¸"}, - {"¢", "¢"}, {"χ", "χ"}, {"ˆ", "ˆ"}, {"♣", "♣"}, {"≅", "≅"}, - {"©", "©"}, {"↵", "↵"}, {"∪", "∪"}, {"¤", "¤"}, {"⇓", "⇓"}, - {"†", "†"}, {"↓", "↓"}, {"°", "°"}, {"δ", "δ"}, {"♦", "♦"}, - {"÷", "÷"}, {"é", "é"}, {"ê", "ê"}, {"è", "è"}, {"∅", "∅"}, - {" ", " "}, {" ", " "}, {"ε", "ε"}, {"≡", "≡"}, {"η", "η"}, - {"ð", "ð"}, {"ë", "ë"}, {"€", "€"}, {"∃", "∃"}, {"ƒ", "ƒ"}, - {"∀", "∀"}, {"½", "½"}, {"¼", "¼"}, {"¾", "¾"}, {"⁄", "⁄"}, - {"γ", "γ"}, {"≥", "≥"}, {">", ">"}, {"⇔", "⇔"}, {"↔", "↔"}, - {"♥", "♥"}, {"…", "…"}, {"í", "í"}, {"î", "î"}, {"¡", "¡"}, - {"ì", "ì"}, {"ℑ", "ℑ"}, {"∞", "∞"}, {"∫", "∫"}, {"ι", "ι"}, - {"¿", "¿"}, {"∈", "∈"}, {"ï", "ï"}, {"κ", "κ"}, {"⇐", "⇐"}, - {"λ", "λ"}, {"⟨", "〈"}, {"«", "«"}, {"←", "←"}, {"⌈", "⌈"}, - {"“", "\u201C"}, {"≤", "≤"}, {"⌊", "⌊"}, {"∗", "∗"}, {"◊", "◊"}, - {"‎", "\u200E"}, {"‹", "‹"}, {"‘", "\u2018"}, {"<", "<"}, {"¯", "¯"}, - {"—", "—"}, {"µ", "µ"}, {"·", "·"}, {"−", "−"}, {"μ", "μ"}, - {"∇", "∇"}, {" ", "\xC2\xA0"}, {"–", "–"}, {"≠", "≠"}, {"∋", "∋"}, - {"¬", "¬"}, {"∉", "∉"}, {"⊄", "⊄"}, {"ñ", "ñ"}, {"ν", "ν"}, - {"ó", "ó"}, {"ô", "ô"}, {"œ", "œ"}, {"ò", "ò"}, {"‾", "‾"}, - {"ω", "ω"}, {"ο", "ο"}, {"⊕", "⊕"}, {"∨", "∨"}, {"ª", "ª"}, - {"º", "º"}, {"ø", "ø"}, {"õ", "õ"}, {"⊗", "⊗"}, {"ö", "ö"}, - {"¶", "¶"}, {"∂", "∂"}, {"‰", "‰"}, {"⊥", "⊥"}, {"φ", "φ"}, - {"π", "π"}, {"ϖ", "ϖ"}, {"±", "±"}, {"£", "£"}, {"′", "′"}, - {"∏", "∏"}, {"∝", "∝"}, {"ψ", "ψ"}, {""", "\""}, {"⇒", "⇒"}, - {"√", "√"}, {"⟩", "〉"}, {"»", "»"}, {"→", "→"}, {"⌉", "⌉"}, - {"”", "\u201D"}, {"ℜ", "\u211C"}, {"®", "®"}, {"⌋", "⌋"}, {"ρ", "ρ"}, - {"‏", "\u200F"}, {"›", "›"}, {"’", "\u2019"}, {"‚", "‚"}, {"š", "š"}, - {"⋅", "⋅"}, {"§", "§"}, {"­", "\xC2\xAD"}, {"σ", "σ"}, {"ς", "ς"}, - {"∼", "∼"}, {"♠", "♠"}, {"⊂", "⊂"}, {"⊆", "⊆"}, {"∑", "∑"}, - {"¹", "¹"}, {"²", "²"}, {"³", "³"}, {"⊃", "⊃"}, {"⊇", "⊇"}, - {"ß", "ß"}, {"τ", "τ"}, {"∴", "∴"}, {"θ", "θ"}, {"ϑ", "ϑ"}, - {" ", " "}, {"þ", "þ"}, {"˜", "˜"}, {"×", "×"}, {"™", "™"}, - {"⇑", "⇑"}, {"ú", "ú"}, {"↑", "↑"}, {"û", "û"}, {"ù", "ù"}, - {"¨", "¨"}, {"ϒ", "ϒ"}, {"υ", "υ"}, {"ü", "ü"}, {"℘", "℘"}, - {"ξ", "ξ"}, {"ý", "ý"}, {"¥", "¥"}, {"ÿ", "ÿ"}, {"ζ", "ζ"}, - {"‍", "\u200D"}, {"‌", "\u200C"}, + {"Æ", "Æ"}, {"Á", "Á"}, {"Â", "Â"}, {"À", "À"}, {"Α", "Α"}, + {"Å", "Å"}, {"Ã", "Ã"}, {"Ä", "Ä"}, {"Β", "Β"}, {"Ç", "Ç"}, + {"Χ", "Χ"}, {"‡", "‡"}, {"Δ", "Δ"}, {"Ð", "Ð"}, {"É", "É"}, + {"Ê", "Ê"}, {"È", "È"}, {"Ε", "Ε"}, {"Η", "Η"}, {"Ë", "Ë"}, + {"Γ", "Γ"}, {"Í", "Í"}, {"Î", "Î"}, {"Ì", "Ì"}, {"Ι", "Ι"}, + {"Ï", "Ï"}, {"Κ", "Κ"}, {"Λ", "Λ"}, {"Μ", "Μ"}, {"Ñ", "Ñ"}, + {"Ν", "Ν"}, {"Œ", "Œ"}, {"Ó", "Ó"}, {"Ô", "Ô"}, {"Ò", "Ò"}, + {"Ω", "Ω"}, {"Ο", "Ο"}, {"Ø", "Ø"}, {"Õ", "Õ"}, {"Ö", "Ö"}, + {"Φ", "Φ"}, {"Π", "Π"}, {"″", "″"}, {"Ψ", "Ψ"}, {"Ρ", "Ρ"}, + {"Š", "Š"}, {"Σ", "Σ"}, {"Þ", "Þ"}, {"Τ", "Τ"}, {"Θ", "Θ"}, + {"Ú", "Ú"}, {"Û", "Û"}, {"Ù", "Ù"}, {"Υ", "Υ"}, {"Ü", "Ü"}, + {"Ξ", "Ξ"}, {"Ý", "Ý"}, {"Ÿ", "Ÿ"}, {"Ζ", "Ζ"}, {"á", "á"}, + {"â", "â"}, {"´", "´"}, {"æ", "æ"}, {"à", "à"}, {"ℵ", "ℵ"}, + {"α", "α"}, {"&", "&"}, {"∧", "∧"}, {"∠", "∠"}, {"'", "'"}, + {"å", "å"}, {"≈", "≈"}, {"ã", "ã"}, {"ä", "ä"}, {"„", "„"}, + {"β", "β"}, {"¦", "¦"}, {"•", "•"}, {"∩", "∩"}, {"ç", "ç"}, + {"¸", "¸"}, {"¢", "¢"}, {"χ", "χ"}, {"ˆ", "ˆ"}, {"♣", "♣"}, + {"≅", "≅"}, {"©", "©"}, {"↵", "↵"}, {"∪", "∪"}, {"¤", "¤"}, + {"⇓", "⇓"}, {"†", "†"}, {"↓", "↓"}, {"°", "°"}, {"δ", "δ"}, + {"♦", "♦"}, {"÷", "÷"}, {"é", "é"}, {"ê", "ê"}, {"è", "è"}, + {"∅", "∅"}, {" ", " "}, {" ", " "}, {"ε", "ε"}, {"≡", "≡"}, + {"η", "η"}, {"ð", "ð"}, {"ë", "ë"}, {"€", "€"}, {"∃", "∃"}, + {"ƒ", "ƒ"}, {"∀", "∀"}, {"½", "½"}, {"¼", "¼"}, {"¾", "¾"}, + {"⁄", "⁄"}, {"γ", "γ"}, {"≥", "≥"}, {">", ">"}, {"⇔", "⇔"}, + {"↔", "↔"}, {"♥", "♥"}, {"…", "…"}, {"í", "í"}, {"î", "î"}, + {"¡", "¡"}, {"ì", "ì"}, {"ℑ", "ℑ"}, {"∞", "∞"}, {"∫", "∫"}, + {"ι", "ι"}, {"¿", "¿"}, {"∈", "∈"}, {"ï", "ï"}, {"κ", "κ"}, + {"⇐", "⇐"}, {"λ", "λ"}, {"⟨", "〈"}, {"«", "«"}, {"←", "←"}, + {"⌈", "⌈"}, {"“", "\u201C"}, {"≤", "≤"}, {"⌊", "⌊"}, {"∗", "∗"}, + {"◊", "◊"}, {"‎", "\u200E"}, {"‹", "‹"}, {"‘", "\u2018"}, {"<", "<"}, + {"¯", "¯"}, {"—", "—"}, {"µ", "µ"}, {"·", "·"}, {"−", "−"}, + {"μ", "μ"}, {"∇", "∇"}, {" ", "\xC2\xA0"}, {"–", "–"}, {"≠", "≠"}, + {"∋", "∋"}, {"¬", "¬"}, {"∉", "∉"}, {"⊄", "⊄"}, {"ñ", "ñ"}, + {"ν", "ν"}, {"ó", "ó"}, {"ô", "ô"}, {"œ", "œ"}, {"ò", "ò"}, + {"‾", "‾"}, {"ω", "ω"}, {"ο", "ο"}, {"⊕", "⊕"}, {"∨", "∨"}, + {"ª", "ª"}, {"º", "º"}, {"ø", "ø"}, {"õ", "õ"}, {"⊗", "⊗"}, + {"ö", "ö"}, {"¶", "¶"}, {"∂", "∂"}, {"‰", "‰"}, {"⊥", "⊥"}, + {"φ", "φ"}, {"π", "π"}, {"ϖ", "ϖ"}, {"±", "±"}, {"£", "£"}, + {"′", "′"}, {"∏", "∏"}, {"∝", "∝"}, {"ψ", "ψ"}, {""", "\""}, + {"⇒", "⇒"}, {"√", "√"}, {"⟩", "〉"}, {"»", "»"}, {"→", "→"}, + {"⌉", "⌉"}, {"”", "\u201D"}, {"ℜ", "\u211C"}, {"®", "®"}, {"⌋", "⌋"}, + {"ρ", "ρ"}, {"‏", "\u200F"}, {"›", "›"}, {"’", "\u2019"}, {"‚", "‚"}, + {"š", "š"}, {"⋅", "⋅"}, {"§", "§"}, {"­", "\xC2\xAD"}, {"σ", "σ"}, + {"ς", "ς"}, {"∼", "∼"}, {"♠", "♠"}, {"⊂", "⊂"}, {"⊆", "⊆"}, + {"∑", "∑"}, {"¹", "¹"}, {"²", "²"}, {"³", "³"}, {"⊃", "⊃"}, + {"⊇", "⊇"}, {"ß", "ß"}, {"τ", "τ"}, {"∴", "∴"}, {"θ", "θ"}, + {"ϑ", "ϑ"}, {" ", " "}, {"þ", "þ"}, {"˜", "˜"}, {"×", "×"}, + {"™", "™"}, {"⇑", "⇑"}, {"ú", "ú"}, {"↑", "↑"}, {"û", "û"}, + {"ù", "ù"}, {"¨", "¨"}, {"ϒ", "ϒ"}, {"υ", "υ"}, {"ü", "ü"}, + {"℘", "℘"}, {"ξ", "ξ"}, {"ý", "ý"}, {"¥", "¥"}, {"ÿ", "ÿ"}, + {"ζ", "ζ"}, {"‍", "\u200D"}, {"‌", "\u200C"}, }; // Verify the table is sorted at compile time. From a4a03c6b4a903eba58fcf1178d0a750f6b6fe924 Mon Sep 17 00:00:00 2001 From: Alexander Matthes Date: Wed, 19 Aug 2026 15:02:04 +0200 Subject: [PATCH 05/77] fix: don't use dithering for non alpha bmp transparent sleep screen (#3119) --- src/activities/boot_sleep/SleepActivity.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/activities/boot_sleep/SleepActivity.cpp b/src/activities/boot_sleep/SleepActivity.cpp index d7b7f5c043..f46145e1b3 100644 --- a/src/activities/boot_sleep/SleepActivity.cpp +++ b/src/activities/boot_sleep/SleepActivity.cpp @@ -666,7 +666,7 @@ bool SleepActivity::renderSleepOverlayFile(HalFile& file, const char* pathForLog if (alphaResult == AlphaOverlayResult::Rendered) return true; if (alphaResult == AlphaOverlayResult::Error) return false; - Bitmap bitmap(file, true); + Bitmap bitmap(file); const auto parseResult = bitmap.parseHeaders(); if (parseResult != BmpReaderError::Ok) { LOG_ERR("SLP", "Invalid sleep overlay BMP %s: %s", pathForLog, Bitmap::errorToString(parseResult)); From d28b9390a2da1db88d916a330bb9a84ddfec0818 Mon Sep 17 00:00:00 2001 From: Hyemin Kang Date: Thu, 20 Aug 2026 00:20:39 +0900 Subject: [PATCH 06/77] feat: strip embedded fonts during EPUB optimization (#3085) --- src/network/html/FilesPage.html | 144 +++++++++++++++++++++++++++++++- 1 file changed, 140 insertions(+), 4 deletions(-) diff --git a/src/network/html/FilesPage.html b/src/network/html/FilesPage.html index 05257b08a3..1f9dd76950 100644 --- a/src/network/html/FilesPage.html +++ b/src/network/html/FilesPage.html @@ -4124,12 +4124,79 @@

🖼️ Preview

return t; } +// ===== Embedded font removal ===== +// CrossPoint renders with built-in / SD-card fonts and never loads fonts +// embedded in an EPUB, so they are dead weight (often 100KB-2MB per book). +const FONT_EXT_REGEX = /\.(ttf|otf|woff2?|eot)$/; +const FONT_MEDIA_TYPE_REGEX = /^(font\/|application\/font-|application\/x-font-|application\/vnd\.ms-(opentype|fontobject)$)/i; + +// Returns a Set of zip-root-relative paths of embedded font files, detected by +// file extension and by OPF manifest media-type (catches odd extensions). +function collectFontPaths(zip, opfContent, opfPath) { + const fontPaths = new Set(); + zip.forEach(p => { if (FONT_EXT_REGEX.test(p.toLowerCase())) fontPaths.add(p); }); + + if (opfContent && opfPath) { + try { + const doc = new DOMParser().parseFromString(opfContent, 'application/xml'); + if (!doc.querySelector('parsererror')) { + for (const item of [...doc.getElementsByTagNameNS('*', 'item')]) { + const type = item.getAttribute('media-type') || ''; + const href = item.getAttribute('href') || ''; + if (href && FONT_MEDIA_TYPE_REGEX.test(type)) { + const resolved = resolvePath(opfPath, decodeHref(href)); + if (zip.files[resolved]) fontPaths.add(resolved); + } + } + } + } catch (e) { + // Extension-based detection already covers the common cases + } + } + return fontPaths; +} + +// Strips all @font-face rules from CSS. Rules can also embed fonts directly as +// multi-hundred-KB data: URIs, so this is a size win even without font files. +function stripFontFaceRules(css) { + let count = 0; + const stripped = css.replace(/@font-face(?:\s|\/\*[\s\S]*?\*\/)*\{[^{}]*\}/gi, () => { count++; return ''; }); + return { css: stripped, count }; +} + +// Removes font-obfuscation entries from META-INF/encryption.xml. +// Returns { dropFile: true } when nothing meaningful remains, { modified, xml } +// when some entries were removed, or { modified: false } to keep the file as-is. +function stripFontEncryptionEntries(xmlText, fontPaths) { + try { + const doc = new DOMParser().parseFromString(xmlText, 'application/xml'); + if (doc.querySelector('parsererror')) return { modified: false }; + const entries = [...doc.getElementsByTagNameNS('*', 'EncryptedData')]; + let removed = 0; + for (const entry of entries) { + const ref = entry.getElementsByTagNameNS('*', 'CipherReference')[0]; + const uri = ref ? decodeHref(ref.getAttribute('URI') || '') : ''; + if (fontPaths.has(uri.replace(/^\//, ''))) { + entry.parentNode.removeChild(entry); + removed++; + } + } + if (!removed) return { modified: false }; + const remaining = doc.getElementsByTagNameNS('*', 'EncryptedData').length + + doc.getElementsByTagNameNS('*', 'EncryptedKey').length; + if (remaining === 0) return { dropFile: true }; + return { modified: true, xml: safeSerialize(doc, xmlText) }; + } catch (e) { + return { modified: false }; + } +} + /** * Fix OPF content: fix media-types, strip svg properties, - * update split image manifest entries, ensure cover meta. - * DOMParser with regex fallback. + * update split image manifest entries, remove stripped font + * entries, ensure cover meta. DOMParser with regex fallback. */ -function fixOPF(opfText, opfOriginal, opfDir, splitImages = {}) { +function fixOPF(opfText, opfOriginal, opfDir, splitImages = {}, fontPaths = null) { let t = opfText; try { @@ -4159,6 +4226,15 @@

🖼️ Preview

} } + // Remove manifest entries for stripped embedded fonts + if (fontPaths && fontPaths.size) { + for (const item of items) { + const href = decodeHref(item.getAttribute('href') || ''); + const full = resolvePath((opfDir ? opfDir + '/' : '') + 'x', href); + if (fontPaths.has(full)) item.parentNode.removeChild(item); + } + } + // Update split image hrefs and add manifest entries for parts for (const [splitKey, splitInfo] of Object.entries(splitImages)) { const parts = splitInfo.parts || splitInfo; @@ -4194,6 +4270,13 @@

🖼️ Preview

t = t.replace(/(<(?:\w+:)?item\b[^>]*href="[^"]+\.jpg"[^>]*)media-type="image\/(png|gif|webp|bmp)"/g, '$1media-type="image/jpeg"'); t = t.replace(/(<(?:\w+:)?item\b[^>]*)media-type="image\/(png|gif|webp|bmp)"([^>]*href="[^"]+\.jpg")/g, '$1media-type="image/jpeg"$3'); t = t.replace(/\s+svg(?=["'\s>])/g, ''); + if (fontPaths && fontPaths.size) { + for (const p of fontPaths) { + const href = opfDir && p.startsWith(opfDir + '/') ? p.substring(opfDir.length + 1) : p; + const itemRegex = new RegExp(`<(?:\\w+:)?item\\b[^>]*href=["']${escapeRegex(href)}["'][^>]*\\/?>\\s*`, 'gi'); + t = t.replace(itemRegex, ''); + } + } for (const [splitKey, splitInfo] of Object.entries(splitImages)) { const parts = splitInfo.parts || splitInfo; let origHref = opfDir && splitKey.startsWith(opfDir + '/') ? splitKey.substring(opfDir.length + 1) : splitKey; @@ -5078,6 +5161,15 @@

🖼️ Preview

if (progressCallback) progressCallback((i / entries.length) * 60); } + // Embedded fonts are dropped entirely — CrossPoint never renders with them. + // Counters cover font files, @font-face rules (incl. data: URI fonts), and + // encryption.xml shrinkage; they count uncompressed content bytes removed + // (the final archive-size delta is reported separately by logSummary). + const fontPaths = collectFontPaths(zip, opfContent, opfPath); + let removedFontCount = 0; + let removedFontBytes = 0; + let removedFontFaceRules = 0; + // Second pass: update XHTML using DOMParser for (const [xhtmlPath, content] of Object.entries(xhtmlFiles)) { if (operationCancelled) throw new Error('Cancelled by user'); @@ -5236,6 +5328,17 @@

🖼️ Preview

console.warn('DOMParser error for', xhtmlPath, e.message); } + // Strip @font-face rules from inline