99 * Functions to render images from filesystem to segments, used by the "Image" effect
1010 */
1111
12- File file;
13- char lastFilename[34 ] = " /" ;
14- GifDecoder<320 ,320 ,12 ,true > decoder;
15- bool gifDecodeFailed = false ;
16- unsigned long lastFrameDisplayTime = 0 , currentFrameDelay = 0 ;
12+ static File file;
13+ static char lastFilename[WLED_MAX_SEGNAME_LEN+ 2 ] = " /" ; // enough space for "/" + seg.name + '\0'
14+ static GifDecoder<320 ,320 ,12 ,true > decoder; // this creates the basic object; parameter lzwMaxBits is not used; decoder.alloc() always allocated "everything else" = 24Kb
15+ static bool gifDecodeFailed = false ;
16+ static unsigned long lastFrameDisplayTime = 0 , currentFrameDelay = 0 ;
1717
1818bool fileSeekCallback (unsigned long position) {
1919 return file.seek (position);
@@ -35,29 +35,62 @@ int fileSizeCallback(void) {
3535 return file.size ();
3636}
3737
38- bool openGif (const char *filename) {
38+ bool openGif (const char *filename) { // side-effect: updates "file"
3939 file = WLED_FS.open (filename, " r" );
40+ DEBUG_PRINTF_P (PSTR (" opening GIF file %s\n " ), filename);
4041
4142 if (!file) return false ;
4243 return true ;
4344}
4445
45- Segment* activeSeg;
46- uint16_t gifWidth, gifHeight;
46+ static Segment* activeSeg;
47+ static uint16_t gifWidth, gifHeight;
48+ static int lastCoordinate; // last coordinate (x+y) that was set, used to reduce redundant pixel writes
49+ static uint16_t perPixelX, perPixelY; // scaling factors when upscaling
4750
4851void screenClearCallback (void ) {
4952 activeSeg->fill (0 );
5053}
5154
52- void updateScreenCallback (void ) {}
55+ // this callback runs when the decoder has finished painting all pixels
56+ void updateScreenCallback (void ) {
57+ // perfect time for adding blur
58+ if (activeSeg->intensity > 1 ) {
59+ uint8_t blurAmount = activeSeg->intensity ;
60+ if ((blurAmount < 24 ) && (activeSeg->is2D ())) activeSeg->blurRows (activeSeg->intensity ); // some blur - fast
61+ else activeSeg->blur (blurAmount); // more blur - slower
62+ }
63+ lastCoordinate = -1 ; // invalidate last position
64+ }
65+
66+ // note: GifDecoder drawing is done top right to bottom left, line by line
67+
68+ // callbacks to draw a pixel at (x,y) without scaling: used if GIF size matches (virtual)segment size (faster) works for 1D and 2D segments
69+ void drawPixelCallbackNoScale (int16_t x, int16_t y, uint8_t red, uint8_t green, uint8_t blue) {
70+ activeSeg->setPixelColor (y * gifWidth + x, red, green, blue);
71+ }
5372
54- void drawPixelCallback (int16_t x, int16_t y, uint8_t red, uint8_t green, uint8_t blue) {
55- // simple nearest-neighbor scaling
56- int16_t outY = y * activeSeg->height () / gifHeight;
57- int16_t outX = x * activeSeg->width () / gifWidth;
73+ void drawPixelCallback1D (int16_t x, int16_t y, uint8_t red, uint8_t green, uint8_t blue) {
74+ // 1D strip: load pixel-by-pixel left to right, top to bottom (0/0 = top-left in gifs)
75+ int totalImgPix = (int )gifWidth * gifHeight;
76+ int start = ((int )y * gifWidth + (int )x) * activeSeg->vLength () / totalImgPix; // simple nearest-neighbor scaling
77+ if (start == lastCoordinate) return ; // skip setting same coordinate again
78+ lastCoordinate = start;
79+ for (int i = 0 ; i < perPixelX; i++) {
80+ activeSeg->setPixelColor (start + i, red, green, blue);
81+ }
82+ }
83+
84+ void drawPixelCallback2D (int16_t x, int16_t y, uint8_t red, uint8_t green, uint8_t blue) {
85+ // simple nearest-neighbor scaling
86+ int outY = (int )y * activeSeg->vHeight () / gifHeight;
87+ int outX = (int )x * activeSeg->vWidth () / gifWidth;
88+ // Pack coordinates uniquely: outY into upper 16 bits, outX into lower 16 bits
89+ if (((outY << 16 ) | outX) == lastCoordinate) return ; // skip setting same coordinate again
90+ lastCoordinate = (outY << 16 ) | outX; // since input is a "scanline" this is sufficient to identify a "unique" coordinate
5891 // set multiple pixels if upscaling
59- for (int16_t i = 0 ; i < (activeSeg-> width ()+(gifWidth- 1 )) / gifWidth ; i++) {
60- for (int16_t j = 0 ; j < (activeSeg-> height ()+(gifHeight- 1 )) / gifHeight ; j++) {
92+ for (int i = 0 ; i < perPixelX ; i++) {
93+ for (int j = 0 ; j < perPixelY ; j++) {
6194 activeSeg->setPixelColorXY (outX + i, outY + j, red, green, blue);
6295 }
6396 }
@@ -82,28 +115,78 @@ byte renderImageToSegment(Segment &seg) {
82115 if (activeSeg && activeSeg != &seg) return IMAGE_ERROR_SEG_LIMIT; // only one segment at a time
83116 activeSeg = &seg;
84117
85- if (strncmp (lastFilename +1 , seg.name , 32 ) != 0 ) { // segment name changed, load new image
86- strncpy (lastFilename +1 , seg.name , 32 );
118+ if (strncmp (lastFilename +1 , seg.name , WLED_MAX_SEGNAME_LEN) != 0 ) { // segment name changed, load new image
119+ strcpy (lastFilename, " /" ); // filename always starts with '/'
120+ strncpy (lastFilename +1 , seg.name , WLED_MAX_SEGNAME_LEN);
121+ lastFilename[WLED_MAX_SEGNAME_LEN+1 ] =' \0 ' ; // ensure proper string termination when segment name was truncated
87122 gifDecodeFailed = false ;
88- if (strcmp (lastFilename + strlen (lastFilename) - 4 , " .gif" ) != 0 ) {
123+ size_t fnameLen = strlen (lastFilename);
124+ if ((fnameLen < 4 ) || strcmp (lastFilename + fnameLen - 4 , " .gif" ) != 0 ) { // empty segment name, name too short, or name not ending in .gif
89125 gifDecodeFailed = true ;
126+ DEBUG_PRINTF_P (PSTR (" GIF decoder unsupported file: %s\n " ), lastFilename);
90127 return IMAGE_ERROR_UNSUPPORTED_FORMAT;
91128 }
92129 if (file) file.close ();
93- openGif (lastFilename);
94- if (!file) { gifDecodeFailed = true ; return IMAGE_ERROR_FILE_MISSING; }
130+ if (!openGif (lastFilename)) {
131+ gifDecodeFailed = true ;
132+ DEBUG_PRINTF_P (PSTR (" GIF file not found: %s\n " ), lastFilename);
133+ return IMAGE_ERROR_FILE_MISSING;
134+ }
135+ lastCoordinate = -1 ;
95136 decoder.setScreenClearCallback (screenClearCallback);
96137 decoder.setUpdateScreenCallback (updateScreenCallback);
97- decoder.setDrawPixelCallback (drawPixelCallback);
138+ decoder.setDrawPixelCallback (drawPixelCallbackNoScale); // default: use "fast path" callback without scaling
98139 decoder.setFileSeekCallback (fileSeekCallback);
99140 decoder.setFilePositionCallback (filePositionCallback);
100141 decoder.setFileReadCallback (fileReadCallback);
101142 decoder.setFileReadBlockCallback (fileReadBlockCallback);
102143 decoder.setFileSizeCallback (fileSizeCallback);
103- decoder.alloc ();
144+ #if __cpp_exceptions // use exception handler if we can (some targets don't support exceptions)
145+ try {
146+ #endif
147+ decoder.alloc (); // this function may throw out-of memory and cause a crash
148+ #if __cpp_exceptions
149+ } catch (...) { // if we arrive here, the decoder has thrown an OOM exception
150+ gifDecodeFailed = true ;
151+ errorFlag = ERR_NORAM_PX;
152+ DEBUG_PRINTLN (F (" \n GIF decoder out of memory. Please try a smaller image file.\n " ));
153+ return IMAGE_ERROR_DECODER_ALLOC;
154+ // decoder cleanup (hi @coderabbitai): No additonal cleanup necessary - decoder.alloc() ultimately uses "new AnimatedGIF".
155+ // If new throws, no pointer is assigned, previous decoder state (if any) has already been deleted inside alloc(), so calling decoder.dealloc() here is unnecessary.
156+ }
157+ #endif
104158 DEBUG_PRINTLN (F (" Starting decoding" ));
105- if (decoder.startDecoding () < 0 ) { gifDecodeFailed = true ; return IMAGE_ERROR_GIF_DECODE; }
159+ int decoderError = decoder.startDecoding ();
160+ if (decoderError < 0 ) {
161+ DEBUG_PRINTF_P (PSTR (" GIF Decoding error %d in startDecoding().\n " ), decoderError);
162+ errorFlag = ERR_NORAM_PX;
163+ gifDecodeFailed = true ;
164+ return IMAGE_ERROR_GIF_DECODE;
165+ }
106166 DEBUG_PRINTLN (F (" Decoding started" ));
167+ // after startDecoding, we can get GIF size, update static variables and callbacks
168+ decoder.getSize (&gifWidth, &gifHeight);
169+ if (gifWidth == 0 || gifHeight == 0 ) { // bad gif size: prevent division by zero
170+ gifDecodeFailed = true ;
171+ DEBUG_PRINTF_P (PSTR (" Invalid GIF dimensions: %dx%d\n " ), gifWidth, gifHeight);
172+ return IMAGE_ERROR_GIF_DECODE;
173+ }
174+ if (activeSeg->is2D ()) {
175+ perPixelX = (activeSeg->vWidth () + gifWidth -1 ) / gifWidth;
176+ perPixelY = (activeSeg->vHeight () + gifHeight-1 ) / gifHeight;
177+ if (activeSeg->vWidth () != gifWidth || activeSeg->vHeight () != gifHeight) {
178+ decoder.setDrawPixelCallback (drawPixelCallback2D); // use 2D callback with scaling
179+ // DEBUG_PRINTLN(F("scaling image"));
180+ }
181+ } else {
182+ int totalImgPix = (int )gifWidth * gifHeight;
183+ if (totalImgPix - activeSeg->vLength () == 1 ) totalImgPix--; // handle off-by-one: skip last pixel instead of first (gifs constructed from 1D input pad last pixel if length is odd)
184+ perPixelX = (activeSeg->vLength () + totalImgPix-1 ) / totalImgPix;
185+ if (totalImgPix != activeSeg->vLength ()) {
186+ decoder.setDrawPixelCallback (drawPixelCallback1D); // use 1D callback with scaling
187+ // DEBUG_PRINTLN(F("scaling image"));
188+ }
189+ }
107190 }
108191
109192 if (gifDecodeFailed) return IMAGE_ERROR_PREV;
@@ -117,10 +200,12 @@ byte renderImageToSegment(Segment &seg) {
117200 // TODO consider handling this on FX level with a different frametime, but that would cause slow gifs to speed up during transitions
118201 if (millis () - lastFrameDisplayTime < wait) return IMAGE_ERROR_WAITING;
119202
120- decoder.getSize (&gifWidth, &gifHeight);
121-
122203 int result = decoder.decodeFrame (false );
123- if (result < 0 ) { gifDecodeFailed = true ; return IMAGE_ERROR_FRAME_DECODE; }
204+ if (result < 0 ) {
205+ DEBUG_PRINTF_P (PSTR (" GIF Decoding error %d in decodeFrame().\n " ), result);
206+ gifDecodeFailed = true ;
207+ return IMAGE_ERROR_FRAME_DECODE;
208+ }
124209
125210 currentFrameDelay = decoder.getFrameDelay_ms ();
126211 unsigned long tooSlowBy = (millis () - lastFrameDisplayTime) - wait; // if last frame was longer than intended, compensate
@@ -137,7 +222,7 @@ void endImagePlayback(Segment *seg) {
137222 decoder.dealloc ();
138223 gifDecodeFailed = false ;
139224 activeSeg = nullptr ;
140- lastFilename[ 1 ] = ' \0 ' ;
225+ strcpy ( lastFilename, " / " ); // reset filename
141226 DEBUG_PRINTLN (F (" Image playback ended" ));
142227}
143228
0 commit comments