Reduce memory footprint

This commit is contained in:
jpirnay
2026-05-11 21:50:44 +02:00
parent 5b3f1ad200
commit fb16db2b48
7 changed files with 373 additions and 32 deletions
+68 -15
View File
@@ -12,7 +12,7 @@
#include "parsers/ChapterHtmlSlimParser.h"
namespace {
constexpr uint8_t SECTION_FILE_VERSION = 26;
constexpr uint8_t SECTION_FILE_VERSION = 27;
constexpr uint32_t HEADER_SIZE = sizeof(uint8_t) + // SECTION_FILE_VERSION
sizeof(int) + // fontId
sizeof(float) + // lineCompression
@@ -20,6 +20,7 @@ constexpr uint32_t HEADER_SIZE = sizeof(uint8_t) + // SECTION_FILE_VERSION
sizeof(uint8_t) + // paragraphAlignment
sizeof(uint16_t) + // viewportWidth
sizeof(uint16_t) + // viewportHeight
sizeof(bool) + // parseComplete
sizeof(uint16_t) + // pageCount (stored as 16-bit in header)
sizeof(bool) + // hyphenationEnabled
sizeof(bool) + // embeddedStyle
@@ -195,9 +196,9 @@ void Section::writeSectionFileHeader(const int fontId, const float lineCompressi
}
static_assert(HEADER_SIZE == sizeof(SECTION_FILE_VERSION) + sizeof(fontId) + sizeof(lineCompression) +
sizeof(extraParagraphSpacing) + sizeof(paragraphAlignment) + sizeof(viewportWidth) +
sizeof(viewportHeight) + sizeof(pageCount) + sizeof(hyphenationEnabled) +
sizeof(embeddedStyle) + sizeof(bionicReadingEnabled) + sizeof(imageRendering) +
sizeof(uint32_t) + sizeof(uint32_t) + sizeof(uint32_t),
sizeof(viewportHeight) + sizeof(bool) + sizeof(pageCount) +
sizeof(hyphenationEnabled) + sizeof(embeddedStyle) + sizeof(bionicReadingEnabled) +
sizeof(imageRendering) + sizeof(uint32_t) + sizeof(uint32_t) + sizeof(uint32_t),
"Header size mismatch");
serialization::writePod(file, SECTION_FILE_VERSION);
serialization::writePod(file, fontId);
@@ -210,6 +211,7 @@ void Section::writeSectionFileHeader(const int fontId, const float lineCompressi
serialization::writePod(file, embeddedStyle);
serialization::writePod(file, bionicReadingEnabled);
serialization::writePod(file, imageRendering);
serialization::writePod(file, false); // Placeholder for parseComplete (patched later)
serialization::writePod(file, pageCount); // Placeholder for page count (will be initially 0, patched later)
serialization::writePod(file, static_cast<uint32_t>(0)); // Placeholder for LUT offset (patched later)
serialization::writePod(file, static_cast<uint32_t>(0)); // Placeholder for anchor map offset (patched later)
@@ -220,13 +222,30 @@ 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) {
truncatedCache = false;
uint32_t propertyHash =
calculatePropertyHash(fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth,
viewportHeight, hyphenationEnabled, embeddedStyle, bionicReadingEnabled, imageRendering);
filePath = getSectionFilePath(propertyHash);
bool usingEmbeddedStyleFallback = false;
if (!Storage.openFileForRead("SCT", filePath, file)) {
return false;
// Fallback: allow loading a no-CSS cache variant when embedded CSS is enabled.
if (embeddedStyle) {
const uint32_t fallbackHash =
calculatePropertyHash(fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth,
viewportHeight, hyphenationEnabled, false, bionicReadingEnabled, imageRendering);
const std::string fallbackPath = getSectionFilePath(fallbackHash);
if (Storage.openFileForRead("SCT", fallbackPath, file)) {
filePath = fallbackPath;
usingEmbeddedStyleFallback = true;
LOG_ERR("SCT", "Using no-CSS section cache fallback: %s", filePath.c_str());
} else {
return false;
}
} else {
return false;
}
}
// Match parameters
@@ -248,6 +267,7 @@ bool Section::loadSectionFile(const int fontId, const float lineCompression, con
bool fileEmbeddedStyle;
bool fileBionicReadingEnabled;
uint8_t fileImageRendering;
bool fileParseComplete;
serialization::readPod(file, fileFontId);
serialization::readPod(file, fileLineCompression);
serialization::readPod(file, fileExtraParagraphSpacing);
@@ -258,16 +278,21 @@ bool Section::loadSectionFile(const int fontId, const float lineCompression, con
serialization::readPod(file, fileEmbeddedStyle);
serialization::readPod(file, fileBionicReadingEnabled);
serialization::readPod(file, fileImageRendering);
serialization::readPod(file, fileParseComplete);
const bool embeddedStyleMatches =
(embeddedStyle == fileEmbeddedStyle) || (usingEmbeddedStyleFallback && !fileEmbeddedStyle);
if (fontId != fileFontId || lineCompression != fileLineCompression ||
extraParagraphSpacing != fileExtraParagraphSpacing || paragraphAlignment != fileParagraphAlignment ||
viewportWidth != fileViewportWidth || viewportHeight != fileViewportHeight ||
hyphenationEnabled != fileHyphenationEnabled || embeddedStyle != fileEmbeddedStyle ||
hyphenationEnabled != fileHyphenationEnabled || !embeddedStyleMatches ||
bionicReadingEnabled != fileBionicReadingEnabled || imageRendering != fileImageRendering) {
LOG_ERR("SCT", "Deserialization failed: Parameters do not match");
clearCache(); // closes file before removal
return false;
}
truncatedCache = !fileParseComplete;
}
serialization::readPod(file, pageCount);
@@ -311,6 +336,7 @@ bool Section::clearCache() {
lut.clear();
pageCount = 0;
currentPage = 0;
truncatedCache = false;
if (!Storage.exists(filePath.c_str())) {
LOG_DBG("SCT", "Cache does not exist, no action needed");
@@ -413,7 +439,10 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
const uint32_t phaseParseStart = millis();
const bool streamOk = epub->readItemContentsToStream(localPath, visitor, 1024);
const bool finalizeOk = visitor.finalize();
bool success = streamOk && finalizeOk && visitor.streamSucceeded();
const bool parserStreamOk = visitor.streamSucceeded();
const bool parseComplete = streamOk && finalizeOk && parserStreamOk;
bool success = parseComplete;
const bool hasParsedPages = pageCount > 0;
const uint32_t parseMs = millis() - phaseParseStart;
// streamMs is no longer a separate phase (SD-write of temp file is gone); keep the
// log breakdown stable by reporting it as 0.
@@ -421,13 +450,35 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
const uint32_t phaseFinalizeStart = millis();
if (!success) {
LOG_ERR("SCT", "Failed to parse XML and build pages (stream=%d finalize=%d)", streamOk ? 1 : 0, finalizeOk ? 1 : 0);
file.close();
Storage.remove(filePath.c_str());
if (cssParser) {
cssParser->clear();
// If parsing fails mid-stream due low memory but some pages were already serialized,
// keep the partial section cache so the chapter remains readable instead of failing hard.
if (hasParsedPages) {
LOG_ERR("SCT", "Parse incomplete; keeping partial section cache with %u pages (stream=%d finalize=%d parser=%d)",
pageCount, streamOk ? 1 : 0, finalizeOk ? 1 : 0, parserStreamOk ? 1 : 0);
success = true;
} else if (embeddedStyle) {
LOG_ERR("SCT",
"Parse failed with embedded CSS enabled; retrying section creation with embeddedStyle=0 "
"(stream=%d finalize=%d)",
streamOk ? 1 : 0, finalizeOk ? 1 : 0);
file.close();
Storage.remove(filePath.c_str());
if (cssParser) {
cssParser->clear();
}
return createSectionFile(fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth,
viewportHeight, hyphenationEnabled, false, bionicReadingEnabled, imageRendering,
progressFn);
} else {
LOG_ERR("SCT", "Failed to parse XML and build pages (stream=%d finalize=%d)", streamOk ? 1 : 0,
finalizeOk ? 1 : 0);
file.close();
Storage.remove(filePath.c_str());
if (cssParser) {
cssParser->clear();
}
return false;
}
return false;
}
const uint32_t fileSize = static_cast<uint32_t>(inflatedSize);
@@ -477,8 +528,9 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
serialization::writePod(file, entry.listItemIndex);
}
// Patch header with final pageCount, lutOffset, anchorMapOffset, and paragraphLutOffset
file.seek(HEADER_SIZE - sizeof(uint32_t) * 3 - sizeof(pageCount));
// Patch header with final parseComplete/pageCount and offsets.
file.seek(HEADER_SIZE - sizeof(uint32_t) * 3 - sizeof(pageCount) - sizeof(bool));
serialization::writePod(file, parseComplete);
serialization::writePod(file, pageCount);
serialization::writePod(file, lutOffset);
serialization::writePod(file, anchorMapOffset);
@@ -499,6 +551,7 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
LOG_ERR("SCT", "Failed to open section file for reading after creation");
return false;
}
truncatedCache = !parseComplete;
this->lut = std::move(lut);
const uint32_t finalizeMs = millis() - phaseFinalizeStart;
const uint32_t totalMs = millis() - phaseTotalStart;
+2
View File
@@ -17,6 +17,7 @@ class Section {
std::string filePath;
FsFile file;
std::vector<uint32_t> lut; // Cached page byte-offsets; loaded once, avoids per-page LUT seek
bool truncatedCache = false;
void writeSectionFileHeader(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment,
uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled,
@@ -68,6 +69,7 @@ class Section {
bool bionicReadingEnabled, uint8_t imageRendering,
const std::function<void(int)>& progressFn = nullptr);
std::unique_ptr<Page> loadPageFromSectionFile();
bool isTruncatedCache() const { return truncatedCache; }
// Given a page in this section, return the TOC index for that page.
int getTocIndexForPage(int page) const;
@@ -26,6 +26,7 @@ struct JpegContext {
const RenderConfig* config{nullptr};
int screenWidth{0};
int screenHeight{0};
ImageDitherMode effectiveDitherMode{ImageDitherMode::Bayer};
// Source dimensions after JPEGDEC's built-in scaling
int scaledSrcWidth{0};
@@ -103,7 +104,7 @@ uint8_t ditherGray(JpegContext& ctx, uint8_t gray, int localX, int outX, int out
return quantizeGray4Level(gray);
}
switch (ctx.config->ditherMode) {
switch (ctx.effectiveDitherMode) {
case ImageDitherMode::Atkinson:
if (ctx.atkinsonDitherer) {
return ctx.atkinsonDitherer->processPixel(gray, localX);
@@ -179,6 +180,71 @@ int32_t jpegSeek(JPEGFILE* pFile, int32_t pos) {
constexpr size_t JPEG_DECODER_APPROX_SIZE = 20 * 1024;
constexpr size_t MIN_FREE_HEAP_FOR_JPEG = JPEG_DECODER_APPROX_SIZE + 16 * 1024;
// Optional memory-behavior knobs for embedded targets.
#ifndef JPEG_ENABLE_FIRST_RENDER_NO_CACHE
#define JPEG_ENABLE_FIRST_RENDER_NO_CACHE 1
#endif
#ifndef JPEG_CACHE_MIN_FREE_HEAP_MARGIN
#define JPEG_CACHE_MIN_FREE_HEAP_MARGIN (24 * 1024)
#endif
#ifndef JPEG_CACHE_MIN_MAX_ALLOC_MARGIN
#define JPEG_CACHE_MIN_MAX_ALLOC_MARGIN (20 * 1024)
#endif
#ifndef JPEG_DITHER_LOW_MEM_MIN_FREE_HEAP
#define JPEG_DITHER_LOW_MEM_MIN_FREE_HEAP (MIN_FREE_HEAP_FOR_JPEG + 8 * 1024)
#endif
#ifndef JPEG_DITHER_LOW_MEM_MIN_MAX_ALLOC
#define JPEG_DITHER_LOW_MEM_MIN_MAX_ALLOC (48 * 1024)
#endif
size_t jpegCacheBytes(int width, int height) {
return static_cast<size_t>((width + 3) / 4) * static_cast<size_t>(height);
}
bool shouldEnableJpegCache(const RenderConfig& config, int width, int height) {
if (config.cachePath.empty()) return false;
#if JPEG_ENABLE_FIRST_RENDER_NO_CACHE
if (!Storage.exists(config.cachePath.c_str())) {
LOG_DBG("JPG", "Skipping cache on first render (compile-time policy): %s", config.cachePath.c_str());
return false;
}
#endif
const size_t cacheBytes = jpegCacheBytes(width, height);
const size_t freeHeap = ESP.getFreeHeap();
const size_t maxAlloc = ESP.getMaxAllocHeap();
const size_t minFreeForCaching = MIN_FREE_HEAP_FOR_JPEG + JPEG_CACHE_MIN_FREE_HEAP_MARGIN;
const size_t minMaxAllocForCaching = cacheBytes + JPEG_CACHE_MIN_MAX_ALLOC_MARGIN;
if (freeHeap < minFreeForCaching) {
LOG_DBG("JPG", "Skipping cache: free heap %u < %u (cache %u bytes)", static_cast<unsigned>(freeHeap),
static_cast<unsigned>(minFreeForCaching), static_cast<unsigned>(cacheBytes));
return false;
}
if (maxAlloc < minMaxAllocForCaching) {
LOG_DBG("JPG", "Skipping cache: max alloc %u < %u (cache %u bytes)", static_cast<unsigned>(maxAlloc),
static_cast<unsigned>(minMaxAllocForCaching), static_cast<unsigned>(cacheBytes));
return false;
}
return true;
}
bool shouldForceBayerDither(const RenderConfig& config) {
if (!config.useDithering) return false;
if (config.ditherMode == ImageDitherMode::Bayer) return false;
const size_t freeHeap = ESP.getFreeHeap();
const size_t maxAlloc = ESP.getMaxAllocHeap();
return freeHeap < JPEG_DITHER_LOW_MEM_MIN_FREE_HEAP || maxAlloc < JPEG_DITHER_LOW_MEM_MIN_MAX_ALLOC;
}
bool readJpegDimensionsFromHeader(const std::string& imagePath, ImageDimensions& out) {
FsFile f;
if (!Storage.openFileForRead("JPG", imagePath, f)) {
@@ -526,6 +592,7 @@ bool JpegToFramebufferConverter::decodeToFramebuffer(const std::string& imagePat
ctx.config = &config;
ctx.screenWidth = renderer.getScreenWidth();
ctx.screenHeight = renderer.getScreenHeight();
ctx.effectiveDitherMode = config.ditherMode;
int rc = jpeg->open(imagePath.c_str(), jpegOpen, jpegClose, jpegRead, jpegSeek, jpegDrawCallback);
if (rc != 1) {
@@ -605,7 +672,7 @@ bool JpegToFramebufferConverter::decodeToFramebuffer(const std::string& imagePat
jpeg->setUserPointer(&ctx);
// Allocate cache buffer using final output dimensions
ctx.caching = !config.cachePath.empty();
ctx.caching = shouldEnableJpegCache(config, destWidth, destHeight);
if (ctx.caching) {
if (!ctx.cache.allocate(destWidth, destHeight, config.x, config.y)) {
LOG_ERR("JPG", "Failed to allocate cache buffer, continuing without caching");
@@ -613,6 +680,12 @@ bool JpegToFramebufferConverter::decodeToFramebuffer(const std::string& imagePat
}
}
if (shouldForceBayerDither(config)) {
LOG_DBG("JPG", "Low-memory mode: forcing Bayer dithering (%u free, %u max alloc)",
static_cast<unsigned>(ESP.getFreeHeap()), static_cast<unsigned>(ESP.getMaxAllocHeap()));
ctx.effectiveDitherMode = ImageDitherMode::Bayer;
}
// See PngToFramebufferConverter for rationale: BW-only display needs a 1-bit
// dither so mid-grays don't collapse to black under DirectPixelWriter's `< 3` rule.
if (config.monochromeOutput) {
@@ -624,7 +697,7 @@ bool JpegToFramebufferConverter::decodeToFramebuffer(const std::string& imagePat
if (config.useDithering && !ctx.atkinson1BitDitherer) {
#ifdef ENABLE_IMAGE_DITHERING_EXTENSION
switch (config.ditherMode) {
switch (ctx.effectiveDitherMode) {
case ImageDitherMode::Atkinson:
ctx.atkinsonDitherer.reset(new (std::nothrow) AtkinsonDitherer(destWidth));
if (!ctx.atkinsonDitherer) {
@@ -6,6 +6,7 @@
#include <HalStorage.h>
#include <Logging.h>
#include <Utf8.h>
#include <esp_heap_caps.h>
#include <expat.h>
#include <algorithm>
@@ -30,11 +31,42 @@ constexpr int NUM_HEADER_TAGS = sizeof(HEADER_TAGS) / sizeof(HEADER_TAGS[0]);
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 MIN_FREE_HEAP_FOR_INDEXING_POPUP = 32 * 1024;
constexpr size_t MIN_CONTIG_HEAP_FOR_INDEXING_POPUP = 12 * 1024;
// Parser progress popup/ticks trigger full-screen refreshes that can temporarily
// collapse heap during section cache builds on constrained targets. Keep disabled
// by default; enable only when debugging parse progress behavior.
#ifndef EHP_ENABLE_PARSE_PROGRESS_UI
#define EHP_ENABLE_PARSE_PROGRESS_UI 0
#endif
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;
#ifndef EHP_TEXT_LAYOUT_SOFT_MIN_FREE_HEAP
#define EHP_TEXT_LAYOUT_SOFT_MIN_FREE_HEAP (18 * 1024)
#endif
#ifndef EHP_TEXT_LAYOUT_SOFT_MIN_MAX_ALLOC
#define EHP_TEXT_LAYOUT_SOFT_MIN_MAX_ALLOC (12 * 1024)
#endif
#ifndef EHP_TEXT_LAYOUT_HARD_MIN_FREE_HEAP
#define EHP_TEXT_LAYOUT_HARD_MIN_FREE_HEAP (9 * 1024)
#endif
#ifndef EHP_TEXT_LAYOUT_HARD_MIN_MAX_ALLOC
#define EHP_TEXT_LAYOUT_HARD_MIN_MAX_ALLOC (6 * 1024)
#endif
constexpr size_t MIN_FREE_HEAP_FOR_TEXT_LAYOUT = EHP_TEXT_LAYOUT_SOFT_MIN_FREE_HEAP;
constexpr size_t MIN_MAX_ALLOC_FOR_TEXT_LAYOUT = EHP_TEXT_LAYOUT_SOFT_MIN_MAX_ALLOC;
constexpr size_t MIN_FREE_HEAP_FOR_TEXT_LAYOUT_HARD = EHP_TEXT_LAYOUT_HARD_MIN_FREE_HEAP;
constexpr size_t MIN_MAX_ALLOC_FOR_TEXT_LAYOUT_HARD = EHP_TEXT_LAYOUT_HARD_MIN_MAX_ALLOC;
const char* BLOCK_TAGS[] = {"p", "li", "div", "br", "blockquote", "pre"};
constexpr int NUM_BLOCK_TAGS = sizeof(BLOCK_TAGS) / sizeof(BLOCK_TAGS[0]);
@@ -198,6 +230,34 @@ void ChapterHtmlSlimParser::updateEffectiveInlineStyle() {
}
}
bool ChapterHtmlSlimParser::ensureHeapForTextLayout(const char* phase) {
if (streamFailed) {
return false;
}
const uint32_t freeHeap = ESP.getFreeHeap();
const uint32_t maxAllocHeap = ESP.getMaxAllocHeap();
if (freeHeap >= MIN_FREE_HEAP_FOR_TEXT_LAYOUT || maxAllocHeap >= MIN_MAX_ALLOC_FOR_TEXT_LAYOUT) {
return true;
}
// Soft low-memory zone: keep parsing in degraded mode and only hard-abort when
// both free and contiguous heap fall to critical levels.
if (freeHeap >= MIN_FREE_HEAP_FOR_TEXT_LAYOUT_HARD && maxAllocHeap >= MIN_MAX_ALLOC_FOR_TEXT_LAYOUT_HARD) {
lowMemoryImageFallback = true;
LOG_DBG("EHP", "Low heap (%u free, %u max alloc) before %s; continuing in degraded mode", freeHeap, maxAllocHeap,
phase);
return true;
}
LOG_ERR("EHP", "Low heap (%u free, %u max alloc), aborting parse before %s", freeHeap, maxAllocHeap, phase);
streamFailed = true;
if (activeParser) {
XML_StopParser(activeParser, XML_FALSE);
}
return false;
}
// flush the contents of partWordBuffer to currentTextBlock
void ChapterHtmlSlimParser::flushPartWordBuffer() {
// Determine font style from depth-based tracking and CSS effective style
@@ -229,6 +289,11 @@ void ChapterHtmlSlimParser::flushPartWordBuffer() {
currentTextBlock->addWord(partWordBuffer, fontStyle, false, nextWordContinues);
if (currentTextBlock->size() > 96) {
if (!ensureHeapForTextLayout("long-block split")) {
partWordBufferIndex = 0;
nextWordContinues = false;
return;
}
LOG_DBG("EHP", "Text block too long, splitting into multiple pages");
const int horizontalInset = currentTextBlock->getBlockStyle().totalHorizontalInset();
const uint16_t effectiveWidth =
@@ -1572,8 +1637,25 @@ bool ChapterHtmlSlimParser::setup(const size_t totalInflatedSize) {
progressStepPercent = 50;
}
const uint32_t popupFreeHeap = ESP.getFreeHeap();
const uint32_t popupContigHeap = heap_caps_get_largest_free_block(MALLOC_CAP_8BIT | MALLOC_CAP_DEFAULT);
#if EHP_ENABLE_PARSE_PROGRESS_UI
progressUiEnabled =
popupFreeHeap >= MIN_FREE_HEAP_FOR_INDEXING_POPUP && popupContigHeap >= MIN_CONTIG_HEAP_FOR_INDEXING_POPUP;
if (!progressUiEnabled) {
LOG_DBG("EHP", "Skipping indexing popup due to low heap (free=%u contig=%u)", popupFreeHeap, popupContigHeap);
// When popup is disabled, also disable mid-parse ticks.
progressStepPercent = 0;
}
#else
progressUiEnabled = false;
progressStepPercent = 0;
LOG_DBG("EHP", "Skipping parser progress popup/ticks (EHP_ENABLE_PARSE_PROGRESS_UI=0, free=%u contig=%u)",
popupFreeHeap, popupContigHeap);
#endif
// Show initial progress popup for files above threshold.
if (progressFn && totalStreamSize >= MIN_SIZE_FOR_POPUP) {
if (progressFn && progressUiEnabled && totalStreamSize >= MIN_SIZE_FOR_POPUP) {
progressFn(0);
}
return true;
@@ -1614,7 +1696,7 @@ size_t ChapterHtmlSlimParser::write(const uint8_t* buffer, const size_t size) {
// Report progress at the granularity chosen up-front (see progressStepPercent).
// Skip the 100% callback — the page render that follows immediately replaces the popup,
// so the final tick is wasted work.
if (progressFn && progressStepPercent > 0 && totalStreamSize > 0) {
if (progressFn && progressUiEnabled && progressStepPercent > 0 && totalStreamSize > 0) {
const int progress = static_cast<int>(bytesStreamed * 100 / totalStreamSize);
if (progress < 100 && progress / progressStepPercent > lastReportedProgress / progressStepPercent) {
lastReportedProgress = progress;
@@ -1736,6 +1818,10 @@ void ChapterHtmlSlimParser::makePages() {
const uint16_t effectiveWidth =
(horizontalInset < viewportWidth) ? static_cast<uint16_t>(viewportWidth - horizontalInset) : viewportWidth;
if (!ensureHeapForTextLayout("paragraph layout")) {
return;
}
currentTextBlock->layoutAndExtractLines(
renderer, fontId, effectiveWidth,
[this](const std::shared_ptr<TextBlock>& textBlock, const bool lineEndsWithHyphenatedWord,
@@ -137,6 +137,7 @@ class ChapterHtmlSlimParser final : public Print {
size_t bytesStreamed = 0;
int lastReportedProgress = -1;
int progressStepPercent = 0;
bool progressUiEnabled = true;
bool streamFailed = false;
uint32_t streamStartTimeMs = 0;
@@ -155,6 +156,7 @@ class ChapterHtmlSlimParser final : public Print {
std::unordered_map<std::string, CssStyle> inlineStyleCache_;
void updateEffectiveInlineStyle();
bool ensureHeapForTextLayout(const char* phase);
void startNewTextBlock(const BlockStyle& blockStyle);
void flushPartWordBuffer();
void makePages();