From d1abcc00a2bcb83bfd7c03c6e995abaee7cd2615 Mon Sep 17 00:00:00 2001 From: Julia Date: Mon, 22 Jun 2026 10:15:27 -0400 Subject: [PATCH] feat: render grayscale epub images without text aa (#2393) --- lib/Epub/Epub/Page.cpp | 23 ++++++++-- lib/Epub/Epub/Page.h | 1 + src/activities/reader/EpubReaderActivity.cpp | 45 +++++++++++++------- 3 files changed, 51 insertions(+), 18 deletions(-) diff --git a/lib/Epub/Epub/Page.cpp b/lib/Epub/Epub/Page.cpp index 69caa30c..5032056c 100644 --- a/lib/Epub/Epub/Page.cpp +++ b/lib/Epub/Epub/Page.cpp @@ -6,6 +6,20 @@ #include +namespace { + +template +void renderFilteredPageElements(const std::vector>& elements, GfxRenderer& renderer, + const int fontId, const int xOffset, const int yOffset, Predicate&& predicate) { + for (const auto& element : elements) { + if (predicate(*element)) { + element->render(renderer, fontId, xOffset, yOffset); + } + } +} + +} // namespace + void PageLine::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) { block->render(renderer, fontId, xPos + xOffset, yPos + yOffset); } @@ -93,9 +107,12 @@ std::unique_ptr PageHorizontalRule::deserialize(HalFile& fil } void Page::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) const { - for (auto& element : elements) { - element->render(renderer, fontId, xOffset, yOffset); - } + renderFilteredPageElements(elements, renderer, fontId, xOffset, yOffset, [](const PageElement&) { return true; }); +} + +void Page::renderImages(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) const { + renderFilteredPageElements(elements, renderer, fontId, xOffset, yOffset, + [](const PageElement& element) { return element.getTag() == TAG_PageImage; }); } bool Page::serialize(HalFile& file) const { diff --git a/lib/Epub/Epub/Page.h b/lib/Epub/Epub/Page.h index f9ad2603..09474699 100644 --- a/lib/Epub/Epub/Page.h +++ b/lib/Epub/Epub/Page.h @@ -88,6 +88,7 @@ class Page { } void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) const; + void renderImages(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) const; bool serialize(HalFile& file) const; static std::unique_ptr deserialize(HalFile& file); diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index 59d189a8..e1328787 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -976,22 +976,31 @@ void EpubReaderActivity::renderContents(std::unique_ptr page, const int or const int orientedMarginRight, const int orientedMarginBottom, const int orientedMarginLeft) { const auto t0 = millis(); + const int fontId = SETTINGS.getReaderFontId(); // Font prewarm: scan pass accumulates text, then prewarm, then real render auto* fcm = renderer.getFontCacheManager(); auto scope = fcm->createPrewarmScope(); - page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop); // scan pass + page->render(renderer, fontId, orientedMarginLeft, orientedMarginTop); // scan pass scope.endScanAndPrewarm(); const auto tPrewarm = millis(); - // Force special handling for pages with images when anti-aliasing is on - bool imagePageWithAA = page->hasImages() && SETTINGS.textAntiAliasing; + const bool pageHasImages = page->hasImages(); + const bool needsTextGrayscale = SETTINGS.textAntiAliasing; + const bool needsAnyGrayscale = needsTextGrayscale || pageHasImages; + auto renderGrayscalePass = [&]() { + if (needsTextGrayscale) { + page->render(renderer, fontId, orientedMarginLeft, orientedMarginTop); + } else { + page->renderImages(renderer, fontId, orientedMarginLeft, orientedMarginTop); + } + }; - page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop); + page->render(renderer, fontId, orientedMarginLeft, orientedMarginTop); renderStatusBar(); const auto tBwRender = millis(); - if (imagePageWithAA) { + if (pageHasImages) { // 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. @@ -1004,7 +1013,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, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop); + page->render(renderer, fontId, orientedMarginLeft, orientedMarginTop); renderer.displayBuffer(HalDisplay::FAST_REFRESH); } else { renderer.displayBuffer(HalDisplay::HALF_REFRESH); @@ -1028,7 +1037,7 @@ void EpubReaderActivity::renderContents(std::unique_ptr page, const int or // per plane, but renderCharImpl culls out-of-band glyphs before decode so the // cost stays close to one render. Both text (drawPixel) and images // (DirectPixelWriter) honor the active strip target. - if (SETTINGS.textAntiAliasing && renderer.supportsStripGrayscale()) { + if (needsAnyGrayscale && renderer.supportsStripGrayscale()) { constexpr int STRIP_ROWS = 80; const int gh = renderer.getDisplayHeight(); const int gwBytes = renderer.getDisplayWidthBytes(); @@ -1044,7 +1053,7 @@ void EpubReaderActivity::renderContents(std::unique_ptr page, const int or const int rows = (gh - y < STRIP_ROWS) ? (gh - y) : STRIP_ROWS; renderer.beginStripTarget(scratch.get(), y, rows); renderer.clearScreen(0x00); - page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop); + renderGrayscalePass(); renderer.endStripTarget(); renderer.writeGrayscalePlaneStrip(true, scratch.get(), y, rows); } @@ -1056,7 +1065,7 @@ void EpubReaderActivity::renderContents(std::unique_ptr page, const int or const int rows = (gh - y < STRIP_ROWS) ? (gh - y) : STRIP_ROWS; renderer.beginStripTarget(scratch.get(), y, rows); renderer.clearScreen(0x00); - page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop); + renderGrayscalePass(); renderer.endStripTarget(); renderer.writeGrayscalePlaneStrip(false, scratch.get(), y, rows); } @@ -1081,22 +1090,28 @@ void EpubReaderActivity::renderContents(std::unique_ptr page, const int or } else { // Fallback path for a controller without strip support. grayscale rendering // TODO: Only do this if font supports it - if (SETTINGS.textAntiAliasing) { + if (needsAnyGrayscale) { // Save the BW frame before the grayscale passes overwrite it, restore // after. Only needed when grayscale actually renders. - renderer.storeBwBuffer(); + if (!renderer.storeBwBuffer()) { + LOG_ERR("ERS", "Failed to store BW buffer for grayscale render; skipping grayscale this page"); + const auto tEnd = millis(); + LOG_DBG("ERS", "Page render: prewarm=%lums bw_render=%lums display=%lums total=%lums", tPrewarm - t0, + tBwRender - tPrewarm, tDisplay - tBwRender, tEnd - t0); + return; + } const auto tBwStore = millis(); renderer.clearScreen(0x00); renderer.setRenderMode(GfxRenderer::GRAYSCALE_LSB); - page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop); + renderGrayscalePass(); renderer.copyGrayscaleLsbBuffers(); const auto tGrayLsb = millis(); // Render and copy to MSB buffer renderer.clearScreen(0x00); renderer.setRenderMode(GfxRenderer::GRAYSCALE_MSB); - page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop); + renderGrayscalePass(); renderer.copyGrayscaleMsbBuffers(); const auto tGrayMsb = millis(); @@ -1114,8 +1129,8 @@ void EpubReaderActivity::renderContents(std::unique_ptr page, const int or tPrewarm - t0, tBwRender - tPrewarm, tDisplay - tBwRender, tBwStore - tDisplay, tGrayLsb - tBwStore, tGrayMsb - tGrayLsb, tGrayDisplay - tGrayMsb, tBwRestore - tGrayDisplay, tEnd - t0); } else { - // No anti-aliasing: BW frame already displayed above, no grayscale to - // render, so no save/restore. + // No text AA and no images: BW frame already displayed above, no grayscale + // to render, so no save/restore. const auto tEnd = millis(); LOG_DBG("ERS", "Page render: prewarm=%lums bw_render=%lums display=%lums total=%lums", tPrewarm - t0, tBwRender - tPrewarm, tDisplay - tBwRender, tEnd - t0);