diff --git a/lib/Epub/Epub.cpp b/lib/Epub/Epub.cpp index 010f000d..b48d7ea3 100644 --- a/lib/Epub/Epub.cpp +++ b/lib/Epub/Epub.cpp @@ -6,8 +6,6 @@ #include #include -#include - #include "Epub/parsers/ContainerParser.h" #include "Epub/parsers/ContentOpfParser.h" #include "Epub/parsers/TocNcxParser.h" @@ -44,7 +42,15 @@ bool Epub::findContentOpfFile(std::string* contentOpfFile) const { return true; } -bool Epub::parseContentOpf(const std::string& contentOpfFilePath) { +bool Epub::parseContentOpf(BookMetadataCache::BookMetadata& bookMetadata) { + std::string contentOpfFilePath; + if (!findContentOpfFile(&contentOpfFilePath)) { + Serial.printf("[%lu] [EBP] Could not find content.opf in zip\n", millis()); + return false; + } + + contentBasePath = contentOpfFilePath.substr(0, contentOpfFilePath.find_last_of('/') + 1); + Serial.printf("[%lu] [EBP] Parsing content.opf: %s\n", millis(), contentOpfFilePath.c_str()); size_t contentOpfSize; @@ -53,7 +59,9 @@ bool Epub::parseContentOpf(const std::string& contentOpfFilePath) { return false; } - ContentOpfParser opfParser(getBasePath(), contentOpfSize); + ContentOpfParser opfParser(getCachePath(), getBasePath(), contentOpfSize, bookMetadataCache.get()); + Serial.printf("[%lu] [MEM] Free: %d bytes, Total: %d bytes, Min Free: %d bytes\n", millis(), ESP.getFreeHeap(), + ESP.getHeapSize(), ESP.getMinFreeHeap()); if (!opfParser.setup()) { Serial.printf("[%lu] [EBP] Could not setup content.opf parser\n", millis()); @@ -66,26 +74,20 @@ bool Epub::parseContentOpf(const std::string& contentOpfFilePath) { } // Grab data from opfParser into epub - title = opfParser.title; - if (!opfParser.coverItemId.empty() && opfParser.items.count(opfParser.coverItemId) > 0) { - coverImageItem = opfParser.items.at(opfParser.coverItemId); - } + bookMetadata.title = opfParser.title; + // TODO: Parse author + bookMetadata.author = ""; + bookMetadata.coverItemHref = opfParser.coverItemHref; if (!opfParser.tocNcxPath.empty()) { tocNcxItem = opfParser.tocNcxPath; } - for (auto& spineRef : opfParser.spineRefs) { - if (opfParser.items.count(spineRef)) { - spine.emplace_back(spineRef, opfParser.items.at(spineRef)); - } - } - Serial.printf("[%lu] [EBP] Successfully parsed content.opf\n", millis()); return true; } -bool Epub::parseTocNcxFile() { +bool Epub::parseTocNcxFile() const { // the ncx file should have been specified in the content.opf file if (tocNcxItem.empty()) { Serial.printf("[%lu] [EBP] No ncx file specified\n", millis()); @@ -106,7 +108,7 @@ bool Epub::parseTocNcxFile() { } const auto ncxSize = tempNcxFile.size(); - TocNcxParser ncxParser(contentBasePath, ncxSize); + TocNcxParser ncxParser(contentBasePath, ncxSize, bookMetadataCache.get()); if (!ncxParser.setup()) { Serial.printf("[%lu] [EBP] Could not setup toc ncx parser\n", millis()); @@ -135,9 +137,7 @@ bool Epub::parseTocNcxFile() { tempNcxFile.close(); SD.remove(tmpNcxPath.c_str()); - this->toc = std::move(ncxParser.toc); - - Serial.printf("[%lu] [EBP] Parsed %d TOC items\n", millis(), this->toc.size()); + Serial.printf("[%lu] [EBP] Parsed TOC items\n", millis()); return true; } @@ -145,48 +145,79 @@ bool Epub::parseTocNcxFile() { bool Epub::load() { Serial.printf("[%lu] [EBP] Loading ePub: %s\n", millis(), filepath.c_str()); - std::string contentOpfFilePath; - if (!findContentOpfFile(&contentOpfFilePath)) { - Serial.printf("[%lu] [EBP] Could not find content.opf in zip\n", millis()); + // Initialize spine/TOC cache + bookMetadataCache.reset(new BookMetadataCache(cachePath)); + + // Try to load existing cache first + if (bookMetadataCache->load()) { + Serial.printf("[%lu] [EBP] Loaded ePub: %s\n", millis(), filepath.c_str()); + return true; + } + + // Cache doesn't exist or is invalid, build it + Serial.printf("[%lu] [EBP] Cache not found, building spine/TOC cache\n", millis()); + setupCacheDir(); + + // Begin building cache - stream entries to disk immediately + if (!bookMetadataCache->beginWrite()) { + Serial.printf("[%lu] [EBP] Could not begin writing cache\n", millis()); return false; } - Serial.printf("[%lu] [EBP] Found content.opf at: %s\n", millis(), contentOpfFilePath.c_str()); - - contentBasePath = contentOpfFilePath.substr(0, contentOpfFilePath.find_last_of('/') + 1); - - if (!parseContentOpf(contentOpfFilePath)) { + // OPF Pass + BookMetadataCache::BookMetadata bookMetadata; + if (!bookMetadataCache->beginContentOpfPass()) { + Serial.printf("[%lu] [EBP] Could not begin writing content.opf pass\n", millis()); + return false; + } + if (!parseContentOpf(bookMetadata)) { Serial.printf("[%lu] [EBP] Could not parse content.opf\n", millis()); return false; } + if (!bookMetadataCache->endContentOpfPass()) { + Serial.printf("[%lu] [EBP] Could not end writing content.opf pass\n", millis()); + return false; + } + // TOC Pass + if (!bookMetadataCache->beginTocPass()) { + Serial.printf("[%lu] [EBP] Could not begin writing toc pass\n", millis()); + return false; + } if (!parseTocNcxFile()) { Serial.printf("[%lu] [EBP] Could not parse toc\n", millis()); return false; } - - initializeSpineItemSizes(); - Serial.printf("[%lu] [EBP] Loaded ePub: %s\n", millis(), filepath.c_str()); - - return true; -} - -void Epub::initializeSpineItemSizes() { - Serial.printf("[%lu] [EBP] Calculating book size\n", millis()); - - const size_t spineItemsCount = getSpineItemsCount(); - size_t cumSpineItemSize = 0; - const ZipFile zip("/sd" + filepath); - - for (size_t i = 0; i < spineItemsCount; i++) { - std::string spineItem = getSpineItem(i); - size_t s = 0; - getItemSize(zip, spineItem, &s); - cumSpineItemSize += s; - cumulativeSpineItemSize.emplace_back(cumSpineItemSize); + if (!bookMetadataCache->endTocPass()) { + Serial.printf("[%lu] [EBP] Could not end writing toc pass\n", millis()); + return false; } - Serial.printf("[%lu] [EBP] Book size: %lu\n", millis(), cumSpineItemSize); + // Close the cache files + if (!bookMetadataCache->endWrite()) { + Serial.printf("[%lu] [EBP] Could not end writing cache\n", millis()); + return false; + } + + // Build final book.bin + if (!bookMetadataCache->buildBookBin(filepath, bookMetadata)) { + Serial.printf("[%lu] [EBP] Could not update mappings and sizes\n", millis()); + return false; + } + + if (!bookMetadataCache->cleanupTmpFiles()) { + Serial.printf("[%lu] [EBP] Could not cleanup tmp files - ignoring\n", millis()); + } + + // Reload the cache from disk so it's in the correct state + bookMetadataCache.reset(new BookMetadataCache(cachePath)); + if (!bookMetadataCache->load()) { + Serial.printf("[%lu] [EBP] Failed to reload cache after writing\n", millis()); + return false; + } + + Serial.printf("[%lu] [EBP] Loaded ePub: %s\n", millis(), filepath.c_str()); + return true; } bool Epub::clearCache() const { @@ -222,7 +253,14 @@ const std::string& Epub::getCachePath() const { return cachePath; } const std::string& Epub::getPath() const { return filepath; } -const std::string& Epub::getTitle() const { return title; } +const std::string& Epub::getTitle() const { + static std::string blank; + if (!bookMetadataCache || !bookMetadataCache->isLoaded()) { + return blank; + } + + return bookMetadataCache->coreMetadata.title; +} std::string Epub::getCoverBmpPath() const { return cachePath + "/cover.bmp"; } @@ -232,13 +270,19 @@ bool Epub::generateCoverBmp() const { return true; } - if (coverImageItem.empty()) { + if (!bookMetadataCache || !bookMetadataCache->isLoaded()) { + Serial.printf("[%lu] [EBP] Cannot generate cover BMP, cache not loaded\n", millis()); + return false; + } + + const auto coverImageHref = bookMetadataCache->coreMetadata.coverItemHref; + if (coverImageHref.empty()) { Serial.printf("[%lu] [EBP] No known cover image\n", millis()); return false; } - if (coverImageItem.substr(coverImageItem.length() - 4) == ".jpg" || - coverImageItem.substr(coverImageItem.length() - 5) == ".jpeg") { + if (coverImageHref.substr(coverImageHref.length() - 4) == ".jpg" || + coverImageHref.substr(coverImageHref.length() - 5) == ".jpeg") { Serial.printf("[%lu] [EBP] Generating BMP from JPG cover image\n", millis()); const auto coverJpgTempPath = getCachePath() + "/.cover.jpg"; @@ -246,7 +290,7 @@ bool Epub::generateCoverBmp() const { if (!FsHelpers::openFileForWrite("EBP", coverJpgTempPath, coverJpg)) { return false; } - readItemContentsToStream(coverImageItem, coverJpg, 1024); + readItemContentsToStream(coverImageHref, coverJpg, 1024); coverJpg.close(); if (!FsHelpers::openFileForRead("EBP", coverJpgTempPath, coverJpg)) { @@ -276,7 +320,7 @@ bool Epub::generateCoverBmp() const { return false; } -uint8_t* Epub::readItemContentsToBytes(const std::string& itemHref, size_t* size, bool trailingNullByte) const { +uint8_t* Epub::readItemContentsToBytes(const std::string& itemHref, size_t* size, const bool trailingNullByte) const { const ZipFile zip("/sd" + filepath); const std::string path = FsHelpers::normalisePath(itemHref); @@ -306,99 +350,89 @@ bool Epub::getItemSize(const ZipFile& zip, const std::string& itemHref, size_t* return zip.getInflatedFileSize(path.c_str(), size); } -int Epub::getSpineItemsCount() const { return spine.size(); } - -size_t Epub::getCumulativeSpineItemSize(const int spineIndex) const { - if (spineIndex < 0 || spineIndex >= static_cast(cumulativeSpineItemSize.size())) { - Serial.printf("[%lu] [EBP] getCumulativeSpineItemSize index:%d is out of range\n", millis(), spineIndex); +int Epub::getSpineItemsCount() const { + if (!bookMetadataCache || !bookMetadataCache->isLoaded()) { return 0; } - return cumulativeSpineItemSize.at(spineIndex); + return bookMetadataCache->getSpineCount(); } -std::string& Epub::getSpineItem(const int spineIndex) { - static std::string emptyString; - if (spine.empty()) { - Serial.printf("[%lu] [EBP] getSpineItem called but spine is empty\n", millis()); - return emptyString; +size_t Epub::getCumulativeSpineItemSize(const int spineIndex) const { return getSpineItem(spineIndex).cumulativeSize; } + +BookMetadataCache::SpineEntry Epub::getSpineItem(const int spineIndex) const { + if (!bookMetadataCache || !bookMetadataCache->isLoaded()) { + Serial.printf("[%lu] [EBP] getSpineItem called but cache not loaded\n", millis()); + return {}; } - if (spineIndex < 0 || spineIndex >= static_cast(spine.size())) { + + if (spineIndex < 0 || spineIndex >= bookMetadataCache->getSpineCount()) { Serial.printf("[%lu] [EBP] getSpineItem index:%d is out of range\n", millis(), spineIndex); - return spine.at(0).second; + return bookMetadataCache->getSpineEntry(0); } - return spine.at(spineIndex).second; + return bookMetadataCache->getSpineEntry(spineIndex); } -EpubTocEntry& Epub::getTocItem(const int tocIndex) { - static EpubTocEntry emptyEntry = {}; - if (toc.empty()) { - Serial.printf("[%lu] [EBP] getTocItem called but toc is empty\n", millis()); - return emptyEntry; +BookMetadataCache::TocEntry Epub::getTocItem(const int tocIndex) const { + if (!bookMetadataCache || !bookMetadataCache->isLoaded()) { + Serial.printf("[%lu] [EBP] getTocItem called but cache not loaded\n", millis()); + return {}; } - if (tocIndex < 0 || tocIndex >= static_cast(toc.size())) { + + if (tocIndex < 0 || tocIndex >= bookMetadataCache->getTocCount()) { Serial.printf("[%lu] [EBP] getTocItem index:%d is out of range\n", millis(), tocIndex); - return toc.at(0); + return {}; } - return toc.at(tocIndex); + return bookMetadataCache->getTocEntry(tocIndex); } -int Epub::getTocItemsCount() const { return toc.size(); } +int Epub::getTocItemsCount() const { + if (!bookMetadataCache || !bookMetadataCache->isLoaded()) { + return 0; + } + + return bookMetadataCache->getTocCount(); +} // work out the section index for a toc index int Epub::getSpineIndexForTocIndex(const int tocIndex) const { - if (tocIndex < 0 || tocIndex >= toc.size()) { + if (!bookMetadataCache || !bookMetadataCache->isLoaded()) { + Serial.printf("[%lu] [EBP] getSpineIndexForTocIndex called but cache not loaded\n", millis()); + return 0; + } + + if (tocIndex < 0 || tocIndex >= bookMetadataCache->getTocCount()) { Serial.printf("[%lu] [EBP] getSpineIndexForTocIndex: tocIndex %d out of range\n", millis(), tocIndex); return 0; } - // the toc entry should have an href that matches the spine item - // so we can find the spine index by looking for the href - for (int i = 0; i < spine.size(); i++) { - if (spine[i].second == toc[tocIndex].href) { - return i; - } + const int spineIndex = bookMetadataCache->getTocEntry(tocIndex).spineIndex; + if (spineIndex < 0) { + Serial.printf("[%lu] [EBP] Section not found for TOC index %d\n", millis(), tocIndex); + return 0; } - Serial.printf("[%lu] [EBP] Section not found\n", millis()); - // not found - default to the start of the book - return 0; + return spineIndex; } -int Epub::getTocIndexForSpineIndex(const int spineIndex) const { - if (spineIndex < 0 || spineIndex >= spine.size()) { - Serial.printf("[%lu] [EBP] getTocIndexForSpineIndex: spineIndex %d out of range\n", millis(), spineIndex); - return -1; - } - - // the toc entry should have an href that matches the spine item - // so we can find the toc index by looking for the href - for (int i = 0; i < toc.size(); i++) { - if (toc[i].href == spine[spineIndex].second) { - return i; - } - } - - Serial.printf("[%lu] [EBP] TOC item not found\n", millis()); - return -1; -} +int Epub::getTocIndexForSpineIndex(const int spineIndex) const { return getSpineItem(spineIndex).tocIndex; } size_t Epub::getBookSize() const { - if (spine.empty()) { + if (!bookMetadataCache || !bookMetadataCache->isLoaded() || bookMetadataCache->getSpineCount() == 0) { return 0; } return getCumulativeSpineItemSize(getSpineItemsCount() - 1); } // Calculate progress in book -uint8_t Epub::calculateProgress(const int currentSpineIndex, const float currentSpineRead) { - size_t bookSize = getBookSize(); +uint8_t Epub::calculateProgress(const int currentSpineIndex, const float currentSpineRead) const { + const size_t bookSize = getBookSize(); if (bookSize == 0) { return 0; } - size_t prevChapterSize = (currentSpineIndex >= 1) ? getCumulativeSpineItemSize(currentSpineIndex - 1) : 0; - size_t curChapterSize = getCumulativeSpineItemSize(currentSpineIndex) - prevChapterSize; - size_t sectionProgSize = currentSpineRead * curChapterSize; + const size_t prevChapterSize = (currentSpineIndex >= 1) ? getCumulativeSpineItemSize(currentSpineIndex - 1) : 0; + const size_t curChapterSize = getCumulativeSpineItemSize(currentSpineIndex) - prevChapterSize; + const size_t sectionProgSize = currentSpineRead * curChapterSize; return round(static_cast(prevChapterSize + sectionProgSize) / bookSize * 100.0); } diff --git a/lib/Epub/Epub.h b/lib/Epub/Epub.h index 381379c5..acdd32c8 100644 --- a/lib/Epub/Epub.h +++ b/lib/Epub/Epub.h @@ -1,38 +1,29 @@ #pragma once -#include +#include #include #include #include -#include "Epub/EpubTocEntry.h" +#include "Epub/BookMetadataCache.h" class ZipFile; class Epub { - // the title read from the EPUB meta data - std::string title; - // the cover image - std::string coverImageItem; // the ncx file std::string tocNcxItem; // where is the EPUBfile? std::string filepath; - // the spine of the EPUB file - std::vector> spine; - // the file size of the spine items (proxy to book progress) - std::vector cumulativeSpineItemSize; - // the toc of the EPUB file - std::vector toc; // the base path for items in the EPUB file std::string contentBasePath; // Uniq cache key based on filepath std::string cachePath; + // Spine and TOC cache + std::unique_ptr bookMetadataCache; bool findContentOpfFile(std::string* contentOpfFile) const; - bool parseContentOpf(const std::string& contentOpfFilePath); - bool parseTocNcxFile(); - void initializeSpineItemSizes(); + bool parseContentOpf(BookMetadataCache::BookMetadata& bookMetadata); + bool parseTocNcxFile() const; static bool getItemSize(const ZipFile& zip, const std::string& itemHref, size_t* size); public: @@ -54,14 +45,14 @@ class Epub { bool trailingNullByte = false) const; bool readItemContentsToStream(const std::string& itemHref, Print& out, size_t chunkSize) const; bool getItemSize(const std::string& itemHref, size_t* size) const; - std::string& getSpineItem(int spineIndex); + BookMetadataCache::SpineEntry getSpineItem(int spineIndex) const; + BookMetadataCache::TocEntry getTocItem(int tocIndex) const; int getSpineItemsCount() const; - size_t getCumulativeSpineItemSize(const int spineIndex) const; - EpubTocEntry& getTocItem(int tocIndex); int getTocItemsCount() const; int getSpineIndexForTocIndex(int tocIndex) const; int getTocIndexForSpineIndex(int spineIndex) const; + size_t getCumulativeSpineItemSize(int spineIndex) const; size_t getBookSize() const; - uint8_t calculateProgress(const int currentSpineIndex, const float currentSpineRead); + uint8_t calculateProgress(const int currentSpineIndex, const float currentSpineRead) const; }; diff --git a/lib/Epub/Epub/BookMetadataCache.cpp b/lib/Epub/Epub/BookMetadataCache.cpp new file mode 100644 index 00000000..3cef851a --- /dev/null +++ b/lib/Epub/Epub/BookMetadataCache.cpp @@ -0,0 +1,326 @@ +#include "BookMetadataCache.h" + +#include +#include +#include +#include + +#include + +#include "FsHelpers.h" + +namespace { +constexpr uint8_t BOOK_CACHE_VERSION = 1; +constexpr char bookBinFile[] = "/book.bin"; +constexpr char tmpSpineBinFile[] = "/spine.bin.tmp"; +constexpr char tmpTocBinFile[] = "/toc.bin.tmp"; +} // namespace + +/* ============= WRITING / BUILDING FUNCTIONS ================ */ + +bool BookMetadataCache::beginWrite() { + buildMode = true; + spineCount = 0; + tocCount = 0; + Serial.printf("[%lu] [BMC] Entering write mode\n", millis()); + return true; +} + +bool BookMetadataCache::beginContentOpfPass() { + Serial.printf("[%lu] [BMC] Beginning content opf pass\n", millis()); + + // Open spine file for writing + return FsHelpers::openFileForWrite("BMC", cachePath + tmpSpineBinFile, spineFile); +} + +bool BookMetadataCache::endContentOpfPass() { + spineFile.close(); + return true; +} + +bool BookMetadataCache::beginTocPass() { + Serial.printf("[%lu] [BMC] Beginning toc pass\n", millis()); + + // Open spine file for reading + if (!FsHelpers::openFileForRead("BMC", cachePath + tmpSpineBinFile, spineFile)) { + return false; + } + if (!FsHelpers::openFileForWrite("BMC", cachePath + tmpTocBinFile, tocFile)) { + spineFile.close(); + return false; + } + return true; +} + +bool BookMetadataCache::endTocPass() { + tocFile.close(); + spineFile.close(); + return true; +} + +bool BookMetadataCache::endWrite() { + if (!buildMode) { + Serial.printf("[%lu] [BMC] endWrite called but not in build mode\n", millis()); + return false; + } + + buildMode = false; + Serial.printf("[%lu] [BMC] Wrote %d spine, %d TOC entries\n", millis(), spineCount, tocCount); + return true; +} + +bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMetadata& metadata) { + // Open all three files, writing to meta, reading from spine and toc + if (!FsHelpers::openFileForWrite("BMC", cachePath + bookBinFile, bookFile)) { + return false; + } + + if (!FsHelpers::openFileForRead("BMC", cachePath + tmpSpineBinFile, spineFile)) { + bookFile.close(); + return false; + } + + if (!FsHelpers::openFileForRead("BMC", cachePath + tmpTocBinFile, tocFile)) { + bookFile.close(); + spineFile.close(); + return false; + } + + constexpr size_t headerASize = + sizeof(BOOK_CACHE_VERSION) + /* LUT Offset */ sizeof(size_t) + sizeof(spineCount) + sizeof(tocCount); + const size_t metadataSize = + metadata.title.size() + metadata.author.size() + metadata.coverItemHref.size() + sizeof(uint32_t) * 3; + const size_t lutSize = sizeof(size_t) * spineCount + sizeof(size_t) * tocCount; + const size_t lutOffset = headerASize + metadataSize; + + // Header A + serialization::writePod(bookFile, BOOK_CACHE_VERSION); + serialization::writePod(bookFile, lutOffset); + serialization::writePod(bookFile, spineCount); + serialization::writePod(bookFile, tocCount); + // Metadata + serialization::writeString(bookFile, metadata.title); + serialization::writeString(bookFile, metadata.author); + serialization::writeString(bookFile, metadata.coverItemHref); + + // Loop through spine entries, writing LUT positions + spineFile.seek(0); + for (int i = 0; i < spineCount; i++) { + auto pos = spineFile.position(); + auto spineEntry = readSpineEntry(spineFile); + serialization::writePod(bookFile, pos + lutOffset + lutSize); + } + + // Loop through toc entries, writing LUT positions + tocFile.seek(0); + for (int i = 0; i < tocCount; i++) { + auto pos = tocFile.position(); + auto tocEntry = readTocEntry(tocFile); + serialization::writePod(bookFile, pos + lutOffset + lutSize + spineFile.position()); + } + + // LUTs complete + // Loop through spines from spine file matching up TOC indexes, calculating cumulative size and writing to book.bin + + const ZipFile zip("/sd" + epubPath); + size_t cumSize = 0; + spineFile.seek(0); + for (int i = 0; i < spineCount; i++) { + auto spineEntry = readSpineEntry(spineFile); + + tocFile.seek(0); + for (int j = 0; j < tocCount; j++) { + auto tocEntry = readTocEntry(tocFile); + if (tocEntry.spineIndex == i) { + spineEntry.tocIndex = j; + break; + } + } + + // Not a huge deal if we don't fine a TOC entry for the spine entry, this is expected behaviour for EPUBs + // Logging here is for debugging + if (spineEntry.tocIndex == -1) { + Serial.printf("[%lu] [BMC] Warning: Could not find TOC entry for spine item %d: %s\n", millis(), i, + spineEntry.href.c_str()); + } + + // Calculate size for cumulative size + size_t itemSize = 0; + const std::string path = FsHelpers::normalisePath(spineEntry.href); + if (zip.getInflatedFileSize(path.c_str(), &itemSize)) { + cumSize += itemSize; + spineEntry.cumulativeSize = cumSize; + } else { + Serial.printf("[%lu] [BMC] Warning: Could not get size for spine item: %s\n", millis(), path.c_str()); + } + + // Write out spine data to book.bin + writeSpineEntry(bookFile, spineEntry); + } + + // Loop through toc entries from toc file writing to book.bin + tocFile.seek(0); + for (int i = 0; i < tocCount; i++) { + auto tocEntry = readTocEntry(tocFile); + writeTocEntry(bookFile, tocEntry); + } + + bookFile.close(); + spineFile.close(); + tocFile.close(); + + Serial.printf("[%lu] [BMC] Successfully built book.bin\n", millis()); + return true; +} + +bool BookMetadataCache::cleanupTmpFiles() const { + if (SD.exists((cachePath + tmpSpineBinFile).c_str())) { + SD.remove((cachePath + tmpSpineBinFile).c_str()); + } + if (SD.exists((cachePath + tmpTocBinFile).c_str())) { + SD.remove((cachePath + tmpTocBinFile).c_str()); + } + return true; +} + +size_t BookMetadataCache::writeSpineEntry(File& file, const SpineEntry& entry) const { + const auto pos = file.position(); + serialization::writeString(file, entry.href); + serialization::writePod(file, entry.cumulativeSize); + serialization::writePod(file, entry.tocIndex); + return pos; +} + +size_t BookMetadataCache::writeTocEntry(File& file, const TocEntry& entry) const { + const auto pos = file.position(); + serialization::writeString(file, entry.title); + serialization::writeString(file, entry.href); + serialization::writeString(file, entry.anchor); + serialization::writePod(file, entry.level); + serialization::writePod(file, entry.spineIndex); + return pos; +} + +// Note: for the LUT to be accurate, this **MUST** be called for all spine items before `addTocEntry` is ever called +// this is because in this function we're marking positions of the items +void BookMetadataCache::createSpineEntry(const std::string& href) { + if (!buildMode || !spineFile) { + Serial.printf("[%lu] [BMC] createSpineEntry called but not in build mode\n", millis()); + return; + } + + const SpineEntry entry(href, 0, -1); + writeSpineEntry(spineFile, entry); + spineCount++; +} + +void BookMetadataCache::createTocEntry(const std::string& title, const std::string& href, const std::string& anchor, + const uint8_t level) { + if (!buildMode || !tocFile || !spineFile) { + Serial.printf("[%lu] [BMC] createTocEntry called but not in build mode\n", millis()); + return; + } + + int spineIndex = -1; + // find spine index + spineFile.seek(0); + for (int i = 0; i < spineCount; i++) { + auto spineEntry = readSpineEntry(spineFile); + if (spineEntry.href == href) { + spineIndex = i; + break; + } + } + + if (spineIndex == -1) { + Serial.printf("[%lu] [BMC] addTocEntry: Could not find spine item for TOC href %s\n", millis(), href.c_str()); + } + + const TocEntry entry(title, href, anchor, level, spineIndex); + writeTocEntry(tocFile, entry); + tocCount++; +} + +/* ============= READING / LOADING FUNCTIONS ================ */ + +bool BookMetadataCache::load() { + if (!FsHelpers::openFileForRead("BMC", cachePath + bookBinFile, bookFile)) { + return false; + } + + uint8_t version; + serialization::readPod(bookFile, version); + if (version != BOOK_CACHE_VERSION) { + Serial.printf("[%lu] [BMC] Cache version mismatch: expected %d, got %d\n", millis(), BOOK_CACHE_VERSION, version); + bookFile.close(); + return false; + } + + serialization::readPod(bookFile, lutOffset); + serialization::readPod(bookFile, spineCount); + serialization::readPod(bookFile, tocCount); + + serialization::readString(bookFile, coreMetadata.title); + serialization::readString(bookFile, coreMetadata.author); + serialization::readString(bookFile, coreMetadata.coverItemHref); + + loaded = true; + Serial.printf("[%lu] [BMC] Loaded cache data: %d spine, %d TOC entries\n", millis(), spineCount, tocCount); + return true; +} + +BookMetadataCache::SpineEntry BookMetadataCache::getSpineEntry(const int index) { + if (!loaded) { + Serial.printf("[%lu] [BMC] getSpineEntry called but cache not loaded\n", millis()); + return {}; + } + + if (index < 0 || index >= static_cast(spineCount)) { + Serial.printf("[%lu] [BMC] getSpineEntry index %d out of range\n", millis(), index); + return {}; + } + + // Seek to spine LUT item, read from LUT and get out data + bookFile.seek(lutOffset + sizeof(size_t) * index); + size_t spineEntryPos; + serialization::readPod(bookFile, spineEntryPos); + bookFile.seek(spineEntryPos); + return readSpineEntry(bookFile); +} + +BookMetadataCache::TocEntry BookMetadataCache::getTocEntry(const int index) { + if (!loaded) { + Serial.printf("[%lu] [BMC] getTocEntry called but cache not loaded\n", millis()); + return {}; + } + + if (index < 0 || index >= static_cast(tocCount)) { + Serial.printf("[%lu] [BMC] getTocEntry index %d out of range\n", millis(), index); + return {}; + } + + // Seek to TOC LUT item, read from LUT and get out data + bookFile.seek(lutOffset + sizeof(size_t) * spineCount + sizeof(size_t) * index); + size_t tocEntryPos; + serialization::readPod(bookFile, tocEntryPos); + bookFile.seek(tocEntryPos); + return readTocEntry(bookFile); +} + +BookMetadataCache::SpineEntry BookMetadataCache::readSpineEntry(File& file) const { + SpineEntry entry; + serialization::readString(file, entry.href); + serialization::readPod(file, entry.cumulativeSize); + serialization::readPod(file, entry.tocIndex); + return entry; +} + +BookMetadataCache::TocEntry BookMetadataCache::readTocEntry(File& file) const { + TocEntry entry; + serialization::readString(file, entry.title); + serialization::readString(file, entry.href); + serialization::readString(file, entry.anchor); + serialization::readPod(file, entry.level); + serialization::readPod(file, entry.spineIndex); + return entry; +} diff --git a/lib/Epub/Epub/BookMetadataCache.h b/lib/Epub/Epub/BookMetadataCache.h new file mode 100644 index 00000000..7f9f419c --- /dev/null +++ b/lib/Epub/Epub/BookMetadataCache.h @@ -0,0 +1,87 @@ +#pragma once + +#include + +#include + +class BookMetadataCache { + public: + struct BookMetadata { + std::string title; + std::string author; + std::string coverItemHref; + }; + + struct SpineEntry { + std::string href; + size_t cumulativeSize; + int16_t tocIndex; + + SpineEntry() : cumulativeSize(0), tocIndex(-1) {} + SpineEntry(std::string href, const size_t cumulativeSize, const int16_t tocIndex) + : href(std::move(href)), cumulativeSize(cumulativeSize), tocIndex(tocIndex) {} + }; + + struct TocEntry { + std::string title; + std::string href; + std::string anchor; + uint8_t level; + int16_t spineIndex; + + TocEntry() : level(0), spineIndex(-1) {} + TocEntry(std::string title, std::string href, std::string anchor, const uint8_t level, const int16_t spineIndex) + : title(std::move(title)), + href(std::move(href)), + anchor(std::move(anchor)), + level(level), + spineIndex(spineIndex) {} + }; + + private: + std::string cachePath; + size_t lutOffset; + uint16_t spineCount; + uint16_t tocCount; + bool loaded; + bool buildMode; + + File bookFile; + // Temp file handles during build + File spineFile; + File tocFile; + + size_t writeSpineEntry(File& file, const SpineEntry& entry) const; + size_t writeTocEntry(File& file, const TocEntry& entry) const; + SpineEntry readSpineEntry(File& file) const; + TocEntry readTocEntry(File& file) const; + + public: + BookMetadata coreMetadata; + + explicit BookMetadataCache(std::string cachePath) + : cachePath(std::move(cachePath)), lutOffset(0), spineCount(0), tocCount(0), loaded(false), buildMode(false) {} + ~BookMetadataCache() = default; + + // Building phase (stream to disk immediately) + bool beginWrite(); + bool beginContentOpfPass(); + void createSpineEntry(const std::string& href); + bool endContentOpfPass(); + bool beginTocPass(); + void createTocEntry(const std::string& title, const std::string& href, const std::string& anchor, uint8_t level); + bool endTocPass(); + bool endWrite(); + bool cleanupTmpFiles() const; + + // Post-processing to update mappings and sizes + bool buildBookBin(const std::string& epubPath, const BookMetadata& metadata); + + // Reading phase (read mode) + bool load(); + SpineEntry getSpineEntry(int index); + TocEntry getTocEntry(int index); + int getSpineCount() const { return spineCount; } + int getTocCount() const { return tocCount; } + bool isLoaded() const { return loaded; } +}; diff --git a/lib/Epub/Epub/EpubTocEntry.h b/lib/Epub/Epub/EpubTocEntry.h deleted file mode 100644 index 94f0c90f..00000000 --- a/lib/Epub/Epub/EpubTocEntry.h +++ /dev/null @@ -1,10 +0,0 @@ -#pragma once - -#include - -struct EpubTocEntry { - std::string title; - std::string href; - std::string anchor; - uint8_t level; -}; diff --git a/lib/Epub/Epub/FsHelpers.cpp b/lib/Epub/Epub/FsHelpers.cpp new file mode 100644 index 00000000..743ac59b --- /dev/null +++ b/lib/Epub/Epub/FsHelpers.cpp @@ -0,0 +1,92 @@ +#include "FsHelpers.h" + +#include + +#include + +bool FsHelpers::openFileForRead(const char* moduleName, const std::string& path, File& file) { + file = SD.open(path.c_str(), FILE_READ); + if (!file) { + Serial.printf("[%lu] [%s] Failed to open file for reading: %s\n", millis(), moduleName, path.c_str()); + return false; + } + return true; +} + +bool FsHelpers::openFileForWrite(const char* moduleName, const std::string& path, File& file) { + file = SD.open(path.c_str(), FILE_WRITE, true); + if (!file) { + Serial.printf("[%lu] [%s] Failed to open file for writing: %s\n", millis(), moduleName, path.c_str()); + return false; + } + return true; +} + +bool FsHelpers::removeDir(const char* path) { + // 1. Open the directory + File dir = SD.open(path); + if (!dir) { + return false; + } + if (!dir.isDirectory()) { + return false; + } + + File file = dir.openNextFile(); + while (file) { + String filePath = path; + if (!filePath.endsWith("/")) { + filePath += "/"; + } + filePath += file.name(); + + if (file.isDirectory()) { + if (!removeDir(filePath.c_str())) { + return false; + } + } else { + if (!SD.remove(filePath.c_str())) { + return false; + } + } + file = dir.openNextFile(); + } + + return SD.rmdir(path); +} + +std::string FsHelpers::normalisePath(const std::string& path) { + std::vector components; + std::string component; + + for (const auto c : path) { + if (c == '/') { + if (!component.empty()) { + if (component == "..") { + if (!components.empty()) { + components.pop_back(); + } + } else { + components.push_back(component); + } + component.clear(); + } + } else { + component += c; + } + } + + if (!component.empty()) { + components.push_back(component); + } + + std::string result; + for (const auto& c : components) { + if (!result.empty()) { + result += "/"; + } + result += c; + } + + return result; +} diff --git a/lib/Epub/Epub/FsHelpers.h b/lib/Epub/Epub/FsHelpers.h new file mode 100644 index 00000000..193db65f --- /dev/null +++ b/lib/Epub/Epub/FsHelpers.h @@ -0,0 +1,12 @@ +#pragma once +#include + +#include + +class FsHelpers { + public: + static bool openFileForRead(const char* moduleName, const std::string& path, File& file); + static bool openFileForWrite(const char* moduleName, const std::string& path, File& file); + static bool removeDir(const char* path); + static std::string normalisePath(const std::string& path); +}; diff --git a/lib/Epub/Epub/Section.cpp b/lib/Epub/Epub/Section.cpp index ec2993f0..5323a7a5 100644 --- a/lib/Epub/Epub/Section.cpp +++ b/lib/Epub/Epub/Section.cpp @@ -116,8 +116,7 @@ bool Section::clearCache() const { bool Section::persistPageDataToSD(const int fontId, const float lineCompression, const int marginTop, const int marginRight, const int marginBottom, const int marginLeft, const bool extraParagraphSpacing) { - const auto localPath = epub->getSpineItem(spineIndex); - + const auto localPath = epub->getSpineItem(spineIndex).href; const auto tmpHtmlPath = epub->getCachePath() + "/.tmp_" + std::to_string(spineIndex) + ".html"; File tmpHtml; if (!FsHelpers::openFileForWrite("SCT", tmpHtmlPath, tmpHtml)) { diff --git a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp index 4a9b86cc..766e5ca6 100644 --- a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp +++ b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp @@ -11,13 +11,13 @@ const char* HEADER_TAGS[] = {"h1", "h2", "h3", "h4", "h5", "h6"}; constexpr int NUM_HEADER_TAGS = sizeof(HEADER_TAGS) / sizeof(HEADER_TAGS[0]); -const char* BLOCK_TAGS[] = {"p", "li", "div", "br"}; +const char* BLOCK_TAGS[] = {"p", "li", "div", "br", "blockquote"}; constexpr int NUM_BLOCK_TAGS = sizeof(BLOCK_TAGS) / sizeof(BLOCK_TAGS[0]); -const char* BOLD_TAGS[] = {"b"}; +const char* BOLD_TAGS[] = {"b", "strong"}; constexpr int NUM_BOLD_TAGS = sizeof(BOLD_TAGS) / sizeof(BOLD_TAGS[0]); -const char* ITALIC_TAGS[] = {"i"}; +const char* ITALIC_TAGS[] = {"i", "em"}; constexpr int NUM_ITALIC_TAGS = sizeof(ITALIC_TAGS) / sizeof(ITALIC_TAGS[0]); const char* IMAGE_TAGS[] = {"img"}; diff --git a/lib/Epub/Epub/parsers/ContentOpfParser.cpp b/lib/Epub/Epub/parsers/ContentOpfParser.cpp index 4d3d776f..3cc64014 100644 --- a/lib/Epub/Epub/parsers/ContentOpfParser.cpp +++ b/lib/Epub/Epub/parsers/ContentOpfParser.cpp @@ -1,11 +1,16 @@ #include "ContentOpfParser.h" +#include #include +#include #include +#include "../BookMetadataCache.h" + namespace { constexpr char MEDIA_TYPE_NCX[] = "application/x-dtbncx+xml"; -} +constexpr char itemCacheFile[] = "/.items.bin"; +} // namespace bool ContentOpfParser::setup() { parser = XML_ParserCreate(nullptr); @@ -28,6 +33,12 @@ ContentOpfParser::~ContentOpfParser() { XML_ParserFree(parser); parser = nullptr; } + if (tempItemStore) { + tempItemStore.close(); + } + if (SD.exists((cachePath + itemCacheFile).c_str())) { + SD.remove((cachePath + itemCacheFile).c_str()); + } } size_t ContentOpfParser::write(const uint8_t data) { return write(&data, 1); } @@ -94,11 +105,21 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name if (self->state == IN_PACKAGE && (strcmp(name, "manifest") == 0 || strcmp(name, "opf:manifest") == 0)) { self->state = IN_MANIFEST; + if (!FsHelpers::openFileForWrite("COF", self->cachePath + itemCacheFile, self->tempItemStore)) { + Serial.printf( + "[%lu] [COF] Couldn't open temp items file for writing. This is probably going to be a fatal error.\n", + millis()); + } return; } if (self->state == IN_PACKAGE && (strcmp(name, "spine") == 0 || strcmp(name, "opf:spine") == 0)) { self->state = IN_SPINE; + if (!FsHelpers::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; } @@ -135,7 +156,13 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name } } - self->items[itemId] = href; + // Write items down to SD card + serialization::writeString(self->tempItemStore, itemId); + serialization::writeString(self->tempItemStore, href); + + if (itemId == self->coverItemId) { + self->coverItemHref = href; + } if (mediaType == MEDIA_TYPE_NCX) { if (self->tocNcxPath.empty()) { @@ -148,14 +175,29 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name return; } - if (self->state == IN_SPINE && (strcmp(name, "itemref") == 0 || strcmp(name, "opf:itemref") == 0)) { - for (int i = 0; atts[i]; i += 2) { - if (strcmp(atts[i], "idref") == 0) { - self->spineRefs.emplace_back(atts[i + 1]); - break; + // NOTE: This relies on spine appearing after item manifest (which is pretty safe as it's part of the EPUB spec) + // Only run the spine parsing if there's a cache to add it to + if (self->cache) { + if (self->state == IN_SPINE && (strcmp(name, "itemref") == 0 || strcmp(name, "opf:itemref") == 0)) { + for (int i = 0; atts[i]; i += 2) { + if (strcmp(atts[i], "idref") == 0) { + const std::string idref = atts[i + 1]; + // Resolve the idref to href using items map + self->tempItemStore.seek(0); + std::string itemId; + std::string href; + while (self->tempItemStore.available()) { + serialization::readString(self->tempItemStore, itemId); + serialization::readString(self->tempItemStore, href); + if (itemId == idref) { + self->cache->createSpineEntry(href); + break; + } + } + } } + return; } - return; } } @@ -174,11 +216,13 @@ void XMLCALL ContentOpfParser::endElement(void* userData, const XML_Char* name) if (self->state == IN_SPINE && (strcmp(name, "spine") == 0 || strcmp(name, "opf:spine") == 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(); return; } diff --git a/lib/Epub/Epub/parsers/ContentOpfParser.h b/lib/Epub/Epub/parsers/ContentOpfParser.h index a3070fcc..5415de67 100644 --- a/lib/Epub/Epub/parsers/ContentOpfParser.h +++ b/lib/Epub/Epub/parsers/ContentOpfParser.h @@ -1,11 +1,11 @@ #pragma once #include -#include - #include "Epub.h" #include "expat.h" +class BookMetadataCache; + class ContentOpfParser final : public Print { enum ParserState { START, @@ -16,10 +16,14 @@ class ContentOpfParser final : public Print { IN_SPINE, }; + const std::string& cachePath; const std::string& baseContentPath; size_t remainingSize; XML_Parser parser = nullptr; ParserState state = START; + BookMetadataCache* cache; + File tempItemStore; + std::string coverItemId; static void startElement(void* userData, const XML_Char* name, const XML_Char** atts); static void characterData(void* userData, const XML_Char* s, int len); @@ -28,12 +32,11 @@ class ContentOpfParser final : public Print { public: std::string title; std::string tocNcxPath; - std::string coverItemId; - std::map items; - std::vector spineRefs; + std::string coverItemHref; - explicit ContentOpfParser(const std::string& baseContentPath, const size_t xmlSize) - : baseContentPath(baseContentPath), remainingSize(xmlSize) {} + explicit ContentOpfParser(const std::string& cachePath, const std::string& baseContentPath, const size_t xmlSize, + BookMetadataCache* cache) + : cachePath(cachePath), baseContentPath(baseContentPath), remainingSize(xmlSize), cache(cache) {} ~ContentOpfParser() override; bool setup(); diff --git a/lib/Epub/Epub/parsers/TocNcxParser.cpp b/lib/Epub/Epub/parsers/TocNcxParser.cpp index f4700558..b1fbb2fe 100644 --- a/lib/Epub/Epub/parsers/TocNcxParser.cpp +++ b/lib/Epub/Epub/parsers/TocNcxParser.cpp @@ -1,8 +1,9 @@ #include "TocNcxParser.h" -#include #include +#include "../BookMetadataCache.h" + bool TocNcxParser::setup() { parser = XML_ParserCreate(nullptr); if (!parser) { @@ -167,8 +168,9 @@ void XMLCALL TocNcxParser::endElement(void* userData, const XML_Char* name) { href = href.substr(0, pos); } - // Push to vector - self->toc.push_back({std::move(self->currentLabel), std::move(href), std::move(anchor), self->currentDepth}); + if (self->cache) { + self->cache->createTocEntry(self->currentLabel, href, anchor, self->currentDepth); + } // Clear them so we don't re-add them if there are weird XML structures self->currentLabel.clear(); diff --git a/lib/Epub/Epub/parsers/TocNcxParser.h b/lib/Epub/Epub/parsers/TocNcxParser.h index 2f3601a1..e2c86205 100644 --- a/lib/Epub/Epub/parsers/TocNcxParser.h +++ b/lib/Epub/Epub/parsers/TocNcxParser.h @@ -1,11 +1,10 @@ #pragma once #include +#include #include -#include -#include "Epub/EpubTocEntry.h" -#include "expat.h" +class BookMetadataCache; class TocNcxParser final : public Print { enum ParserState { START, IN_NCX, IN_NAV_MAP, IN_NAV_POINT, IN_NAV_LABEL, IN_NAV_LABEL_TEXT, IN_CONTENT }; @@ -14,6 +13,7 @@ class TocNcxParser final : public Print { size_t remainingSize; XML_Parser parser = nullptr; ParserState state = START; + BookMetadataCache* cache; std::string currentLabel; std::string currentSrc; @@ -24,10 +24,8 @@ class TocNcxParser final : public Print { static void endElement(void* userData, const XML_Char* name); public: - std::vector toc; - - explicit TocNcxParser(const std::string& baseContentPath, const size_t xmlSize) - : baseContentPath(baseContentPath), remainingSize(xmlSize) {} + explicit TocNcxParser(const std::string& baseContentPath, const size_t xmlSize, BookMetadataCache* cache) + : baseContentPath(baseContentPath), remainingSize(xmlSize), cache(cache) {} ~TocNcxParser() override; bool setup(); diff --git a/lib/JpegToBmpConverter/JpegToBmpConverter.cpp b/lib/JpegToBmpConverter/JpegToBmpConverter.cpp index d34a3cc4..e1d0d0b4 100644 --- a/lib/JpegToBmpConverter/JpegToBmpConverter.cpp +++ b/lib/JpegToBmpConverter/JpegToBmpConverter.cpp @@ -208,6 +208,9 @@ bool JpegToBmpConverter::jpegFileToBmpStream(File& jpegFile, Print& bmpOut) { } // Process MCU block into MCU row buffer + // MCUs are composed of 8x8 blocks. For 16x16 MCUs, there are four 8x8 blocks: + // Block layout for 16x16 MCU: [0, 64] (top row of blocks) + // [128, 192] (bottom row of blocks) for (int blockY = 0; blockY < mcuPixelHeight; blockY++) { for (int blockX = 0; blockX < mcuPixelWidth; blockX++) { const int pixelX = mcuX * mcuPixelWidth + blockX; @@ -217,16 +220,27 @@ bool JpegToBmpConverter::jpegFileToBmpStream(File& jpegFile, Print& bmpOut) { continue; } + // Calculate which 8x8 block and position within that block + const int block8x8Col = blockX / 8; // 0 or 1 for 16-wide MCU + const int block8x8Row = blockY / 8; // 0 or 1 for 16-tall MCU + const int pixelInBlockX = blockX % 8; + const int pixelInBlockY = blockY % 8; + + // Calculate byte offset: each 8x8 block is 64 bytes + // Blocks are arranged: [0, 64], [128, 192] + const int blockOffset = (block8x8Row * (mcuPixelWidth / 8) + block8x8Col) * 64; + const int mcuIndex = blockOffset + pixelInBlockY * 8 + pixelInBlockX; + // Get grayscale value uint8_t gray; if (imageInfo.m_comps == 1) { // Grayscale image - gray = imageInfo.m_pMCUBufR[blockY * mcuPixelWidth + blockX]; + gray = imageInfo.m_pMCUBufR[mcuIndex]; } else { // RGB image - convert to grayscale - const uint8_t r = imageInfo.m_pMCUBufR[blockY * mcuPixelWidth + blockX]; - const uint8_t g = imageInfo.m_pMCUBufG[blockY * mcuPixelWidth + blockX]; - const uint8_t b = imageInfo.m_pMCUBufB[blockY * mcuPixelWidth + blockX]; + const uint8_t r = imageInfo.m_pMCUBufR[mcuIndex]; + const uint8_t g = imageInfo.m_pMCUBufG[mcuIndex]; + const uint8_t b = imageInfo.m_pMCUBufB[mcuIndex]; // Luminance formula: Y = 0.299*R + 0.587*G + 0.114*B // Using integer approximation: (30*R + 59*G + 11*B) / 100 gray = (r * 30 + g * 59 + b * 11) / 100; diff --git a/platformio.ini b/platformio.ini index 42de32c0..9cd5df2e 100644 --- a/platformio.ini +++ b/platformio.ini @@ -1,5 +1,5 @@ [platformio] -crosspoint_version = 0.8.1 +crosspoint_version = 0.9.0 default_envs = default [base] diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index 7843f5bb..6195ec22 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -212,7 +212,7 @@ void EpubReaderActivity::renderScreen() { } if (!section) { - const auto filepath = epub->getSpineItem(currentSpineIndex); + const auto filepath = epub->getSpineItem(currentSpineIndex).href; Serial.printf("[%lu] [ERS] Loading file: %s, index: %d\n", millis(), filepath.c_str(), currentSpineIndex); section = std::unique_ptr
(new Section(epub, currentSpineIndex, renderer)); if (!section->loadCacheMetadata(READER_FONT_ID, lineCompression, marginTop, marginRight, marginBottom, marginLeft, diff --git a/src/activities/reader/EpubReaderChapterSelectionActivity.cpp b/src/activities/reader/EpubReaderChapterSelectionActivity.cpp index 1cda06ea..3754fa04 100644 --- a/src/activities/reader/EpubReaderChapterSelectionActivity.cpp +++ b/src/activities/reader/EpubReaderChapterSelectionActivity.cpp @@ -29,7 +29,7 @@ void EpubReaderChapterSelectionActivity::onEnter() { // Trigger first update updateRequired = true; xTaskCreate(&EpubReaderChapterSelectionActivity::taskTrampoline, "EpubReaderChapterSelectionActivityTask", - 2048, // Stack size + 4096, // Stack size this, // Parameters 1, // Priority &displayTaskHandle // Task handle diff --git a/src/main.cpp b/src/main.cpp index 03f51508..b71ea399 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -158,6 +158,15 @@ void onGoHome() { enterNewActivity(new HomeActivity(renderer, inputManager, onGoToReaderHome, onGoToSettings, onGoToFileTransfer)); } +void setupDisplayAndFonts() { + einkDisplay.begin(); + Serial.printf("[%lu] [ ] Display initialized\n", millis()); + renderer.insertFont(READER_FONT_ID, bookerlyFontFamily); + renderer.insertFont(UI_FONT_ID, ubuntuFontFamily); + renderer.insertFont(SMALL_FONT_ID, smallFontFamily); + Serial.printf("[%lu] [ ] Fonts setup\n", millis()); +} + void setup() { t1 = millis(); @@ -177,8 +186,10 @@ void setup() { SPI.begin(EPD_SCLK, SD_SPI_MISO, EPD_MOSI, EPD_CS); // SD Card Initialization - if (!SD.begin(SD_SPI_CS, SPI, SPI_FQ)) { + // We need 6 open files concurrently when parsing a new chapter + if (!SD.begin(SD_SPI_CS, SPI, SPI_FQ, "/sd", 6)) { Serial.printf("[%lu] [ ] SD card initialization failed\n", millis()); + setupDisplayAndFonts(); exitActivity(); enterNewActivity(new FullScreenMessageActivity(renderer, inputManager, "SD card error", BOLD)); return; @@ -189,14 +200,7 @@ void setup() { // verify power button press duration after we've read settings. verifyWakeupLongPress(); - // Initialize display - einkDisplay.begin(); - Serial.printf("[%lu] [ ] Display initialized\n", millis()); - - renderer.insertFont(READER_FONT_ID, bookerlyFontFamily); - renderer.insertFont(UI_FONT_ID, ubuntuFontFamily); - renderer.insertFont(SMALL_FONT_ID, smallFontFamily); - Serial.printf("[%lu] [ ] Fonts setup\n", millis()); + setupDisplayAndFonts(); exitActivity(); enterNewActivity(new BootActivity(renderer, inputManager));