From 01c84b014085ab7e4f550ef669c001d13f14eb2b Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Thu, 6 Nov 2025 14:55:26 +0100 Subject: [PATCH 01/17] add better 1D support for gif images Instead of showing a scaled, single line of the GIF: map the full gif to the strip --- wled00/image_loader.cpp | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/wled00/image_loader.cpp b/wled00/image_loader.cpp index 691ede1a..599c528e 100644 --- a/wled00/image_loader.cpp +++ b/wled00/image_loader.cpp @@ -52,13 +52,25 @@ void screenClearCallback(void) { void updateScreenCallback(void) {} void drawPixelCallback(int16_t x, int16_t y, uint8_t red, uint8_t green, uint8_t blue) { - // simple nearest-neighbor scaling - int16_t outY = y * activeSeg->height() / gifHeight; - int16_t outX = x * activeSeg->width() / gifWidth; - // set multiple pixels if upscaling - for (int16_t i = 0; i < (activeSeg->width()+(gifWidth-1)) / gifWidth; i++) { - for (int16_t j = 0; j < (activeSeg->height()+(gifHeight-1)) / gifHeight; j++) { - activeSeg->setPixelColorXY(outX + i, outY + j, red, green, blue); + if (activeSeg->height() == 1) { + // 1D strip: load pixel-by-pixel left to right, top to bottom (0/0 = top-left in gifs), scale if needed + int totalImgPix = (int)gifWidth * gifHeight; + int stripLen = activeSeg->width(); + if (totalImgPix - stripLen == 1) totalImgPix--; // handle off-by-one: skip last pixel instead of first + int start = ((int)y * gifWidth + (int)x) * stripLen / totalImgPix; // simple nearest-neighbor scaling + int end = (((int)y * gifWidth + (int)x+1) * stripLen + totalImgPix-1) / totalImgPix; + for (int i = start; i < end; i++) { + activeSeg->setPixelColor(i, red, green, blue); + } + } else { + // simple nearest-neighbor scaling + int outY = (int)y * activeSeg->height() / gifHeight; + int outX = (int)x * activeSeg->width() / gifWidth; + // set multiple pixels if upscaling + for (int i = 0; i < (activeSeg->width()+(gifWidth-1)) / gifWidth; i++) { + for (int j = 0; j < (activeSeg->height()+(gifHeight-1)) / gifHeight; j++) { + activeSeg->setPixelColorXY(outX + i, outY + j, red, green, blue); + } } } } From 0e043b2a1b8eab11ce25ac0fd7ad3a5de8f499b4 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Thu, 6 Nov 2025 15:23:43 +0100 Subject: [PATCH 02/17] changed to vWidth/vHeight - since we draw on a segment, we need to use virtual segment dimensions or scaling will be off when using any virtualisation like grouping/spacing/mirror etc. --- wled00/image_loader.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/wled00/image_loader.cpp b/wled00/image_loader.cpp index 599c528e..a72babd4 100644 --- a/wled00/image_loader.cpp +++ b/wled00/image_loader.cpp @@ -55,7 +55,7 @@ void drawPixelCallback(int16_t x, int16_t y, uint8_t red, uint8_t green, uint8_t if (activeSeg->height() == 1) { // 1D strip: load pixel-by-pixel left to right, top to bottom (0/0 = top-left in gifs), scale if needed int totalImgPix = (int)gifWidth * gifHeight; - int stripLen = activeSeg->width(); + int stripLen = activeSeg->vWidth(); if (totalImgPix - stripLen == 1) totalImgPix--; // handle off-by-one: skip last pixel instead of first int start = ((int)y * gifWidth + (int)x) * stripLen / totalImgPix; // simple nearest-neighbor scaling int end = (((int)y * gifWidth + (int)x+1) * stripLen + totalImgPix-1) / totalImgPix; @@ -64,11 +64,11 @@ void drawPixelCallback(int16_t x, int16_t y, uint8_t red, uint8_t green, uint8_t } } else { // simple nearest-neighbor scaling - int outY = (int)y * activeSeg->height() / gifHeight; - int outX = (int)x * activeSeg->width() / gifWidth; + int outY = (int)y * activeSeg->vHeight() / gifHeight; + int outX = (int)x * activeSeg->vWidth() / gifWidth; // set multiple pixels if upscaling - for (int i = 0; i < (activeSeg->width()+(gifWidth-1)) / gifWidth; i++) { - for (int j = 0; j < (activeSeg->height()+(gifHeight-1)) / gifHeight; j++) { + for (int i = 0; i < (activeSeg->vWidth()+(gifWidth-1)) / gifWidth; i++) { + for (int j = 0; j < (activeSeg->vHeight()+(gifHeight-1)) / gifHeight; j++) { activeSeg->setPixelColorXY(outX + i, outY + j, red, green, blue); } } From 69dfe6c8a188547f517355bf836f01e4548dafdc Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sat, 8 Nov 2025 12:01:40 +0100 Subject: [PATCH 03/17] speed optimizations: skip setting multiple times, "fastpath" if no scaling needed --- wled00/image_loader.cpp | 71 ++++++++++++++++++++++++++++------------- 1 file changed, 48 insertions(+), 23 deletions(-) diff --git a/wled00/image_loader.cpp b/wled00/image_loader.cpp index a72babd4..04716ff0 100644 --- a/wled00/image_loader.cpp +++ b/wled00/image_loader.cpp @@ -44,6 +44,8 @@ bool openGif(const char *filename) { Segment* activeSeg; uint16_t gifWidth, gifHeight; +int lastCoordinate; // last coordinate (x+y) that was set, used to reduce redundant pixel writes +uint16_t perPixelX, perPixelY; // scaling factors when upscaling void screenClearCallback(void) { activeSeg->fill(0); @@ -51,26 +53,34 @@ void screenClearCallback(void) { void updateScreenCallback(void) {} -void drawPixelCallback(int16_t x, int16_t y, uint8_t red, uint8_t green, uint8_t blue) { - if (activeSeg->height() == 1) { - // 1D strip: load pixel-by-pixel left to right, top to bottom (0/0 = top-left in gifs), scale if needed - int totalImgPix = (int)gifWidth * gifHeight; - int stripLen = activeSeg->vWidth(); - if (totalImgPix - stripLen == 1) totalImgPix--; // handle off-by-one: skip last pixel instead of first - int start = ((int)y * gifWidth + (int)x) * stripLen / totalImgPix; // simple nearest-neighbor scaling - int end = (((int)y * gifWidth + (int)x+1) * stripLen + totalImgPix-1) / totalImgPix; - for (int i = start; i < end; i++) { - activeSeg->setPixelColor(i, red, green, blue); - } - } else { - // simple nearest-neighbor scaling - int outY = (int)y * activeSeg->vHeight() / gifHeight; - int outX = (int)x * activeSeg->vWidth() / gifWidth; - // set multiple pixels if upscaling - for (int i = 0; i < (activeSeg->vWidth()+(gifWidth-1)) / gifWidth; i++) { - for (int j = 0; j < (activeSeg->vHeight()+(gifHeight-1)) / gifHeight; j++) { - activeSeg->setPixelColorXY(outX + i, outY + j, red, green, blue); - } +// note: GifDecoder drawing is done top right to bottom left, line by line + +// callback to draw a pixel at (x,y) without scaling: used if GIF size matches segment size (faster) +void drawPixelCallbackNoScale(int16_t x, int16_t y, uint8_t red, uint8_t green, uint8_t blue) { + activeSeg->setPixelColor(y * activeSeg->width() + x, red, green, blue); +} + +void drawPixelCallback1D(int16_t x, int16_t y, uint8_t red, uint8_t green, uint8_t blue) { + // 1D strip: load pixel-by-pixel left to right, top to bottom (0/0 = top-left in gifs) + int totalImgPix = (int)gifWidth * gifHeight; + int start = ((int)y * gifWidth + (int)x) * activeSeg->vWidth() / totalImgPix; // simple nearest-neighbor scaling + if (start == lastCoordinate) return; // skip setting same coordinate again + lastCoordinate = start; + for (int i = 0; i < perPixelX; i++) { + activeSeg->setPixelColor(start + i, red, green, blue); + } +} + +void drawPixelCallback2D(int16_t x, int16_t y, uint8_t red, uint8_t green, uint8_t blue) { + // simple nearest-neighbor scaling + int outY = (int)y * activeSeg->vHeight() / gifHeight; + int outX = (int)x * activeSeg->vWidth() / gifWidth; + if (outX + outY == lastCoordinate) return; // skip setting same coordinate again + lastCoordinate = outX + outY; // since input is a "scanline" this is sufficient to identify a "unique" coordinate + // set multiple pixels if upscaling + for (int i = 0; i < perPixelX; i++) { + for (int j = 0; j < perPixelY; j++) { + activeSeg->setPixelColorXY(outX + i, outY + j, red, green, blue); } } } @@ -104,9 +114,10 @@ byte renderImageToSegment(Segment &seg) { if (file) file.close(); openGif(lastFilename); if (!file) { gifDecodeFailed = true; return IMAGE_ERROR_FILE_MISSING; } + lastCoordinate = -1; decoder.setScreenClearCallback(screenClearCallback); decoder.setUpdateScreenCallback(updateScreenCallback); - decoder.setDrawPixelCallback(drawPixelCallback); + decoder.setDrawPixelCallback(drawPixelCallbackNoScale); decoder.setFileSeekCallback(fileSeekCallback); decoder.setFilePositionCallback(filePositionCallback); decoder.setFileReadCallback(fileReadCallback); @@ -116,6 +127,22 @@ byte renderImageToSegment(Segment &seg) { DEBUG_PRINTLN(F("Starting decoding")); if(decoder.startDecoding() < 0) { gifDecodeFailed = true; return IMAGE_ERROR_GIF_DECODE; } DEBUG_PRINTLN(F("Decoding started")); + // after startDecoding, we can get GIF size, update static variables and callbacks if needed + decoder.getSize(&gifWidth, &gifHeight); + if (activeSeg->height() == 1) { + int totalImgPix = (int)gifWidth * gifHeight; + if (totalImgPix - activeSeg->vWidth() == 1) totalImgPix--; // handle off-by-one: skip last pixel instead of first (gifs constructed from 1D input padds last pixel if length is odd) + perPixelX = (activeSeg->vWidth() + totalImgPix-1) / totalImgPix; + if (totalImgPix != activeSeg->vWidth()) { + decoder.setDrawPixelCallback(drawPixelCallback1D); // use 1D callback with scaling + } + } else { + perPixelX = (activeSeg->vWidth() + gifWidth -1) / gifWidth; + perPixelY = (activeSeg->vHeight() + gifHeight-1) / gifHeight; + if (activeSeg->vWidth() != gifWidth || activeSeg->vHeight() != gifHeight) { + decoder.setDrawPixelCallback(drawPixelCallback2D); // use 2D callback with scaling + } + } } if (gifDecodeFailed) return IMAGE_ERROR_PREV; @@ -129,8 +156,6 @@ byte renderImageToSegment(Segment &seg) { // TODO consider handling this on FX level with a different frametime, but that would cause slow gifs to speed up during transitions if (millis() - lastFrameDisplayTime < wait) return IMAGE_ERROR_WAITING; - decoder.getSize(&gifWidth, &gifHeight); - int result = decoder.decodeFrame(false); if (result < 0) { gifDecodeFailed = true; return IMAGE_ERROR_FRAME_DECODE; } From 0eef321f8841dd6aad7ff4e124de4393b4919ae8 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sat, 8 Nov 2025 12:54:25 +0100 Subject: [PATCH 04/17] uising is2D() to check if segment is 2D, use vLength() on 1D setups --- wled00/image_loader.cpp | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/wled00/image_loader.cpp b/wled00/image_loader.cpp index 04716ff0..3fded97f 100644 --- a/wled00/image_loader.cpp +++ b/wled00/image_loader.cpp @@ -63,7 +63,7 @@ void drawPixelCallbackNoScale(int16_t x, int16_t y, uint8_t red, uint8_t green, void drawPixelCallback1D(int16_t x, int16_t y, uint8_t red, uint8_t green, uint8_t blue) { // 1D strip: load pixel-by-pixel left to right, top to bottom (0/0 = top-left in gifs) int totalImgPix = (int)gifWidth * gifHeight; - int start = ((int)y * gifWidth + (int)x) * activeSeg->vWidth() / totalImgPix; // simple nearest-neighbor scaling + int start = ((int)y * gifWidth + (int)x) * activeSeg->vLength() / totalImgPix; // simple nearest-neighbor scaling if (start == lastCoordinate) return; // skip setting same coordinate again lastCoordinate = start; for (int i = 0; i < perPixelX; i++) { @@ -127,21 +127,21 @@ byte renderImageToSegment(Segment &seg) { DEBUG_PRINTLN(F("Starting decoding")); if(decoder.startDecoding() < 0) { gifDecodeFailed = true; return IMAGE_ERROR_GIF_DECODE; } DEBUG_PRINTLN(F("Decoding started")); - // after startDecoding, we can get GIF size, update static variables and callbacks if needed + // after startDecoding, we can get GIF size, update static variables and callbacks decoder.getSize(&gifWidth, &gifHeight); - if (activeSeg->height() == 1) { - int totalImgPix = (int)gifWidth * gifHeight; - if (totalImgPix - activeSeg->vWidth() == 1) totalImgPix--; // handle off-by-one: skip last pixel instead of first (gifs constructed from 1D input padds last pixel if length is odd) - perPixelX = (activeSeg->vWidth() + totalImgPix-1) / totalImgPix; - if (totalImgPix != activeSeg->vWidth()) { - decoder.setDrawPixelCallback(drawPixelCallback1D); // use 1D callback with scaling - } - } else { + if (activeSeg->is2D()) { perPixelX = (activeSeg->vWidth() + gifWidth -1) / gifWidth; perPixelY = (activeSeg->vHeight() + gifHeight-1) / gifHeight; if (activeSeg->vWidth() != gifWidth || activeSeg->vHeight() != gifHeight) { decoder.setDrawPixelCallback(drawPixelCallback2D); // use 2D callback with scaling } + } else { + int totalImgPix = (int)gifWidth * gifHeight; + if (totalImgPix - activeSeg->vLength() == 1) totalImgPix--; // handle off-by-one: skip last pixel instead of first (gifs constructed from 1D input padds last pixel if length is odd) + perPixelX = (activeSeg->vLength() + totalImgPix-1) / totalImgPix; + if (totalImgPix != activeSeg->vLength()) { + decoder.setDrawPixelCallback(drawPixelCallback1D); // use 1D callback with scaling + } } } From 790be35ab8e56d49b44fae50cc3219dc8dd21ff9 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sat, 8 Nov 2025 16:04:08 +0100 Subject: [PATCH 05/17] make all globals static --- wled00/image_loader.cpp | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/wled00/image_loader.cpp b/wled00/image_loader.cpp index 3fded97f..5a41f740 100644 --- a/wled00/image_loader.cpp +++ b/wled00/image_loader.cpp @@ -9,11 +9,11 @@ * Functions to render images from filesystem to segments, used by the "Image" effect */ -File file; -char lastFilename[34] = "/"; -GifDecoder<320,320,12,true> decoder; -bool gifDecodeFailed = false; -unsigned long lastFrameDisplayTime = 0, currentFrameDelay = 0; +static File file; +static char lastFilename[34] = "/"; +static GifDecoder<320,320,12,true> decoder; +static bool gifDecodeFailed = false; +static unsigned long lastFrameDisplayTime = 0, currentFrameDelay = 0; bool fileSeekCallback(unsigned long position) { return file.seek(position); @@ -42,10 +42,10 @@ bool openGif(const char *filename) { return true; } -Segment* activeSeg; -uint16_t gifWidth, gifHeight; -int lastCoordinate; // last coordinate (x+y) that was set, used to reduce redundant pixel writes -uint16_t perPixelX, perPixelY; // scaling factors when upscaling +static Segment* activeSeg; +static uint16_t gifWidth, gifHeight; +static int lastCoordinate; // last coordinate (x+y) that was set, used to reduce redundant pixel writes +static uint16_t perPixelX, perPixelY; // scaling factors when upscaling void screenClearCallback(void) { activeSeg->fill(0); From 465993954701a2864f55d29b8dcc2ceca3da34c3 Mon Sep 17 00:00:00 2001 From: Frank <91616163+softhack007@users.noreply.github.com> Date: Sun, 9 Nov 2025 17:29:56 +0100 Subject: [PATCH 06/17] error handling and robustness improvements * catch some error that would lead to undefined behavior * additional debug messages in case of errors * robustness: handle OOM exception from decoder.alloc() gracefully --- wled00/image_loader.cpp | 46 ++++++++++++++++++++++++++++++++++------- 1 file changed, 39 insertions(+), 7 deletions(-) diff --git a/wled00/image_loader.cpp b/wled00/image_loader.cpp index 5a41f740..5e5a0f7c 100644 --- a/wled00/image_loader.cpp +++ b/wled00/image_loader.cpp @@ -11,7 +11,11 @@ static File file; static char lastFilename[34] = "/"; -static GifDecoder<320,320,12,true> decoder; +#if !defined(BOARD_HAS_PSRAM) + static GifDecoder<256,256,11,true> decoder; // use less RAM on boards without PSRAM - avoids crashes due to out-of-memory +#else + static GifDecoder<320,320,12,true> decoder; +#endif static bool gifDecodeFailed = false; static unsigned long lastFrameDisplayTime = 0, currentFrameDelay = 0; @@ -35,8 +39,9 @@ int fileSizeCallback(void) { return file.size(); } -bool openGif(const char *filename) { +bool openGif(const char *filename) { // side-effect: updates "file" file = WLED_FS.open(filename, "r"); + DEBUG_PRINTF_P(PSTR("opening GIF file %s\n"), filename); if (!file) return false; return true; @@ -107,13 +112,18 @@ byte renderImageToSegment(Segment &seg) { if (strncmp(lastFilename +1, seg.name, 32) != 0) { // segment name changed, load new image strncpy(lastFilename +1, seg.name, 32); gifDecodeFailed = false; - if (strcmp(lastFilename + strlen(lastFilename) - 4, ".gif") != 0) { + size_t fnameLen = strlen(lastFilename); + if ((fnameLen < 4) || strcmp(lastFilename + fnameLen - 4, ".gif") != 0) { // empty segment name, name too short, or name not ending in .gif gifDecodeFailed = true; + DEBUG_PRINTF_P(PSTR("GIF decoder unsupported file: %s\n"), lastFilename); return IMAGE_ERROR_UNSUPPORTED_FORMAT; } if (file) file.close(); - openGif(lastFilename); - if (!file) { gifDecodeFailed = true; return IMAGE_ERROR_FILE_MISSING; } + if (!openGif(lastFilename)) { + gifDecodeFailed = true; + DEBUG_PRINTF_P(PSTR("GIF file not found: %s\n"), lastFilename); + return IMAGE_ERROR_FILE_MISSING; + } lastCoordinate = -1; decoder.setScreenClearCallback(screenClearCallback); decoder.setUpdateScreenCallback(updateScreenCallback); @@ -123,12 +133,34 @@ byte renderImageToSegment(Segment &seg) { decoder.setFileReadCallback(fileReadCallback); decoder.setFileReadBlockCallback(fileReadBlockCallback); decoder.setFileSizeCallback(fileSizeCallback); - decoder.alloc(); +#if __cpp_exceptions // use exception handler if we can (some targets don't support exceptions) + try { +#endif + decoder.alloc(); // this function may throw out-of memory and cause a crash +#if __cpp_exceptions + } catch (...) { // if we arrive here, the decoder has thrown an OOM exception + gifDecodeFailed = true; + errorFlag = ERR_NORAM_PX; + DEBUG_PRINTLN(F("\nGIF decoder out of memory. Please try a smaller image file.\n")); + return IMAGE_ERROR_DECODER_ALLOC; + } +#endif DEBUG_PRINTLN(F("Starting decoding")); - if(decoder.startDecoding() < 0) { gifDecodeFailed = true; return IMAGE_ERROR_GIF_DECODE; } + int decoderError = decoder.startDecoding(); + if(decoderError < 0) { + DEBUG_PRINTF_P(PSTR("GIF Decoding error %d\n"), decoderError); + errorFlag = ERR_NORAM_PX; + gifDecodeFailed = true; + return IMAGE_ERROR_GIF_DECODE; + } DEBUG_PRINTLN(F("Decoding started")); // after startDecoding, we can get GIF size, update static variables and callbacks decoder.getSize(&gifWidth, &gifHeight); + if (gifWidth == 0 || gifHeight == 0) { // bad gif size: prevent division by zero + gifDecodeFailed = true; + DEBUG_PRINTF_P(PSTR("Invalid GIF dimensions: %dx%d\n"), gifWidth, gifHeight); + return IMAGE_ERROR_GIF_DECODE; + } if (activeSeg->is2D()) { perPixelX = (activeSeg->vWidth() + gifWidth -1) / gifWidth; perPixelY = (activeSeg->vHeight() + gifHeight-1) / gifHeight; From 6581dd6ff9ac17decf4b0847c2ac788bef64f01d Mon Sep 17 00:00:00 2001 From: Frank <91616163+softhack007@users.noreply.github.com> Date: Sun, 9 Nov 2025 17:33:04 +0100 Subject: [PATCH 07/17] add blur option --- wled00/FX.cpp | 2 +- wled00/image_loader.cpp | 11 ++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index f0f4276f..a6ce48a8 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -4509,7 +4509,7 @@ uint16_t mode_image(void) { // Serial.println(status); // } } -static const char _data_FX_MODE_IMAGE[] PROGMEM = "Image@!,;;;12;sx=128"; +static const char _data_FX_MODE_IMAGE[] PROGMEM = "Image@!,Blur,;;;12;sx=128,ix=0"; /* Blends random colors across palette diff --git a/wled00/image_loader.cpp b/wled00/image_loader.cpp index 5e5a0f7c..2ab71a34 100644 --- a/wled00/image_loader.cpp +++ b/wled00/image_loader.cpp @@ -56,7 +56,16 @@ void screenClearCallback(void) { activeSeg->fill(0); } -void updateScreenCallback(void) {} +// this callback runs when the decoder has finished painting all pixels +void updateScreenCallback(void) { + // perfect time for adding blur + if (activeSeg->intensity > 1) { + uint8_t blurAmount = activeSeg->intensity >> 2; + if ((blurAmount < 24) && (activeSeg->is2D())) activeSeg->blurRows(activeSeg->intensity >> 1); // some blur - fast + else activeSeg->blur(blurAmount); // more blur - slower + } + lastCoordinate = -1; // invalidate last position +} // note: GifDecoder drawing is done top right to bottom left, line by line From 79a52a60ffaf6f38f67d18b98b2162edc5a32b03 Mon Sep 17 00:00:00 2001 From: Frank <91616163+softhack007@users.noreply.github.com> Date: Sun, 9 Nov 2025 18:14:50 +0100 Subject: [PATCH 08/17] small optimization: fast 2D drawing without scaling for 2D segments, setPixelColorXY() should be used because it is faster than setPixelColor(). --- wled00/image_loader.cpp | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/wled00/image_loader.cpp b/wled00/image_loader.cpp index 2ab71a34..71e7436f 100644 --- a/wled00/image_loader.cpp +++ b/wled00/image_loader.cpp @@ -69,10 +69,13 @@ void updateScreenCallback(void) { // note: GifDecoder drawing is done top right to bottom left, line by line -// callback to draw a pixel at (x,y) without scaling: used if GIF size matches segment size (faster) -void drawPixelCallbackNoScale(int16_t x, int16_t y, uint8_t red, uint8_t green, uint8_t blue) { +// callbacks to draw a pixel at (x,y) without scaling: used if GIF size matches segment size (faster) +void drawPixelCallbackNoScale1D(int16_t x, int16_t y, uint8_t red, uint8_t green, uint8_t blue) { activeSeg->setPixelColor(y * activeSeg->width() + x, red, green, blue); } +void drawPixelCallbackNoScale2D(int16_t x, int16_t y, uint8_t red, uint8_t green, uint8_t blue) { + activeSeg->setPixelColorXY(x, y, red, green, blue); +} void drawPixelCallback1D(int16_t x, int16_t y, uint8_t red, uint8_t green, uint8_t blue) { // 1D strip: load pixel-by-pixel left to right, top to bottom (0/0 = top-left in gifs) @@ -136,7 +139,7 @@ byte renderImageToSegment(Segment &seg) { lastCoordinate = -1; decoder.setScreenClearCallback(screenClearCallback); decoder.setUpdateScreenCallback(updateScreenCallback); - decoder.setDrawPixelCallback(drawPixelCallbackNoScale); + decoder.setDrawPixelCallback(drawPixelCallbackNoScale1D); // default: use "fast path" 1D callback without scaling decoder.setFileSeekCallback(fileSeekCallback); decoder.setFilePositionCallback(filePositionCallback); decoder.setFileReadCallback(fileReadCallback); @@ -174,14 +177,16 @@ byte renderImageToSegment(Segment &seg) { perPixelX = (activeSeg->vWidth() + gifWidth -1) / gifWidth; perPixelY = (activeSeg->vHeight() + gifHeight-1) / gifHeight; if (activeSeg->vWidth() != gifWidth || activeSeg->vHeight() != gifHeight) { - decoder.setDrawPixelCallback(drawPixelCallback2D); // use 2D callback with scaling + decoder.setDrawPixelCallback(drawPixelCallback2D); // use 2D callback with scaling + } else { + decoder.setDrawPixelCallback(drawPixelCallbackNoScale2D); // use "fast path" 2D callback without scaling } } else { int totalImgPix = (int)gifWidth * gifHeight; if (totalImgPix - activeSeg->vLength() == 1) totalImgPix--; // handle off-by-one: skip last pixel instead of first (gifs constructed from 1D input padds last pixel if length is odd) perPixelX = (activeSeg->vLength() + totalImgPix-1) / totalImgPix; if (totalImgPix != activeSeg->vLength()) { - decoder.setDrawPixelCallback(drawPixelCallback1D); // use 1D callback with scaling + decoder.setDrawPixelCallback(drawPixelCallback1D); // use 1D callback with scaling } } } From 1324d490985882a5ed81111da36fe15e0cadc284 Mon Sep 17 00:00:00 2001 From: Frank <91616163+softhack007@users.noreply.github.com> Date: Sun, 9 Nov 2025 18:28:12 +0100 Subject: [PATCH 09/17] revert smaller gif size limits for board without PSRAM see discussion in PR#5040 --- wled00/image_loader.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/wled00/image_loader.cpp b/wled00/image_loader.cpp index 71e7436f..9c168cdc 100644 --- a/wled00/image_loader.cpp +++ b/wled00/image_loader.cpp @@ -11,11 +11,11 @@ static File file; static char lastFilename[34] = "/"; -#if !defined(BOARD_HAS_PSRAM) - static GifDecoder<256,256,11,true> decoder; // use less RAM on boards without PSRAM - avoids crashes due to out-of-memory -#else +//#if !defined(BOARD_HAS_PSRAM) //removed, to avoid compilcations in external tools that assume WLED allows 320 pixels width +// static GifDecoder<256,256,11,true> decoder; // use less RAM on boards without PSRAM - avoids crashes due to out-of-memory +//#else static GifDecoder<320,320,12,true> decoder; -#endif +//#endif static bool gifDecodeFailed = false; static unsigned long lastFrameDisplayTime = 0, currentFrameDelay = 0; From 29d2f7fc1bf8333398041238f2812ea232b3c5a9 Mon Sep 17 00:00:00 2001 From: Frank <91616163+softhack007@users.noreply.github.com> Date: Sun, 9 Nov 2025 19:06:59 +0100 Subject: [PATCH 10/17] debug print for decodeFrame error codes --- wled00/image_loader.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/wled00/image_loader.cpp b/wled00/image_loader.cpp index 9c168cdc..7c782c59 100644 --- a/wled00/image_loader.cpp +++ b/wled00/image_loader.cpp @@ -160,7 +160,7 @@ byte renderImageToSegment(Segment &seg) { DEBUG_PRINTLN(F("Starting decoding")); int decoderError = decoder.startDecoding(); if(decoderError < 0) { - DEBUG_PRINTF_P(PSTR("GIF Decoding error %d\n"), decoderError); + DEBUG_PRINTF_P(PSTR("GIF Decoding error %d in startDecoding().\n"), decoderError); errorFlag = ERR_NORAM_PX; gifDecodeFailed = true; return IMAGE_ERROR_GIF_DECODE; @@ -203,7 +203,11 @@ byte renderImageToSegment(Segment &seg) { if (millis() - lastFrameDisplayTime < wait) return IMAGE_ERROR_WAITING; int result = decoder.decodeFrame(false); - if (result < 0) { gifDecodeFailed = true; return IMAGE_ERROR_FRAME_DECODE; } + if (result < 0) { + DEBUG_PRINTF_P(PSTR("GIF Decoding error %d in decodeFrame().\n"), result); + gifDecodeFailed = true; + return IMAGE_ERROR_FRAME_DECODE; + } currentFrameDelay = decoder.getFrameDelay_ms(); unsigned long tooSlowBy = (millis() - lastFrameDisplayTime) - wait; // if last frame was longer than intended, compensate From a96e88043d243d665021abca496b19303a3f1785 Mon Sep 17 00:00:00 2001 From: Frank <91616163+softhack007@users.noreply.github.com> Date: Sun, 9 Nov 2025 20:24:57 +0100 Subject: [PATCH 11/17] remove commented code for no-PSRAM boards *sigh* changing gifdecoder parameters seems to have _no_ effect on RAM needed --- wled00/image_loader.cpp | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/wled00/image_loader.cpp b/wled00/image_loader.cpp index 7c782c59..cc0ba5b8 100644 --- a/wled00/image_loader.cpp +++ b/wled00/image_loader.cpp @@ -11,11 +11,7 @@ static File file; static char lastFilename[34] = "/"; -//#if !defined(BOARD_HAS_PSRAM) //removed, to avoid compilcations in external tools that assume WLED allows 320 pixels width -// static GifDecoder<256,256,11,true> decoder; // use less RAM on boards without PSRAM - avoids crashes due to out-of-memory -//#else - static GifDecoder<320,320,12,true> decoder; -//#endif +static GifDecoder<320,320,12,true> decoder; // this creates the basic object; parameter lzwMaxBits is not used; decoder.alloc() always allocated "everything else" = 24Kb static bool gifDecodeFailed = false; static unsigned long lastFrameDisplayTime = 0, currentFrameDelay = 0; From 3b14c31e00fdc8e33374774efffc306190c2cf0c Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Tue, 11 Nov 2025 21:09:48 +0100 Subject: [PATCH 12/17] fix noScale callback, allow for more blur, removed some whitespaces --- wled00/image_loader.cpp | 33 +++++++++++++++------------------ 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/wled00/image_loader.cpp b/wled00/image_loader.cpp index cc0ba5b8..05f0209a 100644 --- a/wled00/image_loader.cpp +++ b/wled00/image_loader.cpp @@ -56,21 +56,18 @@ void screenClearCallback(void) { void updateScreenCallback(void) { // perfect time for adding blur if (activeSeg->intensity > 1) { - uint8_t blurAmount = activeSeg->intensity >> 2; - if ((blurAmount < 24) && (activeSeg->is2D())) activeSeg->blurRows(activeSeg->intensity >> 1); // some blur - fast - else activeSeg->blur(blurAmount); // more blur - slower + uint8_t blurAmount = activeSeg->intensity; + if ((blurAmount < 24) && (activeSeg->is2D())) activeSeg->blurRows(activeSeg->intensity); // some blur - fast + else activeSeg->blur(blurAmount); // more blur - slower } lastCoordinate = -1; // invalidate last position } // note: GifDecoder drawing is done top right to bottom left, line by line -// callbacks to draw a pixel at (x,y) without scaling: used if GIF size matches segment size (faster) -void drawPixelCallbackNoScale1D(int16_t x, int16_t y, uint8_t red, uint8_t green, uint8_t blue) { - activeSeg->setPixelColor(y * activeSeg->width() + x, red, green, blue); -} -void drawPixelCallbackNoScale2D(int16_t x, int16_t y, uint8_t red, uint8_t green, uint8_t blue) { - activeSeg->setPixelColorXY(x, y, red, green, blue); +// 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 +void drawPixelCallbackNoScale(int16_t x, int16_t y, uint8_t red, uint8_t green, uint8_t blue) { + activeSeg->setPixelColor(y * gifWidth + x, red, green, blue); } void drawPixelCallback1D(int16_t x, int16_t y, uint8_t red, uint8_t green, uint8_t blue) { @@ -128,21 +125,21 @@ byte renderImageToSegment(Segment &seg) { } if (file) file.close(); if (!openGif(lastFilename)) { - gifDecodeFailed = true; + gifDecodeFailed = true; DEBUG_PRINTF_P(PSTR("GIF file not found: %s\n"), lastFilename); - return IMAGE_ERROR_FILE_MISSING; + return IMAGE_ERROR_FILE_MISSING; } lastCoordinate = -1; decoder.setScreenClearCallback(screenClearCallback); decoder.setUpdateScreenCallback(updateScreenCallback); - decoder.setDrawPixelCallback(drawPixelCallbackNoScale1D); // default: use "fast path" 1D callback without scaling + decoder.setDrawPixelCallback(drawPixelCallbackNoScale); // default: use "fast path" callback without scaling decoder.setFileSeekCallback(fileSeekCallback); decoder.setFilePositionCallback(filePositionCallback); decoder.setFileReadCallback(fileReadCallback); decoder.setFileReadBlockCallback(fileReadBlockCallback); decoder.setFileSizeCallback(fileSizeCallback); #if __cpp_exceptions // use exception handler if we can (some targets don't support exceptions) - try { + try { #endif decoder.alloc(); // this function may throw out-of memory and cause a crash #if __cpp_exceptions @@ -174,15 +171,15 @@ byte renderImageToSegment(Segment &seg) { perPixelY = (activeSeg->vHeight() + gifHeight-1) / gifHeight; if (activeSeg->vWidth() != gifWidth || activeSeg->vHeight() != gifHeight) { decoder.setDrawPixelCallback(drawPixelCallback2D); // use 2D callback with scaling - } else { - decoder.setDrawPixelCallback(drawPixelCallbackNoScale2D); // use "fast path" 2D callback without scaling + //DEBUG_PRINTLN(F("scaling image")); } } else { int totalImgPix = (int)gifWidth * gifHeight; - if (totalImgPix - activeSeg->vLength() == 1) totalImgPix--; // handle off-by-one: skip last pixel instead of first (gifs constructed from 1D input padds last pixel if length is odd) + 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) perPixelX = (activeSeg->vLength() + totalImgPix-1) / totalImgPix; if (totalImgPix != activeSeg->vLength()) { decoder.setDrawPixelCallback(drawPixelCallback1D); // use 1D callback with scaling + //DEBUG_PRINTLN(F("scaling image")); } } } @@ -199,9 +196,9 @@ byte renderImageToSegment(Segment &seg) { if (millis() - lastFrameDisplayTime < wait) return IMAGE_ERROR_WAITING; int result = decoder.decodeFrame(false); - if (result < 0) { + if (result < 0) { DEBUG_PRINTF_P(PSTR("GIF Decoding error %d in decodeFrame().\n"), result); - gifDecodeFailed = true; + gifDecodeFailed = true; return IMAGE_ERROR_FRAME_DECODE; } From 79376bbc58e8e571eacd02bed8ac76aeec9a2200 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Thu, 13 Nov 2025 18:26:00 +0100 Subject: [PATCH 13/17] improved lastCoordinate calculation --- wled00/image_loader.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/wled00/image_loader.cpp b/wled00/image_loader.cpp index 05f0209a..34991210 100644 --- a/wled00/image_loader.cpp +++ b/wled00/image_loader.cpp @@ -85,8 +85,8 @@ void drawPixelCallback2D(int16_t x, int16_t y, uint8_t red, uint8_t green, uint8 // simple nearest-neighbor scaling int outY = (int)y * activeSeg->vHeight() / gifHeight; int outX = (int)x * activeSeg->vWidth() / gifWidth; - if (outX + outY == lastCoordinate) return; // skip setting same coordinate again - lastCoordinate = outX + outY; // since input is a "scanline" this is sufficient to identify a "unique" coordinate + if (((outY << 16) | outX) == lastCoordinate) return; // skip setting same coordinate again + lastCoordinate = (outY << 16) | outX; // since input is a "scanline" this is sufficient to identify a "unique" coordinate // set multiple pixels if upscaling for (int i = 0; i < perPixelX; i++) { for (int j = 0; j < perPixelY; j++) { From fc776eeb1646daf002f5b6d37f27d21c858e8d61 Mon Sep 17 00:00:00 2001 From: Frank <91616163+softhack007@users.noreply.github.com> Date: Fri, 14 Nov 2025 01:08:48 +0100 Subject: [PATCH 14/17] add comment to explain coordinate packing logic --- wled00/image_loader.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/wled00/image_loader.cpp b/wled00/image_loader.cpp index 34991210..b201996e 100644 --- a/wled00/image_loader.cpp +++ b/wled00/image_loader.cpp @@ -85,6 +85,7 @@ void drawPixelCallback2D(int16_t x, int16_t y, uint8_t red, uint8_t green, uint8 // simple nearest-neighbor scaling int outY = (int)y * activeSeg->vHeight() / gifHeight; int outX = (int)x * activeSeg->vWidth() / gifWidth; + // Pack coordinates uniquely: outY into upper 16 bits, outX into lower 16 bits if (((outY << 16) | outX) == lastCoordinate) return; // skip setting same coordinate again lastCoordinate = (outY << 16) | outX; // since input is a "scanline" this is sufficient to identify a "unique" coordinate // set multiple pixels if upscaling From 6ae4b1fc383e8db9a13cd769b5df4bb04a5682f2 Mon Sep 17 00:00:00 2001 From: Frank <91616163+softhack007@users.noreply.github.com> Date: Fri, 14 Nov 2025 01:26:52 +0100 Subject: [PATCH 15/17] comment to prevent future "false improvement" attempts --- wled00/image_loader.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/wled00/image_loader.cpp b/wled00/image_loader.cpp index b201996e..882b6bd6 100644 --- a/wled00/image_loader.cpp +++ b/wled00/image_loader.cpp @@ -149,6 +149,8 @@ byte renderImageToSegment(Segment &seg) { errorFlag = ERR_NORAM_PX; DEBUG_PRINTLN(F("\nGIF decoder out of memory. Please try a smaller image file.\n")); return IMAGE_ERROR_DECODER_ALLOC; + // decoder cleanup (hi @coderabbitai): No additonal cleanup necessary - decoder.alloc() ultimately uses "new AnimatedGIF". + // 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. } #endif DEBUG_PRINTLN(F("Starting decoding")); From f95dae1b1b25a76c6848356a87bf041edc1be9f7 Mon Sep 17 00:00:00 2001 From: Frank <91616163+softhack007@users.noreply.github.com> Date: Fri, 14 Nov 2025 01:40:46 +0100 Subject: [PATCH 16/17] ensure that lastFilename is always terminated properly --- wled00/image_loader.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/wled00/image_loader.cpp b/wled00/image_loader.cpp index 882b6bd6..dd9054f4 100644 --- a/wled00/image_loader.cpp +++ b/wled00/image_loader.cpp @@ -116,7 +116,9 @@ byte renderImageToSegment(Segment &seg) { activeSeg = &seg; if (strncmp(lastFilename +1, seg.name, 32) != 0) { // segment name changed, load new image + strcpy(lastFilename, "/"); // filename always starts with '/' strncpy(lastFilename +1, seg.name, 32); + lastFilename[33] ='\0'; // ensure proper string termination when segment name was truncated gifDecodeFailed = false; size_t fnameLen = strlen(lastFilename); if ((fnameLen < 4) || strcmp(lastFilename + fnameLen - 4, ".gif") != 0) { // empty segment name, name too short, or name not ending in .gif From cd2dc437a33a905f43ec7adaed9428a916cb176b Mon Sep 17 00:00:00 2001 From: Frank <91616163+softhack007@users.noreply.github.com> Date: Fri, 14 Nov 2025 11:40:26 +0100 Subject: [PATCH 17/17] replace magic number by constant 32 => WLED_MAX_SEGNAME_LEN --- wled00/image_loader.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/wled00/image_loader.cpp b/wled00/image_loader.cpp index dd9054f4..d03e4fa4 100644 --- a/wled00/image_loader.cpp +++ b/wled00/image_loader.cpp @@ -10,7 +10,7 @@ */ static File file; -static char lastFilename[34] = "/"; +static char lastFilename[WLED_MAX_SEGNAME_LEN+2] = "/"; // enough space for "/" + seg.name + '\0' static GifDecoder<320,320,12,true> decoder; // this creates the basic object; parameter lzwMaxBits is not used; decoder.alloc() always allocated "everything else" = 24Kb static bool gifDecodeFailed = false; static unsigned long lastFrameDisplayTime = 0, currentFrameDelay = 0; @@ -115,10 +115,10 @@ byte renderImageToSegment(Segment &seg) { if (activeSeg && activeSeg != &seg) return IMAGE_ERROR_SEG_LIMIT; // only one segment at a time activeSeg = &seg; - if (strncmp(lastFilename +1, seg.name, 32) != 0) { // segment name changed, load new image + if (strncmp(lastFilename +1, seg.name, WLED_MAX_SEGNAME_LEN) != 0) { // segment name changed, load new image strcpy(lastFilename, "/"); // filename always starts with '/' - strncpy(lastFilename +1, seg.name, 32); - lastFilename[33] ='\0'; // ensure proper string termination when segment name was truncated + strncpy(lastFilename +1, seg.name, WLED_MAX_SEGNAME_LEN); + lastFilename[WLED_MAX_SEGNAME_LEN+1] ='\0'; // ensure proper string termination when segment name was truncated gifDecodeFailed = false; size_t fnameLen = strlen(lastFilename); if ((fnameLen < 4) || strcmp(lastFilename + fnameLen - 4, ".gif") != 0) { // empty segment name, name too short, or name not ending in .gif @@ -222,7 +222,7 @@ void endImagePlayback(Segment *seg) { decoder.dealloc(); gifDecodeFailed = false; activeSeg = nullptr; - lastFilename[1] = '\0'; + strcpy(lastFilename, "/"); // reset filename DEBUG_PRINTLN(F("Image playback ended")); }