Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions platformio.ini
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,8 @@ lib_deps =
IRremoteESP8266 @ 2.8.2
;;makuna/NeoPixelBus @ 2.7.5 ;; WLEDMM will be added in board specific sections
https://github.com/Aircoookie/ESPAsyncWebServer.git#v2.4.0
bitbank2/AnimatedGIF@^1.4.7
https://github.com/Aircoookie/GifDecoder#bc3af18
#For use of the TTGO T-Display ESP32 Module with integrated TFT display uncomment the following line
#TFT_eSPI
#For compatible OLED display uncomment following
Expand Down Expand Up @@ -1108,6 +1110,7 @@ build_flags_S =
; -D USERMOD_ARTIFX ;; WLEDMM usermod - temporarily moved into "_M", due to problems in "_S" when compiling with -O2
-D WLEDMM_FASTPATH ;; WLEDMM experimental option. Reduces audio lag (latency), and allows for faster LED framerates. May break compatibility with previous versions.
; -D WLED_DEBUG_HEAP ;; WLEDMM enable heap debugging
-D WLED_ENABLE_GIF

lib_deps_S =
;; https://github.com/kosme/arduinoFFT#develop @ 1.9.2+sha.419d7b0 ;; used for USERMOD_AUDIOREACTIVE - using "known working" hash
Expand Down
22 changes: 21 additions & 1 deletion wled00/FX.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4650,6 +4650,24 @@ uint16_t mode_washing_machine(void) {
static const char _data_FX_MODE_WASHING_MACHINE[] PROGMEM = "Washing Machine@!,!;;!";


/*
Image effect
Draws a .gif image from filesystem on the matrix/strip
*/
uint16_t mode_image(void) {
#ifndef WLED_ENABLE_GIF
return mode_static();
#else
renderImageToSegment(SEGMENT);
return FRAMETIME;
#endif
// if (status != 0 && status != 254 && status != 255) {
// Serial.print("GIF renderer return: ");
// Serial.println(status);
// }
}
static const char _data_FX_MODE_IMAGE[] PROGMEM = "Image@!,;;;12;sx=128";

/*
Blends random colors across palette
Modified, originally by Mark Kriegsman https://gist.github.com/kriegsman/1f7ccbbfa492a73c015e
Expand Down Expand Up @@ -9071,7 +9089,9 @@ void WS2812FX::setupEffectData() {
addEffect(FX_MODE_TWO_DOTS, &mode_two_dots, _data_FX_MODE_TWO_DOTS);
addEffect(FX_MODE_FAIRYTWINKLE, &mode_fairytwinkle, _data_FX_MODE_FAIRYTWINKLE);
addEffect(FX_MODE_RUNNING_DUAL, &mode_running_dual, _data_FX_MODE_RUNNING_DUAL);

#ifdef WLED_ENABLE_GIF
addEffect(FX_MODE_IMAGE, &mode_image, _data_FX_MODE_IMAGE);
#endif
addEffect(FX_MODE_TRICOLOR_CHASE, &mode_tricolor_chase, _data_FX_MODE_TRICOLOR_CHASE);
addEffect(FX_MODE_TRICOLOR_WIPE, &mode_tricolor_wipe, _data_FX_MODE_TRICOLOR_WIPE);
addEffect(FX_MODE_TRICOLOR_FADE, &mode_tricolor_fade, _data_FX_MODE_TRICOLOR_FADE);
Expand Down
2 changes: 1 addition & 1 deletion wled00/FX.h
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ bool strip_uses_global_leds(void) __attribute__((pure)); // WLEDMM implemented
#define FX_MODE_TWO_DOTS 50
#define FX_MODE_FAIRYTWINKLE 51 //was Two Areas prior to 0.13.0-b6 (use "Two Dots" with full intensity)
#define FX_MODE_RUNNING_DUAL 52
// #define FX_MODE_HALLOWEEN 53 // removed in 0.14!
#define FX_MODE_IMAGE 53
#define FX_MODE_TRICOLOR_CHASE 54
#define FX_MODE_TRICOLOR_WIPE 55
#define FX_MODE_TRICOLOR_FADE 56
Expand Down
3 changes: 3 additions & 0 deletions wled00/FX_fcn.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,9 @@ void Segment::resetIfRequired() {
reset = false; // setOption(SEG_OPTION_RESET, false);
startFrame(); // WLEDMM update cached propoerties
if (isActive() && !freeze) { fill(BLACK); needsBlank = false; } // WLEDMM start clean
#ifdef WLED_ENABLE_GIF
endImagePlayback(this);
#endif
DEBUG_PRINTLN("Segment reset");
} else if (needsBlank) {
startFrame(); // WLEDMM update cached propoerties
Expand Down
13 changes: 13 additions & 0 deletions wled00/fcn_declare.h
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,19 @@ void onHueConnect(void* arg, AsyncClient* client);
void sendHuePoll();
void onHueData(void* arg, AsyncClient* client, void *data, size_t len);

#include "FX.h" // must be below colors.cpp declarations (potentially due to duplicate declarations of e.g. color_blend)

//image_loader.cpp
#ifdef WLED_ENABLE_GIF
bool fileSeekCallback(unsigned long position);
unsigned long filePositionCallback(void);
int fileReadCallback(void);
int fileReadBlockCallback(void * buffer, int numberOfBytes);
int fileSizeCallback(void);
byte renderImageToSegment(Segment &seg);
void endImagePlayback(Segment* seg);
#endif

//improv.cpp
enum ImprovRPCType {
Command_Wifi = 0x01,
Expand Down
144 changes: 144 additions & 0 deletions wled00/image_loader.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
#include "wled.h"

#ifdef WLED_ENABLE_GIF

#include "GifDecoder.h"


/*
* Functions to render images from filesystem to segments, used by the "Image" effect
*/

File file;
char lastFilename[34] = "/";
GifDecoder<320,320,12,true> decoder;
Comment on lines +13 to +14
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Consider validating lastFilename length before substring operations.
Storing the segment name at lastFilename + 1 and later relying on strlen(lastFilename) - 4 to verify whether it ends with ".gif" may risk out-of-bounds reads if seg.name is too short. A length check would harden this logic against malformed or truncated segment names.

 if (strncmp(lastFilename +1, seg.name, 32) != 0) {
+  if (strlen(seg.name) < 4) {
+    gifDecodeFailed = true;
+    return IMAGE_ERROR_UNSUPPORTED_FORMAT; 
+  }
   strncpy(lastFilename +1, seg.name, 32);
   ...
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
char lastFilename[34] = "/";
GifDecoder<320,320,12,true> decoder;
if (strncmp(lastFilename + 1, seg.name, 32) != 0) {
if (strlen(seg.name) < 4) {
gifDecodeFailed = true;
return IMAGE_ERROR_UNSUPPORTED_FORMAT;
}
strncpy(lastFilename + 1, seg.name, 32);
// ... rest of the code
}

bool gifDecodeFailed = false;
unsigned long lastFrameDisplayTime = 0, currentFrameDelay = 0;

bool fileSeekCallback(unsigned long position) {
return file.seek(position);
}

unsigned long filePositionCallback(void) {
return file.position();
}

int fileReadCallback(void) {
return file.read();
}

int fileReadBlockCallback(void * buffer, int numberOfBytes) {
return file.read((uint8_t*)buffer, numberOfBytes);
}

int fileSizeCallback(void) {
return file.size();
}

bool openGif(const char *filename) {
file = WLED_FS.open(filename, "r");

if (!file) return false;
return true;
}

Segment* activeSeg;
uint16_t gifWidth, gifHeight;

void screenClearCallback(void) {
activeSeg->fill(0);
}

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, gamma8(red), gamma8(green), gamma8(blue));
}
}
}

