From f45b7809008226bc32ffee8f750a8e2e4d4d18fc Mon Sep 17 00:00:00 2001 From: jpirnay Date: Mon, 6 Apr 2026 12:39:37 +0200 Subject: [PATCH] Support png and jpg in Image Viewer --- src/activities/boot_sleep/SleepActivity.cpp | 31 ++- src/activities/home/FileBrowserActivity.cpp | 3 +- src/activities/reader/ReaderActivity.cpp | 6 +- src/activities/reader/ReaderActivity.h | 2 +- src/activities/util/BmpViewerActivity.cpp | 224 +++++++++++++------- src/activities/util/BmpViewerActivity.h | 3 + src/components/UITheme.cpp | 3 +- 7 files changed, 179 insertions(+), 93 deletions(-) diff --git a/src/activities/boot_sleep/SleepActivity.cpp b/src/activities/boot_sleep/SleepActivity.cpp index e87c54bd..d5b4a440 100644 --- a/src/activities/boot_sleep/SleepActivity.cpp +++ b/src/activities/boot_sleep/SleepActivity.cpp @@ -187,6 +187,21 @@ void SleepActivity::renderCustomSleepScreen() const { const bool shouldLoadOverlayInfo = SETTINGS.sleepCoverOverlay != 0 && APP_STATE.lastSleepFromReader && !APP_STATE.openEpubPath.empty(); + // An explicitly selected custom sleep image should override random images from /.sleep or /sleep. + FsFile explicitSleepFile; + if (Storage.openFileForRead("SLP", "/sleep.bmp", explicitSleepFile)) { + Bitmap bitmap(explicitSleepFile, true); + if (bitmap.parseHeaders() == BmpReaderError::Ok) { + LOG_DBG("SLP", "Loading explicit custom sleep image: /sleep.bmp"); + const BookOverlayInfo resolvedOverlayInfo = + shouldLoadOverlayInfo ? getBookOverlayInfo(APP_STATE.openEpubPath) : overlayInfo; + renderBitmapSleepScreen(bitmap, resolvedOverlayInfo); + explicitSleepFile.close(); + return; + } + explicitSleepFile.close(); + } + // Check if we have a /.sleep (preferred) or /sleep directory const char* sleepDir = nullptr; auto dir = Storage.open("/.sleep"); @@ -260,22 +275,6 @@ void SleepActivity::renderCustomSleepScreen() const { } if (dir) dir.close(); - // Look for sleep.bmp on the root of the sd card to determine if we should - // render a custom sleep screen instead of the default. - FsFile file; - if (Storage.openFileForRead("SLP", "/sleep.bmp", file)) { - Bitmap bitmap(file, true); - if (bitmap.parseHeaders() == BmpReaderError::Ok) { - LOG_DBG("SLP", "Loading: /sleep.bmp"); - const BookOverlayInfo resolvedOverlayInfo = - shouldLoadOverlayInfo ? getBookOverlayInfo(APP_STATE.openEpubPath) : overlayInfo; - renderBitmapSleepScreen(bitmap, resolvedOverlayInfo); - file.close(); - return; - } - file.close(); - } - renderDefaultSleepScreen(); } diff --git a/src/activities/home/FileBrowserActivity.cpp b/src/activities/home/FileBrowserActivity.cpp index 46a1b294..557ef8cb 100644 --- a/src/activities/home/FileBrowserActivity.cpp +++ b/src/activities/home/FileBrowserActivity.cpp @@ -96,7 +96,8 @@ void FileBrowserActivity::loadFiles() { std::string_view filename{name}; if (FsHelpers::hasEpubExtension(filename) || FsHelpers::hasXtcExtension(filename) || FsHelpers::hasTxtExtension(filename) || FsHelpers::hasMarkdownExtension(filename) || - FsHelpers::hasBmpExtension(filename)) { + FsHelpers::hasBmpExtension(filename) || FsHelpers::hasJpgExtension(filename) || + FsHelpers::hasPngExtension(filename)) { files.emplace_back(filename); } } diff --git a/src/activities/reader/ReaderActivity.cpp b/src/activities/reader/ReaderActivity.cpp index a1ccf789..3a19ea39 100644 --- a/src/activities/reader/ReaderActivity.cpp +++ b/src/activities/reader/ReaderActivity.cpp @@ -31,7 +31,9 @@ bool ReaderActivity::isTxtFile(const std::string& path) { FsHelpers::hasMarkdownExtension(path); // Treat .md as txt files (until we have a markdown reader) } -bool ReaderActivity::isBmpFile(const std::string& path) { return FsHelpers::hasBmpExtension(path); } +bool ReaderActivity::isImageFile(const std::string& path) { + return FsHelpers::hasBmpExtension(path) || FsHelpers::hasJpgExtension(path) || FsHelpers::hasPngExtension(path); +} std::unique_ptr ReaderActivity::loadEpub(const std::string& path) { if (!Storage.exists(path.c_str())) { @@ -120,7 +122,7 @@ void ReaderActivity::onEnter() { } currentBookPath = initialBookPath; - if (isBmpFile(initialBookPath)) { + if (isImageFile(initialBookPath)) { onGoToBmpViewer(initialBookPath); } else if (isXtcFile(initialBookPath)) { renderer.clearScreen(); diff --git a/src/activities/reader/ReaderActivity.h b/src/activities/reader/ReaderActivity.h index 6a3756db..efdfd201 100644 --- a/src/activities/reader/ReaderActivity.h +++ b/src/activities/reader/ReaderActivity.h @@ -16,7 +16,7 @@ class ReaderActivity final : public Activity { static std::unique_ptr loadTxt(const std::string& path); static bool isXtcFile(const std::string& path); static bool isTxtFile(const std::string& path); - static bool isBmpFile(const std::string& path); + static bool isImageFile(const std::string& path); static std::string extractFolderPath(const std::string& filePath); void goToLibrary(const std::string& fromBookPath = ""); diff --git a/src/activities/util/BmpViewerActivity.cpp b/src/activities/util/BmpViewerActivity.cpp index dae4fe62..c7946e06 100644 --- a/src/activities/util/BmpViewerActivity.cpp +++ b/src/activities/util/BmpViewerActivity.cpp @@ -1,17 +1,55 @@ #include "BmpViewerActivity.h" #include +#include +#include #include #include #include +#include + #include "../reader/ReaderUtils.h" #include "CrossPointSettings.h" #include "components/UITheme.h" #include "fontIds.h" +#include "util/ScreenshotUtil.h" namespace { constexpr const char* SLEEP_BMP_PATH = "/sleep.bmp"; + +bool isBmpFile(const std::string& path) { return FsHelpers::hasBmpExtension(path); } + +bool isSupportedImageFile(const std::string& path) { + return FsHelpers::hasBmpExtension(path) || FsHelpers::hasJpgExtension(path) || FsHelpers::hasPngExtension(path); +} + +void computeCenteredImagePlacement(const int imageWidth, const int imageHeight, const int pageWidth, + const int pageHeight, int& x, int& y, int& renderWidth, int& renderHeight) { + renderWidth = imageWidth; + renderHeight = imageHeight; + + if (imageWidth > pageWidth || imageHeight > pageHeight) { + const float ratio = static_cast(imageWidth) / static_cast(imageHeight); + const float screenRatio = static_cast(pageWidth) / static_cast(pageHeight); + + if (ratio > screenRatio) { + renderWidth = pageWidth; + renderHeight = std::round(static_cast(pageWidth) / ratio); + x = 0; + y = std::round((static_cast(pageHeight) - renderHeight) / 2.0f); + } else { + renderHeight = pageHeight; + renderWidth = std::round(static_cast(pageHeight) * ratio); + x = std::round((static_cast(pageWidth) - renderWidth) / 2.0f); + y = 0; + } + return; + } + + x = (pageWidth - imageWidth) / 2; + y = (pageHeight - imageHeight) / 2; +} } // namespace BmpViewerActivity::BmpViewerActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::string path) @@ -19,73 +57,14 @@ BmpViewerActivity::BmpViewerActivity(GfxRenderer& renderer, MappedInputManager& void BmpViewerActivity::onEnter() { Activity::onEnter(); - // Removed the redundant initial renderer.clearScreen() + if (!isSupportedImageFile(filePath)) { + renderError("Unsupported image format"); + return; + } - FsFile file; - - const auto pageWidth = renderer.getScreenWidth(); - const auto pageHeight = renderer.getScreenHeight(); - Rect popupRect = GUI.drawPopup(renderer, tr(STR_LOADING_POPUP)); - GUI.fillPopupProgress(renderer, popupRect, 20); // Initial 20% progress - // 1. Open the file - if (Storage.openFileForRead("BMP", filePath, file)) { - Bitmap bitmap(file, true); - - // 2. Parse headers to get dimensions - if (bitmap.parseHeaders() == BmpReaderError::Ok) { - int x, y; - - 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) { - // Wider than screen - x = 0; - y = std::round((static_cast(pageHeight) - static_cast(pageWidth) / ratio) / 2); - } else { - // Taller than screen - x = std::round((static_cast(pageWidth) - static_cast(pageHeight) * ratio) / 2); - y = 0; - } - } else { - // Center small images - x = (pageWidth - bitmap.getWidth()) / 2; - y = (pageHeight - bitmap.getHeight()) / 2; - } - - // 4. Prepare Rendering - const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", tr(STR_SET_SLEEP_SCREEN)); - GUI.fillPopupProgress(renderer, popupRect, 50); - - renderer.clearScreen(); - // Assuming drawBitmap defaults to 0,0 crop if omitted, or pass explicitly: drawBitmap(bitmap, x, y, pageWidth, - // pageHeight, 0, 0) - renderer.drawBitmap(bitmap, x, y, pageWidth, pageHeight, 0, 0); - - // Draw UI hints on the base layer - GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); - // Single pass for non-grayscale images - - renderer.displayBuffer(HalDisplay::HALF_REFRESH); - - } else { - // Handle file parsing error - renderer.clearScreen(); - renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2, "Invalid BMP File"); - const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", ""); - GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); - renderer.displayBuffer(HalDisplay::HALF_REFRESH); - } - - file.close(); - } else { - // Handle file open error - renderer.clearScreen(); - renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2, "Could not open file"); - const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", ""); - GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); - renderer.displayBuffer(HalDisplay::HALF_REFRESH); + const bool rendered = isBmpFile(filePath) ? renderBmpImage() : renderDecodedImage(); + if (!rendered) { + renderError("Could not render image"); } } @@ -95,16 +74,117 @@ void BmpViewerActivity::onExit() { renderer.displayBuffer(HalDisplay::FULL_REFRESH); } +bool BmpViewerActivity::renderBmpImage(const bool showControls) { + FsFile file; + const auto pageWidth = renderer.getScreenWidth(); + const auto pageHeight = renderer.getScreenHeight(); + Rect popupRect = GUI.drawPopup(renderer, tr(STR_LOADING_POPUP)); + GUI.fillPopupProgress(renderer, popupRect, 20); + + if (!Storage.openFileForRead("BMP", filePath, file)) { + return false; + } + + Bitmap bitmap(file, true); + if (bitmap.parseHeaders() != BmpReaderError::Ok) { + file.close(); + return false; + } + + int x, y, renderWidth, renderHeight; + computeCenteredImagePlacement(bitmap.getWidth(), bitmap.getHeight(), pageWidth, pageHeight, x, y, renderWidth, + renderHeight); + + GUI.fillPopupProgress(renderer, popupRect, 50); + + renderer.clearScreen(); + renderer.drawBitmap(bitmap, x, y, pageWidth, pageHeight, 0, 0); + if (showControls) { + const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", tr(STR_SET_SLEEP_SCREEN)); + GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); + } + renderer.displayBuffer(HalDisplay::HALF_REFRESH); + + file.close(); + return true; +} + +bool BmpViewerActivity::renderDecodedImage(const bool showControls) { + const auto pageWidth = renderer.getScreenWidth(); + const auto pageHeight = renderer.getScreenHeight(); + Rect popupRect = GUI.drawPopup(renderer, tr(STR_LOADING_POPUP)); + GUI.fillPopupProgress(renderer, popupRect, 20); + + ImageToFramebufferDecoder* decoder = ImageDecoderFactory::getDecoder(filePath); + if (!decoder) { + return false; + } + + ImageDimensions dims{}; + if (!decoder->getDimensions(filePath, dims) || dims.width <= 0 || dims.height <= 0) { + return false; + } + + int x, y, renderWidth, renderHeight; + computeCenteredImagePlacement(dims.width, dims.height, pageWidth, pageHeight, x, y, renderWidth, renderHeight); + + GUI.fillPopupProgress(renderer, popupRect, 50); + renderer.clearScreen(); + + RenderConfig config{}; + config.x = x; + config.y = y; + config.maxWidth = renderWidth; + config.maxHeight = renderHeight; + config.useExactDimensions = true; + config.useGrayscale = true; + config.useDithering = true; + + if (!decoder->decodeToFramebuffer(filePath, renderer, config)) { + return false; + } + + if (showControls) { + const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", tr(STR_SET_SLEEP_SCREEN)); + GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); + } + renderer.displayBuffer(HalDisplay::HALF_REFRESH); + return true; +} + +void BmpViewerActivity::renderError(const char* message) { + const auto pageHeight = renderer.getScreenHeight(); + renderer.clearScreen(); + renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2, message); + const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", ""); + GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); + renderer.displayBuffer(HalDisplay::HALF_REFRESH); +} + void BmpViewerActivity::setAsSleepScreen() { - // Only copy if the source isn't already /sleep.bmp - if (filePath != SLEEP_BMP_PATH) { - if (!Storage.copyFile("BMP", filePath, SLEEP_BMP_PATH)) { - LOG_ERR("BMP", "Failed to copy %s to %s", filePath.c_str(), SLEEP_BMP_PATH); - return; + bool success = false; + + if (FsHelpers::hasBmpExtension(filePath)) { + success = (filePath == SLEEP_BMP_PATH) ? true : Storage.copyFile("BMP", filePath, SLEEP_BMP_PATH); + } else { + const bool renderedForCapture = isBmpFile(filePath) ? renderBmpImage(false) : renderDecodedImage(false); + if (renderedForCapture) { + success = ScreenshotUtil::saveFramebufferAsBmp(SLEEP_BMP_PATH, renderer.getFrameBuffer(), + display.getDisplayWidth(), display.getDisplayHeight()); + } + + if (!success && Storage.exists(SLEEP_BMP_PATH)) { + Storage.remove(SLEEP_BMP_PATH); } } - // Switch sleep screen mode to CUSTOM so the copied image is used + if (!success) { + LOG_ERR("BMP", "Failed to set %s as sleep screen", filePath.c_str()); + GUI.drawPopup(renderer, "Failed to set sleep screen"); + renderer.displayBuffer(HalDisplay::HALF_REFRESH); + return; + } + SETTINGS.sleepScreen = CrossPointSettings::SLEEP_SCREEN_MODE::CUSTOM; SETTINGS.saveToFile(); LOG_INF("BMP", "Set %s as sleep screen", filePath.c_str()); diff --git a/src/activities/util/BmpViewerActivity.h b/src/activities/util/BmpViewerActivity.h index 2f19d236..65607799 100644 --- a/src/activities/util/BmpViewerActivity.h +++ b/src/activities/util/BmpViewerActivity.h @@ -16,5 +16,8 @@ class BmpViewerActivity final : public Activity { private: std::string filePath; + bool renderBmpImage(bool showControls = true); + bool renderDecodedImage(bool showControls = true); + void renderError(const char* message); void setAsSleepScreen(); }; \ No newline at end of file diff --git a/src/components/UITheme.cpp b/src/components/UITheme.cpp index 1b637757..4a61f00d 100644 --- a/src/components/UITheme.cpp +++ b/src/components/UITheme.cpp @@ -115,7 +115,8 @@ UIIcon UITheme::getFileIcon(const std::string& filename) { if (FsHelpers::hasTxtExtension(filename) || FsHelpers::hasMarkdownExtension(filename)) { return Text; } - if (FsHelpers::hasBmpExtension(filename)) { + if (FsHelpers::hasBmpExtension(filename) || FsHelpers::hasJpgExtension(filename) || + FsHelpers::hasPngExtension(filename)) { return Image; } return File;