Something went wrong. Try again.
A fork of https://github.com/crosspoint-reader/crosspoint-reader
Something went wrong. Try again.
13 kB · 331 lines
C++
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332#include "BmpViewerActivity.h"
#include <Bitmap.h>#include <Epub/converters/PngToFramebufferConverter.h>#include <FsHelpers.h>#include <GfxRenderer.h>#include <HalDisplay.h>#include <HalStorage.h>#include <I18n.h>#include <Memory.h>
#include <algorithm>
#include "CrossPointSettings.h"#include "components/UITheme.h"#include "fontIds.h"
namespace {constexpr char CUSTOM_SLEEP_ROOT_BMP[] = "/sleep.bmp";constexpr char TRANSPARENT_SLEEP_ROOT_BMP[] = "/sleep-overlay.bmp";constexpr char TRANSPARENT_SLEEP_ROOT_PNG[] = "/sleep-overlay.png";constexpr size_t COPY_BUFFER_SIZE = 2048;} // namespace
BmpViewerActivity::BmpViewerActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::string path) : Activity("BmpViewer", renderer, mappedInput), filePath(std::move(path)) {}
void BmpViewerActivity::loadSiblingImages() { siblingImages.clear(); currentImageIndex = -1;
if (filePath.empty()) return;
std::string dirPath = FsHelpers::extractFolderPath(filePath); size_t lastSlash = filePath.find_last_of('/'); std::string fileName = (lastSlash != std::string::npos) ? filePath.substr(lastSlash + 1) : filePath;
auto dir = Storage.open(dirPath.c_str()); if (!dir || !dir.isDirectory()) { if (dir) dir.close(); return; }
char name[500]; for (auto file = dir.openNextFile(); file; file = dir.openNextFile()) { if (!file.isDirectory()) { file.getName(name, sizeof(name)); if (name[0] != '.') { std::string fname(name); if (FsHelpers::hasBmpExtension(fname) || FsHelpers::hasPngExtension(fname)) { siblingImages.push_back(fname); } } } file.close(); } dir.close();
FsHelpers::sortFileList(siblingImages);
const auto image = std::find(siblingImages.begin(), siblingImages.end(), fileName); if (image != siblingImages.end()) { currentImageIndex = static_cast<int>(image - siblingImages.begin()); }}
bool BmpViewerActivity::canSetSleepCover() const { return FsHelpers::hasBmpExtension(filePath) || (SETTINGS.sleepScreen == CrossPointSettings::SLEEP_SCREEN_MODE::TRANSPARENT_CUSTOM && FsHelpers::hasPngExtension(filePath));}
bool BmpViewerActivity::renderPng() { ImageDimensions dimensions; if (!PngToFramebufferConverter::getDimensionsStatic(filePath, dimensions)) return false; if (dimensions.width <= 0 || dimensions.height <= 0) return false;
const float scale = std::min(static_cast<float>(renderer.getScreenWidth()) / dimensions.width, static_cast<float>(renderer.getScreenHeight()) / dimensions.height); const int width = std::min(renderer.getScreenWidth(), static_cast<int>(dimensions.width * std::min(scale, 1.0f))); const int height = std::min(renderer.getScreenHeight(), static_cast<int>(dimensions.height * std::min(scale, 1.0f))); RenderConfig config{(renderer.getScreenWidth() - width) / 2, (renderer.getScreenHeight() - height) / 2, width, height};
PngToFramebufferConverter converter; return converter.decodeToFramebuffer(filePath, renderer, config);}
void BmpViewerActivity::onEnter() { Activity::onEnter();
if (siblingImages.empty() && !filePath.empty()) { loadSiblingImages(); }
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 if (FsHelpers::hasPngExtension(filePath)) { renderer.clearScreen(); const bool hasPrevious = siblingImages.size() > 1 && currentImageIndex > 0; const bool hasNext = siblingImages.size() > 1 && currentImageIndex != -1 && currentImageIndex < static_cast<int>(siblingImages.size()) - 1; const auto labels = mappedInput.mapLabels(tr(STR_BACK), canSetSleepCover() ? tr(STR_SET_SLEEP_COVER) : "", hasPrevious ? "<" : "", hasNext ? ">" : ""); if (renderPng()) { GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); renderer.displayBuffer(HalDisplay::FAST_REFRESH); } else { renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2, tr(STR_FILE_OPEN_FAILED)); GUI.drawButtonHints(renderer, labels.btn1, "", "", ""); renderer.displayBuffer(HalDisplay::HALF_REFRESH); } return; }
HalFile file; // 1. Open the BMP file if (Storage.openFileForRead("BMP", filePath, file)) { Bitmap bitmap(file, true, renderer.grayscaleCapabilities(HalDisplay::GrayscaleMode::Absolute).supported() && display.getController() == HalDisplay::Controller::SSD1677);
// 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<float>(bitmap.getWidth()) / static_cast<float>(bitmap.getHeight()); const float screenRatio = static_cast<float>(pageWidth) / static_cast<float>(pageHeight);
if (ratio > screenRatio) { // Wider than screen x = 0; y = std::round((static_cast<float>(pageHeight) - static_cast<float>(pageWidth) / ratio) / 2); } else { // Taller than screen x = std::round((static_cast<float>(pageWidth) - static_cast<float>(pageHeight) * ratio) / 2); y = 0; } } else { // Center small images x = (pageWidth - bitmap.getWidth()) / 2; y = (pageHeight - bitmap.getHeight()) / 2; }
// 4. Prepare Rendering bool hasPrevious = (siblingImages.size() > 1 && currentImageIndex > 0); bool hasNext = (siblingImages.size() > 1 && currentImageIndex != -1 && currentImageIndex < static_cast<int>(siblingImages.size()) - 1);
const auto labels = mappedInput.mapLabels(tr(STR_BACK), canSetSleepCover() ? tr(STR_SET_SLEEP_COVER) : "", (hasPrevious ? "<" : ""), (hasNext ? ">" : ""));
GUI.fillPopupProgress(renderer, popupRect, 50);
renderer.clearScreen(); if (!renderer.drawBitmap(bitmap, x, y, pageWidth, pageHeight, 0, 0)) { renderer.clearScreen(); renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2, tr(STR_FILE_OPEN_FAILED)); renderer.displayBuffer(HalDisplay::HALF_REFRESH); return; }
// Draw UI hints on the base layer GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); if (bitmap.hasGreyscale()) { const bool absolute = renderer.grayscaleCapabilities(HalDisplay::GrayscaleMode::Absolute).supported(); if (absolute && !renderer.displayGrayscaleBase(HalDisplay::GrayscaleMode::Absolute)) return; if (!absolute) renderer.displayGrayscaleBase(HalDisplay::HALF_REFRESH); bool planesReady = true; for (const auto mode : {GfxRenderer::GRAYSCALE_LSB, GfxRenderer::GRAYSCALE_MSB}) { if (bitmap.rewindToData() != BmpReaderError::Ok) { LOG_ERR("BMP", "Failed to rewind bitmap for grayscale rendering"); planesReady = false; break; } renderer.clearScreen(absolute ? 0xFF : 0x00); renderer.setRenderMode(mode); if (!renderer.drawBitmap(bitmap, x, y, pageWidth, pageHeight, 0, 0)) { planesReady = false; break; } GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); if (mode == GfxRenderer::GRAYSCALE_LSB) { renderer.copyGrayscaleLsbBuffers(); } else { renderer.copyGrayscaleMsbBuffers(); } } if (planesReady) renderer.displayGrayBuffer();
// Rebuild the BW framebuffer for popups and subsequent differential updates. renderer.setRenderMode(GfxRenderer::BW); renderer.clearScreen(); if (bitmap.rewindToData() != BmpReaderError::Ok || !renderer.drawBitmap(bitmap, x, y, pageWidth, pageHeight, 0, 0)) { LOG_ERR("BMP", "Failed to rewind bitmap to restore the BW framebuffer"); renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2, tr(STR_FILE_OPEN_FAILED)); planesReady = false; } GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); renderer.cleanupGrayscaleWithFrameBuffer(); if (!planesReady) renderer.displayBuffer(HalDisplay::HALF_REFRESH); } else { renderer.displayBuffer(HalDisplay::FAST_REFRESH); }
} else { // Handle file parsing error renderer.clearScreen(); renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2, tr(STR_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, tr(STR_FILE_OPEN_FAILED)); 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::onExit() { Activity::onExit(); renderer.clearScreen(); renderer.displayBuffer(HalDisplay::HALF_REFRESH);}
void BmpViewerActivity::doSetSleepCover() { GUI.drawPopup(renderer, tr(STR_LOADING_POPUP));
const bool transparentMode = SETTINGS.sleepScreen == CrossPointSettings::SLEEP_SCREEN_MODE::TRANSPARENT_CUSTOM; if (!canSetSleepCover()) return;
const char* destination = transparentMode ? (FsHelpers::hasPngExtension(filePath) ? TRANSPARENT_SLEEP_ROOT_PNG : TRANSPARENT_SLEEP_ROOT_BMP) : CUSTOM_SLEEP_ROOT_BMP; bool success = filePath == destination;
if (!success) { auto buffer = makeUniqueNoThrow<uint8_t[]>(COPY_BUFFER_SIZE); if (!buffer) { LOG_ERR("BMP", "OOM: sleep cover copy buffer"); } else { HalFile inFile, outFile; if (Storage.openFileForRead("BMP", filePath, inFile) && Storage.openFileForWrite("BMP", destination, outFile)) { int bytesRead; success = true; while ((bytesRead = inFile.read(buffer.get(), COPY_BUFFER_SIZE)) > 0) { if (outFile.write(buffer.get(), static_cast<size_t>(bytesRead)) != static_cast<size_t>(bytesRead)) { success = false; break; } } if (bytesRead < 0) success = false; outFile.close(); } } }
if (success) { if (!transparentMode) SETTINGS.sleepScreen = CrossPointSettings::SLEEP_SCREEN_MODE::CUSTOM; SETTINGS.saveToFile(); GUI.drawPopup(renderer, tr(STR_DONE)); } else { GUI.drawPopup(renderer, tr(STR_FAILED_LOWER)); }
delay(1000); onEnter();}
void BmpViewerActivity::loop() { // Keep CPU awake/polling so 1st click works Activity::loop();
auto openSibling = [this](const int delta) { if (currentImageIndex < 0) { return false; } const int nextIndex = currentImageIndex + delta; if (siblingImages.size() <= 1 || nextIndex < 0 || nextIndex >= static_cast<int>(siblingImages.size())) { return false; } currentImageIndex = nextIndex; std::string dirPath = FsHelpers::extractFolderPath(filePath); if (dirPath.back() != '/') dirPath += "/"; filePath = dirPath + siblingImages[currentImageIndex]; onEnter(); return true; };
if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { activityManager.goToFileBrowser(filePath); return; }
const auto swipe = mappedInput.wasSwipe(); if (swipe == MappedInputManager::SwipeDir::Left) { openSibling(1); return; } if (swipe == MappedInputManager::SwipeDir::Right) { openSibling(-1); return; }
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { if (canSetSleepCover()) doSetSleepCover(); return; }
if (mappedInput.wasReleased(MappedInputManager::Button::Left) || mappedInput.wasReleased(MappedInputManager::Button::Up)) { openSibling(-1); return; }
if (mappedInput.wasReleased(MappedInputManager::Button::Right) || mappedInput.wasReleased(MappedInputManager::Button::Down)) { openSibling(1); return; }}