diff --git a/ESP32-RGB64x32MatrixPanel-I2S-DMA.cpp b/ESP32-RGB64x32MatrixPanel-I2S-DMA.cpp index 7005c10..37b4a96 100644 --- a/ESP32-RGB64x32MatrixPanel-I2S-DMA.cpp +++ b/ESP32-RGB64x32MatrixPanel-I2S-DMA.cpp @@ -61,7 +61,7 @@ void RGB64x32MatrixPanel_I2S_DMA::configureDMA() Serial.printf("lsbMsbTransitionBit of %d gives %d Hz refresh: \r\n", lsbMsbTransitionBit, actualRefreshRate); - if (actualRefreshRate > 200) // HACK Hard Coded: Minimum frame rate of 160 + if (actualRefreshRate > 250) // HACK Hard Coded: Minimum frame rate of 160 break; @@ -316,6 +316,127 @@ void RGB64x32MatrixPanel_I2S_DMA::updateMatrixDMABuffer(int16_t x_coord, int16_t } // updateDMABuffer +void RGB64x32MatrixPanel_I2S_DMA::writeRGB24Frame2DMABuffer(rgb_24 *framedata, int16_t frame_width = MATRIX_WIDTH, int16_t frame_height = MATRIX_HEIGHT) +{ + if ( !dma_configuration_success) + assert("DMA configuration in begin() not performed or completed successfully."); + + + for (unsigned int y = 0; y < ROWS_PER_FRAME; y++) // half height - 16 iterations + { + unsigned char currentRow = y; + + for(int j = 0; j < COLOR_DEPTH_BITS; j++) // color depth - 8 iterations + { + uint16_t mask = (1 << (j)); // 24 bit color + + //MATRIX_DATA_STORAGE_TYPE *p=matrixUpdateFrames[backbuf_id].rowdata[y].rowbits[pl].data; //matrixUpdateFrames + rowBitStruct *p=&matrixUpdateFrames[backbuf_id].rowdata[currentRow].rowbits[j]; //matrixUpdateFrames location to write to + + int i=0; + while(i < PIXELS_PER_LATCH) // row pixels (64) iterations + { + for(int k=0; k < MATRIX_WIDTH; k++) // row pixel width 64 iterations + { + int v=0; // the output bitstream + + //#if (CLKS_DURING_LATCH == 0) + // if there is no latch to hold address, output ADDX lines directly to GPIO and latch data at end of cycle + int gpioRowAddress = currentRow; + + // normally output current rows ADDX, special case for LSB, output previous row's ADDX (as previous row is being displayed for one latch cycle) + if(j == 0) + gpioRowAddress = currentRow-1; + + if (gpioRowAddress & 0x01) v|=BIT_A; // 1 + if (gpioRowAddress & 0x02) v|=BIT_B; // 2 + if (gpioRowAddress & 0x04) v|=BIT_C; // 4 + if (gpioRowAddress & 0x08) v|=BIT_D; // 8 + if (gpioRowAddress & 0x10) v|=BIT_E; // 16 + + // need to disable OE after latch to hide row transition + if((i+k) == 0) v|=BIT_OE; + + // drive latch while shifting out last bit of RGB data + if((i+k) == PIXELS_PER_LATCH-1) v|=BIT_LAT; +//#endif + + // turn off OE after brightness value is reached when displaying MSBs + // MSBs always output normal brightness + // LSB (!j) outputs normal brightness as MSB from previous row is being displayed + if((j > lsbMsbTransitionBit || !j) && ((i+k) >= brightness)) v|=BIT_OE; + + // special case for the bits *after* LSB through (lsbMsbTransitionBit) - OE is output after data is shifted, so need to set OE to fractional brightness + if(j && j <= lsbMsbTransitionBit) { + // divide brightness in half for each bit below lsbMsbTransitionBit + int lsbBrightness = brightness >> (lsbMsbTransitionBit - j + 1); + if((i+k) >= lsbBrightness) v|=BIT_OE; + } + + // need to turn off OE one clock before latch, otherwise can get ghosting + if((i+k)==PIXELS_PER_LATCH-1) v|=BIT_OE; + + +//#if 0 +// +// int c1=getpixel(pix, k, y); +// int c2=getpixel(pix, k, y+(MATRIX_HEIGHT/2)); +// +// if (c1 & (mask<<16)) v|=BIT_R1; +// if (c1 & (mask<<8)) v|=BIT_G1; +// if (c1 & (mask<<0)) v|=BIT_B1; +// if (c2 & (mask<<16)) v|=BIT_R2; +// if ( c2 & (mask<<8)) v|=BIT_G2; +// if (c2 & (mask<<0)) v|=BIT_B2; +// +//#else + + struct rgb_24 c1( 255,0,0); + struct rgb_24 c2 = { 255,255,255 }; +// struct rgb24 c2 = { 0,0,255 }; + + + if (c1.red & mask) + v|=BIT_R1; + if (c1.green & mask) + v|=BIT_G1; + if (c1.blue & mask) + v|=BIT_B1; + if (c2.red & mask) + v|=BIT_R2; + if (c2.green & mask) + v|=BIT_G2; + if (c2.blue & mask) + v|=BIT_B2; + +//#endif + + + // 16 bit parallel mode + //Save the calculated value to the bitplane memory in reverse order to account for I2S Tx FIFO mode1 ordering + if(k%2){ + p->data[(i+k)-1] = v; + } else { + p->data[(i+k)+1] = v; + } // end reordering + + } // end for MATRIX_WIDTH + + i += MATRIX_WIDTH; + + } // end pixels per latch loop (64) + } // color depth loop (8) + + } // end half matrix length + + + //Show our work! + i2s_parallel_flip_to_buffer(&I2S1, backbuf_id); + swapBuffer(); + + +} // updateDMABuffer + diff --git a/ESP32-RGB64x32MatrixPanel-I2S-DMA.h b/ESP32-RGB64x32MatrixPanel-I2S-DMA.h index b73ad47..adce821 100644 --- a/ESP32-RGB64x32MatrixPanel-I2S-DMA.h +++ b/ESP32-RGB64x32MatrixPanel-I2S-DMA.h @@ -90,9 +90,9 @@ /***************************************************************************************/ /* HUB75 RGB Panel definitions and DMA Config. It's best you don't change any of this. */ -#define MATRIX_HEIGHT 32 -#define MATRIX_WIDTH 64 -#define MATRIX_ROWS_IN_PARALLEL 2 +#define MATRIX_HEIGHT 32 +#define MATRIX_WIDTH 64 +#define MATRIX_ROWS_IN_PARALLEL 2 // Panel Upper half RGB (numbering according to order in DMA gpio_bus configuration) #define BIT_R1 (1<<0) @@ -116,8 +116,8 @@ #define BIT_E (1<<12) // RGB Panel Constants / Calculated Values -#define PIXELS_PER_LATCH ((MATRIX_WIDTH * MATRIX_HEIGHT) / MATRIX_HEIGHT) // = 64 #define COLOR_CHANNELS_PER_PIXEL 3 +#define PIXELS_PER_LATCH ((MATRIX_WIDTH * MATRIX_HEIGHT) / MATRIX_HEIGHT) // = 64 #define COLOR_DEPTH_BITS (COLOR_DEPTH/COLOR_CHANNELS_PER_PIXEL) // = 8 #define ROWS_PER_FRAME (MATRIX_HEIGHT/MATRIX_ROWS_IN_PARALLEL) // = 2 @@ -150,6 +150,17 @@ struct frameStruct { rowColorDepthStruct rowdata[ROWS_PER_FRAME]; }; +typedef struct rgb_24 { + rgb_24() : rgb_24(0,0,0) {} + rgb_24(uint8_t r, uint8_t g, uint8_t b) { + red = r; green = g; blue = b; + } + rgb_24& operator=(const rgb_24& col); + + uint8_t red; + uint8_t green; + uint8_t blue; +} rgb_24; /***************************************************************************************/ class RGB64x32MatrixPanel_I2S_DMA : public Adafruit_GFX { @@ -158,18 +169,20 @@ class RGB64x32MatrixPanel_I2S_DMA : public Adafruit_GFX { RGB64x32MatrixPanel_I2S_DMA(bool _doubleBuffer = false) // doublebuffer always enabled, option makes no difference : Adafruit_GFX(MATRIX_WIDTH, MATRIX_HEIGHT), doubleBuffer(_doubleBuffer) { allocateDMAbuffers(); - backbuf_id = 0; - brightness = 64; + + backbuf_id = 0; + brightness = 16; + } void begin(void) { configureDMA(); //DMA and I2S configuration and setup - // Need to wipe the contents of the matrix buffers or weird things happen. - for (int y=0;y> 3); } diff --git a/examples/AnimatedGIF/AnimatedGIF.ino b/examples/AnimatedGIF/AnimatedGIF.ino new file mode 100644 index 0000000..856199c --- /dev/null +++ b/examples/AnimatedGIF/AnimatedGIF.ino @@ -0,0 +1,127 @@ +/* + * Written by: Craig A. Lindley + * + * Copyright (c) 2014 Craig A. Lindley + * Refactoring by Louis Beaudoin (Pixelmatix) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#include +#include +#include + +RGB64x32MatrixPanel_I2S_DMA matrix; + + +#include "GifDecoder.h" +#include "FilenameFunctions.h" + +#define GIF_DIRECTORY "/" +#define DISPLAY_TIME_SECONDS 5 + +const uint8_t kMatrixWidth = 64; // known working: 32, 64, 96, 128 +const uint8_t kMatrixHeight = 32; // known working: 16, 32, 48, 64 + +/* template parameters are maxGifWidth, maxGifHeight, lzwMaxBits + * + * The lzwMaxBits value of 12 supports all GIFs, but uses 16kB RAM + * lzwMaxBits can be set to 10 or 11 for small displays, 12 for large displays + * All 32x32-pixel GIFs tested work with 11, most work with 10 + */ +GifDecoder decoder; + +int num_files; + +void screenClearCallback(void) { + matrix.fillScreen(matrix.color565(0,0,0)); +} + +void updateScreenCallback(void) { + //backgroundLayer.swapBuffers(); +} + +void drawPixelCallback(int16_t x, int16_t y, uint8_t red, uint8_t green, uint8_t blue) { + matrix.drawPixel(x, y, matrix.color565(red, green, blue)); +} + +// Setup method runs once, when the sketch starts +void setup() { + + Serial.begin(115200); + Serial.println("Starting AnimatedGIFs Sketch"); + + // Start filesystem + Serial.println(" * Loading SPIFFS"); + if(!SPIFFS.begin()){ + Serial.println("SPIFFS Mount Failed"); + } + + decoder.setScreenClearCallback(screenClearCallback); + decoder.setUpdateScreenCallback(updateScreenCallback); + decoder.setDrawPixelCallback(drawPixelCallback); + + decoder.setFileSeekCallback(fileSeekCallback); + decoder.setFilePositionCallback(filePositionCallback); + decoder.setFileReadCallback(fileReadCallback); + decoder.setFileReadBlockCallback(fileReadBlockCallback); + + matrix.begin(); + + // Clear screen + matrix.fillScreen(matrix.color565(0,0,0)); + + // Determine how many animated GIF files exist + num_files = enumerateGIFFiles(GIF_DIRECTORY, false); + + if(num_files < 0) { + Serial.println("No gifs directory"); + while(1); + } + + if(!num_files) { + Serial.println("Empty gifs directory"); + while(1); + } +} + +int file_index = -1; +void loop() { + static unsigned long futureTime; + + // int index = -1; //random(num_files); + + if(futureTime < millis()) { + if (++file_index >= num_files) { + file_index = 0; + } + + if (openGifFilenameByIndex(GIF_DIRECTORY, file_index) >= 0) { + // Can clear screen for new animation here, but this might cause flicker with short animations + // matrix.fillScreen(COLOR_BLACK); + // matrix.swapBuffers(); + + decoder.startDecoding(); + + // Calculate time in the future to terminate animation + futureTime = millis() + (DISPLAY_TIME_SECONDS * 1000); + } + } + + decoder.decodeFrame(); +} diff --git a/examples/AnimatedGIF/FSBrowser.h b/examples/AnimatedGIF/FSBrowser.h new file mode 100644 index 0000000..c380226 --- /dev/null +++ b/examples/AnimatedGIF/FSBrowser.h @@ -0,0 +1,179 @@ +// ADAPTED FROM: https://github.com/espressif/arduino-esp32/blob/master/libraries/WebServer/examples/FSBrowser/FSBrowser.ino + +#define FILESYSTEM SPIFFS +#define FORMAT_FILESYSTEM true +#define DBG_OUTPUT_PORT Serial + + +//holds the current upload +File fsUploadFile; + +//format bytes +String formatBytes(size_t bytes) { + if (bytes < 1024) { + return String(bytes) + "B"; + } else if (bytes < (1024 * 1024)) { + return String(bytes / 1024.0) + "KB"; + } else if (bytes < (1024 * 1024 * 1024)) { + return String(bytes / 1024.0 / 1024.0) + "MB"; + } else { + return String(bytes / 1024.0 / 1024.0 / 1024.0) + "GB"; + } +} + +String getContentType(String filename) { + if (webServer.hasArg("download")) { + return "application/octet-stream"; + } else if (filename.endsWith(".htm")) { + return "text/html"; + } else if (filename.endsWith(".html")) { + return "text/html"; + } else if (filename.endsWith(".css")) { + return "text/css"; + } else if (filename.endsWith(".js")) { + return "application/javascript"; + } else if (filename.endsWith(".png")) { + return "image/png"; + } else if (filename.endsWith(".gif")) { + return "image/gif"; + } else if (filename.endsWith(".jpg")) { + return "image/jpeg"; + } else if (filename.endsWith(".ico")) { + return "image/x-icon"; + } else if (filename.endsWith(".xml")) { + return "text/xml"; + } else if (filename.endsWith(".pdf")) { + return "application/x-pdf"; + } else if (filename.endsWith(".zip")) { + return "application/x-zip"; + } else if (filename.endsWith(".gz")) { + return "application/x-gzip"; + } + return "text/plain"; +} + +bool exists(String path){ + bool yes = false; + File file = FILESYSTEM.open(path, "r"); + if(!file.isDirectory()){ + yes = true; + } + file.close(); + return yes; +} + +bool handleFileRead(String path) { + DBG_OUTPUT_PORT.println("handleFileRead: " + path); + if (path.endsWith("/")) { + path += "index.htm"; + } + String contentType = getContentType(path); + String pathWithGz = path + ".gz"; + if (exists(pathWithGz) || exists(path)) { + if (exists(pathWithGz)) { + path += ".gz"; + } + File file = FILESYSTEM.open(path, "r"); + webServer.streamFile(file, contentType); + file.close(); + return true; + } + return false; +} + +void handleFileUpload() { + if (webServer.uri() != "/edit") { + return; + } + HTTPUpload& upload = webServer.upload(); + if (upload.status == UPLOAD_FILE_START) { + String filename = upload.filename; + if (!filename.startsWith("/")) { + filename = "/" + filename; + } + DBG_OUTPUT_PORT.print("handleFileUpload Name: "); DBG_OUTPUT_PORT.println(filename); + fsUploadFile = FILESYSTEM.open(filename, "w"); + filename = String(); + } else if (upload.status == UPLOAD_FILE_WRITE) { + //DBG_OUTPUT_PORT.print("handleFileUpload Data: "); DBG_OUTPUT_PORT.println(upload.currentSize); + if (fsUploadFile) { + fsUploadFile.write(upload.buf, upload.currentSize); + } + } else if (upload.status == UPLOAD_FILE_END) { + if (fsUploadFile) { + fsUploadFile.close(); + } + DBG_OUTPUT_PORT.print("handleFileUpload Size: "); DBG_OUTPUT_PORT.println(upload.totalSize); + } +} + +void handleFileDelete() { + if (webServer.args() == 0) { + return webServer.send(500, "text/plain", "BAD ARGS"); + } + String path = webServer.arg(0); + DBG_OUTPUT_PORT.println("handleFileDelete: " + path); + if (path == "/") { + return webServer.send(500, "text/plain", "BAD PATH"); + } + if (!exists(path)) { + return webServer.send(404, "text/plain", "FileNotFound"); + } + FILESYSTEM.remove(path); + webServer.send(200, "text/plain", ""); + path = String(); +} + +void handleFileCreate() { + if (webServer.args() == 0) { + return webServer.send(500, "text/plain", "BAD ARGS"); + } + String path = webServer.arg(0); + DBG_OUTPUT_PORT.println("handleFileCreate: " + path); + if (path == "/") { + return webServer.send(500, "text/plain", "BAD PATH"); + } + if (exists(path)) { + return webServer.send(500, "text/plain", "FILE EXISTS"); + } + File file = FILESYSTEM.open(path, "w"); + if (file) { + file.close(); + } else { + return webServer.send(500, "text/plain", "CREATE FAILED"); + } + webServer.send(200, "text/plain", ""); + path = String(); +} + +void handleFileList() { + if (!webServer.hasArg("dir")) { + webServer.send(500, "text/plain", "BAD ARGS"); + return; + } + + String path = webServer.arg("dir"); + DBG_OUTPUT_PORT.println("handleFileList: " + path); + + + File root = FILESYSTEM.open(path); + path = String(); + + String output = "["; + if(root.isDirectory()){ + File file = root.openNextFile(); + while(file){ + if (output != "[") { + output += ','; + } + output += "{\"type\":\""; + output += (file.isDirectory()) ? "dir" : "file"; + output += "\",\"name\":\""; + output += String(file.name()).substring(1); + output += "\"}"; + file = root.openNextFile(); + } + } + output += "]"; + webServer.send(200, "text/json", output); +} diff --git a/examples/AnimatedGIF/FilenameFunctions.cpp b/examples/AnimatedGIF/FilenameFunctions.cpp new file mode 100644 index 0000000..e8a8f04 --- /dev/null +++ b/examples/AnimatedGIF/FilenameFunctions.cpp @@ -0,0 +1,169 @@ +/* + * Animated GIFs Display Code for SmartMatrix and 32x32 RGB LED Panels + * + * This file contains code to enumerate and select animated GIF files by name + * + * Written by: Craig A. Lindley + * + * Other references: https://github.com/espressif/arduino-esp32/blob/master/libraries/WebServer/examples/FSBrowser/FSBrowser.ino + * + */ + +#include "FilenameFunctions.h" + +File file; + +int numberOfFiles; + +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); +} + +bool isAnimationFile(const char filename []) { + String filenameString(filename); + +#if defined(ESP32) + // ESP32 filename includes the full path, so need to remove the path before looking at the filename + int pathindex = filenameString.lastIndexOf("/"); + if(pathindex >= 0) + filenameString.remove(0, pathindex + 1); +#endif + + DBG_OUTPUT_PORT.print(filenameString); + + if ((filenameString[0] == '_') || (filenameString[0] == '~') || (filenameString[0] == '.')) { + DBG_OUTPUT_PORT.println(" ignoring: leading _/~/. character"); + return false; + } + + filenameString.toUpperCase(); + if (filenameString.endsWith(".GIF") != 1) { + DBG_OUTPUT_PORT.println(" ignoring: doesn't end of .GIF"); + return false; + } + + DBG_OUTPUT_PORT.println(); + + return true; +} + +// Enumerate and possibly display the animated GIF filenames in GIFS directory +int enumerateGIFFiles(const char *directoryName, boolean displayFilenames) { + + numberOfFiles = 0; + + File directory = FILESYSTEM.open(directoryName); + if (!directory) { + return -1; + } + + File file = directory.openNextFile(); + while (file) { + if (isAnimationFile(file.name())) { + numberOfFiles++; + if (displayFilenames) { + DBG_OUTPUT_PORT.println(file.name()); + } + } + file.close(); + file = directory.openNextFile(); + } + + file.close(); + directory.close(); + + return numberOfFiles; +} + +// Get the full path/filename of the GIF file with specified index +void getGIFFilenameByIndex(const char *directoryName, int index, char *pnBuffer) { + + char* filename; + + // Make sure index is in range + if ((index < 0) || (index >= numberOfFiles)) + return; + + File directory = FILESYSTEM.open(directoryName); + if (!directory) + return; + + File file = directory.openNextFile(); + while (file && (index >= 0)) { + filename = (char*)file.name(); + + if (isAnimationFile(file.name())) { + index--; + +#if !defined(ESP32) + // Copy the directory name into the pathname buffer - ESP32 SD Library includes the full path name in the filename, so no need to add the directory name + strcpy(pnBuffer, directoryName); + // Append the filename to the pathname + strcat(pnBuffer, filename); +#else + strcpy(pnBuffer, filename); +#endif + } + + file.close(); + file = directory.openNextFile(); + } + + file.close(); + directory.close(); +} + +int openGifFilenameByIndex(const char *directoryName, int index) { + char pathname[30]; // long filename will break this... Smash the stack! i.e: + /* + * Stack smashing protect failure! + * + * abort() was called at PC 0x400d9a90 on core 1 + * + * Backtrace: 0x40088578:0x3ffb1ec0 0x4008877b:0x3ffb1ee0 0x400d9a90:0x3ffb1f00 0x400d1d62:0x3ffb1f20 0x400d182f:0x3ffb1f80 0x400ef86e:0x3ffb1fa0 + * + * Rebooting... + + */ + + getGIFFilenameByIndex(directoryName, index, pathname); + + DBG_OUTPUT_PORT.print("Pathname: "); + DBG_OUTPUT_PORT.println(pathname); + + if(file) + { + file.close(); + DBG_OUTPUT_PORT.print("Closing old file..."); + } + + // Attempt to open the file for reading + DBG_OUTPUT_PORT.print("Opening new file..."); + file = FILESYSTEM.open(pathname); + if (!file) { + DBG_OUTPUT_PORT.println("Error opening GIF file"); + return -1; + } + + return 0; +} + + +// Return a random animated gif path/filename from the specified directory +void chooseRandomGIFFilename(const char *directoryName, char *pnBuffer) { + + int index = random(numberOfFiles); + getGIFFilenameByIndex(directoryName, index, pnBuffer); +} diff --git a/examples/AnimatedGIF/FilenameFunctions.h b/examples/AnimatedGIF/FilenameFunctions.h new file mode 100644 index 0000000..2c92c10 --- /dev/null +++ b/examples/AnimatedGIF/FilenameFunctions.h @@ -0,0 +1,21 @@ +#ifndef FILENAME_FUNCTIONS_H +#define FILENAME_FUNCTIONS_H + +#include "FS.h" // for the 'File' class +#include + + +#define FILESYSTEM SPIFFS +#define DBG_OUTPUT_PORT Serial + + +int enumerateGIFFiles(const char *directoryName, boolean displayFilenames); +void getGIFFilenameByIndex(const char *directoryName, int index, char *pnBuffer); +int openGifFilenameByIndex(const char *directoryName, int index); + +bool fileSeekCallback(unsigned long position); +unsigned long filePositionCallback(void); +int fileReadCallback(void); +int fileReadBlockCallback(void * buffer, int numberOfBytes); + +#endif diff --git a/examples/AnimatedGIF/GifDecoder.h b/examples/AnimatedGIF/GifDecoder.h new file mode 100644 index 0000000..8a3d14a --- /dev/null +++ b/examples/AnimatedGIF/GifDecoder.h @@ -0,0 +1,148 @@ +#ifndef _GIFDECODER_H_ +#define _GIFDECODER_H_ + +#include +#include + + +typedef void (*callback)(void); +typedef void (*pixel_callback)(int16_t x, int16_t y, uint8_t red, uint8_t green, uint8_t blue); +typedef void* (*get_buffer_callback)(void); + +typedef bool (*file_seek_callback)(unsigned long position); +typedef unsigned long (*file_position_callback)(void); +typedef int (*file_read_callback)(void); +typedef int (*file_read_block_callback)(void * buffer, int numberOfBytes); + +// LZW constants +// NOTE: LZW_MAXBITS should be set to 10 or 11 for small displays, 12 for large displays +// all 32x32-pixel GIFs tested work with 11, most work with 10 +// LZW_MAXBITS = 12 will support all GIFs, but takes 16kB RAM +#define LZW_SIZTABLE (1 << lzwMaxBits) + +template +class GifDecoder { +public: + int startDecoding(void); + int decodeFrame(void); + + void setScreenClearCallback(callback f); + void setUpdateScreenCallback(callback f); + void setDrawPixelCallback(pixel_callback f); + void setStartDrawingCallback(callback f); + + void setFileSeekCallback(file_seek_callback f); + void setFilePositionCallback(file_position_callback f); + void setFileReadCallback(file_read_callback f); + void setFileReadBlockCallback(file_read_block_callback f); + +private: + void parseTableBasedImage(void); + void decompressAndDisplayFrame(unsigned long filePositionAfter); + int parseData(void); + int parseGIFFileTerminator(void); + void parseCommentExtension(void); + void parseApplicationExtension(void); + void parseGraphicControlExtension(void); + void parsePlainTextExtension(void); + void parseGlobalColorTable(void); + void parseLogicalScreenDescriptor(void); + bool parseGifHeader(void); + void copyImageDataRect(uint8_t *dst, uint8_t *src, int x, int y, int width, int height); + void fillImageData(uint8_t colorIndex); + void fillImageDataRect(uint8_t colorIndex, int x, int y, int width, int height); + int readIntoBuffer(void *buffer, int numberOfBytes); + int readWord(void); + void backUpStream(int n); + int readByte(void); + + void lzw_decode_init(int csize); + int lzw_decode(uint8_t *buf, int len, uint8_t *bufend); + void lzw_setTempBuffer(uint8_t * tempBuffer); + int lzw_get_code(void); + + // Logical screen descriptor attributes + int lsdWidth; + int lsdHeight; + int lsdPackedField; + int lsdAspectRatio; + int lsdBackgroundIndex; + + // Table based image attributes + int tbiImageX; + int tbiImageY; + int tbiWidth; + int tbiHeight; + int tbiPackedBits; + bool tbiInterlaced; + + int frameDelay; + int transparentColorIndex; + int prevBackgroundIndex; + int prevDisposalMethod; + int disposalMethod; + int lzwCodeSize; + bool keyFrame; + int rectX; + int rectY; + int rectWidth; + int rectHeight; + + unsigned long nextFrameTime_ms; + + int colorCount; + rgb_24 palette[256]; + + char tempBuffer[260]; + + // Buffer image data is decoded into + uint8_t imageData[maxGifWidth * maxGifHeight]; + + // Backup image data buffer for saving portions of image disposal method == 3 + uint8_t imageDataBU[maxGifWidth * maxGifHeight]; + + callback screenClearCallback; + callback updateScreenCallback; + pixel_callback drawPixelCallback; + callback startDrawingCallback; + file_seek_callback fileSeekCallback; + file_position_callback filePositionCallback; + file_read_callback fileReadCallback; + file_read_block_callback fileReadBlockCallback; + + // LZW variables + int bbits; + int bbuf; + int cursize; // The current code size + int curmask; + int codesize; + int clear_code; + int end_code; + int newcodes; // First available code + int top_slot; // Highest code for current size + int extra_slot; + int slot; // Last read code + int fc, oc; + int bs; // Current buffer size for GIF + int bcnt; + uint8_t *sp; + uint8_t * temp_buffer; + + uint8_t stack [LZW_SIZTABLE]; + uint8_t suffix [LZW_SIZTABLE]; + uint16_t prefix [LZW_SIZTABLE]; + + // Masks for 0 .. 16 bits + unsigned int mask[17] = { + 0x0000, 0x0001, 0x0003, 0x0007, + 0x000F, 0x001F, 0x003F, 0x007F, + 0x00FF, 0x01FF, 0x03FF, 0x07FF, + 0x0FFF, 0x1FFF, 0x3FFF, 0x7FFF, + 0xFFFF + }; +}; + +#include "GifDecoder_Impl.h" +#include "LzwDecoder_Impl.h" + +#endif diff --git a/examples/AnimatedGIF/GifDecoder_Impl.h b/examples/AnimatedGIF/GifDecoder_Impl.h new file mode 100644 index 0000000..c7c5a5d --- /dev/null +++ b/examples/AnimatedGIF/GifDecoder_Impl.h @@ -0,0 +1,806 @@ +/* + * This file contains code to parse animated GIF files + * + * Written by: Craig A. Lindley + * + * Copyright (c) 2014 Craig A. Lindley + * Minor modifications by Louis Beaudoin (pixelmatix) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#define GIFDEBUG 0 + +// This file contains C code, and ESP32 Arduino has changed to use the C++ template version of min()/max() which we can't use with C, so we can't depend on a #define min() from Arduino anymore +#ifndef min +#define min(a,b) ((a)<(b)?(a):(b)) +#endif + +#include "GifDecoder.h" + +#if GIFDEBUG == 1 +#define DEBUG_SCREEN_DESCRIPTOR 1 +#define DEBUG_GLOBAL_COLOR_TABLE 1 +#define DEBUG_PROCESSING_PLAIN_TEXT_EXT 1 +#define DEBUG_PROCESSING_GRAPHIC_CONTROL_EXT 1 +#define DEBUG_PROCESSING_APP_EXT 1 +#define DEBUG_PROCESSING_COMMENT_EXT 1 +#define DEBUG_PROCESSING_FILE_TERM 1 +#define DEBUG_PROCESSING_TABLE_IMAGE_DESC 1 +#define DEBUG_PROCESSING_TBI_DESC_START 1 +#define DEBUG_PROCESSING_TBI_DESC_INTERLACED 1 +#define DEBUG_PROCESSING_TBI_DESC_LOCAL_COLOR_TABLE 1 +#define DEBUG_PROCESSING_TBI_DESC_LZWCODESIZE 1 +#define DEBUG_PROCESSING_TBI_DESC_DATABLOCKSIZE 1 +#define DEBUG_PROCESSING_TBI_DESC_LZWIMAGEDATA_OVERFLOW 1 +#define DEBUG_PROCESSING_TBI_DESC_LZWIMAGEDATA_SIZE 1 +#define DEBUG_PARSING_DATA 1 +#define DEBUG_DECOMPRESS_AND_DISPLAY 1 + +#define DEBUG_WAIT_FOR_KEY_PRESS 0 + +#endif + +#include "GifDecoder.h" + + +// Error codes +#define ERROR_NONE 0 +#define ERROR_DONE_PARSING 1 +#define ERROR_WAITING 2 +#define ERROR_FILEOPEN -1 +#define ERROR_FILENOTGIF -2 +#define ERROR_BADGIFFORMAT -3 +#define ERROR_UNKNOWNCONTROLEXT -4 + +#define GIFHDRTAGNORM "GIF87a" // tag in valid GIF file +#define GIFHDRTAGNORM1 "GIF89a" // tag in valid GIF file +#define GIFHDRSIZE 6 + +// Global GIF specific definitions +#define COLORTBLFLAG 0x80 +#define INTERLACEFLAG 0x40 +#define TRANSPARENTFLAG 0x01 + +#define NO_TRANSPARENT_INDEX -1 + +// Disposal methods +#define DISPOSAL_NONE 0 +#define DISPOSAL_LEAVE 1 +#define DISPOSAL_BACKGROUND 2 +#define DISPOSAL_RESTORE 3 + + + +template +void GifDecoder::setStartDrawingCallback(callback f) { + startDrawingCallback = f; +} + +template +void GifDecoder::setUpdateScreenCallback(callback f) { + updateScreenCallback = f; +} + +template +void GifDecoder::setDrawPixelCallback(pixel_callback f) { + drawPixelCallback = f; +} + +template +void GifDecoder::setScreenClearCallback(callback f) { + screenClearCallback = f; +} + +template +void GifDecoder::setFileSeekCallback(file_seek_callback f) { + fileSeekCallback = f; +} + +template +void GifDecoder::setFilePositionCallback(file_position_callback f) { + filePositionCallback = f; +} + +template +void GifDecoder::setFileReadCallback(file_read_callback f) { + fileReadCallback = f; +} + +template +void GifDecoder::setFileReadBlockCallback(file_read_block_callback f) { + fileReadBlockCallback = f; +} + +// Backup the read stream by n bytes +template +void GifDecoder::backUpStream(int n) { + fileSeekCallback(filePositionCallback() - n); +} + +// Read a file byte +template +int GifDecoder::readByte() { + + int b = fileReadCallback(); + if (b == -1) { +#if GIFDEBUG == 1 + Serial.println("Read error or EOF occurred"); +#endif + } + return b; +} + +// Read a file word +template +int GifDecoder::readWord() { + + int b0 = readByte(); + int b1 = readByte(); + return (b1 << 8) | b0; +} + +// Read the specified number of bytes into the specified buffer +template +int GifDecoder::readIntoBuffer(void *buffer, int numberOfBytes) { + + int result = fileReadBlockCallback(buffer, numberOfBytes); + if (result == -1) { + Serial.println("Read error or EOF occurred"); + } + return result; +} + +// Fill a portion of imageData buffer with a color index +template +void GifDecoder::fillImageDataRect(uint8_t colorIndex, int x, int y, int width, int height) { + + int yOffset; + + for (int yy = y; yy < height + y; yy++) { + yOffset = yy * maxGifWidth; + for (int xx = x; xx < width + x; xx++) { + imageData[yOffset + xx] = colorIndex; + } + } +} + +// Fill entire imageData buffer with a color index +template +void GifDecoder::fillImageData(uint8_t colorIndex) { + + memset(imageData, colorIndex, sizeof(imageData)); +} + +// Copy image data in rect from a src to a dst +template +void GifDecoder::copyImageDataRect(uint8_t *dst, uint8_t *src, int x, int y, int width, int height) { + + int yOffset, offset; + + for (int yy = y; yy < height + y; yy++) { + yOffset = yy * maxGifWidth; + for (int xx = x; xx < width + x; xx++) { + offset = yOffset + xx; + dst[offset] = src[offset]; + } + } +} + +// Make sure the file is a Gif file +template +bool GifDecoder::parseGifHeader() { + + char buffer[10]; + + readIntoBuffer(buffer, GIFHDRSIZE); + if ((strncmp(buffer, GIFHDRTAGNORM, GIFHDRSIZE) != 0) && + (strncmp(buffer, GIFHDRTAGNORM1, GIFHDRSIZE) != 0)) { + return false; + } + else { + return true; + } +} + +// Parse the logical screen descriptor +template +void GifDecoder::parseLogicalScreenDescriptor() { + + lsdWidth = readWord(); + lsdHeight = readWord(); + lsdPackedField = readByte(); + lsdBackgroundIndex = readByte(); + lsdAspectRatio = readByte(); + +#if GIFDEBUG == 1 && DEBUG_SCREEN_DESCRIPTOR == 1 + Serial.print("lsdWidth: "); + Serial.println(lsdWidth); + Serial.print("lsdHeight: "); + Serial.println(lsdHeight); + Serial.print("lsdPackedField: "); + Serial.println(lsdPackedField, HEX); + Serial.print("lsdBackgroundIndex: "); + Serial.println(lsdBackgroundIndex); + Serial.print("lsdAspectRatio: "); + Serial.println(lsdAspectRatio); +#endif +} + +// Parse the global color table +template +void GifDecoder::parseGlobalColorTable() { + + // Does a global color table exist? + if (lsdPackedField & COLORTBLFLAG) { + + // A GCT was present determine how many colors it contains + colorCount = 1 << ((lsdPackedField & 7) + 1); + +#if GIFDEBUG == 1 && DEBUG_GLOBAL_COLOR_TABLE == 1 + Serial.print("Global color table with "); + Serial.print(colorCount); + Serial.println(" colors present"); +#endif + // Read color values into the palette array + int colorTableBytes = sizeof(rgb_24) * colorCount; + readIntoBuffer(palette, colorTableBytes); + } +} + +// Parse plain text extension and dispose of it +template +void GifDecoder::parsePlainTextExtension() { + +#if GIFDEBUG == 1 && DEBUG_PROCESSING_PLAIN_TEXT_EXT == 1 + Serial.println("\nProcessing Plain Text Extension"); +#endif + // Read plain text header length + uint8_t len = readByte(); + + // Consume plain text header data + readIntoBuffer(tempBuffer, len); + + // Consume the plain text data in blocks + len = readByte(); + while (len != 0) { + readIntoBuffer(tempBuffer, len); + len = readByte(); + } +} + +// Parse a graphic control extension +template +void GifDecoder::parseGraphicControlExtension() { + +#if GIFDEBUG == 1 && DEBUG_PROCESSING_GRAPHIC_CONTROL_EXT == 1 + Serial.println("\nProcessing Graphic Control Extension"); +#endif + int len = readByte(); // Check length + if (len != 4) { + Serial.println("Bad graphic control extension"); + } + + int packedBits = readByte(); + frameDelay = readWord(); + transparentColorIndex = readByte(); + + if ((packedBits & TRANSPARENTFLAG) == 0) { + // Indicate no transparent index + transparentColorIndex = NO_TRANSPARENT_INDEX; + } + disposalMethod = (packedBits >> 2) & 7; + if (disposalMethod > 3) { + disposalMethod = 0; + Serial.println("Invalid disposal value"); + } + + readByte(); // Toss block end + +#if GIFDEBUG == 1 && DEBUG_PROCESSING_GRAPHIC_CONTROL_EXT == 1 + Serial.print("PacketBits: "); + Serial.println(packedBits, HEX); + Serial.print("Frame delay: "); + Serial.println(frameDelay); + Serial.print("transparentColorIndex: "); + Serial.println(transparentColorIndex); + Serial.print("disposalMethod: "); + Serial.println(disposalMethod); +#endif +} + +// Parse application extension +template +void GifDecoder::parseApplicationExtension() { + + memset(tempBuffer, 0, sizeof(tempBuffer)); + +#if GIFDEBUG == 1 && DEBUG_PROCESSING_APP_EXT == 1 + Serial.println("\nProcessing Application Extension"); +#endif + + // Read block length + uint8_t len = readByte(); + + // Read app data + readIntoBuffer(tempBuffer, len); + +#if GIFDEBUG == 1 && DEBUG_PROCESSING_APP_EXT == 1 + // Conditionally display the application extension string + if (strlen(tempBuffer) != 0) { + Serial.print("Application Extension: "); + Serial.println(tempBuffer); + } +#endif + + // Consume any additional app data + len = readByte(); + while (len != 0) { + readIntoBuffer(tempBuffer, len); + len = readByte(); + } +} + +// Parse comment extension +template +void GifDecoder::parseCommentExtension() { + +#if GIFDEBUG == 1 && DEBUG_PROCESSING_COMMENT_EXT == 1 + Serial.println("\nProcessing Comment Extension"); +#endif + + // Read block length + uint8_t len = readByte(); + while (len != 0) { + // Clear buffer + memset(tempBuffer, 0, sizeof(tempBuffer)); + + // Read len bytes into buffer + readIntoBuffer(tempBuffer, len); + +#if GIFDEBUG == 1 && DEBUG_PROCESSING_COMMENT_EXT == 1 + // Display the comment extension string + if (strlen(tempBuffer) != 0) { + Serial.print("Comment Extension: "); + Serial.println(tempBuffer); + } +#endif + // Read the new block length + len = readByte(); + } +} + +// Parse file terminator +template +int GifDecoder::parseGIFFileTerminator() { + +#if GIFDEBUG == 1 && DEBUG_PROCESSING_FILE_TERM == 1 + Serial.println("\nProcessing file terminator"); +#endif + + uint8_t b = readByte(); + if (b != 0x3B) { + +#if GIFDEBUG == 1 && DEBUG_PROCESSING_FILE_TERM == 1 + Serial.print("Terminator byte: "); + Serial.println(b, HEX); +#endif + Serial.println("Bad GIF file format - Bad terminator"); + return ERROR_BADGIFFORMAT; + } + else { + return ERROR_NONE; + } +} + +// Parse table based image data +template +void GifDecoder::parseTableBasedImage() { + +#if GIFDEBUG == 1 && DEBUG_PROCESSING_TBI_DESC_START == 1 + Serial.println("\nProcessing Table Based Image Descriptor"); +#endif + +#if GIFDEBUG == 1 && DEBUG_PARSING_DATA == 1 + Serial.println("File Position: "); + Serial.println(filePositionCallback()); + Serial.println("File Size: "); + //Serial.println(file.size()); +#endif + + // Parse image descriptor + tbiImageX = readWord(); + tbiImageY = readWord(); + tbiWidth = readWord(); + tbiHeight = readWord(); + tbiPackedBits = readByte(); + +#if GIFDEBUG == 1 + Serial.print("tbiImageX: "); + Serial.println(tbiImageX); + Serial.print("tbiImageY: "); + Serial.println(tbiImageY); + Serial.print("tbiWidth: "); + Serial.println(tbiWidth); + Serial.print("tbiHeight: "); + Serial.println(tbiHeight); + Serial.print("PackedBits: "); + Serial.println(tbiPackedBits, HEX); +#endif + + // Is this image interlaced ? + tbiInterlaced = ((tbiPackedBits & INTERLACEFLAG) != 0); + +#if GIFDEBUG == 1 && DEBUG_PROCESSING_TBI_DESC_INTERLACED == 1 + Serial.print("Image interlaced: "); + Serial.println((tbiInterlaced != 0) ? "Yes" : "No"); +#endif + + // Does this image have a local color table ? + bool localColorTable = ((tbiPackedBits & COLORTBLFLAG) != 0); + + if (localColorTable) { + int colorBits = ((tbiPackedBits & 7) + 1); + colorCount = 1 << colorBits; + +#if GIFDEBUG == 1 && DEBUG_PROCESSING_TBI_DESC_LOCAL_COLOR_TABLE == 1 + Serial.print("Local color table with "); + Serial.print(colorCount); + Serial.println(" colors present"); +#endif + // Read colors into palette + int colorTableBytes = sizeof(rgb_24) * colorCount; + readIntoBuffer(palette, colorTableBytes); + } + + // One time initialization of imageData before first frame + if (keyFrame) { + if (transparentColorIndex == NO_TRANSPARENT_INDEX) { + fillImageData(lsdBackgroundIndex); + } + else { + fillImageData(transparentColorIndex); + } + keyFrame = false; + + rectX = 0; + rectY = 0; + rectWidth = maxGifWidth; + rectHeight = maxGifHeight; + } + // Don't clear matrix screen for these disposal methods + if ((prevDisposalMethod != DISPOSAL_NONE) && (prevDisposalMethod != DISPOSAL_LEAVE)) { + if(screenClearCallback) + (*screenClearCallback)(); + } + + // Process previous disposal method + if (prevDisposalMethod == DISPOSAL_BACKGROUND) { + // Fill portion of imageData with previous background color + fillImageDataRect(prevBackgroundIndex, rectX, rectY, rectWidth, rectHeight); + } + else if (prevDisposalMethod == DISPOSAL_RESTORE) { + copyImageDataRect(imageData, imageDataBU, rectX, rectY, rectWidth, rectHeight); + } + + // Save disposal method for this frame for next time + prevDisposalMethod = disposalMethod; + + if (disposalMethod != DISPOSAL_NONE) { + // Save dimensions of this frame + rectX = tbiImageX; + rectY = tbiImageY; + rectWidth = tbiWidth; + rectHeight = tbiHeight; + + // limit rectangle to the bounds of maxGifWidth*maxGifHeight + if(rectX + rectWidth > maxGifWidth) + rectWidth = maxGifWidth-rectX; + if(rectY + rectHeight > maxGifHeight) + rectHeight = maxGifHeight-rectY; + if(rectX >= maxGifWidth || rectY >= maxGifHeight) { + rectX = rectY = rectWidth = rectHeight = 0; + } + + if (disposalMethod == DISPOSAL_BACKGROUND) { + if (transparentColorIndex != NO_TRANSPARENT_INDEX) { + prevBackgroundIndex = transparentColorIndex; + } + else { + prevBackgroundIndex = lsdBackgroundIndex; + } + } + else if (disposalMethod == DISPOSAL_RESTORE) { + copyImageDataRect(imageDataBU, imageData, rectX, rectY, rectWidth, rectHeight); + } + } + + // Read the min LZW code size + lzwCodeSize = readByte(); + +#if GIFDEBUG == 1 && DEBUG_PROCESSING_TBI_DESC_LZWCODESIZE == 1 + Serial.print("LzwCodeSize: "); + Serial.println(lzwCodeSize); + Serial.println("File Position Before: "); + Serial.println(filePositionCallback()); +#endif + + unsigned long filePositionBefore = filePositionCallback(); + + // Gather the lzw image data + // NOTE: the dataBlockSize byte is left in the data as the lzw decoder needs it + int offset = 0; + int dataBlockSize = readByte(); + while (dataBlockSize != 0) { +#if GIFDEBUG == 1 && DEBUG_PROCESSING_TBI_DESC_DATABLOCKSIZE == 1 + Serial.print("dataBlockSize: "); + Serial.println(dataBlockSize); +#endif + backUpStream(1); + dataBlockSize++; + fileSeekCallback(filePositionCallback() + dataBlockSize); + + offset += dataBlockSize; + dataBlockSize = readByte(); + } + +#if GIFDEBUG == 1 && DEBUG_PROCESSING_TBI_DESC_LZWIMAGEDATA_SIZE == 1 + Serial.print("total lzwImageData Size: "); + Serial.println(offset); + Serial.println("File Position Test: "); + Serial.println(filePositionCallback()); +#endif + + // this is the position where GIF decoding needs to pick up after decompressing frame + unsigned long filePositionAfter = filePositionCallback(); + + fileSeekCallback(filePositionBefore); + + // Process the animation frame for display + + // Initialize the LZW decoder for this frame + lzw_decode_init(lzwCodeSize); + lzw_setTempBuffer((uint8_t*)tempBuffer); + + // Make sure there is at least some delay between frames + if (frameDelay < 1) { + frameDelay = 1; + } + + // Decompress LZW data and display the frame + decompressAndDisplayFrame(filePositionAfter); + + // Graphic control extension is for a single frame + transparentColorIndex = NO_TRANSPARENT_INDEX; + disposalMethod = DISPOSAL_NONE; +} + +// Parse gif data +template +int GifDecoder::parseData() { + if(nextFrameTime_ms > millis()) + return ERROR_WAITING; + +#if GIFDEBUG == 1 && DEBUG_PARSING_DATA == 1 + Serial.println("\nParsing Data Block"); +#endif + + bool parsedFrame = false; + while (!parsedFrame) { + +#if GIFDEBUG == 1 && DEBUG_WAIT_FOR_KEY_PRESS == 1 + Serial.println("\nPress Key For Next"); + while(Serial.read() <= 0); +#endif + + // Determine what kind of data to process + uint8_t b = readByte(); + + if (b == 0x2c) { + // Parse table based image +#if GIFDEBUG == 1 && DEBUG_PARSING_DATA == 1 + Serial.println("\nParsing Table Based"); +#endif + parseTableBasedImage(); + parsedFrame = true; + + } + else if (b == 0x21) { + // Parse extension + b = readByte(); + +#if GIFDEBUG == 1 && DEBUG_PARSING_DATA == 1 + Serial.println("\nParsing Extension"); +#endif + + // Determine which kind of extension to parse + switch (b) { + case 0x01: + // Plain test extension + parsePlainTextExtension(); + break; + case 0xf9: + // Graphic control extension + parseGraphicControlExtension(); + break; + case 0xfe: + // Comment extension + parseCommentExtension(); + break; + case 0xff: + // Application extension + parseApplicationExtension(); + break; + default: + Serial.print("Unknown control extension: "); + Serial.println(b, HEX); + return ERROR_UNKNOWNCONTROLEXT; + } + } + else { +#if GIFDEBUG == 1 && DEBUG_PARSING_DATA == 1 + Serial.println("\nParsing Done"); +#endif + + // Push unprocessed byte back into the stream for later processing + backUpStream(1); + + return ERROR_DONE_PARSING; + } + } + return ERROR_NONE; +} + +template +int GifDecoder::startDecoding(void) { + // Initialize variables + keyFrame = true; + prevDisposalMethod = DISPOSAL_NONE; + transparentColorIndex = NO_TRANSPARENT_INDEX; + nextFrameTime_ms = 0; + fileSeekCallback(0); + + // Validate the header + if (! parseGifHeader()) { + Serial.println("Not a GIF file"); + return ERROR_FILENOTGIF; + } + // If we get here we have a gif file to process + + // Parse the logical screen descriptor + parseLogicalScreenDescriptor(); + + // Parse the global color table + parseGlobalColorTable(); + + return ERROR_NONE; +} + +template +int GifDecoder::decodeFrame(void) { + // Parse gif data + int result = parseData(); + if (result < ERROR_NONE) { + Serial.println("Error: "); + Serial.println(result); + Serial.println(" occurred during parsing of data"); + return result; + } + + if (result == ERROR_DONE_PARSING) { + //startDecoding(); + // Initialize variables like with a new file + keyFrame = true; + prevDisposalMethod = DISPOSAL_NONE; + transparentColorIndex = NO_TRANSPARENT_INDEX; + nextFrameTime_ms = 0; + fileSeekCallback(0); + + // parse Gif Header like with a new file + parseGifHeader(); + + // Parse the logical screen descriptor + parseLogicalScreenDescriptor(); + + // Parse the global color table + parseGlobalColorTable(); + } + + return result; +} + +// Decompress LZW data and display animation frame +template +void GifDecoder::decompressAndDisplayFrame(unsigned long filePositionAfter) { + + // Each pixel of image is 8 bits and is an index into the palette + + // How the image is decoded depends upon whether it is interlaced or not + // Decode the interlaced LZW data into the image buffer + if (tbiInterlaced) { + // Decode every 8th line starting at line 0 + for (int line = tbiImageY + 0; line < tbiHeight + tbiImageY; line += 8) { + lzw_decode(imageData + (line * maxGifWidth) + tbiImageX, tbiWidth, min(imageData + (line * maxGifWidth) + maxGifWidth, imageData + sizeof(imageData))); + } + // Decode every 8th line starting at line 4 + for (int line = tbiImageY + 4; line < tbiHeight + tbiImageY; line += 8) { + lzw_decode(imageData + (line * maxGifWidth) + tbiImageX, tbiWidth, min(imageData + (line * maxGifWidth) + maxGifWidth, imageData + sizeof(imageData))); + } + // Decode every 4th line starting at line 2 + for (int line = tbiImageY + 2; line < tbiHeight + tbiImageY; line += 4) { + lzw_decode(imageData + (line * maxGifWidth) + tbiImageX, tbiWidth, min(imageData + (line * maxGifWidth) + maxGifWidth, imageData + sizeof(imageData))); + } + // Decode every 2nd line starting at line 1 + for (int line = tbiImageY + 1; line < tbiHeight + tbiImageY; line += 2) { + lzw_decode(imageData + (line * maxGifWidth) + tbiImageX, tbiWidth, min(imageData + (line * maxGifWidth) + maxGifWidth, imageData + sizeof(imageData))); + } + } + else { + // Decode the non interlaced LZW data into the image data buffer + for (int line = tbiImageY; line < tbiHeight + tbiImageY; line++) { + lzw_decode(imageData + (line * maxGifWidth) + tbiImageX, tbiWidth, imageData + sizeof(imageData)); + } + } + +#if GIFDEBUG == 1 && DEBUG_DECOMPRESS_AND_DISPLAY == 1 + Serial.println("File Position After: "); + Serial.println(filePositionCallback()); +#endif + +#if GIFDEBUG == 1 && DEBUG_WAIT_FOR_KEY_PRESS == 1 + Serial.println("\nPress Key For Next"); + while(Serial.read() <= 0); +#endif + + // LZW doesn't parse through all the data, manually set position + fileSeekCallback(filePositionAfter); + + // Optional callback can be used to get drawing routines ready + if(startDrawingCallback) + (*startDrawingCallback)(); + + // Image data is decompressed, now display portion of image affected by frame + int yOffset, pixel; + for (int y = tbiImageY; y < tbiHeight + tbiImageY; y++) { + yOffset = y * maxGifWidth; + for (int x = tbiImageX; x < tbiWidth + tbiImageX; x++) { + // Get the next pixel + pixel = imageData[yOffset + x]; + + // Check pixel transparency + if (pixel == transparentColorIndex) { + continue; + } + + // Pixel not transparent so get color from palette and draw the pixel + if(drawPixelCallback) + (*drawPixelCallback)(x, y, palette[pixel].red, palette[pixel].green, palette[pixel].blue); + } + } + // Make animation frame visible + // swapBuffers() call can take up to 1/framerate seconds to return (it waits until a buffer copy is complete) + // note the time before calling + + // wait until time to display next frame + while(nextFrameTime_ms > millis()); + + // calculate time to display next frame + nextFrameTime_ms = millis() + (10 * frameDelay); + if(updateScreenCallback) + (*updateScreenCallback)(); +} diff --git a/examples/AnimatedGIF/LICENSE.txt b/examples/AnimatedGIF/LICENSE.txt new file mode 100644 index 0000000..c69f703 --- /dev/null +++ b/examples/AnimatedGIF/LICENSE.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 Craig A. Lindley + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/examples/AnimatedGIF/LzwDecoder_Impl.h b/examples/AnimatedGIF/LzwDecoder_Impl.h new file mode 100644 index 0000000..720ac24 --- /dev/null +++ b/examples/AnimatedGIF/LzwDecoder_Impl.h @@ -0,0 +1,169 @@ +/* + * This file contains code to decompress the LZW encoded animated GIF data + * + * Written by: Craig A. Lindley, Fabrice Bellard and Steven A. Bennett + * See my book, "Practical Image Processing in C", John Wiley & Sons, Inc. + * + * Copyright (c) 2014 Craig A. Lindley + * Minor modifications by Louis Beaudoin (pixelmatix) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#define LZWDEBUG 0 + +#include "GifDecoder.h" + +template +void GifDecoder::lzw_setTempBuffer(uint8_t * tempBuffer) { + temp_buffer = tempBuffer; +} + +// Initialize LZW decoder +// csize initial code size in bits +// buf input data +template +void GifDecoder::lzw_decode_init (int csize) { + + // Initialize read buffer variables + bbuf = 0; + bbits = 0; + bs = 0; + bcnt = 0; + + // Initialize decoder variables + codesize = csize; + cursize = codesize + 1; + curmask = mask[cursize]; + top_slot = 1 << cursize; + clear_code = 1 << codesize; + end_code = clear_code + 1; + slot = newcodes = clear_code + 2; + oc = fc = -1; + sp = stack; +} + +// Get one code of given number of bits from stream +template +int GifDecoder::lzw_get_code() { + + while (bbits < cursize) { + if (bcnt == bs) { + // get number of bytes in next block + readIntoBuffer(temp_buffer, 1); + bs = temp_buffer[0]; + readIntoBuffer(temp_buffer, bs); + bcnt = 0; + } + bbuf |= temp_buffer[bcnt] << bbits; + bbits += 8; + bcnt++; + } + int c = bbuf; + bbuf >>= cursize; + bbits -= cursize; + return c & curmask; +} + +// Decode given number of bytes +// buf 8 bit output buffer +// len number of pixels to decode +// returns the number of bytes decoded +template +int GifDecoder::lzw_decode(uint8_t *buf, int len, uint8_t *bufend) { + int l, c, code; + +#if LZWDEBUG == 1 + unsigned char debugMessagePrinted = 0; +#endif + + if (end_code < 0) { + return 0; + } + l = len; + + for (;;) { + while (sp > stack) { + // load buf with data if we're still within bounds + if(buf < bufend) { + *buf++ = *(--sp); + } else { + // out of bounds, keep incrementing the pointers, but don't use the data +#if LZWDEBUG == 1 + // only print this message once per call to lzw_decode + if(buf == bufend) + Serial.println("****** LZW imageData buffer overrun *******"); +#endif + } + if ((--l) == 0) { + return len; + } + } + c = lzw_get_code(); + if (c == end_code) { + break; + + } + else if (c == clear_code) { + cursize = codesize + 1; + curmask = mask[cursize]; + slot = newcodes; + top_slot = 1 << cursize; + fc= oc= -1; + + } + else { + + code = c; + if ((code == slot) && (fc >= 0)) { + *sp++ = fc; + code = oc; + } + else if (code >= slot) { + break; + } + while (code >= newcodes) { + *sp++ = suffix[code]; + code = prefix[code]; + } + *sp++ = code; + if ((slot < top_slot) && (oc >= 0)) { + suffix[slot] = code; + prefix[slot++] = oc; + } + fc = code; + oc = c; + if (slot >= top_slot) { + if (cursize < lzwMaxBits) { + top_slot <<= 1; + curmask = mask[++cursize]; + } else { +#if LZWDEBUG == 1 + if(!debugMessagePrinted) { + debugMessagePrinted = 1; + Serial.println("****** cursize >= lzwMaxBits *******"); + } +#endif + } + + } + } + } + end_code = -1; + return len - l; +} diff --git a/examples/AnimatedGIF/data/Text_Animations.txt.txt b/examples/AnimatedGIF/data/Text_Animations.txt.txt new file mode 100644 index 0000000..8431cd4 --- /dev/null +++ b/examples/AnimatedGIF/data/Text_Animations.txt.txt @@ -0,0 +1 @@ +https://loading.io/animation/text/ \ No newline at end of file diff --git a/examples/AnimatedGIF/data/ezgif.com-pacmn.gif b/examples/AnimatedGIF/data/ezgif.com-pacmn.gif new file mode 100644 index 0000000..0a219a4 Binary files /dev/null and b/examples/AnimatedGIF/data/ezgif.com-pacmn.gif differ diff --git a/examples/AnimatedGIF/data/loading.io-64x32px.gif b/examples/AnimatedGIF/data/loading.io-64x32px.gif new file mode 100644 index 0000000..342f8ae Binary files /dev/null and b/examples/AnimatedGIF/data/loading.io-64x32px.gif differ