From 03f0ce04cc5e448a3cb3cf106f2f5eb808535c5a Mon Sep 17 00:00:00 2001 From: Jonas Diemer Date: Tue, 30 Dec 2025 13:02:46 +0100 Subject: [PATCH] Feature: go to text/start reference in epub guide section at first start (#156) This parses the guide section in the content.opf for text/start references and jumps to this on first open of the book. Currently, this behavior will be repeated in case the reader manually jumps to Chapter 0 and then re-opens the book. IMO, this is an acceptable edge case (for which I couldn't see a good fix other than to drag a "first open" boolean around). --------- Co-authored-by: Sam Davis Co-authored-by: Dave Allie --- lib/Epub/Epub.cpp | 30 ++++++++++++++ lib/Epub/Epub.h | 1 + lib/Epub/Epub/BookMetadataCache.cpp | 8 ++-- lib/Epub/Epub/BookMetadataCache.h | 1 + lib/Epub/Epub/parsers/ContentOpfParser.cpp | 41 ++++++++++++++++++++ lib/Epub/Epub/parsers/ContentOpfParser.h | 2 + src/activities/reader/EpubReaderActivity.cpp | 10 +++++ 7 files changed, 90 insertions(+), 3 deletions(-) diff --git a/lib/Epub/Epub.cpp b/lib/Epub/Epub.cpp index ec3106b2..29a89243 100644 --- a/lib/Epub/Epub.cpp +++ b/lib/Epub/Epub.cpp @@ -74,6 +74,7 @@ bool Epub::parseContentOpf(BookMetadataCache::BookMetadata& bookMetadata) { bookMetadata.title = opfParser.title; bookMetadata.author = opfParser.author; bookMetadata.coverItemHref = opfParser.coverItemHref; + bookMetadata.textReferenceHref = opfParser.textReferenceHref; if (!opfParser.tocNcxPath.empty()) { tocNcxItem = opfParser.tocNcxPath; @@ -426,6 +427,35 @@ size_t Epub::getBookSize() const { return getCumulativeSpineItemSize(getSpineItemsCount() - 1); } +int Epub::getSpineIndexForTextReference() const { + if (!bookMetadataCache || !bookMetadataCache->isLoaded()) { + Serial.printf("[%lu] [EBP] getSpineIndexForTextReference called but cache not loaded\n", millis()); + return 0; + } + Serial.printf("[%lu] [ERS] Core Metadata: cover(%d)=%s, textReference(%d)=%s\n", millis(), + bookMetadataCache->coreMetadata.coverItemHref.size(), + bookMetadataCache->coreMetadata.coverItemHref.c_str(), + bookMetadataCache->coreMetadata.textReferenceHref.size(), + bookMetadataCache->coreMetadata.textReferenceHref.c_str()); + + if (bookMetadataCache->coreMetadata.textReferenceHref.empty()) { + // there was no textReference in epub, so we return 0 (the first chapter) + return 0; + } + + // loop through spine items to get the correct index matching the text href + for (size_t i = 0; i < getSpineItemsCount(); i++) { + if (getSpineItem(i).href == bookMetadataCache->coreMetadata.textReferenceHref) { + Serial.printf("[%lu] [ERS] Text reference %s found at index %d\n", millis(), + bookMetadataCache->coreMetadata.textReferenceHref.c_str(), i); + return i; + } + } + // This should not happen, as we checked for empty textReferenceHref earlier + Serial.printf("[%lu] [EBP] Section not found for text reference\n", millis()); + return 0; +} + // Calculate progress in book uint8_t Epub::calculateProgress(const int currentSpineIndex, const float currentSpineRead) const { const size_t bookSize = getBookSize(); diff --git a/lib/Epub/Epub.h b/lib/Epub/Epub.h index b68aac70..4bd9c46f 100644 --- a/lib/Epub/Epub.h +++ b/lib/Epub/Epub.h @@ -54,6 +54,7 @@ class Epub { int getSpineIndexForTocIndex(int tocIndex) const; int getTocIndexForSpineIndex(int spineIndex) const; size_t getCumulativeSpineItemSize(int spineIndex) const; + int getSpineIndexForTextReference() const; size_t getBookSize() const; uint8_t calculateProgress(int currentSpineIndex, float currentSpineRead) const; diff --git a/lib/Epub/Epub/BookMetadataCache.cpp b/lib/Epub/Epub/BookMetadataCache.cpp index 6cc7ea74..06b4f458 100644 --- a/lib/Epub/Epub/BookMetadataCache.cpp +++ b/lib/Epub/Epub/BookMetadataCache.cpp @@ -9,7 +9,7 @@ #include "FsHelpers.h" namespace { -constexpr uint8_t BOOK_CACHE_VERSION = 2; +constexpr uint8_t BOOK_CACHE_VERSION = 3; constexpr char bookBinFile[] = "/book.bin"; constexpr char tmpSpineBinFile[] = "/spine.bin.tmp"; constexpr char tmpTocBinFile[] = "/toc.bin.tmp"; @@ -87,8 +87,8 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta constexpr uint32_t headerASize = sizeof(BOOK_CACHE_VERSION) + /* LUT Offset */ sizeof(uint32_t) + sizeof(spineCount) + sizeof(tocCount); - const uint32_t metadataSize = - metadata.title.size() + metadata.author.size() + metadata.coverItemHref.size() + sizeof(uint32_t) * 3; + const uint32_t metadataSize = metadata.title.size() + metadata.author.size() + metadata.coverItemHref.size() + + metadata.textReferenceHref.size() + sizeof(uint32_t) * 4; const uint32_t lutSize = sizeof(uint32_t) * spineCount + sizeof(uint32_t) * tocCount; const uint32_t lutOffset = headerASize + metadataSize; @@ -101,6 +101,7 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta serialization::writeString(bookFile, metadata.title); serialization::writeString(bookFile, metadata.author); serialization::writeString(bookFile, metadata.coverItemHref); + serialization::writeString(bookFile, metadata.textReferenceHref); // Loop through spine entries, writing LUT positions spineFile.seek(0); @@ -289,6 +290,7 @@ bool BookMetadataCache::load() { serialization::readString(bookFile, coreMetadata.title); serialization::readString(bookFile, coreMetadata.author); serialization::readString(bookFile, coreMetadata.coverItemHref); + serialization::readString(bookFile, coreMetadata.textReferenceHref); loaded = true; Serial.printf("[%lu] [BMC] Loaded cache data: %d spine, %d TOC entries\n", millis(), spineCount, tocCount); diff --git a/lib/Epub/Epub/BookMetadataCache.h b/lib/Epub/Epub/BookMetadataCache.h index a6cf945b..5f1862c5 100644 --- a/lib/Epub/Epub/BookMetadataCache.h +++ b/lib/Epub/Epub/BookMetadataCache.h @@ -10,6 +10,7 @@ class BookMetadataCache { std::string title; std::string author; std::string coverItemHref; + std::string textReferenceHref; }; struct SpineEntry { diff --git a/lib/Epub/Epub/parsers/ContentOpfParser.cpp b/lib/Epub/Epub/parsers/ContentOpfParser.cpp index 721dc871..c9398778 100644 --- a/lib/Epub/Epub/parsers/ContentOpfParser.cpp +++ b/lib/Epub/Epub/parsers/ContentOpfParser.cpp @@ -127,6 +127,18 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name return; } + if (self->state == IN_PACKAGE && (strcmp(name, "guide") == 0 || strcmp(name, "opf:guide") == 0)) { + self->state = IN_GUIDE; + // TODO Remove print + Serial.printf("[%lu] [COF] Entering guide state.\n", millis()); + if (!SdMan.openFileForRead("COF", self->cachePath + itemCacheFile, self->tempItemStore)) { + Serial.printf( + "[%lu] [COF] Couldn't open temp items file for reading. This is probably going to be a fatal error.\n", + millis()); + } + return; + } + if (self->state == IN_METADATA && (strcmp(name, "meta") == 0 || strcmp(name, "opf:meta") == 0)) { bool isCover = false; std::string coverItemId; @@ -205,6 +217,29 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name return; } } + // parse the guide + if (self->state == IN_GUIDE && (strcmp(name, "reference") == 0 || strcmp(name, "opf:reference") == 0)) { + std::string type; + std::string textHref; + for (int i = 0; atts[i]; i += 2) { + if (strcmp(atts[i], "type") == 0) { + type = atts[i + 1]; + if (type == "text" || type == "start") { + continue; + } else { + Serial.printf("[%lu] [COF] Skipping non-text reference in guide: %s\n", millis(), type.c_str()); + break; + } + } else if (strcmp(atts[i], "href") == 0) { + textHref = self->baseContentPath + atts[i + 1]; + } + } + if ((type == "text" || (type == "start" && !self->textReferenceHref.empty())) && (textHref.length() > 0)) { + Serial.printf("[%lu] [COF] Found %s reference in guide: %s.\n", millis(), type.c_str(), textHref.c_str()); + self->textReferenceHref = textHref; + } + return; + } } void XMLCALL ContentOpfParser::characterData(void* userData, const XML_Char* s, const int len) { @@ -231,6 +266,12 @@ void XMLCALL ContentOpfParser::endElement(void* userData, const XML_Char* name) return; } + if (self->state == IN_GUIDE && (strcmp(name, "guide") == 0 || strcmp(name, "opf:guide") == 0)) { + self->state = IN_PACKAGE; + self->tempItemStore.close(); + return; + } + if (self->state == IN_MANIFEST && (strcmp(name, "manifest") == 0 || strcmp(name, "opf:manifest") == 0)) { self->state = IN_PACKAGE; self->tempItemStore.close(); diff --git a/lib/Epub/Epub/parsers/ContentOpfParser.h b/lib/Epub/Epub/parsers/ContentOpfParser.h index ad1f2957..245fca3b 100644 --- a/lib/Epub/Epub/parsers/ContentOpfParser.h +++ b/lib/Epub/Epub/parsers/ContentOpfParser.h @@ -15,6 +15,7 @@ class ContentOpfParser final : public Print { IN_BOOK_AUTHOR, IN_MANIFEST, IN_SPINE, + IN_GUIDE, }; const std::string& cachePath; @@ -35,6 +36,7 @@ class ContentOpfParser final : public Print { std::string author; std::string tocNcxPath; std::string coverItemHref; + std::string textReferenceHref; explicit ContentOpfParser(const std::string& cachePath, const std::string& baseContentPath, const size_t xmlSize, BookMetadataCache* cache) diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index 118decb3..c4a6690a 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -65,6 +65,16 @@ void EpubReaderActivity::onEnter() { } f.close(); } + // We may want a better condition to detect if we are opening for the first time. + // This will trigger if the book is re-opened at Chapter 0. + if (currentSpineIndex == 0) { + int textSpineIndex = epub->getSpineIndexForTextReference(); + if (textSpineIndex != 0) { + currentSpineIndex = textSpineIndex; + Serial.printf("[%lu] [ERS] Opened for first time, navigating to text reference at index %d\n", millis(), + textSpineIndex); + } + } // Save current epub as last opened epub APP_STATE.openEpubPath = epub->getPath();