Merge pull request #172 from jpirnay/refactor-lowmemguard

refactor: Adopt low mem guard idea
This commit is contained in:
jpirnay
2026-05-04 21:37:57 +02:00
committed by GitHub
2 changed files with 70 additions and 16 deletions
+69 -16
View File
@@ -1,5 +1,6 @@
#include "ChapterHtmlSlimParser.h" #include "ChapterHtmlSlimParser.h"
#include <Arduino.h>
#include <FsHelpers.h> #include <FsHelpers.h>
#include <GfxRenderer.h> #include <GfxRenderer.h>
#include <HalStorage.h> #include <HalStorage.h>
@@ -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_HEARTBEAT = 30 * 1024;
constexpr size_t SIZE_FOR_PROGRESS_FINE = 80 * 1024; constexpr size_t SIZE_FOR_PROGRESS_FINE = 80 * 1024;
constexpr size_t PARSE_BUFFER_SIZE = 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"}; const char* BLOCK_TAGS[] = {"p", "li", "div", "br", "blockquote", "pre"};
constexpr int NUM_BLOCK_TAGS = sizeof(BLOCK_TAGS) / sizeof(BLOCK_TAGS[0]); constexpr int NUM_BLOCK_TAGS = sizeof(BLOCK_TAGS) / sizeof(BLOCK_TAGS[0]);
@@ -463,13 +467,49 @@ 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) { if (!src.empty() && self->imageRendering != 1) {
LOG_DBG("EHP", "Found image: src=%s", src.c_str()); LOG_DBG("EHP", "Found image: src=%s", src.c_str());
if (self->lowMemoryImageFallback) {
handleImageFallback();
return;
}
{ {
// Resolve the image path relative to the HTML file // Resolve the image path relative to the HTML file
std::string resolvedPath = FsHelpers::normalisePath(self->contentBase + src); 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) {
handleImageFallback();
return;
}
if (ImageDecoderFactory::isFormatSupported(resolvedPath)) { if (ImageDecoderFactory::isFormatSupported(resolvedPath)) {
// Create a unique filename for the cached image // Create a unique filename for the cached image
std::string ext; std::string ext;
@@ -483,13 +523,40 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
FsFile cachedImageFile; FsFile cachedImageFile;
bool extractSuccess = false; bool extractSuccess = false;
if (Storage.openFileForWrite("EHP", cachedImagePath, cachedImageFile)) { 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.flush();
cachedImageFile.close(); cachedImageFile.close();
if (!extractSuccess) {
Storage.remove(cachedImagePath.c_str());
}
delay(50); // Give SD card time to sync delay(50); // Give SD card time to sync
} }
if (extractSuccess) { 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<uint8_t>(CssTextAlign::None))
? CssTextAlign::Justify
: static_cast<CssTextAlign>(self->paragraphAlignment);
resetStyle.alignment = align;
self->currentTextBlock->setBlockStyle(resetStyle);
}
self->skipUntilDepth = self->depth;
self->depth += 1;
return;
}
// Get image dimensions
// Get image dimensions // Get image dimensions
ImageDimensions dims = {0, 0}; ImageDimensions dims = {0, 0};
ImageToFramebufferDecoder* decoder = ImageDecoderFactory::getDecoder(cachedImagePath); ImageToFramebufferDecoder* decoder = ImageDecoderFactory::getDecoder(cachedImagePath);
@@ -695,21 +762,7 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
} }
} }
// Fallback to alt text if image processing fails handleImageFallback();
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;
return; return;
} }
} }
@@ -57,6 +57,7 @@ class ChapterHtmlSlimParser final : public Print {
std::string contentBase; std::string contentBase;
std::string imageBasePath; std::string imageBasePath;
int imageCounter = 0; int imageCounter = 0;
bool lowMemoryImageFallback = false;
// Style tracking (replaces depth-based approach) // Style tracking (replaces depth-based approach)
struct StyleStackEntry { struct StyleStackEntry {