From d922b5a67a9008530ea3c9f949ed8d36a03eb422 Mon Sep 17 00:00:00 2001 From: jpirnay Date: Tue, 19 May 2026 09:28:36 +0200 Subject: [PATCH 1/5] Render larger images on demand not by default --- lib/Epub/Epub/Page.cpp | 25 ++++++- lib/Epub/Epub/Page.h | 4 +- lib/Epub/Epub/blocks/ImageBlock.cpp | 71 +++++++++++++++++--- lib/Epub/Epub/blocks/ImageBlock.h | 19 +++++- lib/I18n/translations/english.yaml | 1 + src/CrossPointSettings.h | 3 + src/SettingsList.h | 2 + src/activities/reader/EpubReaderActivity.cpp | 24 +++++-- src/activities/reader/EpubReaderActivity.h | 5 ++ 9 files changed, 134 insertions(+), 20 deletions(-) diff --git a/lib/Epub/Epub/Page.cpp b/lib/Epub/Epub/Page.cpp index c156b584..3bfb0251 100644 --- a/lib/Epub/Epub/Page.cpp +++ b/lib/Epub/Epub/Page.cpp @@ -27,10 +27,13 @@ std::unique_ptr PageLine::deserialize(FsFile& file) { } void PageImage::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) { - // Images don't use fontId or text rendering imageBlock->render(renderer, xPos + xOffset, yPos + yOffset); } +void PageImage::renderWithForceLoad(GfxRenderer& renderer, const int xOffset, const int yOffset, const bool forceLoad) { + imageBlock->render(renderer, xPos + xOffset, yPos + yOffset, forceLoad); +} + bool PageImage::serialize(FsFile& file) { serialization::writePod(file, xPos); serialization::writePod(file, yPos); @@ -201,12 +204,28 @@ std::unique_ptr PageTableFragment::deserialize(FsFile& file) new PageTableFragment(columnCount, totalWidth, totalHeight, colWidths, std::move(rows), xPos, yPos)); } -void Page::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) const { +void Page::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset, + const bool forceLoadLargeImages) const { for (auto& element : elements) { - element->render(renderer, fontId, xOffset, yOffset); + if (element->getTag() == TAG_PageImage) { + static_cast(*element).renderWithForceLoad(renderer, xOffset, yOffset, forceLoadLargeImages); + } else { + element->render(renderer, fontId, xOffset, yOffset); + } } } +bool Page::hasPlaceholderImages(const bool forceLoadLargeImages) const { + for (const auto& el : elements) { + if (el->getTag() == TAG_PageImage) { + if (static_cast(*el).getImageBlock().wouldShowPlaceholder(forceLoadLargeImages)) { + return true; + } + } + } + return false; +} + void Page::renderTextOnly(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) const { for (auto& element : elements) { if (element->getTag() == TAG_PageLine) { diff --git a/lib/Epub/Epub/Page.h b/lib/Epub/Epub/Page.h index 8d3fdff6..067f2d5a 100644 --- a/lib/Epub/Epub/Page.h +++ b/lib/Epub/Epub/Page.h @@ -58,6 +58,7 @@ class PageImage final : public PageElement { PageImage(std::shared_ptr block, const int16_t xPos, const int16_t yPos) : PageElement(xPos, yPos), imageBlock(std::move(block)) {} void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) override; + void renderWithForceLoad(GfxRenderer& renderer, int xOffset, int yOffset, bool forceLoad); bool serialize(FsFile& file) override; PageElementTag getTag() const override { return TAG_PageImage; } static std::unique_ptr deserialize(FsFile& file); @@ -128,8 +129,9 @@ class Page { footnotes.push_back(entry); } - void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) const; + void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset, bool forceLoadLargeImages = true) const; void renderTextOnly(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) const; + bool hasPlaceholderImages(bool forceLoadLargeImages) const; bool serialize(FsFile& file) const; static std::unique_ptr deserialize(FsFile& file); diff --git a/lib/Epub/Epub/blocks/ImageBlock.cpp b/lib/Epub/Epub/blocks/ImageBlock.cpp index 3e331572..5cc6728b 100644 --- a/lib/Epub/Epub/blocks/ImageBlock.cpp +++ b/lib/Epub/Epub/blocks/ImageBlock.cpp @@ -1,12 +1,16 @@ #include "ImageBlock.h" +#include #include #include #include #include "../../../../src/CrossPointSettings.h" +#include "../../../../src/fontIds.h" #include "../converters/DirectPixelWriter.h" #include "../converters/ImageDecoderFactory.h" +#include "../converters/JpegToFramebufferConverter.h" +#include "../converters/PngToFramebufferConverter.h" // Cache file format: // - uint16_t width @@ -97,7 +101,51 @@ bool renderFromCache(GfxRenderer& renderer, const std::string& cachePath, int x, } // namespace -void ImageBlock::render(GfxRenderer& renderer, const int x, const int y) { +bool ImageBlock::isLargeImage() const { + if (largeImageCached != 0) return largeImageCached == 1; + ImageDimensions dims{0, 0}; + const bool ok = FsHelpers::hasJpgExtension(imagePath) + ? JpegToFramebufferConverter::getDimensionsStatic(imagePath, dims) + : PngToFramebufferConverter::getDimensionsStatic(imagePath, dims); + if (ok && dims.width > 0 && dims.height > 0) { + largeImageCached = (int32_t(dims.width) * dims.height > LARGE_IMAGE_PIXEL_THRESHOLD) ? 1 : -1; + } else { + largeImageCached = -1; // unreadable header → assume not large, render normally + } + return largeImageCached == 1; +} + +bool ImageBlock::wouldShowPlaceholder(bool forceLoad) const { + if (forceLoad) return false; + if (!isLargeImage()) return false; + // If the pixel cache already exists the render is instant — no placeholder needed + const ImageDitherMode ditherMode = imageDitherModeFromSetting(SETTINGS.imageDithering); + const std::string pxcPath = [&] { + size_t dot = imagePath.rfind('.'); + return (dot != std::string::npos ? imagePath.substr(0, dot) : imagePath) + getImageDitherCacheSuffix(ditherMode) + + ".pxc"; + }(); + return !Storage.exists(pxcPath.c_str()); +} + +void ImageBlock::renderPlaceholder(GfxRenderer& renderer, const int x, const int y) const { + constexpr int BORDER = 1; + constexpr int PADDING = 6; + + renderer.drawRect(x, y, width, height, BORDER, true); + + const int lineH = renderer.getLineHeight(UI_10_FONT_ID); + const int totalTextH = lineH * 2; + + if (lineH > 0 && width > PADDING * 2 && height > totalTextH + PADDING * 2) { + const int textX = x + PADDING; + const int textY = y + (height - totalTextH) / 2; + renderer.drawText(UI_10_FONT_ID, textX, textY, "Image"); + renderer.drawText(UI_10_FONT_ID, textX, textY + lineH, "Press OK to load"); + } +} + +void ImageBlock::render(GfxRenderer& renderer, const int x, const int y, const bool forceLoad) { LOG_DBG("IMG", "Rendering image at %d,%d: %s (%dx%d)", x, y, imagePath.c_str(), width, height); const int screenWidth = renderer.getScreenWidth(); @@ -110,15 +158,21 @@ void ImageBlock::render(GfxRenderer& renderer, const int x, const int y) { return; } - // Try to render from cache first + // Try to render from pixel cache first (always, regardless of forceLoad) const ImageDitherMode ditherMode = imageDitherModeFromSetting(SETTINGS.imageDithering); std::string cachePath = getCachePath(imagePath, ditherMode); if (renderFromCache(renderer, cachePath, x, y, width, height)) { - return; // Successfully rendered from cache + return; } - // No cache - need to decode the image - // Check if image file exists + // No pixel cache — check if this is a large image that should show a placeholder + if (wouldShowPlaceholder(forceLoad)) { + LOG_DBG("IMG", "Large image placeholder at %d,%d (%dx%d): %s", x, y, width, height, imagePath.c_str()); + renderPlaceholder(renderer, x, y); + return; + } + + // Proceed with full decode FsFile file; if (!Storage.openFileForRead("IMG", imagePath, file)) { LOG_ERR("IMG", "Image file not found: %s", imagePath.c_str()); @@ -143,8 +197,8 @@ void ImageBlock::render(GfxRenderer& renderer, const int x, const int y) { config.useDithering = true; config.ditherMode = ditherMode; config.performanceMode = false; - config.useExactDimensions = true; // Use pre-calculated dimensions to avoid rounding mismatches - config.cachePath = cachePath; // Enable caching during decode + config.useExactDimensions = true; + config.cachePath = cachePath; ImageToFramebufferDecoder* decoder = ImageDecoderFactory::getDecoder(imagePath); if (!decoder) { @@ -157,10 +211,7 @@ void ImageBlock::render(GfxRenderer& renderer, const int x, const int y) { bool success = decoder->decodeToFramebuffer(imagePath, renderer, config); if (!success) { LOG_ERR("IMG", "Failed to decode image: %s", imagePath.c_str()); - return; } - - LOG_DBG("IMG", "Decode successful"); } bool ImageBlock::serialize(FsFile& file) { diff --git a/lib/Epub/Epub/blocks/ImageBlock.h b/lib/Epub/Epub/blocks/ImageBlock.h index f3b01e6c..03149bcb 100644 --- a/lib/Epub/Epub/blocks/ImageBlock.h +++ b/lib/Epub/Epub/blocks/ImageBlock.h @@ -6,6 +6,11 @@ #include "Block.h" +// Source pixel area above which an image is considered "large" and rendered +// as a placeholder until the user explicitly requests it. +// 800x600 covers most full-page illustrations that take >1s to dither on ESP32. +static constexpr int32_t LARGE_IMAGE_PIXEL_THRESHOLD = 800 * 600; + class ImageBlock final : public Block { public: ImageBlock(const std::string& imagePath, int16_t width, int16_t height); @@ -17,10 +22,18 @@ class ImageBlock final : public Block { bool imageExists() const; + // Returns true if the source image dimensions exceed LARGE_IMAGE_PIXEL_THRESHOLD. + // Result is cached after the first call to avoid repeated header reads. + bool isLargeImage() const; + + // Returns true if this image would be shown as a placeholder given forceLoad. + // False when: forceLoad is true, image is not large, or pixel cache already exists. + bool wouldShowPlaceholder(bool forceLoad) const; + BlockType getType() override { return IMAGE_BLOCK; } bool isEmpty() override { return false; } - void render(GfxRenderer& renderer, const int x, const int y); + void render(GfxRenderer& renderer, int x, int y, bool forceLoad = true); bool serialize(FsFile& file); static std::unique_ptr deserialize(FsFile& file); @@ -28,4 +41,8 @@ class ImageBlock final : public Block { std::string imagePath; int16_t width; int16_t height; + + mutable int8_t largeImageCached = 0; // 0=unchecked, 1=large, -1=not large + + void renderPlaceholder(GfxRenderer& renderer, int x, int y) const; }; diff --git a/lib/I18n/translations/english.yaml b/lib/I18n/translations/english.yaml index ebd6deca..146d3ea5 100644 --- a/lib/I18n/translations/english.yaml +++ b/lib/I18n/translations/english.yaml @@ -102,6 +102,7 @@ STR_IMAGES: "Images" STR_IMAGES_DISPLAY: "Display" STR_IMAGES_PLACEHOLDER: "Placeholder" STR_IMAGES_SUPPRESS: "Suppress" +STR_LARGE_IMAGE_PLACEHOLDER: "Placeholder for large images" STR_IMAGE_DITHERING: "Image Dithering" STR_IMAGE_DITHER_BAYER: "Bayer" STR_IMAGE_DITHER_ATKINSON: "Atkinson" diff --git a/src/CrossPointSettings.h b/src/CrossPointSettings.h index c9b74dae..c8df545d 100644 --- a/src/CrossPointSettings.h +++ b/src/CrossPointSettings.h @@ -267,6 +267,9 @@ class CrossPointSettings { uint8_t showFileExtensions = 0; // Image rendering mode in EPUB reader uint8_t imageRendering = IMAGES_DISPLAY; + // Show a placeholder for large images (>800×600 source pixels) instead of decoding immediately. + // The user can press OK on the placeholder page to decode the image on demand. + uint8_t largeImagePlaceholder = 1; // Dithering mode for decoded images (EPUB/JPG/PNG) uint8_t imageDithering = IMAGE_DITHER_BAYER; // Tilt-based page turning (X3 only — requires QMI8658 IMU) diff --git a/src/SettingsList.h b/src/SettingsList.h index 576b3853..5cd1dc10 100644 --- a/src/SettingsList.h +++ b/src/SettingsList.h @@ -139,6 +139,8 @@ inline const std::vector list = { SettingInfo::Enum(StrId::STR_IMAGES, &CrossPointSettings::imageRendering, {StrId::STR_IMAGES_DISPLAY, StrId::STR_IMAGES_PLACEHOLDER, StrId::STR_IMAGES_SUPPRESS}, "imageRendering", StrId::STR_CAT_READER), + SettingInfo::Toggle(StrId::STR_LARGE_IMAGE_PLACEHOLDER, &CrossPointSettings::largeImagePlaceholder, + "largeImagePlaceholder", StrId::STR_CAT_READER), SettingInfo::Value(StrId::STR_SCREEN_MARGIN, &CrossPointSettings::screenMargin, {5, 40, 5}, "screenMargin", StrId::STR_CAT_READER) .withSubmenu(StrId::STR_MENU_READER_SPACING), diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index 3f790fdb..155cb792 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -407,6 +407,12 @@ void EpubReaderActivity::loop() { return; } if (ev.type == ButtonEventManager::PressType::Short) { + if (pageHasPlaceholders) { + forceLoadLargeImages = true; + pageHasPlaceholders = false; + requestUpdate(); + return; + } openReaderMenu(); return; } @@ -1512,6 +1518,8 @@ bool EpubReaderActivity::stepPageState(const bool isForwardTurn) { } lastPageTurnTime = millis(); + forceLoadLargeImages = false; + pageHasPlaceholders = false; return true; } @@ -1868,15 +1876,18 @@ void EpubReaderActivity::renderContents(std::unique_ptr page, const int or } lastRenderStats.textAntiAliasing = aaEnabledForThisRender; - // Force special handling for pages with images when anti-aliasing is on - bool imagePageWithAA = page->hasImages() && aaEnabledForThisRender; + // Force special handling for pages with real (non-placeholder) images when anti-aliasing is on + bool imagePageWithAA = page->hasImages() && !pageHasPlaceholders && aaEnabledForThisRender; bool forceHalfRefreshThisPage = pendingHalfRefreshAfterImagePage && SETTINGS.halfRefreshAfterImagePage; pendingHalfRefreshAfterImagePage = false; lastRenderStats.imagePageWithAA = imagePageWithAA; lastRenderStats.forcedHalfRefresh = forceHalfRefreshThisPage; + const bool effectiveForceLoad = forceLoadLargeImages || !SETTINGS.largeImagePlaceholder; + pageHasPlaceholders = page->hasPlaceholderImages(effectiveForceLoad); + logReaderMemSnapshot("before_bw_render"); - page->render(renderer, getEffectiveReaderFontId(), orientedMarginLeft, contentTop); + page->render(renderer, getEffectiveReaderFontId(), orientedMarginLeft, contentTop, effectiveForceLoad); renderStatusBar(); if (showTruncatedSectionHintThisRender) { const int hintX = orientedMarginLeft + 4; @@ -1912,7 +1923,7 @@ void EpubReaderActivity::renderContents(std::unique_ptr page, const int or // Re-render page content to restore images into the blanked area // Status bar is not re-rendered here to avoid reading stale dynamic values (e.g. battery %) - page->render(renderer, getEffectiveReaderFontId(), orientedMarginLeft, contentTop); + page->render(renderer, getEffectiveReaderFontId(), orientedMarginLeft, contentTop, effectiveForceLoad); renderer.displayBuffer(HalDisplay::FAST_REFRESH); } else { renderer.displayBuffer(HalDisplay::HALF_REFRESH); @@ -1970,7 +1981,10 @@ void EpubReaderActivity::renderContents(std::unique_ptr page, const int or LOG_INF("ERS", "Skipping grayscale/BW-restore for this page (insufficient heap for BW snapshot)"); } - if (page->hasImages() && getEffectiveImageRendering() != CrossPointSettings::IMAGES_SUPPRESS) { + // Only schedule the half-refresh if real images were decoded on this page. + // Placeholder-only pages don't deposit grayscale data that needs settling. + if (page->hasImages() && !pageHasPlaceholders && + getEffectiveImageRendering() != CrossPointSettings::IMAGES_SUPPRESS) { pendingHalfRefreshAfterImagePage = true; } diff --git a/src/activities/reader/EpubReaderActivity.h b/src/activities/reader/EpubReaderActivity.h index d242046a..eebf4b72 100644 --- a/src/activities/reader/EpubReaderActivity.h +++ b/src/activities/reader/EpubReaderActivity.h @@ -109,6 +109,11 @@ class EpubReaderActivity final : public Activity { unsigned long lastPageTurnTime = 0UL; unsigned long pageTurnDuration = 0UL; bool pendingHalfRefreshAfterImagePage = false; + // When true, large images on the current page are decoded instead of shown as placeholders. + // Reset to false on every page turn so the next image page starts with a placeholder again. + bool forceLoadLargeImages = false; + // Set after each render: true if the current page contains at least one placeholder image. + bool pageHasPlaceholders = false; // Temporary AA suspension when BW snapshot allocation fails under memory pressure. // Automatically lifted once heap recovers above hysteresis thresholds. bool antiAliasingSuspendedLowMemory = false; From be039ff09b6b78fb8b418b2f006ac984c1d39234 Mon Sep 17 00:00:00 2001 From: jpirnay Date: Tue, 19 May 2026 11:22:05 +0200 Subject: [PATCH 2/5] Review comments --- src/activities/reader/EpubReaderActivity.cpp | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index 155cb792..727db1bd 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -613,6 +613,8 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction : std::nullopt; if (resolvedPage) { section->currentPage = *resolvedPage; + forceLoadLargeImages = false; + pageHasPlaceholders = false; } else { navTarget = chapter.tocIndex ? NavigationTarget::makeTocIndex(*chapter.tocIndex) : NavigationTarget::makePage(0); @@ -1716,6 +1718,8 @@ void EpubReaderActivity::render(RenderLock&& lock) { navTarget.resolveInto(*section, currentSpineIndex); navTarget = NavigationTarget::makePage(section->currentPage); + forceLoadLargeImages = false; + pageHasPlaceholders = false; } renderer.clearScreen(); @@ -1876,6 +1880,9 @@ void EpubReaderActivity::renderContents(std::unique_ptr page, const int or } lastRenderStats.textAntiAliasing = aaEnabledForThisRender; + const bool effectiveForceLoad = forceLoadLargeImages || !SETTINGS.largeImagePlaceholder; + pageHasPlaceholders = page->hasPlaceholderImages(effectiveForceLoad); + // Force special handling for pages with real (non-placeholder) images when anti-aliasing is on bool imagePageWithAA = page->hasImages() && !pageHasPlaceholders && aaEnabledForThisRender; bool forceHalfRefreshThisPage = pendingHalfRefreshAfterImagePage && SETTINGS.halfRefreshAfterImagePage; @@ -1883,9 +1890,6 @@ void EpubReaderActivity::renderContents(std::unique_ptr page, const int or lastRenderStats.imagePageWithAA = imagePageWithAA; lastRenderStats.forcedHalfRefresh = forceHalfRefreshThisPage; - const bool effectiveForceLoad = forceLoadLargeImages || !SETTINGS.largeImagePlaceholder; - pageHasPlaceholders = page->hasPlaceholderImages(effectiveForceLoad); - logReaderMemSnapshot("before_bw_render"); page->render(renderer, getEffectiveReaderFontId(), orientedMarginLeft, contentTop, effectiveForceLoad); renderStatusBar(); @@ -2369,6 +2373,8 @@ void EpubReaderActivity::onButtonAction(const CrossPointSettings::BUTTON_ACTION : std::nullopt; if (resolvedPage) { section->currentPage = *resolvedPage; + forceLoadLargeImages = false; + pageHasPlaceholders = false; } else { navTarget = chapter.tocIndex ? NavigationTarget::makeTocIndex(*chapter.tocIndex) : NavigationTarget::makePage(0); @@ -2395,6 +2401,8 @@ void EpubReaderActivity::onButtonAction(const CrossPointSettings::BUTTON_ACTION if (newSpineIndex == currentSpineIndex) { if (const auto resolvedPage = section->getPageForTocIndex(nextTocIndex)) { section->currentPage = *resolvedPage; + forceLoadLargeImages = false; + pageHasPlaceholders = false; } } else { navTarget = NavigationTarget::makeTocIndex(nextTocIndex); From 113d8d53b62bcee07ae31d2b6e439d4578226bb7 Mon Sep 17 00:00:00 2001 From: jpirnay Date: Tue, 19 May 2026 12:37:24 +0200 Subject: [PATCH 3/5] yaclf --- src/activities/reader/EpubReaderActivity.cpp | 8 ++++---- src/activities/reader/EpubReaderActivity.h | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index d7f6eba9..661488d2 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -2155,8 +2155,8 @@ void EpubReaderActivity::renderContents(std::unique_ptr page, const int or } void EpubReaderActivity::renderPageContentOnly(const Page& page, const int orientedMarginTop, - const int orientedMarginRight, - const int orientedMarginBottom, const int orientedMarginLeft) { + const int orientedMarginRight, const int orientedMarginBottom, + const int orientedMarginLeft) { auto* fcm = renderer.getFontCacheManager(); fcm->resetStats(); @@ -2173,8 +2173,8 @@ void EpubReaderActivity::renderPageContentOnly(const Page& page, const int orien } void EpubReaderActivity::displayPreRenderedPage(const Page& page, const int orientedMarginTop, - const int orientedMarginRight, - const int orientedMarginBottom, const int orientedMarginLeft) { + const int orientedMarginRight, const int orientedMarginBottom, + const int orientedMarginLeft) { const int viewportHeight = std::max(0, renderer.getScreenHeight() - orientedMarginTop - orientedMarginBottom); const int contentTop = orientedMarginTop + getImageOnlyPageYOffset(page, viewportHeight); diff --git a/src/activities/reader/EpubReaderActivity.h b/src/activities/reader/EpubReaderActivity.h index d546bb8a..ddbdf5ce 100644 --- a/src/activities/reader/EpubReaderActivity.h +++ b/src/activities/reader/EpubReaderActivity.h @@ -259,8 +259,8 @@ class EpubReaderActivity final : public Activity { // Draws the status bar over the current frame buffer and flushes to the display. // Handles the refresh cycle and grayscale AA pass. page must be the same page // that was last rendered into the buffer (needed for image AA re-render). - void displayPreRenderedPage(const Page& page, int orientedMarginTop, int orientedMarginRight, int orientedMarginBottom, - int orientedMarginLeft); + void displayPreRenderedPage(const Page& page, int orientedMarginTop, int orientedMarginRight, + int orientedMarginBottom, int orientedMarginLeft); void renderStatusBar() const; void silentIndexNextChapterIfNeeded(uint16_t viewportWidth, uint16_t viewportHeight); void saveProgress(int spineIndex, int currentPage, int pageCount); From ee0a2556acf3d0865dcb0a51af982b672bc2c199 Mon Sep 17 00:00:00 2001 From: jpirnay Date: Tue, 19 May 2026 12:41:26 +0200 Subject: [PATCH 4/5] Use localized strings --- lib/Epub/Epub/BookMetadataCache.cpp | 2 +- lib/Epub/Epub/blocks/ImageBlock.cpp | 21 +++++++++++++------ lib/Epub/Epub/blocks/ImageBlock.h | 4 +++- .../Epub/parsers/ChapterHtmlSlimParser.cpp | 2 +- lib/I18n/translations/english.yaml | 2 ++ 5 files changed, 22 insertions(+), 9 deletions(-) diff --git a/lib/Epub/Epub/BookMetadataCache.cpp b/lib/Epub/Epub/BookMetadataCache.cpp index 7e597a4b..06a5ffd4 100644 --- a/lib/Epub/Epub/BookMetadataCache.cpp +++ b/lib/Epub/Epub/BookMetadataCache.cpp @@ -11,7 +11,7 @@ #include "FsHelpers.h" namespace { -constexpr uint8_t BOOK_CACHE_VERSION = 9; +constexpr uint8_t BOOK_CACHE_VERSION = 10; constexpr char bookBinFile[] = "/book.bin"; constexpr char tmpSpineBinFile[] = "/spine.bin.tmp"; constexpr char tmpTocBinFile[] = "/toc.bin.tmp"; diff --git a/lib/Epub/Epub/blocks/ImageBlock.cpp b/lib/Epub/Epub/blocks/ImageBlock.cpp index 5cc6728b..17e37da8 100644 --- a/lib/Epub/Epub/blocks/ImageBlock.cpp +++ b/lib/Epub/Epub/blocks/ImageBlock.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -17,8 +18,8 @@ // - uint16_t height // - uint8_t pixels[...] - 2 bits per pixel, packed (4 pixels per byte), row-major order -ImageBlock::ImageBlock(const std::string& imagePath, int16_t width, int16_t height) - : imagePath(imagePath), width(width), height(height) {} +ImageBlock::ImageBlock(const std::string& imagePath, int16_t width, int16_t height, const std::string& altText) + : imagePath(imagePath), altText(altText), width(width), height(height) {} bool ImageBlock::imageExists() const { return Storage.exists(imagePath.c_str()); } @@ -135,13 +136,18 @@ void ImageBlock::renderPlaceholder(GfxRenderer& renderer, const int x, const int renderer.drawRect(x, y, width, height, BORDER, true); const int lineH = renderer.getLineHeight(UI_10_FONT_ID); - const int totalTextH = lineH * 2; + const bool hasAlt = !altText.empty(); + const int lineCount = hasAlt ? 3 : 2; + const int totalTextH = lineH * lineCount; if (lineH > 0 && width > PADDING * 2 && height > totalTextH + PADDING * 2) { const int textX = x + PADDING; const int textY = y + (height - totalTextH) / 2; - renderer.drawText(UI_10_FONT_ID, textX, textY, "Image"); - renderer.drawText(UI_10_FONT_ID, textX, textY + lineH, "Press OK to load"); + renderer.drawText(UI_10_FONT_ID, textX, textY, tr(STR_LARGE_IMAGE)); + if (hasAlt) { + renderer.drawText(UI_10_FONT_ID, textX, textY + lineH, altText.c_str()); + } + renderer.drawText(UI_10_FONT_ID, textX, textY + lineH * (lineCount - 1), tr(STR_PRESS_CONFIRM_TO_LOAD)); } } @@ -218,6 +224,7 @@ bool ImageBlock::serialize(FsFile& file) { serialization::writeString(file, imagePath); serialization::writePod(file, width); serialization::writePod(file, height); + serialization::writeString(file, altText); return true; } @@ -227,5 +234,7 @@ std::unique_ptr ImageBlock::deserialize(FsFile& file) { int16_t w, h; serialization::readPod(file, w); serialization::readPod(file, h); - return std::unique_ptr(new ImageBlock(path, w, h)); + std::string alt; + serialization::readString(file, alt); + return std::unique_ptr(new ImageBlock(path, w, h, alt)); } diff --git a/lib/Epub/Epub/blocks/ImageBlock.h b/lib/Epub/Epub/blocks/ImageBlock.h index 03149bcb..7145adc0 100644 --- a/lib/Epub/Epub/blocks/ImageBlock.h +++ b/lib/Epub/Epub/blocks/ImageBlock.h @@ -13,12 +13,13 @@ static constexpr int32_t LARGE_IMAGE_PIXEL_THRESHOLD = 800 * 600; class ImageBlock final : public Block { public: - ImageBlock(const std::string& imagePath, int16_t width, int16_t height); + ImageBlock(const std::string& imagePath, int16_t width, int16_t height, const std::string& altText = ""); ~ImageBlock() override = default; const std::string& getImagePath() const { return imagePath; } int16_t getWidth() const { return width; } int16_t getHeight() const { return height; } + const std::string& getAltText() const { return altText; } bool imageExists() const; @@ -39,6 +40,7 @@ class ImageBlock final : public Block { private: std::string imagePath; + std::string altText; int16_t width; int16_t height; diff --git a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp index fefde980..df6c5d73 100644 --- a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp +++ b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp @@ -849,7 +849,7 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* self->currentPageNextY += imageSpacingTop; // Create ImageBlock and add to page - auto imageBlock = std::make_shared(cachedImagePath, displayWidth, displayHeight); + auto imageBlock = std::make_shared(cachedImagePath, displayWidth, displayHeight, alt); if (!imageBlock) { LOG_ERR("EHP", "Failed to create ImageBlock"); return; diff --git a/lib/I18n/translations/english.yaml b/lib/I18n/translations/english.yaml index 146d3ea5..6310742f 100644 --- a/lib/I18n/translations/english.yaml +++ b/lib/I18n/translations/english.yaml @@ -103,6 +103,8 @@ STR_IMAGES_DISPLAY: "Display" STR_IMAGES_PLACEHOLDER: "Placeholder" STR_IMAGES_SUPPRESS: "Suppress" STR_LARGE_IMAGE_PLACEHOLDER: "Placeholder for large images" +STR_LARGE_IMAGE: "Large Image" +STR_PRESS_CONFIRM_TO_LOAD: "Press Confirm to load" STR_IMAGE_DITHERING: "Image Dithering" STR_IMAGE_DITHER_BAYER: "Bayer" STR_IMAGE_DITHER_ATKINSON: "Atkinson" From 98bc29f41a37b16a0d25b5eb761df59439eb8326 Mon Sep 17 00:00:00 2001 From: jpirnay Date: Tue, 19 May 2026 13:05:42 +0200 Subject: [PATCH 5/5] Review comment --- lib/Epub/Epub/Page.cpp | 13 +++++++++++++ lib/Epub/Epub/Page.h | 1 + src/activities/reader/EpubReaderActivity.cpp | 10 ++++++---- 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/lib/Epub/Epub/Page.cpp b/lib/Epub/Epub/Page.cpp index 3bfb0251..15bcf10e 100644 --- a/lib/Epub/Epub/Page.cpp +++ b/lib/Epub/Epub/Page.cpp @@ -226,6 +226,19 @@ bool Page::hasPlaceholderImages(const bool forceLoadLargeImages) const { return false; } +bool Page::allImagesArePlaceholders(const bool forceLoadLargeImages) const { + bool anyImage = false; + for (const auto& el : elements) { + if (el->getTag() == TAG_PageImage) { + anyImage = true; + if (!static_cast(*el).getImageBlock().wouldShowPlaceholder(forceLoadLargeImages)) { + return false; + } + } + } + return anyImage; +} + void Page::renderTextOnly(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) const { for (auto& element : elements) { if (element->getTag() == TAG_PageLine) { diff --git a/lib/Epub/Epub/Page.h b/lib/Epub/Epub/Page.h index 067f2d5a..54985193 100644 --- a/lib/Epub/Epub/Page.h +++ b/lib/Epub/Epub/Page.h @@ -132,6 +132,7 @@ class Page { void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset, bool forceLoadLargeImages = true) const; void renderTextOnly(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) const; bool hasPlaceholderImages(bool forceLoadLargeImages) const; + bool allImagesArePlaceholders(bool forceLoadLargeImages) const; bool serialize(FsFile& file) const; static std::unique_ptr deserialize(FsFile& file); diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index 661488d2..f01e1d1f 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -1964,8 +1964,10 @@ void EpubReaderActivity::renderContents(std::unique_ptr page, const int or const bool effectiveForceLoad = forceLoadLargeImages || !SETTINGS.largeImagePlaceholder; pageHasPlaceholders = page->hasPlaceholderImages(effectiveForceLoad); - // Force special handling for pages with real (non-placeholder) images when anti-aliasing is on - bool imagePageWithAA = page->hasImages() && !pageHasPlaceholders && aaEnabledForThisRender; + // Force special handling for pages with at least one real (decoded) image when anti-aliasing is on. + // Mixed pages (some decoded, some placeholder) still need the AA codepath. + bool imagePageWithAA = + page->hasImages() && !page->allImagesArePlaceholders(effectiveForceLoad) && aaEnabledForThisRender; bool forceHalfRefreshThisPage = pendingHalfRefreshAfterImagePage && SETTINGS.halfRefreshAfterImagePage; pendingHalfRefreshAfterImagePage = false; lastRenderStats.imagePageWithAA = imagePageWithAA; @@ -2066,9 +2068,9 @@ void EpubReaderActivity::renderContents(std::unique_ptr page, const int or LOG_INF("ERS", "Skipping grayscale/BW-restore for this page (insufficient heap for BW snapshot)"); } - // Only schedule the half-refresh if real images were decoded on this page. + // Only schedule the half-refresh if at least one real image was decoded on this page. // Placeholder-only pages don't deposit grayscale data that needs settling. - if (page->hasImages() && !pageHasPlaceholders && + if (page->hasImages() && !page->allImagesArePlaceholders(effectiveForceLoad) && getEffectiveImageRendering() != CrossPointSettings::IMAGES_SUPPRESS) { pendingHalfRefreshAfterImagePage = true; }