diff --git a/lib/Epub/Epub.cpp b/lib/Epub/Epub.cpp index cb0b1801..68255df4 100644 --- a/lib/Epub/Epub.cpp +++ b/lib/Epub/Epub.cpp @@ -7,11 +7,13 @@ #include #include +#include "CrossPointSettings.h" #include "Epub/parsers/ContainerParser.h" #include "Epub/parsers/ContentOpfParser.h" #include "Epub/parsers/TocNavParser.h" #include "Epub/parsers/TocNcxParser.h" + bool Epub::findContentOpfFile(std::string* contentOpfFile) const { const auto containerPath = "META-INF/container.xml"; size_t containerSize; @@ -333,6 +335,7 @@ void Epub::parseCssFiles() const { // load in the meta data for the epub file bool Epub::load(const bool buildIfMissing, const bool skipLoadingCss) { LOG_DBG("EBP", "Loading ePub: %s", filepath.c_str()); + tocReliabilityState = -1; // Initialize spine/TOC cache bookMetadataCache.reset(new BookMetadataCache(cachePath)); @@ -767,6 +770,18 @@ BookMetadataCache::TocEntry Epub::getTocItem(const int tocIndex) const { return {}; } + if (SETTINGS.syntheticTocFallback && !hasReliableToc()) { + const int spineCount = bookMetadataCache->getSpineCount(); + if (tocIndex < 0 || tocIndex >= spineCount) { + LOG_DBG("EBP", "getTocItem synthetic index:%d is out of range", tocIndex); + return {}; + } + + const auto spine = bookMetadataCache->getSpineEntry(tocIndex); + return BookMetadataCache::TocEntry("Section " + std::to_string(tocIndex + 1), spine.href, "", 1, + static_cast(tocIndex)); + } + if (tocIndex < 0 || tocIndex >= bookMetadataCache->getTocCount()) { LOG_DBG("EBP", "getTocItem index:%d is out of range", tocIndex); return {}; @@ -780,6 +795,10 @@ int Epub::getTocItemsCount() const { return 0; } + if (SETTINGS.syntheticTocFallback && !hasReliableToc()) { + return bookMetadataCache->getSpineCount(); + } + return bookMetadataCache->getTocCount(); } @@ -790,6 +809,14 @@ int Epub::getSpineIndexForTocIndex(const int tocIndex) const { return 0; } + if (SETTINGS.syntheticTocFallback && !hasReliableToc()) { + if (tocIndex < 0 || tocIndex >= bookMetadataCache->getSpineCount()) { + LOG_ERR("EBP", "getSpineIndexForTocIndex synthetic tocIndex %d out of range", tocIndex); + return 0; + } + return tocIndex; + } + if (tocIndex < 0 || tocIndex >= bookMetadataCache->getTocCount()) { LOG_ERR("EBP", "getSpineIndexForTocIndex: tocIndex %d out of range", tocIndex); return 0; @@ -804,7 +831,66 @@ int Epub::getSpineIndexForTocIndex(const int tocIndex) const { return spineIndex; } -int Epub::getTocIndexForSpineIndex(const int spineIndex) const { return getSpineItem(spineIndex).tocIndex; } +bool Epub::hasReliableToc() const { + if (tocReliabilityState != -1) { + return tocReliabilityState == 1; + } + + if (!bookMetadataCache || !bookMetadataCache->isLoaded()) { + tocReliabilityState = 0; + return false; + } + + const int spineCount = bookMetadataCache->getSpineCount(); + const int tocCount = bookMetadataCache->getTocCount(); + + if (spineCount <= 0 || tocCount <= 0) { + tocReliabilityState = 0; + return false; + } + + // If a larger book only exposes one TOC entry, treat TOC as unusable for chapter UX. + if (spineCount >= 8 && tocCount <= 1) { + tocReliabilityState = 0; + return false; + } + + std::vector spineReferenced(static_cast(spineCount), false); + int distinctSpinesReferenced = 0; + for (int i = 0; i < tocCount; i++) { + const auto toc = bookMetadataCache->getTocEntry(i); + if (toc.spineIndex >= 0 && toc.spineIndex < spineCount) { + const size_t idx = static_cast(toc.spineIndex); + if (!spineReferenced[idx]) { + spineReferenced[idx] = true; + distinctSpinesReferenced++; + } + } + } + + // Require at least 25% spine coverage from TOC references. + const bool reliable = (distinctSpinesReferenced * 4 >= spineCount); + tocReliabilityState = reliable ? 1 : 0; + return reliable; +} + +int Epub::getTocIndexForSpineIndex(const int spineIndex) const { + if (!bookMetadataCache || !bookMetadataCache->isLoaded()) { + LOG_ERR("EBP", "getTocIndexForSpineIndex called but cache not loaded"); + return 0; + } + + if (spineIndex < 0 || spineIndex >= bookMetadataCache->getSpineCount()) { + LOG_ERR("EBP", "getTocIndexForSpineIndex: spineIndex %d out of range", spineIndex); + return 0; + } + + if (SETTINGS.syntheticTocFallback && !hasReliableToc()) { + return spineIndex; + } + + return bookMetadataCache->getSpineEntry(spineIndex).tocIndex; +} size_t Epub::getBookSize() const { if (!bookMetadataCache || !bookMetadataCache->isLoaded() || bookMetadataCache->getSpineCount() == 0) { diff --git a/lib/Epub/Epub.h b/lib/Epub/Epub.h index 9ffa8d37..943eaeb5 100644 --- a/lib/Epub/Epub.h +++ b/lib/Epub/Epub.h @@ -29,6 +29,8 @@ class Epub { std::unique_ptr cssParser; // CSS files std::vector cssFiles; + // -1 unknown, 0 unreliable, 1 reliable + mutable int tocReliabilityState = -1; bool findContentOpfFile(std::string* contentOpfFile) const; bool parseContentOpf(BookMetadataCache::BookMetadata& bookMetadata); @@ -66,6 +68,7 @@ class Epub { int getTocItemsCount() const; int getSpineIndexForTocIndex(int tocIndex) const; int getTocIndexForSpineIndex(int spineIndex) const; + bool hasReliableToc() const; size_t getCumulativeSpineItemSize(int spineIndex) const; int getSpineIndexForTextReference() const; diff --git a/lib/I18n/translations/english.yaml b/lib/I18n/translations/english.yaml index df91c1a1..3204b62e 100644 --- a/lib/I18n/translations/english.yaml +++ b/lib/I18n/translations/english.yaml @@ -70,6 +70,7 @@ STR_IMAGES: "Images" STR_IMAGES_DISPLAY: "Display" STR_IMAGES_PLACEHOLDER: "Placeholder" STR_IMAGES_SUPPRESS: "Suppress" +STR_CREATE_FALLBACK_FOR_INVALID_TOC: "Create fallback for invalid TOC" STR_SHORT_PWR_BTN: "Short Power Button Click" STR_ORIENTATION: "Reading Orientation" STR_SIDE_BTN_LAYOUT: "Side Button Layout (reader)" diff --git a/src/CrossPointSettings.h b/src/CrossPointSettings.h index 9a0e298b..c3881589 100644 --- a/src/CrossPointSettings.h +++ b/src/CrossPointSettings.h @@ -199,6 +199,8 @@ class CrossPointSettings { uint8_t showHiddenFiles = 0; // Image rendering mode in EPUB reader uint8_t imageRendering = IMAGES_DISPLAY; + // Enable synthetic TOC fallback for malformed/sparse TOC books (1 = enabled, 0 = disabled) + uint8_t syntheticTocFallback = 1; ~CrossPointSettings() = default; diff --git a/src/SettingsList.h b/src/SettingsList.h index cdbee372..5c21bf23 100644 --- a/src/SettingsList.h +++ b/src/SettingsList.h @@ -65,6 +65,8 @@ inline const std::vector& getSettingsList() { SettingInfo::Enum(StrId::STR_IMAGES, &CrossPointSettings::imageRendering, {StrId::STR_IMAGES_DISPLAY, StrId::STR_IMAGES_PLACEHOLDER, StrId::STR_IMAGES_SUPPRESS}, "imageRendering", StrId::STR_CAT_READER), + SettingInfo::Toggle(StrId::STR_CREATE_FALLBACK_FOR_INVALID_TOC, &CrossPointSettings::syntheticTocFallback, + "syntheticTocFallback", StrId::STR_CAT_READER), // --- Controls --- SettingInfo::Enum(StrId::STR_SIDE_BTN_LAYOUT, &CrossPointSettings::sideButtonLayout, {StrId::STR_PREV_NEXT, StrId::STR_NEXT_PREV}, "sideButtonLayout", StrId::STR_CAT_CONTROLS),