#define IMAGE_ERROR_NONE 0
#define IMAGE_ERROR_NO_NAME 1
#define IMAGE_ERROR_SEG_LIMIT 2
#define IMAGE_ERROR_UNSUPPORTED_FORMAT 3
#define IMAGE_ERROR_FILE_MISSING 4
#define IMAGE_ERROR_DECODER_ALLOC 5
#define IMAGE_ERROR_GIF_DECODE 6
#define IMAGE_ERROR_FRAME_DECODE 7
#define IMAGE_ERROR_WAITING 254
#define IMAGE_ERROR_PREV 255

// renders an image (.gif only; .bmp and .fseq to be added soon) from FS to a segment
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 (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);
gifDecodeFailed = false;
if (strcmp(lastFilename + strlen(lastFilename) - 4, ".gif") != 0) {
gifDecodeFailed = true;
return IMAGE_ERROR_UNSUPPORTED_FORMAT;
}
if (file) file.close();
openGif(lastFilename);
if (!file) { gifDecodeFailed = true; return IMAGE_ERROR_FILE_MISSING; }
decoder.setScreenClearCallback(screenClearCallback);
decoder.setUpdateScreenCallback(updateScreenCallback);
decoder.setDrawPixelCallback(drawPixelCallback);
decoder.setFileSeekCallback(fileSeekCallback);
decoder.setFilePositionCallback(filePositionCallback);
decoder.setFileReadCallback(fileReadCallback);
decoder.setFileReadBlockCallback(fileReadBlockCallback);
decoder.setFileSizeCallback(fileSizeCallback);
decoder.alloc();
DEBUG_PRINTLN(F("Starting decoding"));
if(decoder.startDecoding() < 0) { gifDecodeFailed = true; return IMAGE_ERROR_GIF_DECODE; }
DEBUG_PRINTLN(F("Decoding started"));
}

