From 2a38bfd8af87a382fc3334f0fff532a4a61b3c4f Mon Sep 17 00:00:00 2001 From: martin brook Date: Fri, 20 Feb 2026 01:05:15 +0000 Subject: [PATCH] fix: use double FAST_REFRESH to prevent washout on large grey images (#957) ## Summary Fixes https://github.com/crosspoint-reader/crosspoint-reader/issues/1011 Use double FAST_REFRESH for image pages to prevent grayscale washout, HALF_REFRESH sets e-ink particles too firmly for the grayscale LUT to adjust, causing washed-out images (especially large, light-gray ones). Replace HALF_REFRESH with @pablohc's double FAST_REFRESH technique: blank only the image bounding box area, then re-render with images. This clears ghosting while keeping particles loosely set for grayscale. ## Additional Context --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? _**< PARTIALLY >**_ --- lib/Epub/Epub/Page.h | 29 ++++++++++++++++++++ src/activities/reader/EpubReaderActivity.cpp | 26 +++++++++++++++--- 2 files changed, 51 insertions(+), 4 deletions(-) diff --git a/lib/Epub/Epub/Page.h b/lib/Epub/Epub/Page.h index 88f98dc7..af792f6f 100644 --- a/lib/Epub/Epub/Page.h +++ b/lib/Epub/Epub/Page.h @@ -49,6 +49,7 @@ class PageImage final : public PageElement { bool serialize(FsFile& file) override; PageElementTag getTag() const override { return TAG_PageImage; } static std::unique_ptr deserialize(FsFile& file); + const ImageBlock& getImageBlock() const { return *imageBlock; } }; class Page { @@ -64,4 +65,32 @@ class Page { return std::any_of(elements.begin(), elements.end(), [](const std::shared_ptr& el) { return el->getTag() == TAG_PageImage; }); } + + // Get bounding box of all images on the page (union of image rects) + // Returns false if no images. Coordinates are relative to page origin. + bool getImageBoundingBox(int16_t& outX, int16_t& outY, int16_t& outW, int16_t& outH) const { + bool found = false; + int16_t minX = INT16_MAX, minY = INT16_MAX, maxX = INT16_MIN, maxY = INT16_MIN; + for (const auto& el : elements) { + if (el->getTag() == TAG_PageImage) { + const auto& img = static_cast(*el); + int16_t x = img.xPos; + int16_t y = img.yPos; + int16_t right = x + img.getImageBlock().getWidth(); + int16_t bottom = y + img.getImageBlock().getHeight(); + minX = std::min(minX, x); + minY = std::min(minY, y); + maxX = std::max(maxX, right); + maxY = std::max(maxY, bottom); + found = true; + } + } + if (found) { + outX = minX; + outY = minY; + outW = maxX - minX; + outH = maxY - minY; + } + return found; + } }; diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index 029847c1..7f66d3bf 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -638,13 +638,31 @@ void EpubReaderActivity::saveProgress(int spineIndex, int currentPage, int pageC void EpubReaderActivity::renderContents(std::unique_ptr page, const int orientedMarginTop, const int orientedMarginRight, const int orientedMarginBottom, const int orientedMarginLeft) { - // Force full refresh for pages with images when anti-aliasing is on, - // as grayscale tones require half refresh to display correctly - bool forceFullRefresh = page->hasImages() && SETTINGS.textAntiAliasing; + // Force special handling for pages with images when anti-aliasing is on + bool imagePageWithAA = page->hasImages() && SETTINGS.textAntiAliasing; page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop); renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft); - if (forceFullRefresh || pagesUntilFullRefresh <= 1) { + if (imagePageWithAA) { + // Double FAST_REFRESH with selective image blanking (pablohc's technique): + // HALF_REFRESH sets particles too firmly for the grayscale LUT to adjust. + // Instead, blank only the image area and do two fast refreshes. + // Step 1: Display page with image area blanked (text appears, image area white) + // Step 2: Re-render with images and display again (images appear clean) + int16_t imgX, imgY, imgW, imgH; + if (page->getImageBoundingBox(imgX, imgY, imgW, imgH)) { + renderer.fillRect(imgX + orientedMarginLeft, imgY + orientedMarginTop, imgW, imgH, false); + renderer.displayBuffer(HalDisplay::FAST_REFRESH); + + // Re-render page content to restore images into the blanked area + page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop); + renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft); + renderer.displayBuffer(HalDisplay::FAST_REFRESH); + } else { + renderer.displayBuffer(HalDisplay::HALF_REFRESH); + } + // Double FAST_REFRESH handles ghosting for image pages; don't count toward full refresh cadence + } else if (pagesUntilFullRefresh <= 1) { renderer.displayBuffer(HalDisplay::HALF_REFRESH); pagesUntilFullRefresh = SETTINGS.getRefreshFrequency(); } else {