diff --git a/wled00/FX.cpp b/wled00/FX.cpp index f0f4276f27..a6ce48a8f9 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 691ede1ac5..d03e4fa4b8 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[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; bool fileSeekCallback(unsigned long position) { return file.seek(position); @@ -35,29 +35,62 @@ 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; } -Segment* activeSeg; -uint16_t gifWidth, gifHeight; +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); } -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; + 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 (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 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; +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->vLength() / 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; + // 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 - for (int16_t i = 0; i < (activeSeg->width()+(gifWidth-1)) / gifWidth; i++) { - for (int16_t j = 0; j < (activeSeg->height()+(gifHeight-1)) / gifHeight; j++) { + for (int i = 0; i < perPixelX; i++) { + for (int j = 0; j < perPixelY; j++) { activeSeg->setPixelColorXY(outX + i, outY + j, red, green, blue); } } @@ -82,28 +115,78 @@ 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 - strncpy(lastFilename +1, seg.name, 32); + 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, WLED_MAX_SEGNAME_LEN); + lastFilename[WLED_MAX_SEGNAME_LEN+1] ='\0'; // ensure proper string termination when segment name was truncated 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); - decoder.setDrawPixelCallback(drawPixelCallback); + 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); - 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; + // 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")); - 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 in startDecoding().\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; + if (activeSeg->vWidth() != gifWidth || activeSeg->vHeight() != gifHeight) { + decoder.setDrawPixelCallback(drawPixelCallback2D); // use 2D callback with 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 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")); + } + } } if (gifDecodeFailed) return IMAGE_ERROR_PREV; @@ -117,10 +200,12 @@ 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; } + 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 @@ -137,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")); }