if (gifDecodeFailed) return IMAGE_ERROR_PREV;
if (!file) { gifDecodeFailed = true; return IMAGE_ERROR_FILE_MISSING; }
//if (!decoder) { gifDecodeFailed = true; return IMAGE_ERROR_DECODER_ALLOC; }

// speed 0 = half speed, 128 = normal, 255 = full FX FPS
// TODO: 0 = 4x slow, 64 = 2x slow, 128 = normal, 192 = 2x fast, 255 = 4x fast
uint32_t wait = currentFrameDelay * 2 - seg.speed * currentFrameDelay / 128;

Comment on lines +115 to +116
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Prevent potential integer underflow in frame timing.
When seg.speed is very high, seg.speed * currentFrameDelay / 128 could exceed currentFrameDelay * 2. This may cause underflow upon subtracting, resulting in a large unsigned value for wait and stalling updates.

-  uint32_t wait = currentFrameDelay * 2 - seg.speed * currentFrameDelay / 128;
+  uint32_t speedDelay = seg.speed * currentFrameDelay / 128;
+  uint32_t baseDelay  = currentFrameDelay * 2;
+  uint32_t wait       = (speedDelay >= baseDelay) ? 0 : (baseDelay - speedDelay);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
uint32_t wait = currentFrameDelay * 2 - seg.speed * currentFrameDelay / 128;
uint32_t speedDelay = seg.speed * currentFrameDelay / 128;
uint32_t baseDelay = currentFrameDelay * 2;
uint32_t wait = (speedDelay >= baseDelay) ? 0 : (baseDelay - speedDelay);

// 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; }

currentFrameDelay = decoder.getFrameDelay_ms();
unsigned long tooSlowBy = (millis() - lastFrameDisplayTime) - wait; // if last frame was longer than intended, compensate
currentFrameDelay = tooSlowBy > currentFrameDelay ? 0 : currentFrameDelay - tooSlowBy;
lastFrameDisplayTime = millis();

return IMAGE_ERROR_NONE;
}

void endImagePlayback(Segment *seg) {
DEBUG_PRINTLN(F("Image playback end called"));
if (!activeSeg || activeSeg != seg) return;
if (file) file.close();
decoder.dealloc();
gifDecodeFailed = false;
activeSeg = nullptr;
lastFilename[1] = '\0';
DEBUG_PRINTLN(F("Image playback ended"));
}

#endif