Skip to content

Commit befa30e

Browse files
committed
ImageViewer: Add JPEG/PNG support with auto-conversion to BMP
1 parent 2be261d commit befa30e

File tree

6 files changed

+77
-30
lines changed

6 files changed

+77
-30
lines changed

src/apps/ClockApp.cpp

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,7 @@ static void drawDayOfWeek(const Theme& theme, int x, int y, int wday) {
280280
renderer.drawText(theme.uiFontId, x, y, DAYS[wday], theme.secondaryTextBlack);
281281
}
282282

283-
void render(Core& core) {
283+
bool render(Core& core) {
284284
(void)core;
285285

286286
const Theme& theme = THEME;
@@ -369,6 +369,8 @@ void render(Core& core) {
369369

370370
ui::ButtonBar buttons("Back", "Menu", "", "");
371371
ui::buttonBar(renderer, theme, buttons);
372+
373+
return false;
372374
}
373375

374376
void exit(Core& core) {

src/apps/ClockApp.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ namespace clock_app {
88
void enter(Core& core);
99
bool update(Core& core);
1010
void onButton(Core& core, Button btn);
11-
void render(Core& core);
11+
bool render(Core& core);
1212
void exit(Core& core);
1313
void renderMenu(Core& core);
1414
void onMenuButton(Core& core, Button btn);

src/apps/ImageViewerApp.cpp

Lines changed: 67 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
#include <EInkDisplay.h>
77
#include <FsHelpers.h>
88
#include <GfxRenderer.h>
9+
#include <ImageConverter.h>
910
#include <Logging.h>
1011
#include <SDCardManager.h>
1112
#include <SdFat.h>
@@ -27,11 +28,13 @@ namespace imageviewer_app {
2728

2829
static constexpr const char* IMAGES_DIR = "/images";
2930
static constexpr const char* SETTINGS_PATH = "/.papyrix/apps/image-viewer.txt";
30-
3131
static constexpr uint32_t SLIDESHOW_INTERVALS[] = {0, 30000, 60000, 300000};
3232
static constexpr const char* SLIDESHOW_LABELS[] = {"Off", "30s", "60s", "5min"};
3333
static constexpr int SLIDESHOW_COUNT = 4;
3434

35+
static constexpr int BOTTOM_BAR_HEIGHT = 23;
36+
static constexpr uint32_t MAX_IMAGE_FILE_SIZE = 10 * 1024 * 1024; // 10 MB
37+
3538
static struct {
3639
std::vector<std::string> files;
3740
int currentIndex = 0;
@@ -102,7 +105,7 @@ static void scanImages() {
102105
continue;
103106
}
104107

105-
if (FsHelpers::isBmpFile(name)) {
108+
if (FsHelpers::isImageFile(name) && entry.fileSize() <= MAX_IMAGE_FILE_SIZE) {
106109
std::string path = std::string(IMAGES_DIR) + "/" + name;
107110
state.files.push_back(path);
108111
}
@@ -113,10 +116,39 @@ static void scanImages() {
113116

114117
std::sort(state.files.begin(), state.files.end());
115118

116-
LOG_INF(TAG, "Found %zu BMP images", state.files.size());
119+
LOG_INF(TAG, "Found %zu images", state.files.size());
120+
}
121+
122+
// Convert JPEG/PNG to BMP in-place (same directory, replace original).
123+
// Returns the new BMP path, or empty string on failure.
124+
static std::string convertToBmpInPlace(const std::string& path) {
125+
std::string bmpPath = path.substr(0, path.rfind('.')) + ".bmp";
126+
127+
// If target BMP already exists, just remove the original
128+
if (SdMan.exists(bmpPath.c_str())) {
129+
SdMan.remove(path.c_str());
130+
return bmpPath;
131+
}
132+
133+
const int maxW = renderer.getScreenWidth();
134+
const int maxH = renderer.getScreenHeight() - BOTTOM_BAR_HEIGHT;
135+
136+
ImageConvertConfig config;
137+
config.maxWidth = maxW;
138+
config.maxHeight = maxH;
139+
config.oneBit = false;
140+
config.logTag = TAG;
141+
142+
if (!ImageConverterFactory::convertToBmp(path, bmpPath, config)) {
143+
return "";
144+
}
145+
146+
SdMan.remove(path.c_str());
147+
LOG_INF(TAG, "Converted: %s -> %s", path.c_str(), bmpPath.c_str());
148+
return bmpPath;
117149
}
118150

119-
static void renderImage() {
151+
static bool renderImage() {
120152
const Theme& theme = THEME;
121153
const int screenW = renderer.getScreenWidth();
122154
const int screenH = renderer.getScreenHeight();
@@ -127,46 +159,60 @@ static void renderImage() {
127159
ui::centeredMessage(renderer, theme, theme.uiFontId, "No images in /images/");
128160
ui::ButtonBar buttons("Back", "Menu", "", "");
129161
ui::buttonBar(renderer, theme, buttons);
130-
return;
162+
return false;
131163
}
132164

133165
const std::string& path = state.files[state.currentIndex];
134166

167+
// Convert non-BMP images to BMP in-place (replaces original file)
168+
if (!FsHelpers::isBmpFile(path)) {
169+
ui::centeredMessage(renderer, theme, theme.uiFontId, "Converting...");
170+
renderer.displayBuffer();
171+
172+
std::string bmpPath = convertToBmpInPlace(path);
173+
if (bmpPath.empty()) {
174+
renderer.clearScreen(theme.backgroundColor);
175+
ui::centeredMessage(renderer, theme, theme.uiFontId, "Conversion failed");
176+
ui::ButtonBar buttons("Back", "Menu", "<", ">");
177+
ui::buttonBar(renderer, theme, buttons);
178+
return false;
179+
}
180+
181+
state.files[state.currentIndex] = bmpPath;
182+
renderer.clearScreen(theme.backgroundColor);
183+
}
184+
185+
const std::string& bmpPath = state.files[state.currentIndex];
186+
135187
FsFile file;
136-
if (!SdMan.openFileForRead(TAG, path, file)) {
137-
LOG_ERR(TAG, "Failed to open: %s", path.c_str());
188+
if (!SdMan.openFileForRead(TAG, bmpPath, file)) {
189+
LOG_ERR(TAG, "Failed to open: %s", bmpPath.c_str());
138190
ui::centeredMessage(renderer, theme, theme.uiFontId, "Failed to open image");
139191
ui::ButtonBar buttons("Back", "Menu", "<", ">");
140192
ui::buttonBar(renderer, theme, buttons);
141-
return;
193+
return false;
142194
}
143195

144196
Bitmap bitmap(file);
145197
if (bitmap.parseHeaders() != BmpReaderError::Ok) {
146-
LOG_ERR(TAG, "Invalid BMP: %s", path.c_str());
198+
LOG_ERR(TAG, "Invalid BMP: %s", bmpPath.c_str());
147199
file.close();
148-
ui::centeredMessage(renderer, theme, theme.uiFontId, "Invalid BMP file");
200+
ui::centeredMessage(renderer, theme, theme.uiFontId, "Invalid image file");
149201
ui::ButtonBar buttons("Back", "Menu", "<", ">");
150202
ui::buttonBar(renderer, theme, buttons);
151-
return;
203+
return false;
152204
}
153205

154-
// Reserve bottom 23px for button bar and counter
155-
static constexpr int BOTTOM_BAR_HEIGHT = 23;
156206
const int viewportH = screenH - BOTTOM_BAR_HEIGHT;
157-
158207
auto rect = CoverHelpers::calculateCenteredRect(bitmap.getWidth(), bitmap.getHeight(), 0, 0, screenW, viewportH);
159208
renderer.drawBitmap(bitmap, rect.x, rect.y, rect.width, rect.height);
160209

161210
file.close();
162211

163-
// Image counter
164-
char counter[16];
165-
snprintf(counter, sizeof(counter), "%d/%zu", state.currentIndex + 1, state.files.size());
166-
renderer.drawCenteredText(theme.smallFontId, screenH - BOTTOM_BAR_HEIGHT + 4, counter, theme.primaryTextBlack);
167-
168212
ui::ButtonBar buttons("Back", "Menu", "<", ">");
169213
ui::buttonBar(renderer, theme, buttons);
214+
215+
return false;
170216
}
171217

172218
void enter(Core& core) {
@@ -217,9 +263,9 @@ void onButton(Core& core, Button btn) {
217263
}
218264
}
219265

220-
void render(Core& core) {
266+
bool render(Core& core) {
221267
(void)core;
222-
renderImage();
268+
return renderImage();
223269
}
224270

225271
void exit(Core& core) {

src/apps/ImageViewerApp.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ namespace imageviewer_app {
88
void enter(Core& core);
99
bool update(Core& core);
1010
void onButton(Core& core, Button btn);
11-
void render(Core& core);
11+
bool render(Core& core);
1212
void exit(Core& core);
1313
void renderMenu(Core& core);
1414
void onMenuButton(Core& core, Button btn);

src/apps/MiniApp.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ struct MiniApp {
1313
void (*enter)(Core& core);
1414
bool (*update)(Core& core);
1515
void (*onButton)(Core& core, Button btn);
16-
void (*render)(Core& core);
16+
bool (*render)(Core& core); // Returns true if app handled display itself
1717
void (*exit)(Core& core);
1818
void (*renderMenu)(Core& core);
1919
void (*onMenuButton)(Core& core, Button btn);

src/states/AppLauncherState.cpp

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -174,16 +174,15 @@ void AppLauncherState::render(Core& core) {
174174

175175
case Mode::App:
176176
if (activeApp_ >= 0 && APPS[activeApp_].render) {
177-
APPS[activeApp_].render(core);
178-
renderer_.displayBuffer();
177+
if (!APPS[activeApp_].render(core)) {
178+
renderer_.displayBuffer();
179+
}
179180
}
180181
break;
181182

182183
case Mode::Overlay:
183184
if (activeApp_ >= 0) {
184-
if (APPS[activeApp_].render) {
185-
APPS[activeApp_].render(core);
186-
}
185+
renderer_.clearScreen(THEME.backgroundColor);
187186
if (APPS[activeApp_].renderMenu) {
188187
APPS[activeApp_].renderMenu(core);
189188
}

0 commit comments

Comments
 (0)