Merge pull request #172 from jpirnay/refactor-lowmemguard
refactor: Adopt low mem guard idea
This commit is contained in:
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user