From c65863cee5caf08d99f21ea12bb585d509be9a82 Mon Sep 17 00:00:00 2001 From: jpirnay Date: Mon, 4 May 2026 19:55:18 +0200 Subject: [PATCH 1/2] Adopt crossinks low mem guard idea --- .../Epub/parsers/ChapterHtmlSlimParser.cpp | 68 ++++++++++++++++++- lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h | 1 + 2 files changed, 68 insertions(+), 1 deletion(-) diff --git a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp index f4b42591..b55e2d37 100644 --- a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp +++ b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp @@ -1,5 +1,6 @@ #include "ChapterHtmlSlimParser.h" +#include #include #include #include @@ -30,6 +31,9 @@ constexpr size_t MIN_SIZE_FOR_POPUP = 15 * 1024; constexpr size_t SIZE_FOR_PROGRESS_HEARTBEAT = 30 * 1024; constexpr size_t SIZE_FOR_PROGRESS_FINE = 80 * 1024; constexpr size_t PARSE_BUFFER_SIZE = 1024; +constexpr size_t IMAGE_EXTRACT_CHUNK_SIZE = 1024; +constexpr size_t MIN_FREE_HEAP_FOR_IMAGE_EXTRACT = 48 * 1024; +constexpr size_t MIN_MAX_ALLOC_FOR_IMAGE_EXTRACT = 36 * 1024; const char* BLOCK_TAGS[] = {"p", "li", "div", "br", "blockquote", "pre"}; constexpr int NUM_BLOCK_TAGS = sizeof(BLOCK_TAGS) / sizeof(BLOCK_TAGS[0]); @@ -466,10 +470,48 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* if (!src.empty() && self->imageRendering != 1) { LOG_DBG("EHP", "Found image: src=%s", src.c_str()); + if (self->lowMemoryImageFallback) { + if (self->currentTextBlock && self->currentTextBlock->isEmpty()) { + BlockStyle resetStyle; + resetStyle.textAlignDefined = true; + const auto align = (self->paragraphAlignment == static_cast(CssTextAlign::None)) + ? CssTextAlign::Justify + : static_cast(self->paragraphAlignment); + resetStyle.alignment = align; + self->currentTextBlock->setBlockStyle(resetStyle); + } + self->skipUntilDepth = self->depth; + self->depth += 1; + return; + } + { // Resolve the image path relative to the HTML file std::string resolvedPath = FsHelpers::normalisePath(self->contentBase + src); + const uint32_t freeHeap = ESP.getFreeHeap(); + const uint32_t maxAllocHeap = ESP.getMaxAllocHeap(); + if (!self->lowMemoryImageFallback && + (freeHeap < MIN_FREE_HEAP_FOR_IMAGE_EXTRACT || maxAllocHeap < MIN_MAX_ALLOC_FOR_IMAGE_EXTRACT)) { + self->lowMemoryImageFallback = true; + LOG_ERR("EHP", "Low heap before image extraction (%u free, %u max alloc); suppressing inline images", + freeHeap, maxAllocHeap); + } + if (self->lowMemoryImageFallback) { + if (self->currentTextBlock && self->currentTextBlock->isEmpty()) { + BlockStyle resetStyle; + resetStyle.textAlignDefined = true; + const auto align = (self->paragraphAlignment == static_cast(CssTextAlign::None)) + ? CssTextAlign::Justify + : static_cast(self->paragraphAlignment); + resetStyle.alignment = align; + self->currentTextBlock->setBlockStyle(resetStyle); + } + self->skipUntilDepth = self->depth; + self->depth += 1; + return; + } + if (ImageDecoderFactory::isFormatSupported(resolvedPath)) { // Create a unique filename for the cached image std::string ext; @@ -483,13 +525,37 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* FsFile cachedImageFile; bool extractSuccess = false; if (Storage.openFileForWrite("EHP", cachedImagePath, cachedImageFile)) { - extractSuccess = self->epub->readItemContentsToStream(resolvedPath, cachedImageFile, 4096); + extractSuccess = + self->epub->readItemContentsToStream(resolvedPath, cachedImageFile, IMAGE_EXTRACT_CHUNK_SIZE); cachedImageFile.flush(); cachedImageFile.close(); delay(50); // Give SD card time to sync } if (extractSuccess) { + const uint32_t postExtractFreeHeap = ESP.getFreeHeap(); + const uint32_t postExtractMaxAllocHeap = ESP.getMaxAllocHeap(); + if (postExtractFreeHeap < MIN_FREE_HEAP_FOR_IMAGE_EXTRACT || + postExtractMaxAllocHeap < MIN_MAX_ALLOC_FOR_IMAGE_EXTRACT) { + self->lowMemoryImageFallback = true; + LOG_ERR("EHP", + "Low heap after image extraction (%u free, %u max alloc); suppressing remaining inline images", + postExtractFreeHeap, postExtractMaxAllocHeap); + Storage.remove(cachedImagePath.c_str()); + if (self->currentTextBlock && self->currentTextBlock->isEmpty()) { + BlockStyle resetStyle; + resetStyle.textAlignDefined = true; + const auto align = (self->paragraphAlignment == static_cast(CssTextAlign::None)) + ? CssTextAlign::Justify + : static_cast(self->paragraphAlignment); + resetStyle.alignment = align; + self->currentTextBlock->setBlockStyle(resetStyle); + } + self->skipUntilDepth = self->depth; + self->depth += 1; + return; + } + // Get image dimensions // Get image dimensions ImageDimensions dims = {0, 0}; ImageToFramebufferDecoder* decoder = ImageDecoderFactory::getDecoder(cachedImagePath); diff --git a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h index cb2df47a..5e5e4250 100644 --- a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h +++ b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h @@ -57,6 +57,7 @@ class ChapterHtmlSlimParser final : public Print { std::string contentBase; std::string imageBasePath; int imageCounter = 0; + bool lowMemoryImageFallback = false; // Style tracking (replaces depth-based approach) struct StyleStackEntry { From 0afd1d196f433450a99876e5aeb4851885e42324 Mon Sep 17 00:00:00 2001 From: jpirnay Date: Mon, 4 May 2026 20:41:40 +0200 Subject: [PATCH 2/2] Review --- .../Epub/parsers/ChapterHtmlSlimParser.cpp | 61 ++++++++----------- 1 file changed, 24 insertions(+), 37 deletions(-) diff --git a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp index b55e2d37..132a29c9 100644 --- a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp +++ b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp @@ -467,21 +467,29 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* } } + const auto handleImageFallback = [&]() { + // Fallback to alt text if image processing fails. + if (!alt.empty()) { + alt = "[Image: " + alt + "]"; + self->startNewTextBlock(centeredBlockStyle); + self->italicUntilDepth = std::min(self->italicUntilDepth, self->depth); + self->depth += 1; + self->characterData(userData, alt.c_str(), alt.length()); + // Skip any child content (skip until parent as we pre-advanced depth above) + self->skipUntilDepth = self->depth - 1; + return; + } + + // No alt text, skip. + self->skipUntilDepth = self->depth; + self->depth += 1; + }; + if (!src.empty() && self->imageRendering != 1) { LOG_DBG("EHP", "Found image: src=%s", src.c_str()); if (self->lowMemoryImageFallback) { - if (self->currentTextBlock && self->currentTextBlock->isEmpty()) { - BlockStyle resetStyle; - resetStyle.textAlignDefined = true; - const auto align = (self->paragraphAlignment == static_cast(CssTextAlign::None)) - ? CssTextAlign::Justify - : static_cast(self->paragraphAlignment); - resetStyle.alignment = align; - self->currentTextBlock->setBlockStyle(resetStyle); - } - self->skipUntilDepth = self->depth; - self->depth += 1; + handleImageFallback(); return; } @@ -498,17 +506,7 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* freeHeap, maxAllocHeap); } if (self->lowMemoryImageFallback) { - if (self->currentTextBlock && self->currentTextBlock->isEmpty()) { - BlockStyle resetStyle; - resetStyle.textAlignDefined = true; - const auto align = (self->paragraphAlignment == static_cast(CssTextAlign::None)) - ? CssTextAlign::Justify - : static_cast(self->paragraphAlignment); - resetStyle.alignment = align; - self->currentTextBlock->setBlockStyle(resetStyle); - } - self->skipUntilDepth = self->depth; - self->depth += 1; + handleImageFallback(); return; } @@ -529,6 +527,9 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* self->epub->readItemContentsToStream(resolvedPath, cachedImageFile, IMAGE_EXTRACT_CHUNK_SIZE); cachedImageFile.flush(); cachedImageFile.close(); + if (!extractSuccess) { + Storage.remove(cachedImagePath.c_str()); + } delay(50); // Give SD card time to sync } @@ -761,21 +762,7 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* } } - // Fallback to alt text if image processing fails - if (!alt.empty()) { - alt = "[Image: " + alt + "]"; - self->startNewTextBlock(centeredBlockStyle); - self->italicUntilDepth = std::min(self->italicUntilDepth, self->depth); - self->depth += 1; - self->characterData(userData, alt.c_str(), alt.length()); - // Skip any child content (skip until parent as we pre-advanced depth above) - self->skipUntilDepth = self->depth - 1; - return; - } - - // No alt text, skip - self->skipUntilDepth = self->depth; - self->depth += 1; + handleImageFallback(); return; } }