diff --git a/lib/Epub/Epub/Section.cpp b/lib/Epub/Epub/Section.cpp index 3edb1e70..78b1632f 100644 --- a/lib/Epub/Epub/Section.cpp +++ b/lib/Epub/Epub/Section.cpp @@ -36,6 +36,134 @@ inline uint32_t paragraphLutEntryOffset(uint32_t lutStart, uint16_t page) { } } // namespace +#include + +namespace { +constexpr uint32_t FNV_PRIME = 0x01000193; // 16777619 +constexpr uint32_t FNV_OFFSET_BASIS = 0x811C9DC5; // 2166136261 + +uint32_t fnv1a(const uint8_t* data, size_t length) { + uint32_t hash = FNV_OFFSET_BASIS; + for (size_t i = 0; i < length; ++i) { + hash ^= data[i]; + hash *= FNV_PRIME; + } + return hash; +} +} // namespace + +uint32_t Section::calculatePropertyHash(int fontId, float lineCompression, bool extraParagraphSpacing, + uint8_t paragraphAlignment, uint16_t viewportWidth, uint16_t viewportHeight, + bool hyphenationEnabled, bool embeddedStyle, bool bionicReadingEnabled, + uint8_t imageRendering) { + uint8_t buffer[64]; + size_t offset = 0; + + auto append = [&](const void* ptr, size_t size) { + memcpy(buffer + offset, ptr, size); + offset += size; + }; + + append(&fontId, sizeof(fontId)); + append(&lineCompression, sizeof(lineCompression)); + append(&extraParagraphSpacing, sizeof(extraParagraphSpacing)); + append(¶graphAlignment, sizeof(paragraphAlignment)); + append(&viewportWidth, sizeof(viewportWidth)); + append(&viewportHeight, sizeof(viewportHeight)); + append(&hyphenationEnabled, sizeof(hyphenationEnabled)); + append(&embeddedStyle, sizeof(embeddedStyle)); + append(&bionicReadingEnabled, sizeof(bionicReadingEnabled)); + append(&imageRendering, sizeof(imageRendering)); + + return fnv1a(buffer, offset); +} + +std::string Section::getSectionFilePath(uint32_t propertyHash) const { + char buf[32]; + snprintf(buf, sizeof(buf), "%d_%08x", spineIndex, propertyHash); + return epub->getCachePath() + "/sections/" + buf + ".bin"; +} + +std::string Section::getImageBasePath(uint32_t propertyHash) const { + char buf[32]; + snprintf(buf, sizeof(buf), "img_%d_%08x_", spineIndex, propertyHash); + return epub->getCachePath() + "/" + buf; +} + +struct SectionVariant { + std::string filename; + uint16_t date; + uint16_t time; +}; + +void Section::evictOldVariants() const { + // We keep up to 3 most recently accessed/modified variants to prevent SD card bloat + constexpr size_t MAX_VARIANTS = 3; + + std::string sectionsDir = epub->getCachePath() + "/sections"; + auto files = Storage.listFiles(sectionsDir.c_str(), 100); + + std::vector variants; + + // Find all cache variants belonging to this spineIndex + char prefix[16]; + snprintf(prefix, sizeof(prefix), "%d_", spineIndex); + size_t prefixLen = strlen(prefix); + + for (const auto& file : files) { + if (file.startsWith(prefix) && file.endsWith(".bin")) { + HalFile hf = Storage.open((sectionsDir + "/" + file.c_str()).c_str(), O_RDONLY); + if (hf) { + uint16_t md, mt; + if (hf.getModifyDateTime(&md, &mt)) { + variants.push_back({file.c_str(), md, mt}); + } else { + // If we can't get modified time, assume it's very old to evict it + variants.push_back({file.c_str(), 0, 0}); + } + } + } + } + + if (variants.size() <= MAX_VARIANTS) return; + + // Sort descending by modified date and time + std::sort(variants.begin(), variants.end(), [](const SectionVariant& a, const SectionVariant& b) { + if (a.date != b.date) return a.date > b.date; + return a.time > b.time; + }); + + // Delete everything after MAX_VARIANTS limit + for (size_t i = MAX_VARIANTS; i < variants.size(); ++i) { + std::string targetPath = sectionsDir + "/" + variants[i].filename; + Storage.remove(targetPath.c_str()); + LOG_DBG("SCT", "Evicted old section cache: %s", targetPath.c_str()); + + // Extract the hash to also clean up associated images + // Filename format: spineIndex_hash.bin + size_t underscore = variants[i].filename.find('_'); + size_t dot = variants[i].filename.find('.'); + if (underscore != std::string::npos && dot != std::string::npos && dot > underscore) { + std::string hashStr = variants[i].filename.substr(underscore + 1, dot - underscore - 1); + uint32_t parsedHash = strtoul(hashStr.c_str(), nullptr, 16); + if (parsedHash != 0 || hashStr == "00000000") { + std::string imgBasePath = getImageBasePath(parsedHash); + // Find and delete matching images + auto rootFiles = Storage.listFiles(epub->getCachePath().c_str(), 100); + size_t lastSlash = imgBasePath.find_last_of('/'); + std::string imgPrefix = (lastSlash != std::string::npos) ? imgBasePath.substr(lastSlash + 1) : imgBasePath; + + for (const auto& rf : rootFiles) { + if (rf.startsWith(imgPrefix.c_str())) { + Storage.remove((epub->getCachePath() + "/" + rf.c_str()).c_str()); + LOG_DBG("SCT", "Evicted old image cache: %s", rf.c_str()); + } + } + } + } + } +} + uint32_t Section::onPageComplete(std::unique_ptr page) { if (!file) { LOG_ERR("SCT", "File not open for writing page %d", pageCount); @@ -89,6 +217,11 @@ bool Section::loadSectionFile(const int fontId, const float lineCompression, con const uint8_t paragraphAlignment, const uint16_t viewportWidth, const uint16_t viewportHeight, const bool hyphenationEnabled, const bool embeddedStyle, const bool bionicReadingEnabled, const uint8_t imageRendering) { + uint32_t propertyHash = + calculatePropertyHash(fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth, + viewportHeight, hyphenationEnabled, embeddedStyle, bionicReadingEnabled, imageRendering); + filePath = getSectionFilePath(propertyHash); + if (!Storage.openFileForRead("SCT", filePath, file)) { return false; } @@ -195,6 +328,11 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c const uint16_t viewportHeight, const bool hyphenationEnabled, const bool embeddedStyle, const bool bionicReadingEnabled, const uint8_t imageRendering, const std::function& progressFn) { + uint32_t propertyHash = + calculatePropertyHash(fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth, + viewportHeight, hyphenationEnabled, embeddedStyle, bionicReadingEnabled, imageRendering); + filePath = getSectionFilePath(propertyHash); + const uint32_t phaseTotalStart = millis(); const auto localPath = epub->getSpineItem(spineIndex).href; @@ -222,7 +360,10 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c // Derive the content base directory and image cache path prefix for the parser size_t lastSlash = localPath.find_last_of('/'); std::string contentBase = (lastSlash != std::string::npos) ? localPath.substr(0, lastSlash + 1) : ""; - std::string imageBasePath = epub->getCachePath() + "/img_" + std::to_string(spineIndex) + "_"; + std::string imageBasePath = getImageBasePath(propertyHash); + + // Evict old variants for this spine to keep cache size controlled + evictOldVariants(); CssParser* cssParser = nullptr; if (embeddedStyle) { diff --git a/lib/Epub/Epub/Section.h b/lib/Epub/Epub/Section.h index e4bdab5f..6c2f451b 100644 --- a/lib/Epub/Epub/Section.h +++ b/lib/Epub/Epub/Section.h @@ -38,15 +38,26 @@ class Section { // Caller is responsible for closing `outFile`. Returns false on any I/O or validation error. bool readParagraphLutHeader(FsFile& outFile, uint16_t& outCount, uint32_t& outLutStart) const; + // Calculates a stable hash for a given set of rendering properties. + // Used to suffix cache files so multiple variants can coexist safely without constant recompilation. + static uint32_t calculatePropertyHash(int fontId, float lineCompression, bool extraParagraphSpacing, + uint8_t paragraphAlignment, uint16_t viewportWidth, uint16_t viewportHeight, + bool hyphenationEnabled, bool embeddedStyle, bool bionicReadingEnabled, + uint8_t imageRendering); + + // Computes the active file path for this section based on rendering properties + std::string getSectionFilePath(uint32_t propertyHash) const; + // Computes the image base path for extract images related to this specific section variant + std::string getImageBasePath(uint32_t propertyHash) const; + // Garbage collection: Keep only the most recent N variants per chapter + void evictOldVariants() const; + public: uint16_t pageCount = 0; int currentPage = 0; explicit Section(const std::shared_ptr& epub, const int spineIndex, GfxRenderer& renderer) - : epub(epub), - spineIndex(spineIndex), - renderer(renderer), - filePath(epub->getCachePath() + "/sections/" + std::to_string(spineIndex) + ".bin") {} + : epub(epub), spineIndex(spineIndex), renderer(renderer) {} ~Section() = default; bool loadSectionFile(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment, uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled, bool embeddedStyle, diff --git a/src/SettingsList.h b/src/SettingsList.h index 75d10ac2..12b4503b 100644 --- a/src/SettingsList.h +++ b/src/SettingsList.h @@ -141,12 +141,12 @@ inline const std::vector list = { // All entries share the same ordered action-label list; the submenu groups them behind // a single placeholder row in the device UI. // Shared action options (everything except the first "default" entry). -#define BTN_ACT_OPTIONS \ - StrId::STR_BTN_ACT_PAGE_FORWARD, StrId::STR_BTN_ACT_PAGE_BACK, StrId::STR_BTN_ACT_PAGE_FORWARD_10, \ - StrId::STR_BTN_ACT_PAGE_BACK_10, StrId::STR_BTN_ACT_GO_HOME, StrId::STR_BTN_ACT_SLEEP, \ +#define BTN_ACT_OPTIONS \ + StrId::STR_BTN_ACT_PAGE_FORWARD, StrId::STR_BTN_ACT_PAGE_BACK, StrId::STR_BTN_ACT_PAGE_FORWARD_10, \ + StrId::STR_BTN_ACT_PAGE_BACK_10, StrId::STR_BTN_ACT_GO_HOME, StrId::STR_BTN_ACT_SLEEP, \ StrId::STR_BTN_ACT_FORCE_REFRESH, StrId::STR_BTN_ACT_FORCE_FAST_REFRESH, StrId::STR_BTN_ACT_OPEN_TOC, \ - StrId::STR_BTN_ACT_OPEN_BOOKMARKS, StrId::STR_BTN_ACT_STAR_PAGE, StrId::STR_BTN_ACT_FOOTNOTES, \ - StrId::STR_BTN_ACT_NEXT_SECTION, StrId::STR_BTN_ACT_PREV_SECTION, StrId::STR_BTN_ACT_EXIT_READER, \ + StrId::STR_BTN_ACT_OPEN_BOOKMARKS, StrId::STR_BTN_ACT_STAR_PAGE, StrId::STR_BTN_ACT_FOOTNOTES, \ + StrId::STR_BTN_ACT_NEXT_SECTION, StrId::STR_BTN_ACT_PREV_SECTION, StrId::STR_BTN_ACT_EXIT_READER, \ StrId::STR_BTN_ACT_READER_MENU, StrId::STR_BTN_ACT_TOGGLE_BIONIC_READING, StrId::STR_BTN_ACT_KOREADER_SYNC // Back button: short=exit reader, double=ignore, long=go home