- fixes a bug in text lengths calculations with spaces - adds the onscreen menu option to disable or enable internal apps Many options are moved from webinterface to onscreen menu the last few versions. if you running awtrix light for some versions now, it could be necessary to delete your config.json and restart in order to cleanup your webinterface. closes #14
655 lines
15 KiB
C++
655 lines
15 KiB
C++
#ifndef GifPlayer_H
|
|
#define GifPlayer_H
|
|
|
|
class GifPlayer
|
|
{
|
|
public:
|
|
#define ERROR_NONE 0
|
|
#define ERROR_FILEOPEN 1
|
|
#define ERROR_FILENOTGIF 2
|
|
#define ERROR_BADGIFFORMAT 3
|
|
#define ERROR_UNKNOWNCONTROLEXT 4
|
|
#define ERROR_FINISHED 5
|
|
private:
|
|
bool needNewFrame;
|
|
long lastFrameTime;
|
|
bool firstFrameDone;
|
|
int newframeDelay;
|
|
int lastFrame[8 * 8];
|
|
bool lastFrameDrawn = false;
|
|
unsigned long nextFrameTime = 0;
|
|
#define GIFHDRTAGNORM "GIF87a"
|
|
#define GIFHDRTAGNORM1 "GIF89a"
|
|
#define GIFHDRSIZE 6
|
|
FastLED_NeoMatrix *mtx;
|
|
#define COLORTBLFLAG 0x80
|
|
#define INTERLACEFLAG 0x40
|
|
#define TRANSPARENTFLAG 0x01
|
|
#define NO_TRANSPARENT_INDEX -1
|
|
#define DISPOSAL_NONE 0
|
|
#define DISPOSAL_LEAVE 1
|
|
#define DISPOSAL_BACKGROUND 2
|
|
#define DISPOSAL_RESTORE 3
|
|
typedef struct
|
|
{
|
|
byte Red;
|
|
byte Green;
|
|
byte Blue;
|
|
} _RGB;
|
|
int lsdWidth;
|
|
int lsdHeight;
|
|
int lsdPackedField;
|
|
int lsdAspectRatio;
|
|
int lsdBackgroundIndex;
|
|
int offsetX;
|
|
int offsetY;
|
|
int tbiImageX;
|
|
int tbiImageY;
|
|
int tbiWidth;
|
|
int tbiHeight;
|
|
int tbiPackedBits;
|
|
boolean tbiInterlaced;
|
|
|
|
public:
|
|
int frameDelay;
|
|
int transparentColorIndex;
|
|
int prevBackgroundIndex;
|
|
int prevDisposalMethod;
|
|
int disposalMethod;
|
|
int lzwCodeSize;
|
|
boolean keyFrame;
|
|
int rectX;
|
|
int rectY;
|
|
int rectWidth;
|
|
int rectHeight;
|
|
int colorCount;
|
|
_RGB gifPalette[256];
|
|
byte lzwImageData[1280];
|
|
char tempBuffer[260];
|
|
File file;
|
|
byte imageData[8 * 8];
|
|
byte imageDataBU[8 * 8];
|
|
|
|
void backUpStream(int n)
|
|
{
|
|
file.seek(file.position() - n, SeekSet);
|
|
}
|
|
|
|
int readByte()
|
|
{
|
|
int b = file.read();
|
|
return b;
|
|
}
|
|
|
|
int readWord()
|
|
{
|
|
int b0 = readByte();
|
|
int b1 = readByte();
|
|
return (b1 << 8) | b0;
|
|
}
|
|
|
|
int readIntoBuffer(void *buffer, int numberOfBytes)
|
|
{
|
|
int result = file.read((uint8_t *)buffer, numberOfBytes);
|
|
return result;
|
|
}
|
|
|
|
void fillImageDataRect(byte colorIndex, int x, int y, int width, int height)
|
|
{
|
|
int yOffset;
|
|
for (int yy = y; yy < height + y; yy++)
|
|
{
|
|
yOffset = yy * 8;
|
|
for (int xx = x; xx < width + x; xx++)
|
|
{
|
|
imageData[yOffset + xx] = colorIndex;
|
|
}
|
|
}
|
|
}
|
|
|
|
void fillImageData(byte colorIndex)
|
|
{
|
|
memset(imageData, colorIndex, sizeof(imageData));
|
|
}
|
|
|
|
void copyImageDataRect(byte *src, byte *dst, int x, int y, int width, int height)
|
|
{
|
|
|
|
int yOffset, offset;
|
|
|
|
for (int yy = y; yy < height + y; yy++)
|
|
{
|
|
yOffset = yy * 8;
|
|
for (int xx = x; xx < width + x; xx++)
|
|
{
|
|
offset = yOffset + xx;
|
|
dst[offset] = src[offset];
|
|
}
|
|
}
|
|
}
|
|
|
|
void parsePlainTextExtension()
|
|
{
|
|
byte len = readByte();
|
|
readIntoBuffer(tempBuffer, len);
|
|
len = readByte();
|
|
while (len != 0)
|
|
{
|
|
readIntoBuffer(tempBuffer, len);
|
|
len = readByte();
|
|
}
|
|
}
|
|
|
|
void parseGraphicControlExtension()
|
|
{
|
|
readByte();
|
|
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;
|
|
}
|
|
|
|
readByte(); // Toss block end
|
|
}
|
|
|
|
void parseApplicationExtension()
|
|
{
|
|
memset(tempBuffer, 0, sizeof(tempBuffer));
|
|
byte len = readByte();
|
|
readIntoBuffer(tempBuffer, len);
|
|
len = readByte();
|
|
while (len != 0)
|
|
{
|
|
readIntoBuffer(tempBuffer, len);
|
|
len = readByte();
|
|
}
|
|
}
|
|
|
|
void parseCommentExtension()
|
|
{
|
|
byte len = readByte();
|
|
while (len != 0)
|
|
{
|
|
memset(tempBuffer, 0, sizeof(tempBuffer));
|
|
readIntoBuffer(tempBuffer, len);
|
|
len = readByte();
|
|
}
|
|
}
|
|
|
|
int parseGIFFileTerminator()
|
|
{
|
|
byte b = readByte();
|
|
if (b != 0x3B)
|
|
{
|
|
return ERROR_BADGIFFORMAT;
|
|
}
|
|
else
|
|
{
|
|
return ERROR_NONE;
|
|
}
|
|
}
|
|
|
|
unsigned long parseTableBasedImage()
|
|
{
|
|
tbiImageX = readWord();
|
|
tbiImageY = readWord();
|
|
tbiWidth = readWord();
|
|
tbiHeight = readWord();
|
|
tbiPackedBits = readByte();
|
|
tbiInterlaced = ((tbiPackedBits & INTERLACEFLAG) != 0);
|
|
boolean localColorTable = ((tbiPackedBits & COLORTBLFLAG) != 0);
|
|
if (localColorTable)
|
|
{
|
|
int colorBits = ((tbiPackedBits & 7) + 1);
|
|
colorCount = 1 << colorBits;
|
|
int colorTableBytes = sizeof(_RGB) * colorCount;
|
|
readIntoBuffer(gifPalette, colorTableBytes);
|
|
}
|
|
|
|
if (keyFrame)
|
|
{
|
|
if (transparentColorIndex == NO_TRANSPARENT_INDEX)
|
|
{
|
|
fillImageData(lsdBackgroundIndex);
|
|
}
|
|
else
|
|
{
|
|
fillImageData(transparentColorIndex);
|
|
}
|
|
keyFrame = false;
|
|
|
|
rectX = 0;
|
|
rectY = 0;
|
|
rectWidth = 8;
|
|
rectHeight = 8;
|
|
}
|
|
|
|
if ((prevDisposalMethod != DISPOSAL_NONE) && (prevDisposalMethod != DISPOSAL_LEAVE))
|
|
{
|
|
// mtx->clear();
|
|
}
|
|
|
|
if (prevDisposalMethod == DISPOSAL_BACKGROUND)
|
|
{
|
|
fillImageDataRect(prevBackgroundIndex, rectX, rectY, rectWidth, rectHeight);
|
|
}
|
|
else if (prevDisposalMethod == DISPOSAL_RESTORE)
|
|
{
|
|
copyImageDataRect(imageDataBU, imageData, rectX, rectY, rectWidth, rectHeight);
|
|
}
|
|
prevDisposalMethod = disposalMethod;
|
|
if (disposalMethod != DISPOSAL_NONE)
|
|
{
|
|
rectX = tbiImageX;
|
|
rectY = tbiImageY;
|
|
rectWidth = tbiWidth;
|
|
rectHeight = tbiHeight;
|
|
if (disposalMethod == DISPOSAL_BACKGROUND)
|
|
{
|
|
if (transparentColorIndex != NO_TRANSPARENT_INDEX)
|
|
{
|
|
prevBackgroundIndex = transparentColorIndex;
|
|
}
|
|
else
|
|
{
|
|
prevBackgroundIndex = lsdBackgroundIndex;
|
|
}
|
|
}
|
|
else if (disposalMethod == DISPOSAL_RESTORE)
|
|
{
|
|
copyImageDataRect(imageData, imageDataBU, rectX, rectY, rectWidth, rectHeight);
|
|
}
|
|
}
|
|
lzwCodeSize = readByte();
|
|
int offset = 0;
|
|
int dataBlockSize = readByte();
|
|
while (dataBlockSize != 0)
|
|
{
|
|
backUpStream(1);
|
|
dataBlockSize++;
|
|
if (offset + dataBlockSize <= (int)sizeof(lzwImageData))
|
|
{
|
|
readIntoBuffer(lzwImageData + offset, dataBlockSize);
|
|
}
|
|
else
|
|
{
|
|
int i;
|
|
for (i = 0; i < dataBlockSize; i++)
|
|
file.read();
|
|
}
|
|
|
|
offset += dataBlockSize;
|
|
dataBlockSize = readByte();
|
|
}
|
|
lzw_decode_init(lzwCodeSize, lzwImageData);
|
|
decompressAndDisplayFrame();
|
|
transparentColorIndex = NO_TRANSPARENT_INDEX;
|
|
disposalMethod = DISPOSAL_NONE;
|
|
if (frameDelay < 1)
|
|
{
|
|
frameDelay = 1;
|
|
}
|
|
newframeDelay = frameDelay * 10;
|
|
return frameDelay * 10;
|
|
}
|
|
|
|
#define LZW_MAXBITS 10
|
|
#define LZW_SIZTABLE (1 << LZW_MAXBITS)
|
|
unsigned int mask[17] = {
|
|
0x0000, 0x0001, 0x0003, 0x0007,
|
|
0x000F, 0x001F, 0x003F, 0x007F,
|
|
0x00FF, 0x01FF, 0x03FF, 0x07FF,
|
|
0x0FFF, 0x1FFF, 0x3FFF, 0x7FFF,
|
|
0xFFFF};
|
|
|
|
byte *pbuf;
|
|
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
|
|
byte *sp;
|
|
byte stack[LZW_SIZTABLE];
|
|
byte suffix[LZW_SIZTABLE];
|
|
unsigned int prefix[LZW_SIZTABLE];
|
|
|
|
void lzw_decode_init(int csize, byte *buf)
|
|
{
|
|
pbuf = buf;
|
|
bbuf = 0;
|
|
bbits = 0;
|
|
bs = 0;
|
|
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;
|
|
}
|
|
|
|
int lzw_get_code()
|
|
{
|
|
while (bbits < cursize)
|
|
{
|
|
if (!bs)
|
|
{
|
|
bs = *pbuf++;
|
|
}
|
|
bbuf |= (*pbuf++) << bbits;
|
|
bbits += 8;
|
|
bs--;
|
|
}
|
|
int c = bbuf;
|
|
bbuf >>= cursize;
|
|
bbits -= cursize;
|
|
return c & curmask;
|
|
}
|
|
|
|
int lzw_decode(byte *buf, int len)
|
|
{
|
|
int l, c, code;
|
|
|
|
if (end_code < 0)
|
|
{
|
|
return 0;
|
|
}
|
|
l = len;
|
|
|
|
for (;;)
|
|
{
|
|
while (sp > stack)
|
|
{
|
|
*buf++ = *(--sp);
|
|
if ((--l) == 0)
|
|
{
|
|
goto the_end;
|
|
}
|
|
}
|
|
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 < LZW_MAXBITS)
|
|
{
|
|
top_slot <<= 1;
|
|
curmask = mask[++cursize];
|
|
}
|
|
else
|
|
{
|
|
}
|
|
}
|
|
}
|
|
}
|
|
end_code = -1;
|
|
the_end:
|
|
return len - l;
|
|
}
|
|
|
|
void redrawLastFrame()
|
|
{
|
|
if (needNewFrame)
|
|
return;
|
|
CRGB color;
|
|
int yOffset, pixel;
|
|
for (int y = tbiImageY; y < tbiHeight + tbiImageY; y++)
|
|
{
|
|
yOffset = y * 8;
|
|
for (int x = tbiImageX; x < tbiWidth + tbiImageX; x++)
|
|
{
|
|
pixel = lastFrame[yOffset + x];
|
|
if (pixel == -99)
|
|
{
|
|
mtx->drawPixel(x + offsetX, y + offsetY, mtx->Color(0, 0, 0));
|
|
continue;
|
|
}
|
|
color.red = gifPalette[pixel].Red;
|
|
color.green = gifPalette[pixel].Green;
|
|
color.blue = gifPalette[pixel].Blue;
|
|
mtx->drawPixel(x + offsetX, y + offsetY, color);
|
|
}
|
|
}
|
|
}
|
|
|
|
void decompressAndDisplayFrame()
|
|
{
|
|
CRGB color;
|
|
if (tbiInterlaced)
|
|
{
|
|
for (int line = tbiImageY + 0; line < tbiHeight + tbiImageY; line += 8)
|
|
{
|
|
lzw_decode(imageData + (line * 8) + tbiImageX, tbiWidth);
|
|
}
|
|
for (int line = tbiImageY + 4; line < tbiHeight + tbiImageY; line += 8)
|
|
{
|
|
lzw_decode(imageData + (line * 8) + tbiImageX, tbiWidth);
|
|
}
|
|
for (int line = tbiImageY + 2; line < tbiHeight + tbiImageY; line += 4)
|
|
{
|
|
lzw_decode(imageData + (line * 8) + tbiImageX, tbiWidth);
|
|
}
|
|
for (int line = tbiImageY + 1; line < tbiHeight + tbiImageY; line += 2)
|
|
{
|
|
lzw_decode(imageData + (line * 8) + tbiImageX, tbiWidth);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
for (int line = tbiImageY; line < tbiHeight + tbiImageY; line++)
|
|
{
|
|
lzw_decode(imageData + (line * 8) + tbiImageX, tbiWidth);
|
|
}
|
|
}
|
|
|
|
// Ersetze alle transparenten Pixel durch schwarze Pixel
|
|
for (int i = 0; i < tbiWidth * tbiHeight; i++)
|
|
{
|
|
if (imageData[i] == transparentColorIndex)
|
|
{
|
|
imageData[i] = -99; // Indexwert für Schwarz
|
|
}
|
|
}
|
|
|
|
// Zeichne das Bild auf die Matrix
|
|
int yOffset, pixel;
|
|
for (int y = tbiImageY; y < tbiHeight + tbiImageY; y++)
|
|
{
|
|
yOffset = y * 8;
|
|
for (int x = tbiImageX; x < tbiWidth + tbiImageX; x++)
|
|
{
|
|
pixel = imageData[yOffset + x];
|
|
if (pixel == -99)
|
|
{
|
|
mtx->drawPixel(x + offsetX, y + offsetY, mtx->Color(0, 0, 0));
|
|
continue;
|
|
}
|
|
|
|
lastFrame[yOffset + x] = pixel;
|
|
color.red = gifPalette[pixel].Red;
|
|
color.green = gifPalette[pixel].Green;
|
|
color.blue = gifPalette[pixel].Blue;
|
|
mtx->drawPixel(x + offsetX, y + offsetY, color);
|
|
}
|
|
}
|
|
needNewFrame = false;
|
|
lastFrameTime = millis();
|
|
}
|
|
|
|
public:
|
|
void setMatrix(FastLED_NeoMatrix *matrix)
|
|
{
|
|
mtx = matrix;
|
|
}
|
|
void setFile(File imageFile)
|
|
{
|
|
if (imageFile.name() == file.name())
|
|
return;
|
|
|
|
needNewFrame = true;
|
|
file = imageFile;
|
|
memset(lastFrame, 0, sizeof(lastFrame));
|
|
memset(gifPalette, 0, sizeof(gifPalette));
|
|
memset(lzwImageData, 0, sizeof(lzwImageData));
|
|
memset(imageData, 0, sizeof(imageData));
|
|
memset(imageDataBU, 0, sizeof(imageDataBU));
|
|
memset(stack, 0, sizeof(stack));
|
|
memset(suffix, 0, sizeof(suffix));
|
|
memset(prefix, 0, sizeof(prefix));
|
|
parseGifHeader();
|
|
parseLogicalScreenDescriptor();
|
|
parseGlobalColorTable();
|
|
drawFrame(offsetX, offsetY);
|
|
}
|
|
|
|
boolean parseGifHeader()
|
|
{
|
|
char buffer[10];
|
|
readIntoBuffer(buffer, GIFHDRSIZE);
|
|
if ((strncmp(buffer, GIFHDRTAGNORM, GIFHDRSIZE) != 0) &&
|
|
(strncmp(buffer, GIFHDRTAGNORM1, GIFHDRSIZE) != 0))
|
|
{
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
void parseLogicalScreenDescriptor()
|
|
{
|
|
lsdWidth = readWord();
|
|
lsdHeight = readWord();
|
|
lsdPackedField = readByte();
|
|
lsdBackgroundIndex = readByte();
|
|
lsdAspectRatio = readByte();
|
|
}
|
|
|
|
void parseGlobalColorTable()
|
|
{
|
|
if (lsdPackedField & COLORTBLFLAG)
|
|
{
|
|
colorCount = 1 << ((lsdPackedField & 7) + 1);
|
|
int colorTableBytes = sizeof(_RGB) * colorCount;
|
|
readIntoBuffer(gifPalette, colorTableBytes);
|
|
}
|
|
}
|
|
|
|
unsigned long drawFrame(int x, int y)
|
|
{
|
|
if (!file)
|
|
return 0;
|
|
|
|
if (millis() - lastFrameTime < newframeDelay)
|
|
{
|
|
redrawLastFrame();
|
|
return 0;
|
|
}
|
|
|
|
lastFrameDrawn = false;
|
|
|
|
offsetX = x;
|
|
offsetY = y;
|
|
boolean done = false;
|
|
while (!done)
|
|
{
|
|
byte b = readByte();
|
|
if (b == 0x2c)
|
|
{
|
|
Serial.println("Parse");
|
|
parseTableBasedImage();
|
|
return 0;
|
|
}
|
|
else if (b == 0x21)
|
|
{
|
|
b = readByte();
|
|
switch (b)
|
|
{
|
|
case 0x01:
|
|
parsePlainTextExtension();
|
|
break;
|
|
case 0xf9:
|
|
parseGraphicControlExtension();
|
|
break;
|
|
case 0xfe:
|
|
parseCommentExtension();
|
|
break;
|
|
case 0xff:
|
|
parseApplicationExtension();
|
|
break;
|
|
default:
|
|
return ERROR_UNKNOWNCONTROLEXT;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
done = true;
|
|
file.seek(0);
|
|
Serial.println("Finished");
|
|
parseGifHeader();
|
|
parseLogicalScreenDescriptor();
|
|
parseGlobalColorTable();
|
|
drawFrame(offsetX,offsetY);
|
|
return ERROR_FINISHED;
|
|
}
|
|
}
|
|
return ERROR_NONE;
|
|
}
|
|
};
|
|
#endif |