diff --git a/wled00/image_loader.cpp b/wled00/image_loader.cpp index 3a8fd99d..b6d13efa 100644 --- a/wled00/image_loader.cpp +++ b/wled00/image_loader.cpp @@ -5,13 +5,18 @@ #include "GifDecoder.h" +//upstream compatibility +#if !defined(WLED_MAX_SEGNAME_LEN) +#define WLED_MAX_SEGNAME_LEN 32 +#endif + /* * Functions to render images from filesystem to segments, used by the "Image" effect */ static File file; -static char lastFilename[34] = "/"; -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 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 allocates "everything else" = 24Kb static bool gifDecodeFailed = false; static unsigned long lastFrameDisplayTime = 0, currentFrameDelay = 0; @@ -37,7 +42,7 @@ int fileSizeCallback(void) { bool openGif(const char *filename) { // side-effect: updates "file" file = WLED_FS.open(filename, "r"); - DEBUG_PRINTF("opening GIF file %s\n", filename); + USER_PRINTF("opening GIF file %s\n", filename); if (!file) return false; return true; @@ -47,37 +52,56 @@ static Segment* activeSeg; static uint16_t gifWidth, gifHeight; // these two must stay uint16_t, because they are passed by reference static unsigned segCols = 1; static unsigned segRows = 1; -//static unsigned segLen = 1; // for future 1D support -static int expandX = 1; -static int expandY = 1; -static int lastX = -1, lastY = -1; +static unsigned segLen = 1; // for 1D and 1DExpand support +static int lastCoordinate = -1; // last coordinate (x+y) that was set, used to reduce redundant pixel writes +static unsigned perPixelX, perPixelY; // scaling factors when upscaling void screenClearCallback(void) { activeSeg->fill(0); } +// this callback runs when the decoder has finished painting all pixels void updateScreenCallback(void) { - // this callback runs when the decoder has finished painting all pixels // 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 >> 1; + if ((blurAmount < 48) && (activeSeg->is2D())) activeSeg->blurRows(activeSeg->intensity); // some blur - fast + else activeSeg->blur(blurAmount); // more blur - slower } - lastX = lastY = -1; // invalidate last position + lastCoordinate = -1; // invalidate last position } -void draw2DPixelCallback(int16_t x, int16_t y, uint8_t red, uint8_t green, uint8_t blue) { - // simple nearest-neighbor downscaling - int outY = y * segRows / gifHeight; - int outX = x * segCols / gifWidth; - if ((unsigned(outX) >= segCols) || (unsigned(outY) >= segRows)) return; // out of range - if ((lastX == outX) && (lastY == outY)) return; // downscaling optimization: skip re-painting same pixel - lastX = outX; lastY = outY; - // set multiple pixels if upscaling - // softhack007: changed loop x/y order -> minor speedup from better cache locality - for (int j = 0; j < expandY; j++) { - for (int i = 0; i < expandX; i++) { +// 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 drawPixelCallbackNoScale1D(int16_t x, int16_t y, uint8_t red, uint8_t green, uint8_t blue) { + activeSeg->setPixelColor(y * gifWidth + x, gamma8(red), gamma8(green), gamma8(blue)); +} +void drawPixelCallbackNoScale2D(int16_t x, int16_t y, uint8_t red, uint8_t green, uint8_t blue) { // WLEDMM setPixelColorXY is faster in our fork + activeSeg->setPixelColorXY(x, y, gamma8(red), gamma8(green), gamma8(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) * segLen / 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, gamma8(red), gamma8(green), gamma8(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 * segRows / gifHeight; + int outX = (int)x * segCols / 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 // softhack007: changed loop x/y order -> minor speedup from better cache locality + for (int j = 0; j < perPixelY; j++) { + for (int i = 0; i < perPixelX; i++) { activeSeg->setPixelColorXY(outX + i, outY + j, gamma8(red), gamma8(green), gamma8(blue)); } } @@ -98,16 +122,18 @@ void draw2DPixelCallback(int16_t x, int16_t y, uint8_t red, uint8_t green, uint8 byte renderImageToSegment(Segment &seg) { if (!seg.name) return IMAGE_ERROR_NO_NAME; // disable during effect transition, causes flickering, multiple allocations and depending on image, part of old FX remaining - // TODO: if (seg.mode != seg.currentMode()) return IMAGE_ERROR_WAITING; + //if (seg.mode != seg.currentMode()) return IMAGE_ERROR_WAITING; if (activeSeg && activeSeg != &seg) return IMAGE_ERROR_SEG_LIMIT; // only one segment at a time + activeSeg = &seg; segCols = activeSeg->virtualWidth(); segRows = activeSeg->virtualHeight(); - // segLen = activeSeg->virtualLength(); // for future 1D and expand1D support + segLen = activeSeg->calc_virtualLength(); - if (strncmp(lastFilename +1, seg.name, 32) != 0) { // segment name changed, load new image - strncpy(lastFilename +1, seg.name, 32); - lastFilename[33] = '\0'; // make sure that lastFilename is always null-terminated + 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; 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 @@ -116,45 +142,69 @@ byte renderImageToSegment(Segment &seg) { return IMAGE_ERROR_UNSUPPORTED_FORMAT; } if (file) file.close(); - if (!openGif(lastFilename)) { - gifDecodeFailed = true; + gifDecodeFailed = true; USER_PRINTF("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(draw2DPixelCallback); + decoder.setDrawPixelCallback(drawPixelCallbackNoScale1D); // 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(); // WLEDMM this function may throw out-of memory and cause a crash + 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; - USER_PRINTLN("\nGIF decoder out of memory. Please try a smaller image file.\n"); + USER_PRINTLN(F("\nGIF decoder out of memory. Please try a smaller image file.\n")); //USER_PRINTLN("I'm going to shoot myself now."); 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")); - int derr = 0; - if((derr = decoder.startDecoding()) < 0) { + int decoderError = decoder.startDecoding(); + if(decoderError < 0) { + USER_PRINTF("GIF Decoding error %d in startDecoding().\n", decoderError); + errorFlag = ERR_NORAM_PX; gifDecodeFailed = true; - USER_PRINTF("GIF Decoding error %d\n", derr); - if ((derr == ERROR_GIF_TOO_WIDE) || (derr == ERROR_GIF_UNSUPPORTED_FEATURE) || (derr == ERROR_GIF_INVALID_PARAMETER)) - errorFlag = ERR_NORAM_PX; - return IMAGE_ERROR_GIF_DECODE; + return IMAGE_ERROR_GIF_DECODE; } - if ((errorFlag == ERR_NORAM_PX) || (errorFlag == ERR_NORAM)) errorFlag = ERR_NONE; // success -> reset previous memory error codes 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; + USER_PRINTF("Invalid GIF dimensions: %dx%d\n", gifWidth, gifHeight); + return IMAGE_ERROR_GIF_DECODE; + } + if (activeSeg->is2D()) { + perPixelX = (segCols + gifWidth -1) / gifWidth; + perPixelY = (segRows + gifHeight-1) / gifHeight; + if (segCols != gifWidth || segRows != gifHeight) { + decoder.setDrawPixelCallback(drawPixelCallback2D); // use 2D callback with scaling + //DEBUG_PRINTLN(F("scaling image")); + } else { + decoder.setDrawPixelCallback(drawPixelCallbackNoScale2D); // use 2D callback without scaling + } + } else { + int totalImgPix = (int)gifWidth * gifHeight; + if (totalImgPix - segLen == 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 = (segLen + totalImgPix-1) / totalImgPix; + if (totalImgPix != segLen) { + decoder.setDrawPixelCallback(drawPixelCallback1D); // use 1D callback with scaling + //DEBUG_PRINTLN(F("scaling image")); + } + } } if (gifDecodeFailed) return IMAGE_ERROR_PREV; @@ -168,23 +218,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); - // bad gif size: prevent division by zero - if (gifWidth == 0 || gifHeight == 0) { - gifDecodeFailed = true; - USER_PRINTF("Invalid GIF dimensions: %dx%d\n", gifWidth, gifHeight); - return IMAGE_ERROR_GIF_DECODE; - } - // softhack007: pre-calculate upscaling for speedup - expandX = (segCols+(gifWidth-1)) / gifWidth; - expandY = (segRows+(gifHeight-1)) / gifHeight; - int result = decoder.decodeFrame(false); if (result < 0) { + USER_PRINTF("GIF Decoding error %d in decodeFrame().\n", result); gifDecodeFailed = true; - USER_PRINTF("GIF Frame decode failed %d\n", result); return IMAGE_ERROR_FRAME_DECODE; - } + } currentFrameDelay = decoder.getFrameDelay_ms(); unsigned long tooSlowBy = (millis() - lastFrameDisplayTime) - wait; // if last frame was longer than intended, compensate @@ -201,7 +240,7 @@ void endImagePlayback(Segment *seg) { decoder.dealloc(); gifDecodeFailed = false; activeSeg = nullptr; - lastFilename[1] = '\0'; + strcpy(lastFilename, "/"); // reset filename DEBUG_PRINTLN(F("Image playback ended")); }