From b531077b8c74fa977c3f0efddf236b3db2f9fa86 Mon Sep 17 00:00:00 2001 From: jpirnay Date: Mon, 2 Mar 2026 10:52:22 +0100 Subject: [PATCH 01/13] Support transparent PNG --- lib/I18n/translations/english.yaml | 1 + src/CrossPointSettings.h | 1 + src/SettingsList.h | 2 +- src/activities/boot_sleep/SleepActivity.cpp | 287 ++++++++++++++++++- src/activities/boot_sleep/SleepActivity.h | 1 + src/activities/reader/EpubReaderActivity.cpp | 62 ++++ src/activities/reader/EpubReaderActivity.h | 5 + src/activities/reader/TxtReaderActivity.cpp | 185 ++++++++++++ src/activities/reader/TxtReaderActivity.h | 5 + src/activities/reader/XtcReaderActivity.cpp | 75 +++++ src/activities/reader/XtcReaderActivity.h | 5 + 11 files changed, 627 insertions(+), 2 deletions(-) diff --git a/lib/I18n/translations/english.yaml b/lib/I18n/translations/english.yaml index f5b80b8e..16239f0f 100644 --- a/lib/I18n/translations/english.yaml +++ b/lib/I18n/translations/english.yaml @@ -273,6 +273,7 @@ STR_SUNLIGHT_FADING_FIX: "Sunlight Fading Fix" STR_REMAP_FRONT_BUTTONS: "Remap Front Buttons" STR_OPDS_BROWSER: "OPDS Browser" STR_COVER_CUSTOM: "Cover + Custom" +STR_PAGE_OVERLAY: "Page overlay" STR_RECENTS: "Recents" STR_MENU_RECENT_BOOKS: "Recent Books" STR_NO_RECENT_BOOKS: "No recent books" diff --git a/src/CrossPointSettings.h b/src/CrossPointSettings.h index 5ba7bde2..16c53110 100644 --- a/src/CrossPointSettings.h +++ b/src/CrossPointSettings.h @@ -24,6 +24,7 @@ class CrossPointSettings { COVER = 3, BLANK = 4, COVER_CUSTOM = 5, + OVERLAY = 6, SLEEP_SCREEN_MODE_COUNT }; enum SLEEP_SCREEN_COVER_MODE { FIT = 0, CROP = 1, SLEEP_SCREEN_COVER_MODE_COUNT }; diff --git a/src/SettingsList.h b/src/SettingsList.h index c10d58c6..10374a1e 100644 --- a/src/SettingsList.h +++ b/src/SettingsList.h @@ -16,7 +16,7 @@ inline const std::vector& getSettingsList() { // --- Display --- SettingInfo::Enum(StrId::STR_SLEEP_SCREEN, &CrossPointSettings::sleepScreen, {StrId::STR_DARK, StrId::STR_LIGHT, StrId::STR_CUSTOM, StrId::STR_COVER, StrId::STR_NONE_OPT, - StrId::STR_COVER_CUSTOM}, + StrId::STR_COVER_CUSTOM, StrId::STR_PAGE_OVERLAY}, "sleepScreen", StrId::STR_CAT_DISPLAY), SettingInfo::Enum(StrId::STR_SLEEP_COVER_MODE, &CrossPointSettings::sleepScreenCoverMode, {StrId::STR_FIT, StrId::STR_CROP}, "sleepScreenCoverMode", StrId::STR_CAT_DISPLAY), diff --git a/src/activities/boot_sleep/SleepActivity.cpp b/src/activities/boot_sleep/SleepActivity.cpp index cd6de5fc..9821500b 100644 --- a/src/activities/boot_sleep/SleepActivity.cpp +++ b/src/activities/boot_sleep/SleepActivity.cpp @@ -4,19 +4,142 @@ #include #include #include +#include #include #include +#include + #include "CrossPointSettings.h" #include "CrossPointState.h" #include "components/UITheme.h" #include "fontIds.h" #include "images/Logo120.h" +#include "reader/EpubReaderActivity.h" +#include "reader/TxtReaderActivity.h" +#include "reader/XtcReaderActivity.h" #include "util/StringUtils.h" +namespace { + +// Context passed through PNGdec's decode() user-pointer to the per-scanline draw callback. +struct PngOverlayCtx { + const GfxRenderer* renderer; + int screenW; + int screenH; + int srcWidth; + int dstWidth; + int dstX; + int dstY; + float yScale; + int lastDstY; +}; + +// PNGdec file I/O callbacks — mirror the pattern in PngToFramebufferConverter.cpp. +void* pngSleepOpen(const char* filename, int32_t* size) { + FsFile* f = new FsFile(); + if (!Storage.openFileForRead("SLP", std::string(filename), *f)) { + delete f; + return nullptr; + } + *size = f->size(); + return f; +} +void pngSleepClose(void* handle) { + FsFile* f = reinterpret_cast(handle); + if (f) { + f->close(); + delete f; + } +} +int32_t pngSleepRead(PNGFILE* pFile, uint8_t* pBuf, int32_t len) { + FsFile* f = reinterpret_cast(pFile->fHandle); + return f ? f->read(pBuf, len) : 0; +} +int32_t pngSleepSeek(PNGFILE* pFile, int32_t pos) { + FsFile* f = reinterpret_cast(pFile->fHandle); + if (!f) return -1; + return f->seek(pos); +} + +// Per-scanline draw callback for PNG overlay compositing. +// Transparent pixels (alpha < 128) are skipped so the reader page shows through. +// Opaque pixels are drawn in their grayscale brightness (dark → black, light → white). +int pngOverlayDraw(PNGDRAW* pDraw) { + PngOverlayCtx* ctx = reinterpret_cast(pDraw->pUser); + + const int destY = ctx->dstY + (int)(pDraw->y * ctx->yScale); + if (destY == ctx->lastDstY) return 1; // skip duplicate rows from Y scaling + ctx->lastDstY = destY; + if (destY < 0 || destY >= ctx->screenH) return 1; + + const int srcWidth = ctx->srcWidth; + const int dstWidth = ctx->dstWidth; + const uint8_t* pixels = pDraw->pPixels; + const int pixelType = pDraw->iPixelType; + const int hasAlpha = pDraw->iHasAlpha; + + int srcX = 0, error = 0; + for (int dstX = 0; dstX < dstWidth; dstX++) { + const int outX = ctx->dstX + dstX; + if (outX >= 0 && outX < ctx->screenW) { + uint8_t alpha = 255, gray = 0; + switch (pixelType) { + case PNG_PIXEL_TRUECOLOR_ALPHA: { + const uint8_t* p = &pixels[srcX * 4]; + alpha = p[3]; + gray = (uint8_t)((p[0] * 77 + p[1] * 150 + p[2] * 29) >> 8); + break; + } + case PNG_PIXEL_GRAY_ALPHA: + gray = pixels[srcX * 2]; + alpha = pixels[srcX * 2 + 1]; + break; + case PNG_PIXEL_TRUECOLOR: { + const uint8_t* p = &pixels[srcX * 3]; + gray = (uint8_t)((p[0] * 77 + p[1] * 150 + p[2] * 29) >> 8); + break; + } + case PNG_PIXEL_GRAYSCALE: + gray = pixels[srcX]; + break; + case PNG_PIXEL_INDEXED: + if (pDraw->pPalette) { + const uint8_t idx = pixels[srcX]; + const uint8_t* p = &pDraw->pPalette[idx * 3]; + gray = (uint8_t)((p[0] * 77 + p[1] * 150 + p[2] * 29) >> 8); + if (hasAlpha) alpha = pDraw->pPalette[768 + idx]; + } + break; + default: + gray = pixels[srcX]; + break; + } + + if (alpha >= 128) { + ctx->renderer->drawPixel(outX, destY, gray < 128); // true = black, false = white + } + // alpha < 128: transparent — leave the reader page pixel intact + } + + // Bresenham-style X stepping (handles downscaling; 1:1 when srcWidth == dstWidth) + error += srcWidth; + while (error >= dstWidth) { + error -= dstWidth; + srcX++; + } + } + return 1; +} + +} // namespace + void SleepActivity::onEnter() { Activity::onEnter(); - GUI.drawPopup(renderer, tr(STR_ENTERING_SLEEP)); + // For OVERLAY mode the popup is suppressed so the frame buffer (reader page) stays intact + if (SETTINGS.sleepScreen != CrossPointSettings::SLEEP_SCREEN_MODE::OVERLAY) { + GUI.drawPopup(renderer, tr(STR_ENTERING_SLEEP)); + } switch (SETTINGS.sleepScreen) { case (CrossPointSettings::SLEEP_SCREEN_MODE::BLANK): @@ -26,6 +149,8 @@ void SleepActivity::onEnter() { case (CrossPointSettings::SLEEP_SCREEN_MODE::COVER): case (CrossPointSettings::SLEEP_SCREEN_MODE::COVER_CUSTOM): return renderCoverSleepScreen(); + case (CrossPointSettings::SLEEP_SCREEN_MODE::OVERLAY): + return renderOverlaySleepScreen(); default: return renderDefaultSleepScreen(); } @@ -284,3 +409,163 @@ void SleepActivity::renderBlankSleepScreen() const { renderer.clearScreen(); renderer.displayBuffer(HalDisplay::HALF_REFRESH); } + +void SleepActivity::renderOverlaySleepScreen() const { + const auto pageWidth = renderer.getScreenWidth(); + const auto pageHeight = renderer.getScreenHeight(); + + // Step 1: Ensure the frame buffer contains the reader page. + // When coming from a reader activity the frame buffer already holds the page. + // When coming from a non-reader activity we re-render it from the saved progress. + if (!APP_STATE.lastSleepFromReader && !APP_STATE.openEpubPath.empty()) { + const auto& path = APP_STATE.openEpubPath; + bool rendered = false; + + if (StringUtils::checkFileExtension(path, ".xtc") || StringUtils::checkFileExtension(path, ".xtch")) { + rendered = XtcReaderActivity::drawCurrentPageToBuffer(path, renderer); + } else if (StringUtils::checkFileExtension(path, ".txt")) { + rendered = TxtReaderActivity::drawCurrentPageToBuffer(path, renderer); + } else if (StringUtils::checkFileExtension(path, ".epub")) { + rendered = EpubReaderActivity::drawCurrentPageToBuffer(path, renderer); + } + + if (!rendered) { + LOG_DBG("SLP", "Page re-render failed, using white background"); + renderer.clearScreen(); + } + } + + // Step 2: Load the overlay image using the same selection logic as renderCustomSleepScreen. + // BMP: white pixels are skipped (transparent via drawBitmap), black pixels composited on top. + // PNG: pixels with alpha < 128 are skipped; opaque pixels are drawn with their grayscale value. + auto tryDrawOverlay = [&](const std::string& filename) -> bool { + FsFile file; + if (!Storage.openFileForRead("SLP", filename, file)) return false; + Bitmap bitmap(file, true); + if (bitmap.parseHeaders() != BmpReaderError::Ok) { + file.close(); + return false; + } + + int x, y; + float cropX = 0, cropY = 0; + if (bitmap.getWidth() > pageWidth || bitmap.getHeight() > pageHeight) { + float ratio = static_cast(bitmap.getWidth()) / static_cast(bitmap.getHeight()); + const float screenRatio = static_cast(pageWidth) / static_cast(pageHeight); + if (ratio > screenRatio) { + x = 0; + y = std::round((static_cast(pageHeight) - static_cast(pageWidth) / ratio) / 2); + } else { + x = std::round((static_cast(pageWidth) - static_cast(pageHeight) * ratio) / 2); + y = 0; + } + } else { + x = (pageWidth - bitmap.getWidth()) / 2; + y = (pageHeight - bitmap.getHeight()) / 2; + } + + // Draw without clearScreen so the reader page remains in the frame buffer beneath + renderer.drawBitmap(bitmap, x, y, pageWidth, pageHeight, cropX, cropY); + file.close(); + return true; + }; + + auto tryDrawPngOverlay = [&](const std::string& filename) -> bool { + constexpr size_t MIN_FREE_HEAP = 60 * 1024; // PNG decoder ~42 KB + overhead + if (ESP.getFreeHeap() < MIN_FREE_HEAP) { + LOG_ERR("SLP", "Not enough heap for PNG overlay decoder"); + return false; + } + PNG* png = new (std::nothrow) PNG(); + if (!png) return false; + + int rc = png->open(filename.c_str(), pngSleepOpen, pngSleepClose, pngSleepRead, pngSleepSeek, pngOverlayDraw); + if (rc != PNG_SUCCESS) { + LOG_DBG("SLP", "PNG open failed: %s (%d)", filename.c_str(), rc); + delete png; + return false; + } + + const int srcW = png->getWidth(), srcH = png->getHeight(); + float yScale = 1.0f; + int dstW = srcW, dstH = srcH; + if (srcW > pageWidth || srcH > pageHeight) { + const float scaleX = (float)pageWidth / srcW, scaleY = (float)pageHeight / srcH; + const float scale = (scaleX < scaleY) ? scaleX : scaleY; + dstW = (int)(srcW * scale); + dstH = (int)(srcH * scale); + yScale = (float)dstH / srcH; + } + + PngOverlayCtx ctx; + ctx.renderer = &renderer; + ctx.screenW = pageWidth; + ctx.screenH = pageHeight; + ctx.srcWidth = srcW; + ctx.dstWidth = dstW; + ctx.dstX = (pageWidth - dstW) / 2; + ctx.dstY = (pageHeight - dstH) / 2; + ctx.yScale = yScale; + ctx.lastDstY = -1; + + rc = png->decode(&ctx, 0); + png->close(); + delete png; + return rc == PNG_SUCCESS; + }; + + // Try /sleep/ directory first (random selection, same as renderCustomSleepScreen). + // Accepts both .bmp and .png files; .bmp headers are validated during the scan. + bool overlayDrawn = false; + auto dir = Storage.open("/sleep"); + if (dir && dir.isDirectory()) { + std::vector files; + char name[500]; + for (auto file = dir.openNextFile(); file; file = dir.openNextFile()) { + if (file.isDirectory()) { file.close(); continue; } + file.getName(name, sizeof(name)); + auto filename = std::string(name); + if (filename[0] == '.') { file.close(); continue; } + const auto flen = filename.length(); + const bool isBmp = flen >= 4 && filename.substr(flen - 4) == ".bmp"; + const bool isPng = flen >= 4 && filename.substr(flen - 4) == ".png"; + if (!isBmp && !isPng) { file.close(); continue; } + if (isBmp) { + Bitmap bmp(file); + if (bmp.parseHeaders() != BmpReaderError::Ok) { file.close(); continue; } + } + files.emplace_back(filename); + file.close(); + } + const auto numFiles = files.size(); + if (numFiles > 0) { + auto randomFileIndex = random(numFiles); + while (numFiles > 1 && randomFileIndex == APP_STATE.lastSleepImage) { + randomFileIndex = random(numFiles); + } + APP_STATE.lastSleepImage = randomFileIndex; + APP_STATE.saveToFile(); + const std::string selected = "/sleep/" + files[randomFileIndex]; + const auto slen = selected.length(); + if (slen >= 4 && selected.substr(slen - 4) == ".png") { + overlayDrawn = tryDrawPngOverlay(selected); + } else { + overlayDrawn = tryDrawOverlay(selected); + } + } + } + if (dir) dir.close(); + + if (!overlayDrawn) { + overlayDrawn = tryDrawOverlay("/sleep.bmp"); + } + if (!overlayDrawn) { + overlayDrawn = tryDrawPngOverlay("/sleep.png"); + } + + if (!overlayDrawn) { + LOG_DBG("SLP", "No overlay image found, displaying page without overlay"); + } + + renderer.displayBuffer(HalDisplay::HALF_REFRESH); +} diff --git a/src/activities/boot_sleep/SleepActivity.h b/src/activities/boot_sleep/SleepActivity.h index 87df8ba1..43c4952f 100644 --- a/src/activities/boot_sleep/SleepActivity.h +++ b/src/activities/boot_sleep/SleepActivity.h @@ -15,4 +15,5 @@ class SleepActivity final : public Activity { void renderCoverSleepScreen() const; void renderBitmapSleepScreen(const Bitmap& bitmap) const; void renderBlankSleepScreen() const; + void renderOverlaySleepScreen() const; }; diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index b99cd07e..fff77935 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -829,3 +829,65 @@ void EpubReaderActivity::restoreSavedPosition() { } requestUpdate(); } + +bool EpubReaderActivity::drawCurrentPageToBuffer(const std::string& filePath, GfxRenderer& renderer) { + auto epub = std::make_shared(filePath, "/.crosspoint"); + // skip CSS (second arg) since we only need layout/section data + if (!epub->load(true, false)) { + LOG_DBG("SLP", "EPUB: failed to load %s", filePath.c_str()); + return false; + } + + epub->setupCacheDir(); + + // Load saved spine index and page number + int spineIndex = 0, pageNumber = 0; + FsFile f; + if (Storage.openFileForRead("SLP", epub->getCachePath() + "/progress.bin", f)) { + uint8_t data[6]; + if (f.read(data, 6) == 6) { + spineIndex = data[0] | (data[1] << 8); + pageNumber = data[2] | (data[3] << 8); + } + f.close(); + } + if (spineIndex < 0 || spineIndex >= epub->getSpineItemsCount()) spineIndex = 0; + + // Apply the reader orientation so margins match what the reader would produce + applyReaderOrientation(renderer, SETTINGS.orientation); + + // Compute margins exactly as render() does + int marginTop, marginRight, marginBottom, marginLeft; + renderer.getOrientedViewableTRBL(&marginTop, &marginRight, &marginBottom, &marginLeft); + marginTop += SETTINGS.screenMargin; + marginLeft += SETTINGS.screenMargin; + marginRight += SETTINGS.screenMargin; + const uint8_t statusBarHeight = UITheme::getInstance().getStatusBarHeight(); + marginBottom += std::max(SETTINGS.screenMargin, statusBarHeight); + + const uint16_t viewportWidth = renderer.getScreenWidth() - marginLeft - marginRight; + const uint16_t viewportHeight = renderer.getScreenHeight() - marginTop - marginBottom; + + // Load the cached section file (won't rebuild if cache missing — too slow for sleep) + auto section = std::unique_ptr
(new Section(epub, spineIndex, renderer)); + if (!section->loadSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(), + SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth, + viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle)) { + LOG_DBG("SLP", "EPUB: section cache not found for spine %d", spineIndex); + return false; + } + + if (pageNumber < 0 || pageNumber >= section->pageCount) pageNumber = 0; + section->currentPage = pageNumber; + + auto page = section->loadPageFromSectionFile(); + if (!page) { + LOG_DBG("SLP", "EPUB: failed to load page %d", pageNumber); + return false; + } + + renderer.clearScreen(); + page->render(renderer, SETTINGS.getReaderFontId(), marginLeft, marginTop); + // No displayBuffer call — caller (SleepActivity) handles that after compositing the overlay + return true; +} diff --git a/src/activities/reader/EpubReaderActivity.h b/src/activities/reader/EpubReaderActivity.h index 91c6f049..9acf3a60 100644 --- a/src/activities/reader/EpubReaderActivity.h +++ b/src/activities/reader/EpubReaderActivity.h @@ -58,4 +58,9 @@ class EpubReaderActivity final : public Activity { void loop() override; void render(RenderLock&& lock) override; bool isReaderActivity() const override { return true; } + + // Renders the last saved page to the frame buffer without flushing to display. + // Used by SleepActivity to prepare the background for the overlay sleep mode. + // Returns false if the page cannot be loaded (missing cache / file error). + static bool drawCurrentPageToBuffer(const std::string& filePath, GfxRenderer& renderer); }; diff --git a/src/activities/reader/TxtReaderActivity.cpp b/src/activities/reader/TxtReaderActivity.cpp index 994492ee..157d3cbe 100644 --- a/src/activities/reader/TxtReaderActivity.cpp +++ b/src/activities/reader/TxtReaderActivity.cpp @@ -600,3 +600,188 @@ void TxtReaderActivity::savePageIndexCache() const { f.close(); LOG_DBG("TRS", "Saved page index cache: %d pages", totalPages); } + +bool TxtReaderActivity::drawCurrentPageToBuffer(const std::string& filePath, GfxRenderer& renderer) { + Txt txt(filePath, "/.crosspoint"); + if (!txt.load()) { + LOG_DBG("SLP", "TXT: failed to load %s", filePath.c_str()); + return false; + } + + // Compute layout values that match what initializeReader() produces + const int fontId = SETTINGS.getReaderFontId(); + const uint8_t screenMargin = SETTINGS.screenMargin; + const uint8_t paragraphAlignment = SETTINGS.paragraphAlignment; + + int marginTop, marginRight, marginBottom, marginLeft; + renderer.getOrientedViewableTRBL(&marginTop, &marginRight, &marginBottom, &marginLeft); + marginTop += screenMargin; + marginLeft += screenMargin; + marginRight += screenMargin; + marginBottom += std::max(screenMargin, static_cast(UITheme::getInstance().getStatusBarHeight())); + + const int vw = renderer.getScreenWidth() - marginLeft - marginRight; + const int vh = renderer.getScreenHeight() - marginTop - marginBottom; + const int lineHeight = renderer.getLineHeight(fontId); + const int linesPerPage = std::max(1, vh / lineHeight); + + // Load the page offset index from cache (must already exist from normal reading) + std::string cachePath = txt.getCachePath() + "/index.bin"; + FsFile cacheFile; + if (!Storage.openFileForRead("SLP", cachePath, cacheFile)) { + LOG_DBG("SLP", "TXT: no page index cache"); + return false; + } + + uint32_t magic; + serialization::readPod(cacheFile, magic); + if (magic != CACHE_MAGIC) { + cacheFile.close(); + return false; + } + + uint8_t version; + serialization::readPod(cacheFile, version); + if (version != CACHE_VERSION) { + cacheFile.close(); + return false; + } + + uint32_t cachedFileSize; + serialization::readPod(cacheFile, cachedFileSize); + if (cachedFileSize != txt.getFileSize()) { + cacheFile.close(); + return false; + } + + int32_t cachedVw, cachedLpp, cachedFontId, cachedMargin; + serialization::readPod(cacheFile, cachedVw); + serialization::readPod(cacheFile, cachedLpp); + serialization::readPod(cacheFile, cachedFontId); + serialization::readPod(cacheFile, cachedMargin); + if (cachedVw != vw || cachedLpp != linesPerPage || cachedFontId != fontId || cachedMargin != screenMargin) { + LOG_DBG("SLP", "TXT: cache invalid (settings changed)"); + cacheFile.close(); + return false; + } + + uint8_t cachedAlignment; + serialization::readPod(cacheFile, cachedAlignment); + if (cachedAlignment != paragraphAlignment) { + cacheFile.close(); + return false; + } + + uint32_t numPages; + serialization::readPod(cacheFile, numPages); + if (numPages == 0) { + cacheFile.close(); + return false; + } + + std::vector pageOffsets; + pageOffsets.reserve(numPages); + for (uint32_t i = 0; i < numPages; i++) { + uint32_t offset; + serialization::readPod(cacheFile, offset); + pageOffsets.push_back(offset); + } + cacheFile.close(); + + // Load saved page number from progress file + int savedPage = 0; + FsFile progFile; + if (Storage.openFileForRead("SLP", txt.getCachePath() + "/progress.bin", progFile)) { + uint8_t data[4]; + if (progFile.read(data, 4) == 4) { + savedPage = data[0] + (data[1] << 8); + } + progFile.close(); + } + if (savedPage < 0 || savedPage >= static_cast(numPages)) savedPage = 0; + + // Load the page lines from file + std::vector pageLines; + const size_t fileSize = txt.getFileSize(); + size_t offset = pageOffsets[savedPage]; + if (offset >= fileSize) { + LOG_DBG("SLP", "TXT: page offset out of bounds"); + return false; + } + + // Replicate loadPageAtOffset() logic with local layout variables + size_t chunkSize = std::min(CHUNK_SIZE, fileSize - offset); + auto* buffer = static_cast(malloc(chunkSize + 1)); + if (!buffer) return false; + + if (!txt.readContent(buffer, offset, chunkSize)) { + free(buffer); + return false; + } + buffer[chunkSize] = '\0'; + + size_t pos = 0; + while (pos < chunkSize && static_cast(pageLines.size()) < linesPerPage) { + size_t lineEnd = pos; + while (lineEnd < chunkSize && buffer[lineEnd] != '\n') lineEnd++; + bool lineComplete = (lineEnd < chunkSize) || (offset + lineEnd >= fileSize); + if (!lineComplete && !pageLines.empty()) break; + + size_t lineContentLen = lineEnd - pos; + bool hasCR = (lineContentLen > 0 && buffer[pos + lineContentLen - 1] == '\r'); + size_t displayLen = hasCR ? lineContentLen - 1 : lineContentLen; + std::string line(reinterpret_cast(buffer + pos), displayLen); + size_t lineBytePos = 0; + + while (!line.empty() && static_cast(pageLines.size()) < linesPerPage) { + if (renderer.getTextWidth(fontId, line.c_str()) <= vw) { + pageLines.push_back(line); + lineBytePos = displayLen; + line.clear(); + break; + } + size_t breakPos = line.length(); + while (breakPos > 0 && renderer.getTextWidth(fontId, line.substr(0, breakPos).c_str()) > vw) { + size_t spacePos = line.rfind(' ', breakPos - 1); + if (spacePos != std::string::npos && spacePos > 0) { + breakPos = spacePos; + } else { + breakPos--; + while (breakPos > 0 && (line[breakPos] & 0xC0) == 0x80) breakPos--; + } + } + if (breakPos == 0) breakPos = 1; + pageLines.push_back(line.substr(0, breakPos)); + size_t skipChars = breakPos; + if (breakPos < line.length() && line[breakPos] == ' ') skipChars++; + lineBytePos += skipChars; + line = line.substr(skipChars); + } + pos = line.empty() ? lineEnd + 1 : pos + lineBytePos; + } + free(buffer); + + if (pageLines.empty()) return false; + + // Render lines to frame buffer (no displayBuffer call) + renderer.clearScreen(); + int y = marginTop; + for (const auto& line : pageLines) { + if (!line.empty()) { + int x = marginLeft; + switch (paragraphAlignment) { + case CrossPointSettings::CENTER_ALIGN: + x = marginLeft + (vw - renderer.getTextWidth(fontId, line.c_str())) / 2; + break; + case CrossPointSettings::RIGHT_ALIGN: + x = marginLeft + vw - renderer.getTextWidth(fontId, line.c_str()); + break; + default: + break; + } + renderer.drawText(fontId, x, y, line.c_str()); + } + y += lineHeight; + } + return true; +} diff --git a/src/activities/reader/TxtReaderActivity.h b/src/activities/reader/TxtReaderActivity.h index 45877a8e..21e20c48 100644 --- a/src/activities/reader/TxtReaderActivity.h +++ b/src/activities/reader/TxtReaderActivity.h @@ -49,4 +49,9 @@ class TxtReaderActivity final : public Activity { void loop() override; void render(RenderLock&&) override; bool isReaderActivity() const override { return true; } + + // Renders the last saved page to the frame buffer without flushing to display. + // Used by SleepActivity to prepare the background for the overlay sleep mode. + // Returns false if the page cannot be loaded (missing cache / file error). + static bool drawCurrentPageToBuffer(const std::string& filePath, GfxRenderer& renderer); }; diff --git a/src/activities/reader/XtcReaderActivity.cpp b/src/activities/reader/XtcReaderActivity.cpp index a10d1025..052e2f8f 100644 --- a/src/activities/reader/XtcReaderActivity.cpp +++ b/src/activities/reader/XtcReaderActivity.cpp @@ -351,3 +351,78 @@ void XtcReaderActivity::loadProgress() { f.close(); } } + +bool XtcReaderActivity::drawCurrentPageToBuffer(const std::string& filePath, GfxRenderer& renderer) { + Xtc xtc(filePath, "/.crosspoint"); + if (!xtc.load()) { + LOG_DBG("SLP", "XTC: failed to load %s", filePath.c_str()); + return false; + } + + // Load saved page number + uint32_t savedPage = 0; + FsFile f; + if (Storage.openFileForRead("SLP", xtc.getCachePath() + "/progress.bin", f)) { + uint8_t data[4]; + if (f.read(data, 4) == 4) { + savedPage = data[0] | (data[1] << 8) | (data[2] << 16) | (data[3] << 24); + } + f.close(); + } + if (savedPage >= xtc.getPageCount()) savedPage = 0; + + const uint16_t pageWidth = xtc.getPageWidth(); + const uint16_t pageHeight = xtc.getPageHeight(); + const uint8_t bitDepth = xtc.getBitDepth(); + + // Only use the 1-bit BW path; grayscale is not needed as a background under the overlay + const size_t pageBufferSize = (bitDepth == 2) ? ((static_cast(pageWidth) * pageHeight + 7) / 8) * 2 + : ((pageWidth + 7) / 8) * pageHeight; + + uint8_t* pageBuffer = static_cast(malloc(pageBufferSize)); + if (!pageBuffer) { + LOG_ERR("SLP", "XTC: failed to allocate page buffer"); + return false; + } + + if (xtc.loadPage(savedPage, pageBuffer, pageBufferSize) == 0) { + LOG_ERR("SLP", "XTC: failed to load page %lu", savedPage); + free(pageBuffer); + return false; + } + + renderer.clearScreen(); + + if (bitDepth == 2) { + // 2-bit XTH: draw all non-white pixels as black (BW pass only) + const size_t planeSize = (static_cast(pageWidth) * pageHeight + 7) / 8; + const uint8_t* plane1 = pageBuffer; + const uint8_t* plane2 = pageBuffer + planeSize; + const size_t colBytes = (pageHeight + 7) / 8; + for (uint16_t y = 0; y < pageHeight; y++) { + for (uint16_t x = 0; x < pageWidth; x++) { + const size_t colIndex = pageWidth - 1 - x; + const size_t byteInCol = y / 8; + const size_t bitInByte = 7 - (y % 8); + const size_t byteOffset = colIndex * colBytes + byteInCol; + const uint8_t bit1 = (plane1[byteOffset] >> bitInByte) & 1; + const uint8_t bit2 = (plane2[byteOffset] >> bitInByte) & 1; + if ((bit1 << 1) | bit2) { + renderer.drawPixel(x, y, true); + } + } + } + } else { + // 1-bit XTG: draw black pixels + const size_t srcRowBytes = (pageWidth + 7) / 8; + for (uint16_t srcY = 0; srcY < pageHeight; srcY++) { + for (uint16_t srcX = 0; srcX < pageWidth; srcX++) { + const bool isBlack = !((pageBuffer[srcY * srcRowBytes + srcX / 8] >> (7 - srcX % 8)) & 1); + if (isBlack) renderer.drawPixel(srcX, srcY, true); + } + } + } + + free(pageBuffer); + return true; +} diff --git a/src/activities/reader/XtcReaderActivity.h b/src/activities/reader/XtcReaderActivity.h index 18effaad..85bd973d 100644 --- a/src/activities/reader/XtcReaderActivity.h +++ b/src/activities/reader/XtcReaderActivity.h @@ -29,4 +29,9 @@ class XtcReaderActivity final : public Activity { void loop() override; void render(RenderLock&&) override; bool isReaderActivity() const override { return true; } + + // Renders the last saved page to the frame buffer without flushing to display. + // Used by SleepActivity to prepare the background for the overlay sleep mode. + // Returns false if the page cannot be loaded (missing cache / file error). + static bool drawCurrentPageToBuffer(const std::string& filePath, GfxRenderer& renderer); }; From 0c673f4e92116dde2e8be380a881f6a932360254 Mon Sep 17 00:00:00 2001 From: jpirnay Date: Mon, 2 Mar 2026 11:09:07 +0100 Subject: [PATCH 02/13] Fix headers --- src/activities/boot_sleep/SleepActivity.cpp | 27 +++++++++++++++------ 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/src/activities/boot_sleep/SleepActivity.cpp b/src/activities/boot_sleep/SleepActivity.cpp index 9821500b..d48eb8da 100644 --- a/src/activities/boot_sleep/SleepActivity.cpp +++ b/src/activities/boot_sleep/SleepActivity.cpp @@ -10,16 +10,17 @@ #include +#include "../reader/EpubReaderActivity.h" +#include "../reader/TxtReaderActivity.h" +#include "../reader/XtcReaderActivity.h" #include "CrossPointSettings.h" #include "CrossPointState.h" #include "components/UITheme.h" #include "fontIds.h" #include "images/Logo120.h" -#include "reader/EpubReaderActivity.h" -#include "reader/TxtReaderActivity.h" -#include "reader/XtcReaderActivity.h" #include "util/StringUtils.h" + namespace { // Context passed through PNGdec's decode() user-pointer to the per-scanline draw callback. @@ -522,17 +523,29 @@ void SleepActivity::renderOverlaySleepScreen() const { std::vector files; char name[500]; for (auto file = dir.openNextFile(); file; file = dir.openNextFile()) { - if (file.isDirectory()) { file.close(); continue; } + if (file.isDirectory()) { + file.close(); + continue; + } file.getName(name, sizeof(name)); auto filename = std::string(name); - if (filename[0] == '.') { file.close(); continue; } + if (filename[0] == '.') { + file.close(); + continue; + } const auto flen = filename.length(); const bool isBmp = flen >= 4 && filename.substr(flen - 4) == ".bmp"; const bool isPng = flen >= 4 && filename.substr(flen - 4) == ".png"; - if (!isBmp && !isPng) { file.close(); continue; } + if (!isBmp && !isPng) { + file.close(); + continue; + } if (isBmp) { Bitmap bmp(file); - if (bmp.parseHeaders() != BmpReaderError::Ok) { file.close(); continue; } + if (bmp.parseHeaders() != BmpReaderError::Ok) { + file.close(); + continue; + } } files.emplace_back(filename); file.close(); From b8109a2c5eeb8483ab4ec84b12237d3fc74ba067 Mon Sep 17 00:00:00 2001 From: jpirnay Date: Mon, 2 Mar 2026 11:22:55 +0100 Subject: [PATCH 03/13] Proper transparency color recognition --- src/activities/boot_sleep/SleepActivity.cpp | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/activities/boot_sleep/SleepActivity.cpp b/src/activities/boot_sleep/SleepActivity.cpp index d48eb8da..70d89c69 100644 --- a/src/activities/boot_sleep/SleepActivity.cpp +++ b/src/activities/boot_sleep/SleepActivity.cpp @@ -20,7 +20,6 @@ #include "images/Logo120.h" #include "util/StringUtils.h" - namespace { // Context passed through PNGdec's decode() user-pointer to the per-scanline draw callback. @@ -34,6 +33,9 @@ struct PngOverlayCtx { int dstY; float yScale; int lastDstY; + // Color-key transparency (tRNS chunk) for TRUECOLOR and GRAYSCALE images. + // -1 means no color key. For TRUECOLOR: 0x00RRGGBB; for GRAYSCALE: low byte only. + int32_t transparentColor; }; // PNGdec file I/O callbacks — mirror the pattern in PngToFramebufferConverter.cpp. @@ -99,10 +101,20 @@ int pngOverlayDraw(PNGDRAW* pDraw) { case PNG_PIXEL_TRUECOLOR: { const uint8_t* p = &pixels[srcX * 3]; gray = (uint8_t)((p[0] * 77 + p[1] * 150 + p[2] * 29) >> 8); + // tRNS color-key: if pixel matches the designated transparent color, skip it + if (ctx->transparentColor >= 0 && p[0] == (uint8_t)((ctx->transparentColor >> 16) & 0xFF) && + p[1] == (uint8_t)((ctx->transparentColor >> 8) & 0xFF) && + p[2] == (uint8_t)(ctx->transparentColor & 0xFF)) { + alpha = 0; + } break; } case PNG_PIXEL_GRAYSCALE: gray = pixels[srcX]; + // tRNS color-key: transparent gray value stored in low byte + if (ctx->transparentColor >= 0 && gray == (uint8_t)(ctx->transparentColor & 0xFF)) { + alpha = 0; + } break; case PNG_PIXEL_INDEXED: if (pDraw->pPalette) { @@ -508,6 +520,13 @@ void SleepActivity::renderOverlaySleepScreen() const { ctx.dstY = (pageHeight - dstH) / 2; ctx.yScale = yScale; ctx.lastDstY = -1; + // Populate color-key transparency for TRUECOLOR/GRAYSCALE PNGs with a tRNS chunk. + // TRUECOLOR_ALPHA and GRAY_ALPHA carry per-pixel alpha, handled directly in the callback. + // INDEXED+tRNS stores per-palette alpha at pPalette[768+idx], also handled there. + const int pixType = png->getPixelType(); + ctx.transparentColor = (png->hasAlpha() && (pixType == PNG_PIXEL_TRUECOLOR || pixType == PNG_PIXEL_GRAYSCALE)) + ? (int32_t)png->getTransparentColor() + : -1; rc = png->decode(&ctx, 0); png->close(); From 34233b6ebbfe2b4bf71ea97e95e425009beb74a7 Mon Sep 17 00:00:00 2001 From: jpirnay Date: Mon, 2 Mar 2026 11:43:03 +0100 Subject: [PATCH 04/13] Establish png lazy transparency detection --- src/activities/boot_sleep/SleepActivity.cpp | 23 ++++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/src/activities/boot_sleep/SleepActivity.cpp b/src/activities/boot_sleep/SleepActivity.cpp index 70d89c69..9a54285a 100644 --- a/src/activities/boot_sleep/SleepActivity.cpp +++ b/src/activities/boot_sleep/SleepActivity.cpp @@ -34,8 +34,11 @@ struct PngOverlayCtx { float yScale; int lastDstY; // Color-key transparency (tRNS chunk) for TRUECOLOR and GRAYSCALE images. - // -1 means no color key. For TRUECOLOR: 0x00RRGGBB; for GRAYSCALE: low byte only. + // Initialized lazily on the first draw callback because tRNS is processed during decode(), + // not during open() — so hasAlpha()/getTransparentColor() are only valid once decode() starts. + // -2 = not yet read; -1 = no color key; >=0 = 0x00RRGGBB (TRUECOLOR) or low-byte gray. int32_t transparentColor; + PNG* pngObj; // for lazy-init of transparentColor on first callback }; // PNGdec file I/O callbacks — mirror the pattern in PngToFramebufferConverter.cpp. @@ -71,6 +74,15 @@ int32_t pngSleepSeek(PNGFILE* pFile, int32_t pos) { int pngOverlayDraw(PNGDRAW* pDraw) { PngOverlayCtx* ctx = reinterpret_cast(pDraw->pUser); + // Lazy-init: tRNS chunk is processed during decode() before any IDAT data, so by the time + // the first draw callback fires, hasAlpha() / getTransparentColor() are already valid. + if (ctx->transparentColor == -2) { + const int pt = pDraw->iPixelType; + ctx->transparentColor = (pDraw->iHasAlpha && (pt == PNG_PIXEL_TRUECOLOR || pt == PNG_PIXEL_GRAYSCALE)) + ? (int32_t)ctx->pngObj->getTransparentColor() + : -1; + } + const int destY = ctx->dstY + (int)(pDraw->y * ctx->yScale); if (destY == ctx->lastDstY) return 1; // skip duplicate rows from Y scaling ctx->lastDstY = destY; @@ -520,13 +532,8 @@ void SleepActivity::renderOverlaySleepScreen() const { ctx.dstY = (pageHeight - dstH) / 2; ctx.yScale = yScale; ctx.lastDstY = -1; - // Populate color-key transparency for TRUECOLOR/GRAYSCALE PNGs with a tRNS chunk. - // TRUECOLOR_ALPHA and GRAY_ALPHA carry per-pixel alpha, handled directly in the callback. - // INDEXED+tRNS stores per-palette alpha at pPalette[768+idx], also handled there. - const int pixType = png->getPixelType(); - ctx.transparentColor = (png->hasAlpha() && (pixType == PNG_PIXEL_TRUECOLOR || pixType == PNG_PIXEL_GRAYSCALE)) - ? (int32_t)png->getTransparentColor() - : -1; + ctx.transparentColor = -2; // will be resolved on first draw callback (after tRNS is parsed) + ctx.pngObj = png; rc = png->decode(&ctx, 0); png->close(); From 87cac6f8b47eeb0422f4140c8a126207044f3861 Mon Sep 17 00:00:00 2001 From: jpirnay Date: Mon, 2 Mar 2026 11:54:52 +0100 Subject: [PATCH 05/13] clang --- src/activities/boot_sleep/SleepActivity.cpp | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/activities/boot_sleep/SleepActivity.cpp b/src/activities/boot_sleep/SleepActivity.cpp index 9a54285a..d3635f70 100644 --- a/src/activities/boot_sleep/SleepActivity.cpp +++ b/src/activities/boot_sleep/SleepActivity.cpp @@ -559,9 +559,8 @@ void SleepActivity::renderOverlaySleepScreen() const { file.close(); continue; } - const auto flen = filename.length(); - const bool isBmp = flen >= 4 && filename.substr(flen - 4) == ".bmp"; - const bool isPng = flen >= 4 && filename.substr(flen - 4) == ".png"; + const bool isBmp = StringUtils::checkFileExtension(filename, ".bmp"); + const bool isPng = StringUtils::checkFileExtension(filename, ".png"); if (!isBmp && !isPng) { file.close(); continue; @@ -585,8 +584,7 @@ void SleepActivity::renderOverlaySleepScreen() const { APP_STATE.lastSleepImage = randomFileIndex; APP_STATE.saveToFile(); const std::string selected = "/sleep/" + files[randomFileIndex]; - const auto slen = selected.length(); - if (slen >= 4 && selected.substr(slen - 4) == ".png") { + if (StringUtils::checkFileExtension(selected, ".png")) { overlayDrawn = tryDrawPngOverlay(selected); } else { overlayDrawn = tryDrawOverlay(selected); From 19b3bdf8ead6cd255f36d5f04d10d36d4d4c27dc Mon Sep 17 00:00:00 2001 From: jpirnay Date: Mon, 2 Mar 2026 13:26:54 +0100 Subject: [PATCH 06/13] Review fixes --- src/activities/reader/EpubReaderActivity.cpp | 4 ++-- src/activities/reader/TxtReaderActivity.cpp | 23 ++++++++++++++++++-- src/activities/reader/XtcReaderActivity.cpp | 2 +- 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index fff77935..1cbd84c3 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -846,8 +846,8 @@ bool EpubReaderActivity::drawCurrentPageToBuffer(const std::string& filePath, Gf if (Storage.openFileForRead("SLP", epub->getCachePath() + "/progress.bin", f)) { uint8_t data[6]; if (f.read(data, 6) == 6) { - spineIndex = data[0] | (data[1] << 8); - pageNumber = data[2] | (data[3] << 8); + spineIndex = (int)((uint32_t)data[0] | ((uint32_t)data[1] << 8)); + pageNumber = (int)((uint32_t)data[2] | ((uint32_t)data[3] << 8)); } f.close(); } diff --git a/src/activities/reader/TxtReaderActivity.cpp b/src/activities/reader/TxtReaderActivity.cpp index 157d3cbe..60ad66c1 100644 --- a/src/activities/reader/TxtReaderActivity.cpp +++ b/src/activities/reader/TxtReaderActivity.cpp @@ -20,6 +20,7 @@ constexpr size_t CHUNK_SIZE = 8 * 1024; // 8KB chunk for reading // Cache file magic and version constexpr uint32_t CACHE_MAGIC = 0x54585449; // "TXTI" constexpr uint8_t CACHE_VERSION = 2; // Increment when cache format changes +constexpr uint32_t MAX_CACHE_PAGES = 65535; // Sanity cap to prevent unbounded reserve() } // namespace void TxtReaderActivity::onEnter() { @@ -608,6 +609,24 @@ bool TxtReaderActivity::drawCurrentPageToBuffer(const std::string& filePath, Gfx return false; } + // Apply the reader orientation so margins match what the reader would produce + switch (SETTINGS.orientation) { + case CrossPointSettings::ORIENTATION::PORTRAIT: + renderer.setOrientation(GfxRenderer::Orientation::Portrait); + break; + case CrossPointSettings::ORIENTATION::LANDSCAPE_CW: + renderer.setOrientation(GfxRenderer::Orientation::LandscapeClockwise); + break; + case CrossPointSettings::ORIENTATION::INVERTED: + renderer.setOrientation(GfxRenderer::Orientation::PortraitInverted); + break; + case CrossPointSettings::ORIENTATION::LANDSCAPE_CCW: + renderer.setOrientation(GfxRenderer::Orientation::LandscapeCounterClockwise); + break; + default: + break; + } + // Compute layout values that match what initializeReader() produces const int fontId = SETTINGS.getReaderFontId(); const uint8_t screenMargin = SETTINGS.screenMargin; @@ -674,7 +693,7 @@ bool TxtReaderActivity::drawCurrentPageToBuffer(const std::string& filePath, Gfx uint32_t numPages; serialization::readPod(cacheFile, numPages); - if (numPages == 0) { + if (numPages == 0 || numPages > MAX_CACHE_PAGES) { cacheFile.close(); return false; } @@ -694,7 +713,7 @@ bool TxtReaderActivity::drawCurrentPageToBuffer(const std::string& filePath, Gfx if (Storage.openFileForRead("SLP", txt.getCachePath() + "/progress.bin", progFile)) { uint8_t data[4]; if (progFile.read(data, 4) == 4) { - savedPage = data[0] + (data[1] << 8); + savedPage = (int)((uint32_t)data[0] | ((uint32_t)data[1] << 8)); } progFile.close(); } diff --git a/src/activities/reader/XtcReaderActivity.cpp b/src/activities/reader/XtcReaderActivity.cpp index 052e2f8f..9522c3af 100644 --- a/src/activities/reader/XtcReaderActivity.cpp +++ b/src/activities/reader/XtcReaderActivity.cpp @@ -365,7 +365,7 @@ bool XtcReaderActivity::drawCurrentPageToBuffer(const std::string& filePath, Gfx if (Storage.openFileForRead("SLP", xtc.getCachePath() + "/progress.bin", f)) { uint8_t data[4]; if (f.read(data, 4) == 4) { - savedPage = data[0] | (data[1] << 8) | (data[2] << 16) | (data[3] << 24); + savedPage = (uint32_t)data[0] | ((uint32_t)data[1] << 8) | ((uint32_t)data[2] << 16) | ((uint32_t)data[3] << 24); } f.close(); } From 9b819b99d40df537913ad2e5a09e92781f8dd30d Mon Sep 17 00:00:00 2001 From: jpirnay Date: Wed, 4 Mar 2026 09:40:43 +0100 Subject: [PATCH 07/13] Review comments --- src/activities/reader/TxtReaderActivity.cpp | 220 +++++++------------- 1 file changed, 75 insertions(+), 145 deletions(-) diff --git a/src/activities/reader/TxtReaderActivity.cpp b/src/activities/reader/TxtReaderActivity.cpp index 60ad66c1..7b8bc18c 100644 --- a/src/activities/reader/TxtReaderActivity.cpp +++ b/src/activities/reader/TxtReaderActivity.cpp @@ -21,6 +21,61 @@ constexpr size_t CHUNK_SIZE = 8 * 1024; // 8KB chunk for reading constexpr uint32_t CACHE_MAGIC = 0x54585449; // "TXTI" constexpr uint8_t CACHE_VERSION = 2; // Increment when cache format changes constexpr uint32_t MAX_CACHE_PAGES = 65535; // Sanity cap to prevent unbounded reserve() + +// Parses and word-wraps lines from a file chunk into outLines. +// Returns the number of bytes consumed from the start of buffer. +size_t parseAndWrapLines(const uint8_t* buffer, size_t chunkSize, size_t fileOffset, size_t fileSize, int linesPerPage, + GfxRenderer& renderer, int fontId, int vw, std::vector& outLines) { + size_t pos = 0; + while (pos < chunkSize && static_cast(outLines.size()) < linesPerPage) { + size_t lineEnd = pos; + while (lineEnd < chunkSize && buffer[lineEnd] != '\n') lineEnd++; + bool lineComplete = (lineEnd < chunkSize) || (fileOffset + lineEnd >= fileSize); + if (!lineComplete && !outLines.empty()) break; + + size_t lineContentLen = lineEnd - pos; + bool hasCR = (lineContentLen > 0 && buffer[pos + lineContentLen - 1] == '\r'); + size_t displayLen = hasCR ? lineContentLen - 1 : lineContentLen; + std::string line(reinterpret_cast(buffer + pos), displayLen); + size_t lineBytePos = 0; + + while (!line.empty() && static_cast(outLines.size()) < linesPerPage) { + if (renderer.getTextWidth(fontId, line.c_str()) <= vw) { + outLines.push_back(line); + lineBytePos = displayLen; + line.clear(); + break; + } + size_t breakPos = line.length(); + while (breakPos > 0 && renderer.getTextWidth(fontId, line.substr(0, breakPos).c_str()) > vw) { + size_t spacePos = line.rfind(' ', breakPos - 1); + if (spacePos != std::string::npos && spacePos > 0) { + breakPos = spacePos; + } else { + breakPos--; + while (breakPos > 0 && (line[breakPos] & 0xC0) == 0x80) breakPos--; + } + } + if (breakPos == 0) breakPos = 1; + outLines.push_back(line.substr(0, breakPos)); + size_t skipChars = breakPos; + if (breakPos < line.length() && line[breakPos] == ' ') skipChars++; + lineBytePos += skipChars; + line = line.substr(skipChars); + } + + if (line.empty()) { + pos = lineEnd + 1; + } else { + pos = pos + lineBytePos; + break; + } + } + if (pos == 0 && !outLines.empty()) { + pos = 1; + } + return pos; +} } // namespace void TxtReaderActivity::onEnter() { @@ -217,101 +272,9 @@ bool TxtReaderActivity::loadPageAtOffset(size_t offset, std::vector } buffer[chunkSize] = '\0'; - // Parse lines from buffer - size_t pos = 0; - - while (pos < chunkSize && static_cast(outLines.size()) < linesPerPage) { - // Find end of line - size_t lineEnd = pos; - while (lineEnd < chunkSize && buffer[lineEnd] != '\n') { - lineEnd++; - } - - // Check if we have a complete line - bool lineComplete = (lineEnd < chunkSize) || (offset + lineEnd >= fileSize); - - if (!lineComplete && static_cast(outLines.size()) > 0) { - // Incomplete line and we already have some lines, stop here - break; - } - - // Calculate the actual length of line content in the buffer (excluding newline) - size_t lineContentLen = lineEnd - pos; - - // Check for carriage return - bool hasCR = (lineContentLen > 0 && buffer[pos + lineContentLen - 1] == '\r'); - size_t displayLen = hasCR ? lineContentLen - 1 : lineContentLen; - - // Extract line content for display (without CR/LF) - std::string line(reinterpret_cast(buffer + pos), displayLen); - - // Track position within this source line (in bytes from pos) - size_t lineBytePos = 0; - - // Word wrap if needed - while (!line.empty() && static_cast(outLines.size()) < linesPerPage) { - int lineWidth = renderer.getTextWidth(cachedFontId, line.c_str()); - - if (lineWidth <= viewportWidth) { - outLines.push_back(line); - lineBytePos = displayLen; // Consumed entire display content - line.clear(); - break; - } - - // Find break point - size_t breakPos = line.length(); - while (breakPos > 0 && renderer.getTextWidth(cachedFontId, line.substr(0, breakPos).c_str()) > viewportWidth) { - // Try to break at space - size_t spacePos = line.rfind(' ', breakPos - 1); - if (spacePos != std::string::npos && spacePos > 0) { - breakPos = spacePos; - } else { - // Break at character boundary for UTF-8 - breakPos--; - // Make sure we don't break in the middle of a UTF-8 sequence - while (breakPos > 0 && (line[breakPos] & 0xC0) == 0x80) { - breakPos--; - } - } - } - - if (breakPos == 0) { - breakPos = 1; - } - - outLines.push_back(line.substr(0, breakPos)); - - // Skip space at break point - size_t skipChars = breakPos; - if (breakPos < line.length() && line[breakPos] == ' ') { - skipChars++; - } - lineBytePos += skipChars; - line = line.substr(skipChars); - } - - // Determine how much of the source buffer we consumed - if (line.empty()) { - // Fully consumed this source line, move past the newline - pos = lineEnd + 1; - } else { - // Partially consumed - page is full mid-line - // Move pos to where we stopped in the line (NOT past the line) - pos = pos + lineBytePos; - break; - } - } - - // Ensure we make progress even if calculations go wrong - if (pos == 0 && !outLines.empty()) { - // Fallback: at minimum, consume something to avoid infinite loop - pos = 1; - } - + size_t pos = parseAndWrapLines(buffer, chunkSize, offset, fileSize, linesPerPage, renderer, cachedFontId, + viewportWidth, outLines); nextOffset = offset + pos; - - // Make sure we don't go past the file if (nextOffset > fileSize) { nextOffset = fileSize; } @@ -557,6 +520,10 @@ bool TxtReaderActivity::loadPageIndexCache() { uint32_t numPages; serialization::readPod(f, numPages); + if (numPages > MAX_CACHE_PAGES) { + LOG_WRN("TRS", "Cache numPages %u exceeds cap %u, truncating", numPages, MAX_CACHE_PAGES); + numPages = MAX_CACHE_PAGES; + } // Read page offsets pageOffsets.clear(); @@ -698,16 +665,7 @@ bool TxtReaderActivity::drawCurrentPageToBuffer(const std::string& filePath, Gfx return false; } - std::vector pageOffsets; - pageOffsets.reserve(numPages); - for (uint32_t i = 0; i < numPages; i++) { - uint32_t offset; - serialization::readPod(cacheFile, offset); - pageOffsets.push_back(offset); - } - cacheFile.close(); - - // Load saved page number from progress file + // Load saved page number before reading offsets int savedPage = 0; FsFile progFile; if (Storage.openFileForRead("SLP", txt.getCachePath() + "/progress.bin", progFile)) { @@ -719,16 +677,26 @@ bool TxtReaderActivity::drawCurrentPageToBuffer(const std::string& filePath, Gfx } if (savedPage < 0 || savedPage >= static_cast(numPages)) savedPage = 0; + // Read offsets sequentially, retaining only the one we need + size_t savedOffset = 0; + for (uint32_t i = 0; i < numPages; i++) { + uint32_t off; + serialization::readPod(cacheFile, off); + if (static_cast(i) == savedPage) { + savedOffset = off; + } + } + cacheFile.close(); + // Load the page lines from file std::vector pageLines; const size_t fileSize = txt.getFileSize(); - size_t offset = pageOffsets[savedPage]; + size_t offset = savedOffset; if (offset >= fileSize) { LOG_DBG("SLP", "TXT: page offset out of bounds"); return false; } - // Replicate loadPageAtOffset() logic with local layout variables size_t chunkSize = std::min(CHUNK_SIZE, fileSize - offset); auto* buffer = static_cast(malloc(chunkSize + 1)); if (!buffer) return false; @@ -739,45 +707,7 @@ bool TxtReaderActivity::drawCurrentPageToBuffer(const std::string& filePath, Gfx } buffer[chunkSize] = '\0'; - size_t pos = 0; - while (pos < chunkSize && static_cast(pageLines.size()) < linesPerPage) { - size_t lineEnd = pos; - while (lineEnd < chunkSize && buffer[lineEnd] != '\n') lineEnd++; - bool lineComplete = (lineEnd < chunkSize) || (offset + lineEnd >= fileSize); - if (!lineComplete && !pageLines.empty()) break; - - size_t lineContentLen = lineEnd - pos; - bool hasCR = (lineContentLen > 0 && buffer[pos + lineContentLen - 1] == '\r'); - size_t displayLen = hasCR ? lineContentLen - 1 : lineContentLen; - std::string line(reinterpret_cast(buffer + pos), displayLen); - size_t lineBytePos = 0; - - while (!line.empty() && static_cast(pageLines.size()) < linesPerPage) { - if (renderer.getTextWidth(fontId, line.c_str()) <= vw) { - pageLines.push_back(line); - lineBytePos = displayLen; - line.clear(); - break; - } - size_t breakPos = line.length(); - while (breakPos > 0 && renderer.getTextWidth(fontId, line.substr(0, breakPos).c_str()) > vw) { - size_t spacePos = line.rfind(' ', breakPos - 1); - if (spacePos != std::string::npos && spacePos > 0) { - breakPos = spacePos; - } else { - breakPos--; - while (breakPos > 0 && (line[breakPos] & 0xC0) == 0x80) breakPos--; - } - } - if (breakPos == 0) breakPos = 1; - pageLines.push_back(line.substr(0, breakPos)); - size_t skipChars = breakPos; - if (breakPos < line.length() && line[breakPos] == ' ') skipChars++; - lineBytePos += skipChars; - line = line.substr(skipChars); - } - pos = line.empty() ? lineEnd + 1 : pos + lineBytePos; - } + parseAndWrapLines(buffer, chunkSize, offset, fileSize, linesPerPage, renderer, fontId, vw, pageLines); free(buffer); if (pageLines.empty()) return false; From 1516c51e14b224f315bac59378edd910717b6538 Mon Sep 17 00:00:00 2001 From: jpirnay Date: Wed, 4 Mar 2026 09:46:00 +0100 Subject: [PATCH 08/13] typo --- src/activities/reader/TxtReaderActivity.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/activities/reader/TxtReaderActivity.cpp b/src/activities/reader/TxtReaderActivity.cpp index 7b8bc18c..d4f91076 100644 --- a/src/activities/reader/TxtReaderActivity.cpp +++ b/src/activities/reader/TxtReaderActivity.cpp @@ -521,7 +521,7 @@ bool TxtReaderActivity::loadPageIndexCache() { uint32_t numPages; serialization::readPod(f, numPages); if (numPages > MAX_CACHE_PAGES) { - LOG_WRN("TRS", "Cache numPages %u exceeds cap %u, truncating", numPages, MAX_CACHE_PAGES); + LOG_ERR("TRS", "Cache numPages %u exceeds cap %u, truncating", numPages, MAX_CACHE_PAGES); numPages = MAX_CACHE_PAGES; } From 08cdd06c41d7832b2413f14e42cda541e01fdf60 Mon Sep 17 00:00:00 2001 From: jpirnay Date: Wed, 4 Mar 2026 10:46:56 +0100 Subject: [PATCH 09/13] Deal with stale caches --- src/activities/reader/EpubReaderActivity.cpp | 16 +- src/activities/reader/TxtReaderActivity.cpp | 154 ++++++++++--------- 2 files changed, 89 insertions(+), 81 deletions(-) diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index 1cbd84c3..5cebce5f 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -832,8 +832,8 @@ void EpubReaderActivity::restoreSavedPosition() { bool EpubReaderActivity::drawCurrentPageToBuffer(const std::string& filePath, GfxRenderer& renderer) { auto epub = std::make_shared(filePath, "/.crosspoint"); - // skip CSS (second arg) since we only need layout/section data - if (!epub->load(true, false)) { + // Load CSS when embeddedStyle is enabled, as createSectionFile may need it to rebuild the cache. + if (!epub->load(true, SETTINGS.embeddedStyle == 0)) { LOG_DBG("SLP", "EPUB: failed to load %s", filePath.c_str()); return false; } @@ -868,13 +868,19 @@ bool EpubReaderActivity::drawCurrentPageToBuffer(const std::string& filePath, Gf const uint16_t viewportWidth = renderer.getScreenWidth() - marginLeft - marginRight; const uint16_t viewportHeight = renderer.getScreenHeight() - marginTop - marginBottom; - // Load the cached section file (won't rebuild if cache missing — too slow for sleep) + // Load or rebuild the section cache. Rebuilding is needed when the cache is missing or stale + // (e.g. after a firmware update). A no-op popup callback avoids any UI during sleep preparation. auto section = std::unique_ptr
(new Section(epub, spineIndex, renderer)); if (!section->loadSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(), SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth, viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle)) { - LOG_DBG("SLP", "EPUB: section cache not found for spine %d", spineIndex); - return false; + LOG_DBG("SLP", "EPUB: section cache not found for spine %d, rebuilding", spineIndex); + if (!section->createSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(), + SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth, + viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle, []() {})) { + LOG_ERR("SLP", "EPUB: failed to rebuild section cache for spine %d", spineIndex); + return false; + } } if (pageNumber < 0 || pageNumber >= section->pageCount) pageNumber = 0; diff --git a/src/activities/reader/TxtReaderActivity.cpp b/src/activities/reader/TxtReaderActivity.cpp index d4f91076..fc091dcf 100644 --- a/src/activities/reader/TxtReaderActivity.cpp +++ b/src/activities/reader/TxtReaderActivity.cpp @@ -405,12 +405,17 @@ void TxtReaderActivity::renderStatusBar() const { void TxtReaderActivity::saveProgress() const { FsFile f; if (Storage.openFileForWrite("TRS", txt->getCachePath() + "/progress.bin", f)) { - uint8_t data[4]; + // 6-byte format: page(2 bytes LE) + file offset(4 bytes LE) + // The offset lets drawCurrentPageToBuffer render without requiring index.bin. + const size_t offset = (currentPage < static_cast(pageOffsets.size())) ? pageOffsets[currentPage] : 0; + uint8_t data[6]; data[0] = currentPage & 0xFF; data[1] = (currentPage >> 8) & 0xFF; - data[2] = 0; - data[3] = 0; - f.write(data, 4); + data[2] = offset & 0xFF; + data[3] = (offset >> 8) & 0xFF; + data[4] = (offset >> 16) & 0xFF; + data[5] = (offset >> 24) & 0xFF; + f.write(data, 6); f.close(); } } @@ -611,82 +616,79 @@ bool TxtReaderActivity::drawCurrentPageToBuffer(const std::string& filePath, Gfx const int lineHeight = renderer.getLineHeight(fontId); const int linesPerPage = std::max(1, vh / lineHeight); - // Load the page offset index from cache (must already exist from normal reading) - std::string cachePath = txt.getCachePath() + "/index.bin"; - FsFile cacheFile; - if (!Storage.openFileForRead("SLP", cachePath, cacheFile)) { - LOG_DBG("SLP", "TXT: no page index cache"); - return false; - } - - uint32_t magic; - serialization::readPod(cacheFile, magic); - if (magic != CACHE_MAGIC) { - cacheFile.close(); - return false; - } - - uint8_t version; - serialization::readPod(cacheFile, version); - if (version != CACHE_VERSION) { - cacheFile.close(); - return false; - } - - uint32_t cachedFileSize; - serialization::readPod(cacheFile, cachedFileSize); - if (cachedFileSize != txt.getFileSize()) { - cacheFile.close(); - return false; - } - - int32_t cachedVw, cachedLpp, cachedFontId, cachedMargin; - serialization::readPod(cacheFile, cachedVw); - serialization::readPod(cacheFile, cachedLpp); - serialization::readPod(cacheFile, cachedFontId); - serialization::readPod(cacheFile, cachedMargin); - if (cachedVw != vw || cachedLpp != linesPerPage || cachedFontId != fontId || cachedMargin != screenMargin) { - LOG_DBG("SLP", "TXT: cache invalid (settings changed)"); - cacheFile.close(); - return false; - } - - uint8_t cachedAlignment; - serialization::readPod(cacheFile, cachedAlignment); - if (cachedAlignment != paragraphAlignment) { - cacheFile.close(); - return false; - } - - uint32_t numPages; - serialization::readPod(cacheFile, numPages); - if (numPages == 0 || numPages > MAX_CACHE_PAGES) { - cacheFile.close(); - return false; - } - - // Load saved page number before reading offsets + // Step 1: Try to read the saved page and its file offset from progress.bin. + // The 6-byte format (written by saveProgress) stores: page(2) + offset(4). + // This lets us skip index.bin entirely, so the overlay works even when the + // page index cache is missing or stale (e.g. after a firmware update). int savedPage = 0; - FsFile progFile; - if (Storage.openFileForRead("SLP", txt.getCachePath() + "/progress.bin", progFile)) { - uint8_t data[4]; - if (progFile.read(data, 4) == 4) { - savedPage = (int)((uint32_t)data[0] | ((uint32_t)data[1] << 8)); - } - progFile.close(); - } - if (savedPage < 0 || savedPage >= static_cast(numPages)) savedPage = 0; - - // Read offsets sequentially, retaining only the one we need size_t savedOffset = 0; - for (uint32_t i = 0; i < numPages; i++) { - uint32_t off; - serialization::readPod(cacheFile, off); - if (static_cast(i) == savedPage) { - savedOffset = off; + bool offsetKnown = false; + { + FsFile progFile; + if (Storage.openFileForRead("SLP", txt.getCachePath() + "/progress.bin", progFile)) { + uint8_t data[6] = {0}; + const int n = progFile.read(data, 6); + progFile.close(); + if (n >= 2) { + savedPage = (int)((uint32_t)data[0] | ((uint32_t)data[1] << 8)); + } + if (n >= 6) { + const uint32_t off = + (uint32_t)data[2] | ((uint32_t)data[3] << 8) | ((uint32_t)data[4] << 16) | ((uint32_t)data[5] << 24); + if (off < txt.getFileSize()) { + savedOffset = off; + offsetKnown = true; + } + } + } + } + + // Step 2: If progress.bin didn't provide the offset, fall back to index.bin. + if (!offsetKnown) { + std::string cachePath = txt.getCachePath() + "/index.bin"; + FsFile cacheFile; + if (Storage.openFileForRead("SLP", cachePath, cacheFile)) { + uint32_t magic; + serialization::readPod(cacheFile, magic); + uint8_t version; + serialization::readPod(cacheFile, version); + uint32_t cachedFileSize; + serialization::readPod(cacheFile, cachedFileSize); + int32_t cachedVw, cachedLpp, cachedFontId, cachedMargin; + serialization::readPod(cacheFile, cachedVw); + serialization::readPod(cacheFile, cachedLpp); + serialization::readPod(cacheFile, cachedFontId); + serialization::readPod(cacheFile, cachedMargin); + uint8_t cachedAlignment; + serialization::readPod(cacheFile, cachedAlignment); + uint32_t numPages; + serialization::readPod(cacheFile, numPages); + + if (magic == CACHE_MAGIC && version == CACHE_VERSION && cachedFileSize == txt.getFileSize() && cachedVw == vw && + cachedLpp == linesPerPage && cachedFontId == fontId && cachedMargin == screenMargin && + cachedAlignment == paragraphAlignment && numPages > 0 && numPages <= MAX_CACHE_PAGES) { + if (savedPage < 0 || savedPage >= static_cast(numPages)) savedPage = 0; + for (uint32_t i = 0; i < numPages; i++) { + uint32_t off; + serialization::readPod(cacheFile, off); + if (static_cast(i) == savedPage) { + savedOffset = off; + offsetKnown = true; + } + } + } else { + LOG_DBG("SLP", "TXT: index cache invalid or stale"); + } + cacheFile.close(); + } + + // Step 3: No valid cache at all — render from the start of the file as a last resort. + // This shows page 1 rather than a blank screen, which is always preferable. + if (!offsetKnown) { + LOG_DBG("SLP", "TXT: no valid cache, falling back to start of file"); + savedOffset = 0; } } - cacheFile.close(); // Load the page lines from file std::vector pageLines; From d0d4dc5c1885a790da0fc28d0027cac458a358fa Mon Sep 17 00:00:00 2001 From: jpirnay Date: Wed, 4 Mar 2026 12:27:33 +0100 Subject: [PATCH 10/13] Review changes --- src/activities/reader/EpubReaderActivity.cpp | 5 ++++- src/activities/reader/TxtReaderActivity.cpp | 18 +++++++++++++----- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index 5cebce5f..a1dc66ad 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -8,6 +8,8 @@ #include #include +#include + #include "CrossPointSettings.h" #include "CrossPointState.h" #include "EpubReaderChapterSelectionActivity.h" @@ -22,6 +24,7 @@ #include "fontIds.h" #include "util/ScreenshotUtil.h" + namespace { // pagesPerRefresh now comes from SETTINGS.getRefreshFrequency() constexpr unsigned long skipChapterMs = 700; @@ -870,7 +873,7 @@ bool EpubReaderActivity::drawCurrentPageToBuffer(const std::string& filePath, Gf // Load or rebuild the section cache. Rebuilding is needed when the cache is missing or stale // (e.g. after a firmware update). A no-op popup callback avoids any UI during sleep preparation. - auto section = std::unique_ptr
(new Section(epub, spineIndex, renderer)); + auto section = std::make_unique
(epub, spineIndex, renderer); if (!section->loadSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(), SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth, viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle)) { diff --git a/src/activities/reader/TxtReaderActivity.cpp b/src/activities/reader/TxtReaderActivity.cpp index fc091dcf..7d7180e5 100644 --- a/src/activities/reader/TxtReaderActivity.cpp +++ b/src/activities/reader/TxtReaderActivity.cpp @@ -56,7 +56,10 @@ size_t parseAndWrapLines(const uint8_t* buffer, size_t chunkSize, size_t fileOff while (breakPos > 0 && (line[breakPos] & 0xC0) == 0x80) breakPos--; } } - if (breakPos == 0) breakPos = 1; + if (breakPos == 0) { + breakPos = 1; + while (breakPos < line.length() && (line[breakPos] & 0xC0) == 0x80) breakPos++; + } outLines.push_back(line.substr(0, breakPos)); size_t skipChars = breakPos; if (breakPos < line.length() && line[breakPos] == ' ') skipChars++; @@ -526,8 +529,9 @@ bool TxtReaderActivity::loadPageIndexCache() { uint32_t numPages; serialization::readPod(f, numPages); if (numPages > MAX_CACHE_PAGES) { - LOG_ERR("TRS", "Cache numPages %u exceeds cap %u, truncating", numPages, MAX_CACHE_PAGES); - numPages = MAX_CACHE_PAGES; + LOG_ERR("TRS", "Cache numPages %u exceeds cap %u, cache invalid", numPages, MAX_CACHE_PAGES); + f.close(); + return false; } // Read page offsets @@ -672,8 +676,12 @@ bool TxtReaderActivity::drawCurrentPageToBuffer(const std::string& filePath, Gfx uint32_t off; serialization::readPod(cacheFile, off); if (static_cast(i) == savedPage) { - savedOffset = off; - offsetKnown = true; + if (off < txt.getFileSize()) { + savedOffset = off; + offsetKnown = true; + } else { + LOG_DBG("SLP", "TXT: index.bin offset %u out of range (fileSize=%u), ignoring", off, txt.getFileSize()); + } } } } else { From 5c1ff9b5bbf01a11fe2efd71bb3f9a3938f5a2e4 Mon Sep 17 00:00:00 2001 From: jpirnay Date: Wed, 4 Mar 2026 19:47:12 +0100 Subject: [PATCH 11/13] yaclf --- src/activities/reader/EpubReaderActivity.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index a1dc66ad..90c20500 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -24,7 +24,6 @@ #include "fontIds.h" #include "util/ScreenshotUtil.h" - namespace { // pagesPerRefresh now comes from SETTINGS.getRefreshFrequency() constexpr unsigned long skipChapterMs = 700; From f5cc2915d72002ce06e6f0ca0973d8fa80d2bbb1 Mon Sep 17 00:00:00 2001 From: jpirnay Date: Thu, 5 Mar 2026 18:29:33 +0100 Subject: [PATCH 12/13] Sync to new master --- src/activities/reader/EpubReaderActivity.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index c32fb000..020c6401 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -877,11 +877,13 @@ bool EpubReaderActivity::drawCurrentPageToBuffer(const std::string& filePath, Gf auto section = std::make_unique
(epub, spineIndex, renderer); if (!section->loadSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(), SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth, - viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle)) { + viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle, + SETTINGS.imageRendering)) { LOG_DBG("SLP", "EPUB: section cache not found for spine %d, rebuilding", spineIndex); if (!section->createSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(), SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth, - viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle, []() {})) { + viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle, + SETTINGS.imageRendering, []() {})) { LOG_ERR("SLP", "EPUB: failed to rebuild section cache for spine %d", spineIndex); return false; } From 9ca0f30b16f0c8f02685f5aef9bcd82616a5d0aa Mon Sep 17 00:00:00 2001 From: jpirnay Date: Sat, 7 Mar 2026 10:29:41 +0100 Subject: [PATCH 13/13] Sync to master --- src/activities/boot_sleep/SleepActivity.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/activities/boot_sleep/SleepActivity.cpp b/src/activities/boot_sleep/SleepActivity.cpp index 9c37b6e9..630ac691 100644 --- a/src/activities/boot_sleep/SleepActivity.cpp +++ b/src/activities/boot_sleep/SleepActivity.cpp @@ -456,11 +456,11 @@ void SleepActivity::renderOverlaySleepScreen() const { const auto& path = APP_STATE.openEpubPath; bool rendered = false; - if (StringUtils::checkFileExtension(path, ".xtc") || StringUtils::checkFileExtension(path, ".xtch")) { + if (FsHelpers::checkFileExtension(path, ".xtc") || FsHelpers::checkFileExtension(path, ".xtch")) { rendered = XtcReaderActivity::drawCurrentPageToBuffer(path, renderer); - } else if (StringUtils::checkFileExtension(path, ".txt")) { + } else if (FsHelpers::checkFileExtension(path, ".txt")) { rendered = TxtReaderActivity::drawCurrentPageToBuffer(path, renderer); - } else if (StringUtils::checkFileExtension(path, ".epub")) { + } else if (FsHelpers::checkFileExtension(path, ".epub")) { rendered = EpubReaderActivity::drawCurrentPageToBuffer(path, renderer); } @@ -569,8 +569,8 @@ void SleepActivity::renderOverlaySleepScreen() const { file.close(); continue; } - const bool isBmp = StringUtils::checkFileExtension(filename, ".bmp"); - const bool isPng = StringUtils::checkFileExtension(filename, ".png"); + const bool isBmp = FsHelpers::checkFileExtension(filename, ".bmp"); + const bool isPng = FsHelpers::checkFileExtension(filename, ".png"); if (!isBmp && !isPng) { file.close(); continue; @@ -594,7 +594,7 @@ void SleepActivity::renderOverlaySleepScreen() const { APP_STATE.lastSleepImage = randomFileIndex; APP_STATE.saveToFile(); const std::string selected = "/sleep/" + files[randomFileIndex]; - if (StringUtils::checkFileExtension(selected, ".png")) { + if (FsHelpers::checkFileExtension(selected, ".png")) { overlayDrawn = tryDrawPngOverlay(selected); } else { overlayDrawn = tryDrawOverlay(selected);