Skip to content

Commit 4f93661

Browse files
authored
Improved 1D support for GIF images, bugfixes, blur option by @DedeHai & @softhack007
- add better support for 1D gifs: use the full gif, row by row, scale if needed - add blur slider to image FX - improved safety checks to avoid crashes - add "fast path" if image size matches virtual segment size
2 parents bd933ff + cd2dc43 commit 4f93661

File tree

2 files changed

+113
-28
lines changed

2 files changed

+113
-28
lines changed

wled00/FX.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4509,7 +4509,7 @@ uint16_t mode_image(void) {
45094509
// Serial.println(status);
45104510
// }
45114511
}
4512-
static const char _data_FX_MODE_IMAGE[] PROGMEM = "Image@!,;;;12;sx=128";
4512+
static const char _data_FX_MODE_IMAGE[] PROGMEM = "Image@!,Blur,;;;12;sx=128,ix=0";
45134513

45144514
/*
45154515
Blends random colors across palette

wled00/image_loader.cpp

Lines changed: 112 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@
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

1818
bool 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

4851
void 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("\nGIF 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

Comments
 (0)