From 2eae521b6a87181dac94405a9f4026981018a522 Mon Sep 17 00:00:00 2001 From: cottongin Date: Sat, 21 Feb 2026 18:37:43 -0500 Subject: [PATCH] feat: add BmpViewer activity for viewing .bmp images in file browser (port upstream PR #887) New BmpViewerActivity opens, parses, and renders BMP files with centered aspect-ratio-preserving display and localized back navigation. Library file filter extended to include .bmp. ReaderActivity routes BMP paths to the new viewer. LyraTheme button hint backgrounds switched to rounded rect fills to prevent overflow artifacts. Co-authored-by: Cursor --- src/activities/home/MyLibraryActivity.cpp | 2 +- src/activities/reader/ReaderActivity.cpp | 14 +++++ src/activities/reader/ReaderActivity.h | 2 + src/activities/util/BmpViewerActivity.cpp | 72 +++++++++++++++++++++++ src/activities/util/BmpViewerActivity.h | 21 +++++++ src/components/themes/lyra/LyraTheme.cpp | 6 +- 6 files changed, 114 insertions(+), 3 deletions(-) create mode 100644 src/activities/util/BmpViewerActivity.cpp create mode 100644 src/activities/util/BmpViewerActivity.h diff --git a/src/activities/home/MyLibraryActivity.cpp b/src/activities/home/MyLibraryActivity.cpp index 5c2dd874..21414e57 100644 --- a/src/activities/home/MyLibraryActivity.cpp +++ b/src/activities/home/MyLibraryActivity.cpp @@ -94,7 +94,7 @@ void MyLibraryActivity::loadFiles() { auto filename = std::string(name); if (StringUtils::checkFileExtension(filename, ".epub") || StringUtils::checkFileExtension(filename, ".xtch") || StringUtils::checkFileExtension(filename, ".xtc") || StringUtils::checkFileExtension(filename, ".txt") || - StringUtils::checkFileExtension(filename, ".md")) { + StringUtils::checkFileExtension(filename, ".md") || StringUtils::checkFileExtension(filename, ".bmp")) { files.emplace_back(filename); } } diff --git a/src/activities/reader/ReaderActivity.cpp b/src/activities/reader/ReaderActivity.cpp index f2f9199a..df42fc87 100644 --- a/src/activities/reader/ReaderActivity.cpp +++ b/src/activities/reader/ReaderActivity.cpp @@ -9,6 +9,7 @@ #include "TxtReaderActivity.h" #include "Xtc.h" #include "XtcReaderActivity.h" +#include "activities/util/BmpViewerActivity.h" #include "activities/util/FullScreenMessageActivity.h" #include "util/StringUtils.h" @@ -29,6 +30,8 @@ bool ReaderActivity::isTxtFile(const std::string& path) { StringUtils::checkFileExtension(path, ".md"); // Treat .md as txt files (until we have a markdown reader) } +bool ReaderActivity::isBmpFile(const std::string& path) { return StringUtils::checkFileExtension(path, ".bmp"); } + std::unique_ptr ReaderActivity::loadEpub(const std::string& path) { if (!Storage.exists(path.c_str())) { LOG_ERR("READER", "File does not exist: %s", path.c_str()); @@ -104,6 +107,12 @@ void ReaderActivity::onGoToTxtReader(std::unique_ptr txt) { renderer, mappedInput, std::move(txt), [this, txtPath] { goToLibrary(txtPath); }, [this] { onGoBack(); })); } +void ReaderActivity::onGoToBmpViewer(const std::string& path) { + currentBookPath = path; + exitActivity(); + enterNewActivity(new BmpViewerActivity(renderer, mappedInput, path, [this, path] { goToLibrary(path); })); +} + void ReaderActivity::onEnter() { ActivityWithSubactivity::onEnter(); @@ -114,6 +123,11 @@ void ReaderActivity::onEnter() { currentBookPath = initialBookPath; + if (isBmpFile(initialBookPath)) { + onGoToBmpViewer(initialBookPath); + return; + } + if (isXtcFile(initialBookPath)) { auto xtc = loadXtc(initialBookPath); if (!xtc) { diff --git a/src/activities/reader/ReaderActivity.h b/src/activities/reader/ReaderActivity.h index 5a2c1012..52a3e1d1 100644 --- a/src/activities/reader/ReaderActivity.h +++ b/src/activities/reader/ReaderActivity.h @@ -18,12 +18,14 @@ class ReaderActivity final : public ActivityWithSubactivity { 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 std::string extractFolderPath(const std::string& filePath); void goToLibrary(const std::string& fromBookPath = ""); void onGoToEpubReader(std::unique_ptr epub); void onGoToXtcReader(std::unique_ptr xtc); void onGoToTxtReader(std::unique_ptr txt); + void onGoToBmpViewer(const std::string& path); public: explicit ReaderActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::string initialBookPath, diff --git a/src/activities/util/BmpViewerActivity.cpp b/src/activities/util/BmpViewerActivity.cpp new file mode 100644 index 00000000..21459eed --- /dev/null +++ b/src/activities/util/BmpViewerActivity.cpp @@ -0,0 +1,72 @@ +#include "BmpViewerActivity.h" + +#include +#include +#include + +#include "Bitmap.h" +#include "components/UITheme.h" +#include "fontIds.h" + +void BmpViewerActivity::onEnter() { + Activity::onEnter(); + + const auto pageWidth = renderer.getScreenWidth(); + const auto pageHeight = renderer.getScreenHeight(); + + // Show loading indicator while BMP is parsed + renderer.clearScreen(); + renderer.drawCenteredText(UI_10_FONT_ID, (pageHeight - renderer.getLineHeight(UI_10_FONT_ID)) / 2, + tr(STR_LOADING), true, EpdFontFamily::BOLD); + renderer.displayBuffer(HalDisplay::FAST_REFRESH); + + FsFile file; + if (!Storage.openFileForRead("BMP", filePath, file)) { + LOG_ERR("BMP", "Failed to open file: %s", filePath.c_str()); + loadFailed = true; + return; + } + + Bitmap bitmap(file, true); + if (bitmap.parseHeaders() != BmpReaderError::Ok) { + LOG_ERR("BMP", "Failed to parse BMP headers: %s", filePath.c_str()); + file.close(); + loadFailed = true; + return; + } + + LOG_DBG("BMP", "Loaded %s (%d x %d)", filePath.c_str(), bitmap.getWidth(), bitmap.getHeight()); + + // Compute centered position; drawBitmap handles aspect-ratio-preserving scaling + const float ratio = static_cast(bitmap.getWidth()) / static_cast(bitmap.getHeight()); + const float screenRatio = static_cast(pageWidth) / static_cast(pageHeight); + int x, y; + 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; + } + + renderer.clearScreen(); + renderer.drawBitmap(bitmap, x, y, pageWidth, pageHeight); + file.close(); + + 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::loop() { + if (loadFailed) { + loadFailed = false; + onGoBack(); + return; + } + + if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { + onGoBack(); + } +} diff --git a/src/activities/util/BmpViewerActivity.h b/src/activities/util/BmpViewerActivity.h new file mode 100644 index 00000000..5d50ef87 --- /dev/null +++ b/src/activities/util/BmpViewerActivity.h @@ -0,0 +1,21 @@ +#pragma once +#include +#include +#include + +#include "../Activity.h" + +class BmpViewerActivity final : public Activity { + std::string filePath; + const std::function onGoBack; + bool loadFailed = false; + + public: + explicit BmpViewerActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::string path, + std::function onGoBack) + : Activity("BmpViewer", renderer, mappedInput), + filePath(std::move(path)), + onGoBack(std::move(onGoBack)) {} + void onEnter() override; + void loop() override; +}; diff --git a/src/components/themes/lyra/LyraTheme.cpp b/src/components/themes/lyra/LyraTheme.cpp index 806c5bf0..f10b15a6 100644 --- a/src/components/themes/lyra/LyraTheme.cpp +++ b/src/components/themes/lyra/LyraTheme.cpp @@ -569,7 +569,8 @@ void LyraTheme::drawButtonHints(GfxRenderer& renderer, const char* btn1, const c const int x = buttonPositions[i]; if (labels[i] != nullptr && labels[i][0] != '\0') { // Draw the filled background and border for a FULL-sized button - renderer.fillRect(x, pageHeight - buttonY, buttonWidth, buttonHeight, false); + renderer.fillRoundedRect(x, pageHeight - buttonY, buttonWidth, buttonHeight, cornerRadius, true, true, false, + false, Color::White); renderer.drawRoundedRect(x, pageHeight - buttonY, buttonWidth, buttonHeight, 1, cornerRadius, true, true, false, false, true); const int textWidth = renderer.getTextWidth(SMALL_FONT_ID, labels[i]); @@ -577,7 +578,8 @@ void LyraTheme::drawButtonHints(GfxRenderer& renderer, const char* btn1, const c renderer.drawText(SMALL_FONT_ID, textX, pageHeight - buttonY + textYOffset, labels[i]); } else { // Draw the filled background and border for a SMALL-sized button - renderer.fillRect(x, pageHeight - smallButtonHeight, buttonWidth, smallButtonHeight, false); + renderer.fillRoundedRect(x, pageHeight - smallButtonHeight, buttonWidth, smallButtonHeight, cornerRadius, true, + true, false, false, Color::White); renderer.drawRoundedRect(x, pageHeight - smallButtonHeight, buttonWidth, smallButtonHeight, 1, cornerRadius, true, true, false, false, true); }