diff --git a/lib/Epub/Epub/ParsedText.cpp b/lib/Epub/Epub/ParsedText.cpp index c2f13d8b..7a045d56 100644 --- a/lib/Epub/Epub/ParsedText.cpp +++ b/lib/Epub/Epub/ParsedText.cpp @@ -106,6 +106,18 @@ std::vector ParsedText::computeLineBreaks(const int pageWidth, const int ans[i] = j; // j is the index of the last word in this optimal line } } + + // Handle oversized word: if no valid configuration found, force single-word line + // This prevents cascade failure where one oversized word breaks all preceding words + if (dp[i] == MAX_COST) { + ans[i] = i; // Just this word on its own line + // Inherit cost from next word to allow subsequent words to find valid configurations + if (i + 1 < static_cast(totalWordCount)) { + dp[i] = dp[i + 1]; + } else { + dp[i] = 0; + } + } } // Stores the index of the word that starts the next line (last_word_index + 1) diff --git a/lib/Epub/Epub/Section.cpp b/lib/Epub/Epub/Section.cpp index 5323a7a5..bd46d35c 100644 --- a/lib/Epub/Epub/Section.cpp +++ b/lib/Epub/Epub/Section.cpp @@ -115,26 +115,56 @@ 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 bool extraParagraphSpacing, const std::function& progressSetupFn, + const std::function& progressFn) { + constexpr size_t MIN_SIZE_FOR_PROGRESS = 50 * 1024; // 50KB 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)) { - return false; + + // Retry logic for SD card timing issues + bool success = false; + size_t fileSize = 0; + for (int attempt = 0; attempt < 3 && !success; attempt++) { + if (attempt > 0) { + Serial.printf("[%lu] [SCT] Retrying stream (attempt %d)...\n", millis(), attempt + 1); + delay(50); // Brief delay before retry + } + + // Remove any incomplete file from previous attempt before retrying + if (SD.exists(tmpHtmlPath.c_str())) { + SD.remove(tmpHtmlPath.c_str()); + } + + File tmpHtml; + if (!FsHelpers::openFileForWrite("SCT", tmpHtmlPath, tmpHtml)) { + continue; + } + success = epub->readItemContentsToStream(localPath, tmpHtml, 1024); + fileSize = tmpHtml.size(); + tmpHtml.close(); + + // If streaming failed, remove the incomplete file immediately + if (!success && SD.exists(tmpHtmlPath.c_str())) { + SD.remove(tmpHtmlPath.c_str()); + Serial.printf("[%lu] [SCT] Removed incomplete temp file after failed attempt\n", millis()); + } } - bool success = epub->readItemContentsToStream(localPath, tmpHtml, 1024); - tmpHtml.close(); if (!success) { - Serial.printf("[%lu] [SCT] Failed to stream item contents to temp file\n", millis()); + Serial.printf("[%lu] [SCT] Failed to stream item contents to temp file after retries\n", millis()); return false; } - Serial.printf("[%lu] [SCT] Streamed temp HTML to %s\n", millis(), tmpHtmlPath.c_str()); + Serial.printf("[%lu] [SCT] Streamed temp HTML to %s (%d bytes)\n", millis(), tmpHtmlPath.c_str(), fileSize); - ChapterHtmlSlimParser visitor(tmpHtmlPath, renderer, fontId, lineCompression, marginTop, marginRight, marginBottom, - marginLeft, extraParagraphSpacing, - [this](std::unique_ptr page) { this->onPageComplete(std::move(page)); }); + // Only show progress bar for larger chapters where rendering overhead is worth it + if (progressSetupFn && fileSize >= MIN_SIZE_FOR_PROGRESS) { + progressSetupFn(); + } + + ChapterHtmlSlimParser visitor( + tmpHtmlPath, renderer, fontId, lineCompression, marginTop, marginRight, marginBottom, marginLeft, + extraParagraphSpacing, [this](std::unique_ptr page) { this->onPageComplete(std::move(page)); }, progressFn); success = visitor.parseAndBuildPages(); SD.remove(tmpHtmlPath.c_str()); diff --git a/lib/Epub/Epub/Section.h b/lib/Epub/Epub/Section.h index d7a2c721..09a2f90b 100644 --- a/lib/Epub/Epub/Section.h +++ b/lib/Epub/Epub/Section.h @@ -1,4 +1,5 @@ #pragma once +#include #include #include "Epub.h" @@ -31,6 +32,8 @@ class Section { void setupCacheDir() const; bool clearCache() const; bool persistPageDataToSD(int fontId, float lineCompression, int marginTop, int marginRight, int marginBottom, - int marginLeft, bool extraParagraphSpacing); + int marginLeft, bool extraParagraphSpacing, + const std::function& progressSetupFn = nullptr, + const std::function& progressFn = nullptr); std::unique_ptr loadPageFromSD() const; }; diff --git a/lib/Epub/Epub/blocks/TextBlock.cpp b/lib/Epub/Epub/blocks/TextBlock.cpp index bb8b14e8..ef6fdb5d 100644 --- a/lib/Epub/Epub/blocks/TextBlock.cpp +++ b/lib/Epub/Epub/blocks/TextBlock.cpp @@ -4,11 +4,18 @@ #include void TextBlock::render(const GfxRenderer& renderer, const int fontId, const int x, const int y) const { + // Validate iterator bounds before rendering + if (words.size() != wordXpos.size() || words.size() != wordStyles.size()) { + Serial.printf("[%lu] [TXB] Render skipped: size mismatch (words=%u, xpos=%u, styles=%u)\n", millis(), + (uint32_t)words.size(), (uint32_t)wordXpos.size(), (uint32_t)wordStyles.size()); + return; + } + auto wordIt = words.begin(); auto wordStylesIt = wordStyles.begin(); auto wordXposIt = wordXpos.begin(); - for (int i = 0; i < words.size(); i++) { + for (size_t i = 0; i < words.size(); i++) { renderer.drawText(fontId, *wordXposIt + x, y, wordIt->c_str(), true, *wordStylesIt); std::advance(wordIt, 1); @@ -46,6 +53,13 @@ std::unique_ptr TextBlock::deserialize(File& file) { // words serialization::readPod(file, wc); + + // Sanity check: prevent allocation of unreasonably large lists (max 10000 words per block) + if (wc > 10000) { + Serial.printf("[%lu] [TXB] Deserialization failed: word count %u exceeds maximum\n", millis(), wc); + return nullptr; + } + words.resize(wc); for (auto& w : words) serialization::readString(file, w); @@ -59,6 +73,13 @@ std::unique_ptr TextBlock::deserialize(File& file) { wordStyles.resize(sc); for (auto& s : wordStyles) serialization::readPod(file, s); + // Validate data consistency: all three lists must have the same size + if (wc != xc || wc != sc) { + Serial.printf("[%lu] [TXB] Deserialization failed: size mismatch (words=%u, xpos=%u, styles=%u)\n", millis(), wc, + xc, sc); + return nullptr; + } + // style serialization::readPod(file, style); diff --git a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp index 766e5ca6..d2f1c3e6 100644 --- a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp +++ b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp @@ -11,6 +11,9 @@ const char* HEADER_TAGS[] = {"h1", "h2", "h3", "h4", "h5", "h6"}; constexpr int NUM_HEADER_TAGS = sizeof(HEADER_TAGS) / sizeof(HEADER_TAGS[0]); +// Minimum file size (in bytes) to show progress bar - smaller chapters don't benefit from it +constexpr size_t MIN_SIZE_FOR_PROGRESS = 50 * 1024; // 50KB + const char* BLOCK_TAGS[] = {"p", "li", "div", "br", "blockquote"}; constexpr int NUM_BLOCK_TAGS = sizeof(BLOCK_TAGS) / sizeof(BLOCK_TAGS[0]); @@ -221,6 +224,11 @@ bool ChapterHtmlSlimParser::parseAndBuildPages() { return false; } + // Get file size for progress calculation + const size_t totalSize = file.size(); + size_t bytesRead = 0; + int lastProgress = -1; + XML_SetUserData(parser, this); XML_SetElementHandler(parser, startElement, endElement); XML_SetCharacterDataHandler(parser, characterData); @@ -249,6 +257,17 @@ bool ChapterHtmlSlimParser::parseAndBuildPages() { return false; } + // Update progress (call every 10% change to avoid too frequent updates) + // Only show progress for larger chapters where rendering overhead is worth it + bytesRead += len; + if (progressFn && totalSize >= MIN_SIZE_FOR_PROGRESS) { + const int progress = static_cast((bytesRead * 100) / totalSize); + if (lastProgress / 10 != progress / 10) { + lastProgress = progress; + progressFn(progress); + } + } + done = file.available() == 0; if (XML_ParseBuffer(parser, static_cast(len), done) == XML_STATUS_ERROR) { diff --git a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h index 7f74602a..7d753173 100644 --- a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h +++ b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h @@ -18,6 +18,7 @@ class ChapterHtmlSlimParser { const std::string& filepath; GfxRenderer& renderer; std::function)> completePageFn; + std::function progressFn; // Progress callback (0-100) int depth = 0; int skipUntilDepth = INT_MAX; int boldUntilDepth = INT_MAX; @@ -48,7 +49,8 @@ class ChapterHtmlSlimParser { explicit ChapterHtmlSlimParser(const std::string& filepath, GfxRenderer& renderer, const int fontId, const float lineCompression, const int marginTop, const int marginRight, const int marginBottom, const int marginLeft, const bool extraParagraphSpacing, - const std::function)>& completePageFn) + const std::function)>& completePageFn, + const std::function& progressFn = nullptr) : filepath(filepath), renderer(renderer), fontId(fontId), @@ -58,7 +60,8 @@ class ChapterHtmlSlimParser { marginBottom(marginBottom), marginLeft(marginLeft), extraParagraphSpacing(extraParagraphSpacing), - completePageFn(completePageFn) {} + completePageFn(completePageFn), + progressFn(progressFn) {} ~ChapterHtmlSlimParser() = default; bool parseAndBuildPages(); void addLineToPage(std::shared_ptr line); diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index b2242376..0dfda4bb 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -227,23 +227,51 @@ void EpubReaderActivity::renderScreen() { SETTINGS.extraParagraphSpacing)) { Serial.printf("[%lu] [ERS] Cache not found, building...\n", millis()); + // Progress bar dimensions + constexpr int barWidth = 200; + constexpr int barHeight = 10; + constexpr int boxMargin = 20; + const int textWidth = renderer.getTextWidth(READER_FONT_ID, "Indexing..."); + const int boxWidthWithBar = (barWidth > textWidth ? barWidth : textWidth) + boxMargin * 2; + const int boxWidthNoBar = textWidth + boxMargin * 2; + const int boxHeightWithBar = renderer.getLineHeight(READER_FONT_ID) + barHeight + boxMargin * 3; + const int boxHeightNoBar = renderer.getLineHeight(READER_FONT_ID) + boxMargin * 2; + const int boxXWithBar = (GfxRenderer::getScreenWidth() - boxWidthWithBar) / 2; + const int boxXNoBar = (GfxRenderer::getScreenWidth() - boxWidthNoBar) / 2; + constexpr int boxY = 50; + const int barX = boxXWithBar + (boxWidthWithBar - barWidth) / 2; + const int barY = boxY + renderer.getLineHeight(READER_FONT_ID) + boxMargin * 2; + + // Always show "Indexing..." text first { - const int textWidth = renderer.getTextWidth(READER_FONT_ID, "Indexing..."); - constexpr int margin = 20; - const int x = (GfxRenderer::getScreenWidth() - textWidth - margin * 2) / 2; - constexpr int y = 50; - const int w = textWidth + margin * 2; - const int h = renderer.getLineHeight(READER_FONT_ID) + margin * 2; - renderer.fillRect(x, y, w, h, false); - renderer.drawText(READER_FONT_ID, x + margin, y + margin, "Indexing..."); - renderer.drawRect(x + 5, y + 5, w - 10, h - 10); + renderer.fillRect(boxXNoBar, boxY, boxWidthNoBar, boxHeightNoBar, false); + renderer.drawText(READER_FONT_ID, boxXNoBar + boxMargin, boxY + boxMargin, "Indexing..."); + renderer.drawRect(boxXNoBar + 5, boxY + 5, boxWidthNoBar - 10, boxHeightNoBar - 10); renderer.displayBuffer(); pagesUntilFullRefresh = 0; } section->setupCacheDir(); + + // Setup callback - only called for chapters >= 50KB, redraws with progress bar + auto progressSetup = [this, boxXWithBar, boxWidthWithBar, boxHeightWithBar, boxMargin, barX, barY, barWidth, + barHeight]() { + renderer.fillRect(boxXWithBar, boxY, boxWidthWithBar, boxHeightWithBar, false); + renderer.drawText(READER_FONT_ID, boxXWithBar + boxMargin, boxY + boxMargin, "Indexing..."); + renderer.drawRect(boxXWithBar + 5, boxY + 5, boxWidthWithBar - 10, boxHeightWithBar - 10); + renderer.drawRect(barX, barY, barWidth, barHeight); + renderer.displayBuffer(); + }; + + // Progress callback to update progress bar + auto progressCallback = [this, barX, barY, barWidth, barHeight](int progress) { + const int fillWidth = (barWidth - 2) * progress / 100; + renderer.fillRect(barX + 1, barY + 1, fillWidth, barHeight - 2, true); + renderer.displayBuffer(EInkDisplay::FAST_REFRESH); + }; + if (!section->persistPageDataToSD(READER_FONT_ID, lineCompression, marginTop, marginRight, marginBottom, - marginLeft, SETTINGS.extraParagraphSpacing)) { + marginLeft, SETTINGS.extraParagraphSpacing, progressSetup, progressCallback)) { Serial.printf("[%lu] [ERS] Failed to persist page data to SD\n", millis()); section.reset(); return;