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
2829static constexpr const char * IMAGES_DIR = " /images" ;
2930static constexpr const char * SETTINGS_PATH = " /.papyrix/apps/image-viewer.txt" ;
30-
3131static constexpr uint32_t SLIDESHOW_INTERVALS[] = {0 , 30000 , 60000 , 300000 };
3232static constexpr const char * SLIDESHOW_LABELS[] = {" Off" , " 30s" , " 60s" , " 5min" };
3333static 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+
3538static 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
172218void 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
225271void exit (Core& core) {
0 commit comments