From 867bd34798f4e9844b57162d983152696274e1bc Mon Sep 17 00:00:00 2001 From: jpirnay Date: Thu, 30 Apr 2026 12:08:30 +0200 Subject: [PATCH] A couple of fixes --- lib/Epub/Epub/ParsedText.cpp | 66 +++++++++++++++++++ lib/Epub/Epub/Section.cpp | 24 ++++--- lib/Epub/Epub/Section.h | 7 +- .../Epub/parsers/ChapterHtmlSlimParser.cpp | 2 +- lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h | 20 +++--- src/CrossPointSettings.h | 2 + src/JsonSettingsIO.cpp | 2 +- src/RecentBooksStore.cpp | 2 +- src/RecentBooksStore.h | 3 +- src/SettingsList.h | 2 + src/activities/reader/EpubReaderActivity.cpp | 22 ++++--- 11 files changed, 118 insertions(+), 34 deletions(-) diff --git a/lib/Epub/Epub/ParsedText.cpp b/lib/Epub/Epub/ParsedText.cpp index c13a8e99..283ddafc 100644 --- a/lib/Epub/Epub/ParsedText.cpp +++ b/lib/Epub/Epub/ParsedText.cpp @@ -149,6 +149,9 @@ void ParsedText::layoutAndExtractLines( // Apply fixed transforms before any per-line layout work. applyParagraphIndent(); + if (bionicReadingEnabled) { + applyBionicReadingTransform(); + } // Ensure SD card font glyph metrics are loaded before measuring word widths. // For flash-based fonts isSdCardFont() returns false and this block is skipped @@ -506,6 +509,69 @@ void ParsedText::applyParagraphIndent() { } } +void ParsedText::applyBionicReadingTransform() { + if (words.empty()) { + return; + } + + std::vector transformedWords; + std::vector transformedStyles; + std::vector transformedContinues; + transformedWords.reserve(words.size() * 2); + transformedStyles.reserve(wordStyles.size() * 2); + transformedContinues.reserve(wordContinues.size() * 2); + + for (size_t i = 0; i < words.size(); ++i) { + const std::string& word = words[i]; + const auto originalStyle = wordStyles[i]; + const bool attachToPrevious = wordContinues[i]; + + const unsigned char* ptr = reinterpret_cast(word.c_str()); + int codepointCount = 0; + while (utf8NextCodepoint(&ptr)) { + codepointCount++; + } + + if (codepointCount <= 3) { + transformedWords.push_back(word); + transformedStyles.push_back(originalStyle); + transformedContinues.push_back(attachToPrevious); + continue; + } + + const int boldPrefixCount = std::max(1, (codepointCount + 1) / 2); + ptr = reinterpret_cast(word.c_str()); + const unsigned char* prefixEnd = ptr; + for (int j = 0; j < boldPrefixCount && *prefixEnd; ++j) { + utf8NextCodepoint(&prefixEnd); + } + const size_t prefixByteCount = + static_cast(prefixEnd - reinterpret_cast(word.c_str())); + if (prefixByteCount >= word.size()) { + transformedWords.push_back(word); + transformedStyles.push_back(originalStyle); + transformedContinues.push_back(attachToPrevious); + continue; + } + + const std::string prefix(word.data(), prefixByteCount); + const std::string suffix(word.data() + prefixByteCount, word.size() - prefixByteCount); + const auto boldStyle = static_cast(originalStyle | EpdFontFamily::BOLD); + + transformedWords.push_back(prefix); + transformedStyles.push_back(boldStyle); + transformedContinues.push_back(attachToPrevious); + + transformedWords.push_back(suffix); + transformedStyles.push_back(originalStyle); + transformedContinues.push_back(true); + } + + words = std::move(transformedWords); + wordStyles = std::move(transformedStyles); + wordContinues = std::move(transformedContinues); +} + // Builds break indices while opportunistically splitting the word that would overflow the current line. std::vector ParsedText::computeHyphenatedLineBreaks(const GfxRenderer& renderer, const int fontId, const int pageWidth, std::vector& wordWidths, diff --git a/lib/Epub/Epub/Section.cpp b/lib/Epub/Epub/Section.cpp index d6cdc424..3edb1e70 100644 --- a/lib/Epub/Epub/Section.cpp +++ b/lib/Epub/Epub/Section.cpp @@ -12,7 +12,7 @@ #include "parsers/ChapterHtmlSlimParser.h" namespace { -constexpr uint8_t SECTION_FILE_VERSION = 23; +constexpr uint8_t SECTION_FILE_VERSION = 24; constexpr uint32_t HEADER_SIZE = sizeof(uint8_t) + // SECTION_FILE_VERSION sizeof(int) + // fontId sizeof(float) + // lineCompression @@ -23,6 +23,7 @@ constexpr uint32_t HEADER_SIZE = sizeof(uint8_t) + // SECTION_FILE_VERSION sizeof(uint16_t) + // pageCount (stored as 16-bit in header) sizeof(bool) + // hyphenationEnabled sizeof(bool) + // embeddedStyle + sizeof(bool) + // bionicReadingEnabled sizeof(uint8_t) + // imageRendering sizeof(uint32_t) + // page LUT offset sizeof(uint32_t) + // anchor map offset @@ -55,7 +56,8 @@ uint32_t Section::onPageComplete(std::unique_ptr page) { void Section::writeSectionFileHeader(const int fontId, const float lineCompression, const bool extraParagraphSpacing, const uint8_t paragraphAlignment, const uint16_t viewportWidth, const uint16_t viewportHeight, const bool hyphenationEnabled, - const bool embeddedStyle, const uint8_t imageRendering) { + const bool embeddedStyle, const bool bionicReadingEnabled, + const uint8_t imageRendering) { if (!file) { LOG_DBG("SCT", "File not open for writing header"); return; @@ -63,8 +65,8 @@ void Section::writeSectionFileHeader(const int fontId, const float lineCompressi static_assert(HEADER_SIZE == sizeof(SECTION_FILE_VERSION) + sizeof(fontId) + sizeof(lineCompression) + sizeof(extraParagraphSpacing) + sizeof(paragraphAlignment) + sizeof(viewportWidth) + sizeof(viewportHeight) + sizeof(pageCount) + sizeof(hyphenationEnabled) + - sizeof(embeddedStyle) + sizeof(imageRendering) + sizeof(uint32_t) + - sizeof(uint32_t) + sizeof(uint32_t), + sizeof(embeddedStyle) + sizeof(bionicReadingEnabled) + sizeof(imageRendering) + + sizeof(uint32_t) + sizeof(uint32_t) + sizeof(uint32_t), "Header size mismatch"); serialization::writePod(file, SECTION_FILE_VERSION); serialization::writePod(file, fontId); @@ -75,6 +77,7 @@ void Section::writeSectionFileHeader(const int fontId, const float lineCompressi serialization::writePod(file, viewportHeight); serialization::writePod(file, hyphenationEnabled); serialization::writePod(file, embeddedStyle); + serialization::writePod(file, bionicReadingEnabled); serialization::writePod(file, imageRendering); serialization::writePod(file, pageCount); // Placeholder for page count (will be initially 0, patched later) serialization::writePod(file, static_cast(0)); // Placeholder for LUT offset (patched later) @@ -85,7 +88,7 @@ void Section::writeSectionFileHeader(const int fontId, const float lineCompressi bool Section::loadSectionFile(const int fontId, const float lineCompression, const bool extraParagraphSpacing, const uint8_t paragraphAlignment, const uint16_t viewportWidth, const uint16_t viewportHeight, const bool hyphenationEnabled, const bool embeddedStyle, - const uint8_t imageRendering) { + const bool bionicReadingEnabled, const uint8_t imageRendering) { if (!Storage.openFileForRead("SCT", filePath, file)) { return false; } @@ -107,6 +110,7 @@ bool Section::loadSectionFile(const int fontId, const float lineCompression, con uint8_t fileParagraphAlignment; bool fileHyphenationEnabled; bool fileEmbeddedStyle; + bool fileBionicReadingEnabled; uint8_t fileImageRendering; serialization::readPod(file, fileFontId); serialization::readPod(file, fileLineCompression); @@ -116,13 +120,14 @@ bool Section::loadSectionFile(const int fontId, const float lineCompression, con serialization::readPod(file, fileViewportHeight); serialization::readPod(file, fileHyphenationEnabled); serialization::readPod(file, fileEmbeddedStyle); + serialization::readPod(file, fileBionicReadingEnabled); serialization::readPod(file, fileImageRendering); if (fontId != fileFontId || lineCompression != fileLineCompression || extraParagraphSpacing != fileExtraParagraphSpacing || paragraphAlignment != fileParagraphAlignment || viewportWidth != fileViewportWidth || viewportHeight != fileViewportHeight || hyphenationEnabled != fileHyphenationEnabled || embeddedStyle != fileEmbeddedStyle || - imageRendering != fileImageRendering) { + bionicReadingEnabled != fileBionicReadingEnabled || imageRendering != fileImageRendering) { LOG_ERR("SCT", "Deserialization failed: Parameters do not match"); clearCache(); // closes file before removal return false; @@ -188,7 +193,8 @@ bool Section::clearCache() { bool Section::createSectionFile(const int fontId, const float lineCompression, const bool extraParagraphSpacing, const uint8_t paragraphAlignment, const uint16_t viewportWidth, const uint16_t viewportHeight, const bool hyphenationEnabled, const bool embeddedStyle, - const uint8_t imageRendering, const std::function& progressFn) { + const bool bionicReadingEnabled, const uint8_t imageRendering, + const std::function& progressFn) { const uint32_t phaseTotalStart = millis(); const auto localPath = epub->getSpineItem(spineIndex).href; @@ -210,7 +216,7 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c return false; } writeSectionFileHeader(fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth, - viewportHeight, hyphenationEnabled, embeddedStyle, imageRendering); + viewportHeight, hyphenationEnabled, embeddedStyle, bionicReadingEnabled, imageRendering); std::vector lut = {}; // Derive the content base directory and image cache path prefix for the parser @@ -243,7 +249,7 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c ChapterHtmlSlimParser visitor( epub, renderer, fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth, viewportHeight, - hyphenationEnabled, + hyphenationEnabled, bionicReadingEnabled, [this, &lut](std::unique_ptr page) { lut.emplace_back(this->onPageComplete(std::move(page))); }, embeddedStyle, contentBase, imageBasePath, imageRendering, std::move(tocAnchors), progressFn, cssParser); Hyphenator::setPreferredLanguage(epub->getLanguage()); diff --git a/lib/Epub/Epub/Section.h b/lib/Epub/Epub/Section.h index d2c85b4c..e4bdab5f 100644 --- a/lib/Epub/Epub/Section.h +++ b/lib/Epub/Epub/Section.h @@ -20,7 +20,7 @@ class Section { void writeSectionFileHeader(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment, uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled, - bool embeddedStyle, uint8_t imageRendering); + bool embeddedStyle, bool bionicReadingEnabled, uint8_t imageRendering); uint32_t onPageComplete(std::unique_ptr page); struct TocBoundary { @@ -50,11 +50,12 @@ class Section { ~Section() = default; bool loadSectionFile(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment, uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled, bool embeddedStyle, - uint8_t imageRendering); + bool bionicReadingEnabled, uint8_t imageRendering); bool clearCache(); bool createSectionFile(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment, uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled, bool embeddedStyle, - uint8_t imageRendering, const std::function& progressFn = nullptr); + bool bionicReadingEnabled, uint8_t imageRendering, + const std::function& progressFn = nullptr); std::unique_ptr loadPageFromSectionFile(); // Given a page in this section, return the TOC index for that page. diff --git a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp index 6925d0aa..bfa5ad9a 100644 --- a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp +++ b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp @@ -255,7 +255,7 @@ void ChapterHtmlSlimParser::startNewTextBlock(const BlockStyle& blockStyle) { anchorData.push_back({std::move(pendingAnchorId), static_cast(completedPageCount)}); pendingAnchorId.clear(); } - currentTextBlock.reset(new ParsedText(extraParagraphSpacing, hyphenationEnabled, blockStyle)); + currentTextBlock.reset(new ParsedText(extraParagraphSpacing, hyphenationEnabled, blockStyle, bionicReadingEnabled)); wordsExtractedInBlock = 0; } diff --git a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h index 875ecfcc..cb2df47a 100644 --- a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h +++ b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h @@ -128,6 +128,7 @@ class ChapterHtmlSlimParser final : public Print { int currentFootnoteLinkTextLen = 0; std::vector> pendingFootnotes; // int wordsExtractedInBlock = 0; + bool bionicReadingEnabled = false; // Per-chapter caches: resolveStyle and parseInlineStyle are called for every HTML element; // caching by (tag|classAttr) and styleAttr avoids repeated string operations and hash lookups. @@ -149,16 +150,14 @@ class ChapterHtmlSlimParser final : public Print { static void XMLCALL endElement(void* userData, const XML_Char* name); public: - explicit ChapterHtmlSlimParser(std::shared_ptr epub, GfxRenderer& renderer, const int fontId, - const float lineCompression, const bool extraParagraphSpacing, - const uint8_t paragraphAlignment, const uint16_t viewportWidth, - const uint16_t viewportHeight, const bool hyphenationEnabled, - const std::function)>& completePageFn, - const bool embeddedStyle, const std::string& contentBase, - const std::string& imageBasePath, const uint8_t imageRendering = 0, - std::vector tocAnchors = {}, - const std::function& progressFn = nullptr, - const CssParser* cssParser = nullptr) + explicit ChapterHtmlSlimParser( + std::shared_ptr epub, GfxRenderer& renderer, const int fontId, const float lineCompression, + const bool extraParagraphSpacing, const uint8_t paragraphAlignment, const uint16_t viewportWidth, + const uint16_t viewportHeight, const bool hyphenationEnabled, const bool bionicReadingEnabled, + const std::function)>& completePageFn, const bool embeddedStyle, + const std::string& contentBase, const std::string& imageBasePath, const uint8_t imageRendering = 0, + std::vector tocAnchors = {}, const std::function& progressFn = nullptr, + const CssParser* cssParser = nullptr) : epub(epub), renderer(renderer), @@ -169,6 +168,7 @@ class ChapterHtmlSlimParser final : public Print { viewportWidth(viewportWidth), viewportHeight(viewportHeight), hyphenationEnabled(hyphenationEnabled), + bionicReadingEnabled(bionicReadingEnabled), completePageFn(completePageFn), progressFn(progressFn), cssParser(cssParser), diff --git a/src/CrossPointSettings.h b/src/CrossPointSettings.h index 4f1cd376..4daddaf5 100644 --- a/src/CrossPointSettings.h +++ b/src/CrossPointSettings.h @@ -249,6 +249,8 @@ class CrossPointSettings { uint8_t imageDithering = IMAGE_DITHER_BAYER; // Enable synthetic TOC fallback for malformed/sparse TOC books (1 = enabled, 0 = disabled) uint8_t syntheticTocFallback = 1; + // Default bionic reading in EPUB pages when no per-book override is set (1 = enabled, 0 = disabled) + uint8_t bionicReading = 0; // Automatically push reading progress to the KOReader sync server when leaving the reader // (1 = enabled, 0 = disabled). The push only fires when credentials are configured and the // reader session advanced at least 3 pages, and is skipped when remote progress is already ahead. diff --git a/src/JsonSettingsIO.cpp b/src/JsonSettingsIO.cpp index d0f586b4..d743c016 100644 --- a/src/JsonSettingsIO.cpp +++ b/src/JsonSettingsIO.cpp @@ -456,7 +456,7 @@ bool JsonSettingsIO::loadRecentBooks(RecentBooksStore& store, const char* json) book.fontFamilyOverride = clampInt8(obj["fontFamilyOverride"] | -1, -1, CrossPointSettings::FONT_FAMILY_COUNT - 1, -1); book.fontSizeOverride = clampInt8(obj["fontSizeOverride"] | -1, -1, CrossPointSettings::FONT_SIZE_COUNT - 1, -1); - book.bionicReadingOverride = obj["bionicReadingOverride"] | false; + book.bionicReadingOverride = clampInt8(obj["bionicReadingOverride"] | -1, -1, 1, -1); store.recentBooks.push_back(book); } diff --git a/src/RecentBooksStore.cpp b/src/RecentBooksStore.cpp index 5891234c..ab21fba0 100644 --- a/src/RecentBooksStore.cpp +++ b/src/RecentBooksStore.cpp @@ -26,7 +26,7 @@ void RecentBooksStore::addBook(const std::string& path, const std::string& title int8_t imageRenderingOverride = -1; int8_t fontFamilyOverride = -1; int8_t fontSizeOverride = -1; - bool bionicReadingOverride = false; + int8_t bionicReadingOverride = -1; // Remove existing entry if present auto it = diff --git a/src/RecentBooksStore.h b/src/RecentBooksStore.h index aa381153..53652c42 100644 --- a/src/RecentBooksStore.h +++ b/src/RecentBooksStore.h @@ -17,7 +17,8 @@ struct RecentBook { int8_t fontFamilyOverride = -1; // -1 = use global setting, otherwise CrossPointSettings::FONT_SIZE value. int8_t fontSizeOverride = -1; - bool bionicReadingOverride = false; + // -1 = use global default, otherwise explicit per-book override (0 = off, 1 = on). + int8_t bionicReadingOverride = -1; bool operator==(const RecentBook& other) const { return path == other.path; } }; diff --git a/src/SettingsList.h b/src/SettingsList.h index 1f7149ec..f57fe0f6 100644 --- a/src/SettingsList.h +++ b/src/SettingsList.h @@ -117,6 +117,8 @@ inline const std::vector list = { StrId::STR_CAT_READER), SettingInfo::Toggle(StrId::STR_HYPHENATION, &CrossPointSettings::hyphenationEnabled, "hyphenationEnabled", StrId::STR_CAT_READER), + SettingInfo::Toggle(StrId::STR_BIONIC_READING, &CrossPointSettings::bionicReading, "bionicReading", + StrId::STR_CAT_READER), SettingInfo::Enum(StrId::STR_IMAGES, &CrossPointSettings::imageRendering, {StrId::STR_IMAGES_DISPLAY, StrId::STR_IMAGES_PLACEHOLDER, StrId::STR_IMAGES_SUPPRESS}, "imageRendering", StrId::STR_CAT_READER), diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index 490dab4f..9e4e1744 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -191,7 +191,9 @@ void EpubReaderActivity::onEnter() { bookImageRenderingOverride = currentBook.imageRenderingOverride; bookFontFamilyOverride = currentBook.fontFamilyOverride; bookFontSizeOverride = currentBook.fontSizeOverride; - bookBionicReadingOverride = currentBook.bionicReadingOverride; + bookBionicReadingOverride = (currentBook.bionicReadingOverride >= 0) + ? static_cast(currentBook.bionicReadingOverride) + : static_cast(SETTINGS.bionicReading); logReaderMemSnapshot("onEnter_after_recent_books"); // Trigger first update @@ -1254,7 +1256,8 @@ void EpubReaderActivity::render(RenderLock&& lock) { if (!section->loadSectionFile(getEffectiveReaderFontId(), getEffectiveReaderLineCompression(), SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth, - viewportHeight, SETTINGS.hyphenationEnabled, embeddedStyle, imageRendering)) { + viewportHeight, SETTINGS.hyphenationEnabled, embeddedStyle, bookBionicReadingOverride, + imageRendering)) { LOG_DBG("ERS", "Cache not found, building..."); lastRenderStats.cacheRebuilt = true; @@ -1275,8 +1278,8 @@ void EpubReaderActivity::render(RenderLock&& lock) { renderer.clearSdCardFontAccumulation(); if (!section->createSectionFile(getEffectiveReaderFontId(), getEffectiveReaderLineCompression(), SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth, - viewportHeight, SETTINGS.hyphenationEnabled, embeddedStyle, imageRendering, - progressFn)) { + viewportHeight, SETTINGS.hyphenationEnabled, embeddedStyle, + bookBionicReadingOverride, imageRendering, progressFn)) { LOG_ERR("ERS", "Failed to persist page data to SD"); section.reset(); return; @@ -1430,7 +1433,8 @@ void EpubReaderActivity::silentIndexNextChapterIfNeeded(const uint16_t viewportW Section nextSection(epub, nextSpineIndex, renderer); if (nextSection.loadSectionFile(getEffectiveReaderFontId(), getEffectiveReaderLineCompression(), SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth, - viewportHeight, SETTINGS.hyphenationEnabled, embeddedStyle, imageRendering)) { + viewportHeight, SETTINGS.hyphenationEnabled, embeddedStyle, bookBionicReadingOverride, + imageRendering)) { return; } @@ -1439,7 +1443,8 @@ void EpubReaderActivity::silentIndexNextChapterIfNeeded(const uint16_t viewportW renderer.clearSdCardFontAccumulation(); if (!nextSection.createSectionFile(getEffectiveReaderFontId(), getEffectiveReaderLineCompression(), SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth, - viewportHeight, SETTINGS.hyphenationEnabled, embeddedStyle, imageRendering)) { + viewportHeight, SETTINGS.hyphenationEnabled, embeddedStyle, + bookBionicReadingOverride, imageRendering)) { LOG_ERR("ERS", "Failed silent indexing for chapter: %d", nextSpineIndex); } } @@ -1822,12 +1827,13 @@ bool EpubReaderActivity::drawCurrentPageToBuffer(const std::string& filePath, Gf if (!section->loadSectionFile(getEffectiveFontId(effectiveFontFamily, effectiveFontSize), effectiveLineCompression, SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth, viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle, - SETTINGS.imageRendering)) { + static_cast(SETTINGS.bionicReading), SETTINGS.imageRendering)) { LOG_DBG("SLP", "EPUB: section cache not found for spine %d, rebuilding", spineIndex); if (!section->createSectionFile(getEffectiveFontId(effectiveFontFamily, effectiveFontSize), effectiveLineCompression, SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth, viewportHeight, - SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle, SETTINGS.imageRendering)) { + SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle, + static_cast(SETTINGS.bionicReading), SETTINGS.imageRendering)) { LOG_ERR("SLP", "EPUB: failed to rebuild section cache for spine %d", spineIndex); return false; }