diff --git a/lib/Epub/Epub/Page.cpp b/lib/Epub/Epub/Page.cpp index 25c1f512..71e7485d 100644 --- a/lib/Epub/Epub/Page.cpp +++ b/lib/Epub/Epub/Page.cpp @@ -57,6 +57,10 @@ void PageImage::render(GfxRenderer& renderer, const int fontId, const int xOffse imageBlock->render(renderer, xPos + xOffset, yPos + yOffset); } +void PageImage::renderPlaceholder(GfxRenderer& renderer, const int xOffset, const int yOffset) const { + imageBlock->renderPlaceholder(renderer, xPos + xOffset, yPos + yOffset); +} + bool PageImage::serialize(HalFile& file) { serialization::writePod(file, xPos); serialization::writePod(file, yPos); @@ -125,6 +129,17 @@ void Page::renderImages(GfxRenderer& renderer, const int fontId, const int xOffs [](const PageElement& element) { return element.getTag() == TAG_PageImage; }); } +void Page::renderWithImagePlaceholders(GfxRenderer& renderer, const int fontId, const int xOffset, + const int yOffset) const { + for (const auto& element : elements) { + if (element->getTag() == TAG_PageImage) { + static_cast(*element).renderPlaceholder(renderer, xOffset, yOffset); + } else { + element->render(renderer, fontId, xOffset, yOffset); + } + } +} + bool Page::serialize(HalFile& file) const { const uint16_t count = elements.size(); serialization::writePod(file, count); diff --git a/lib/Epub/Epub/Page.h b/lib/Epub/Epub/Page.h index 09474699..d265de10 100644 --- a/lib/Epub/Epub/Page.h +++ b/lib/Epub/Epub/Page.h @@ -50,6 +50,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 renderPlaceholder(GfxRenderer& renderer, int xOffset, int yOffset) const; bool serialize(HalFile& file) override; PageElementTag getTag() const override { return TAG_PageImage; } static std::unique_ptr deserialize(HalFile& file); @@ -89,6 +90,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; + void renderWithImagePlaceholders(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) const; bool serialize(HalFile& file) const; static std::unique_ptr deserialize(HalFile& file); @@ -98,6 +100,13 @@ class Page { [](const std::shared_ptr& el) { return el->getTag() == TAG_PageImage; }); } + bool hasImagesNeedingDecode() const { + return std::any_of(elements.begin(), elements.end(), [](const std::shared_ptr& element) { + return element->getTag() == TAG_PageImage && + static_cast(*element).getImageBlock().needsDecode(); + }); + } + // 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 { diff --git a/lib/Epub/Epub/blocks/ImageBlock.cpp b/lib/Epub/Epub/blocks/ImageBlock.cpp index 709d70e4..aa99951b 100644 --- a/lib/Epub/Epub/blocks/ImageBlock.cpp +++ b/lib/Epub/Epub/blocks/ImageBlock.cpp @@ -5,6 +5,8 @@ #include #include +#include + #include "Epub/converters/DirectPixelWriter.h" #include "Epub/converters/ImageDecoderFactory.h" @@ -29,6 +31,54 @@ std::string getCachePath(const std::string& imagePath) { return imagePath + ".pxc"; } +bool readValidCacheHeader(HalFile& cacheFile, const int expectedWidth, const int expectedHeight, uint16_t& cachedWidth, + uint16_t& cachedHeight) { + if (cacheFile.read(&cachedWidth, 2) != 2 || cacheFile.read(&cachedHeight, 2) != 2) { + return false; + } + + const int widthDiff = abs(cachedWidth - expectedWidth); + const int heightDiff = abs(cachedHeight - expectedHeight); + if (widthDiff > 1 || heightDiff > 1) { + return false; + } + + const size_t bytesPerRow = (cachedWidth + 3) / 4; + const size_t expectedSize = 4 + bytesPerRow * cachedHeight; + return cacheFile.size() >= expectedSize; +} + +// Pages are deserialized afresh on each visit. Keep a bounded, allocation-free +// record so an image that failed renders its placeholder directly for the rest +// of the reader session instead of paying another placeholder refresh and +// decode. The reader clears this on entry so transient memory/storage failures +// are retried. +constexpr size_t MAX_SESSION_IMAGE_FAILURES = 16; +uint64_t failedImageHashes[MAX_SESSION_IMAGE_FAILURES]; +size_t failedImageCount = 0; + +uint64_t imagePathHash(const std::string& path) { + uint64_t hash = 14695981039346656037ull; + for (const char c : path) { + hash ^= static_cast(c); + hash *= 1099511628211ull; + } + return hash; +} + +bool imageFailedThisSession(const std::string& path) { + const uint64_t hash = imagePathHash(path); + for (size_t i = 0; i < failedImageCount; i++) { + if (failedImageHashes[i] == hash) return true; + } + return false; +} + +void rememberImageFailure(const std::string& path) { + if (failedImageCount == MAX_SESSION_IMAGE_FAILURES || imageFailedThisSession(path)) return; + failedImageHashes[failedImageCount++] = imagePathHash(path); +} + bool renderFromCache(GfxRenderer& renderer, const std::string& cachePath, int x, int y, int expectedWidth, int expectedHeight) { HalFile cacheFile; @@ -37,16 +87,8 @@ bool renderFromCache(GfxRenderer& renderer, const std::string& cachePath, int x, } uint16_t cachedWidth, cachedHeight; - if (cacheFile.read(&cachedWidth, 2) != 2 || cacheFile.read(&cachedHeight, 2) != 2) { - return false; - } - - // Verify dimensions are close (allow 1 pixel tolerance for rounding differences) - int widthDiff = abs(cachedWidth - expectedWidth); - int heightDiff = abs(cachedHeight - expectedHeight); - if (widthDiff > 1 || heightDiff > 1) { - LOG_ERR("IMG", "Cache dimension mismatch: %dx%d vs %dx%d", cachedWidth, cachedHeight, expectedWidth, - expectedHeight); + if (!readValidCacheHeader(cacheFile, expectedWidth, expectedHeight, cachedWidth, cachedHeight)) { + LOG_ERR("IMG", "Invalid image cache: %s", cachePath.c_str()); return false; } @@ -119,6 +161,28 @@ bool renderFromCache(GfxRenderer& renderer, const std::string& cachePath, int x, } // namespace +bool ImageBlock::hasValidCache() const { + const auto cachePath = getCachePath(imagePath); + HalFile cacheFile; + if (!Storage.openFileForRead("IMG", cachePath, cacheFile)) { + return false; + } + + uint16_t cachedWidth, cachedHeight; + return readValidCacheHeader(cacheFile, width, height, cachedWidth, cachedHeight); +} + +bool ImageBlock::needsDecode() const { return !imageFailedThisSession(imagePath) && !hasValidCache(); } + +void ImageBlock::clearSessionRenderFailures() { failedImageCount = 0; } + +void ImageBlock::renderPlaceholder(GfxRenderer& renderer, const int x, const int y) const { + renderer.fillRect(x, y, width, height, true); + if (width > 2 && height > 2) { + renderer.fillRect(x + 1, y + 1, width - 2, height - 2, false); + } +} + void ImageBlock::render(GfxRenderer& renderer, const int x, const int y) { // The font-prewarm scan pass only accumulates glyphs; an image contributes // none, and its DirectPixelWriter output bypasses the renderer's scan-mode @@ -150,6 +214,11 @@ void ImageBlock::render(GfxRenderer& renderer, const int x, const int y) { return; } + if (imageFailedThisSession(imagePath)) { + renderPlaceholder(renderer, x, y); + return; + } + // Try to render from cache first std::string cachePath = getCachePath(imagePath); if (renderFromCache(renderer, cachePath, x, y, width, height)) { @@ -161,6 +230,8 @@ void ImageBlock::render(GfxRenderer& renderer, const int x, const int y) { HalFile file; if (!Storage.openFileForRead("IMG", imagePath, file)) { LOG_ERR("IMG", "Image file not found: %s", imagePath.c_str()); + rememberImageFailure(imagePath); + renderPlaceholder(renderer, x, y); return; } size_t fileSize = file.size(); @@ -168,6 +239,8 @@ void ImageBlock::render(GfxRenderer& renderer, const int x, const int y) { if (fileSize == 0) { LOG_ERR("IMG", "Image file is empty: %s", imagePath.c_str()); + rememberImageFailure(imagePath); + renderPlaceholder(renderer, x, y); return; } @@ -187,6 +260,8 @@ void ImageBlock::render(GfxRenderer& renderer, const int x, const int y) { ImageToFramebufferDecoder* decoder = ImageDecoderFactory::getDecoder(imagePath); if (!decoder) { LOG_ERR("IMG", "No decoder found for image: %s", imagePath.c_str()); + rememberImageFailure(imagePath); + renderPlaceholder(renderer, x, y); return; } @@ -195,6 +270,8 @@ 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()); + rememberImageFailure(imagePath); + renderPlaceholder(renderer, x, y); return; } diff --git a/lib/Epub/Epub/blocks/ImageBlock.h b/lib/Epub/Epub/blocks/ImageBlock.h index e6cccae7..886b5e98 100644 --- a/lib/Epub/Epub/blocks/ImageBlock.h +++ b/lib/Epub/Epub/blocks/ImageBlock.h @@ -16,6 +16,10 @@ class ImageBlock final : public Block { int16_t getHeight() const { return height; } bool imageExists() const; + bool hasValidCache() const; + bool needsDecode() const; + void renderPlaceholder(GfxRenderer& renderer, int x, int y) const; + static void clearSessionRenderFailures(); BlockType getType() override { return IMAGE_BLOCK; } bool isEmpty() override { return false; } diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index 21a5d6f3..66c9d9b4 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -155,6 +155,8 @@ void EpubReaderActivity::onEnter() { return; } + ImageBlock::clearSessionRenderFailures(); + // Configure screen orientation based on settings // NOTE: This affects layout math and must be applied before any render calls. ReaderUtils::applyOrientation(renderer, SETTINGS.orientation); @@ -1346,6 +1348,7 @@ void EpubReaderActivity::renderContents(std::unique_ptr page, const int or const auto tPrewarm = millis(); const bool pageHasImages = page->hasImages(); + const bool pageHasImagesNeedingDecode = pageHasImages && page->hasImagesNeedingDecode(); const bool needsTextGrayscale = SETTINGS.textAntiAliasing; const bool needsAnyGrayscale = needsTextGrayscale || pageHasImages; auto renderGrayscalePass = [&]() { @@ -1356,6 +1359,13 @@ void EpubReaderActivity::renderContents(std::unique_ptr page, const int or } }; + if (pageHasImagesNeedingDecode) { + page->renderWithImagePlaceholders(renderer, fontId, orientedMarginLeft, orientedMarginTop); + renderStatusBar(); + renderer.displayBuffer(HalDisplay::FAST_REFRESH); + renderer.clearScreen(); + } + page->render(renderer, fontId, orientedMarginLeft, orientedMarginTop); renderStatusBar(); const auto tBwRender = millis();