Support png and jpg in Image Viewer
This commit is contained in:
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Epub> 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();
|
||||
|
||||
@@ -16,7 +16,7 @@ class ReaderActivity final : public Activity {
|
||||
static std::unique_ptr<Txt> 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 = "");
|
||||
|
||||
@@ -1,17 +1,55 @@
|
||||
#include "BmpViewerActivity.h"
|
||||
|
||||
#include <Bitmap.h>
|
||||
#include <Epub/converters/ImageDecoderFactory.h>
|
||||
#include <FsHelpers.h>
|
||||
#include <GfxRenderer.h>
|
||||
#include <HalStorage.h>
|
||||
#include <I18n.h>
|
||||
|
||||
#include <cmath>
|
||||
|
||||
#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<float>(imageWidth) / static_cast<float>(imageHeight);
|
||||
const float screenRatio = static_cast<float>(pageWidth) / static_cast<float>(pageHeight);
|
||||
|
||||
if (ratio > screenRatio) {
|
||||
renderWidth = pageWidth;
|
||||
renderHeight = std::round(static_cast<float>(pageWidth) / ratio);
|
||||
x = 0;
|
||||
y = std::round((static_cast<float>(pageHeight) - renderHeight) / 2.0f);
|
||||
} else {
|
||||
renderHeight = pageHeight;
|
||||
renderWidth = std::round(static_cast<float>(pageHeight) * ratio);
|
||||
x = std::round((static_cast<float>(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<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
|
||||
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());
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user