diff --git a/ESP32_AP-Flasher/include/makeimage.h b/ESP32_AP-Flasher/include/makeimage.h index 1214e531..9b017f83 100644 --- a/ESP32_AP-Flasher/include/makeimage.h +++ b/ESP32_AP-Flasher/include/makeimage.h @@ -39,6 +39,7 @@ struct imgParam { uint8_t preloadlut; uint8_t zlib; + uint8_t g5; }; void spr2buffer(TFT_eSprite &spr, String &fileout, imgParam &imageParams); diff --git a/ESP32_AP-Flasher/include/tag_db.h b/ESP32_AP-Flasher/include/tag_db.h index 5798cf7f..8b9c23c8 100644 --- a/ESP32_AP-Flasher/include/tag_db.h +++ b/ESP32_AP-Flasher/include/tag_db.h @@ -93,6 +93,7 @@ struct HwType { uint8_t bpp; uint8_t shortlut; uint8_t zlib; + uint8_t g5; uint16_t highlightColor; std::vector colortable; }; diff --git a/ESP32_AP-Flasher/src/contentmanager.cpp b/ESP32_AP-Flasher/src/contentmanager.cpp index f11f84c1..82f4eb24 100644 --- a/ESP32_AP-Flasher/src/contentmanager.cpp +++ b/ESP32_AP-Flasher/src/contentmanager.cpp @@ -222,6 +222,11 @@ void drawNew(const uint8_t mac[8], tagRecord *&taginfo) { } else { imageParams.zlib = 0; } + if (hwdata.g5 != 0 && taginfo->tagSoftwareVersion >= hwdata.g5) { + imageParams.g5 = 1; + } else { + imageParams.g5 = 0; + } imageParams.lut = EPD_LUT_NO_REPEATS; if (taginfo->lut == 2) imageParams.lut = EPD_LUT_FAST_NO_REDS; @@ -283,6 +288,9 @@ void drawNew(const uint8_t mac[8], tagRecord *&taginfo) { } else if (imageParams.zlib) { imageParams.dataType = DATATYPE_IMG_ZLIB; Serial.println("datatype: DATATYPE_IMG_ZLIB"); + } else if (imageParams.g5) { + imageParams.dataType = DATATYPE_IMG_G5; + Serial.println("datatype: DATATYPE_IMG_G5"); } else if (imageParams.hasRed) { imageParams.dataType = DATATYPE_IMG_RAW_2BPP; Serial.println("datatype: DATATYPE_IMG_RAW_2BPP"); @@ -566,6 +574,9 @@ bool updateTagImage(String &filename, const uint8_t *dst, uint16_t nextCheckin, } else if (imageParams.zlib) { imageParams.dataType = DATATYPE_IMG_ZLIB; Serial.println("datatype: DATATYPE_IMG_ZLIB"); + } else if (imageParams.g5) { + imageParams.dataType = DATATYPE_IMG_G5; + Serial.println("datatype: DATATYPE_IMG_G5"); } else if (imageParams.hasRed) { imageParams.dataType = DATATYPE_IMG_RAW_2BPP; Serial.println("datatype: DATATYPE_IMG_RAW_2BPP"); @@ -996,19 +1007,19 @@ void drawForecast(String &filename, JsonObject &cfgobj, const tagRecord *taginfo } if (loc["rain"]) { - if (cfgobj["units"] == "0") { - const int8_t rain = round(daily["precipitation_sum"][dag].as()); - if (rain > 0) { - drawString(spr, String(rain) + "mm", dag * column1 + loc["rain"][0].as(), loc["rain"][1], day[2], TC_DATUM, (rain > 10 ? imageParams.highlightColor : TFT_BLACK)); - } - } else { - double fRain = daily["precipitation_sum"][dag].as(); - fRain = round(fRain*100.0) / 100.0; - if (fRain > 0.0) { - // inch, display if > .01 inches - drawString(spr, String(fRain) + "in", dag * column1 + loc["rain"][0].as(), loc["rain"][1], day[2], TC_DATUM, (fRain > 0.5 ? imageParams.highlightColor : TFT_BLACK)); - } - } + if (cfgobj["units"] == "0") { + const int8_t rain = round(daily["precipitation_sum"][dag].as()); + if (rain > 0) { + drawString(spr, String(rain) + "mm", dag * column1 + loc["rain"][0].as(), loc["rain"][1], day[2], TC_DATUM, (rain > 10 ? imageParams.highlightColor : TFT_BLACK)); + } + } else { + double fRain = daily["precipitation_sum"][dag].as(); + fRain = round(fRain * 100.0) / 100.0; + if (fRain > 0.0) { + // inch, display if > .01 inches + drawString(spr, String(fRain) + "in", dag * column1 + loc["rain"][0].as(), loc["rain"][1], day[2], TC_DATUM, (fRain > 0.5 ? imageParams.highlightColor : TFT_BLACK)); + } + } } drawString(spr, String(tmin) + " ", dag * column1 + day[0].as(), day[4], day[2], TR_DATUM, (tmin < 0 ? imageParams.highlightColor : TFT_BLACK)); @@ -1217,7 +1228,7 @@ bool getCalFeed(String &filename, JsonObject &cfgobj, tagRecord *&taginfo, imgPa int temp = imageParams.height; imageParams.height = imageParams.width; imageParams.width = temp; - imageParams.rotatebuffer = 1 - (imageParams.rotatebuffer%2); + imageParams.rotatebuffer = 1 - (imageParams.rotatebuffer % 2); initSprite(spr, imageParams.width, imageParams.height, imageParams); } else { initSprite(spr, imageParams.width, imageParams.height, imageParams); @@ -1829,14 +1840,18 @@ void drawTimestamp(String &filename, JsonObject &cfgobj, tagRecord *&taginfo, im drawString(spr, "Well done!", spr.width() / 2, 90, "calibrib30.vlw", TC_DATUM, TFT_BLACK); spr2buffer(spr, filename2, imageParams); - if (imageParams.zlib) imageParams.dataType = DATATYPE_IMG_ZLIB; + if (imageParams.zlib) { + imageParams.dataType = DATATYPE_IMG_ZLIB; + } else if (imageParams.g5) { + imageParams.dataType = DATATYPE_IMG_G5; + } struct imageDataTypeArgStruct arg = {0}; arg.preloadImage = 1; arg.specialType = 17; // button 2 arg.lut = 0; - prepareDataAvail(filename2, imageParams.dataType, *((uint8_t *)&arg), taginfo->mac, 5 | 0x8000 ); + prepareDataAvail(filename2, imageParams.dataType, *((uint8_t *)&arg), taginfo->mac, 5 | 0x8000); spr.fillRect(0, 0, spr.width(), spr.height(), TFT_WHITE); @@ -1848,7 +1863,7 @@ void drawTimestamp(String &filename, JsonObject &cfgobj, tagRecord *&taginfo, im arg.preloadImage = 1; arg.specialType = 16; // button 1 arg.lut = 0; - prepareDataAvail(filename2, imageParams.dataType, *((uint8_t *)&arg), taginfo->mac, 5 | 0x8000 ); + prepareDataAvail(filename2, imageParams.dataType, *((uint8_t *)&arg), taginfo->mac, 5 | 0x8000); cfgobj["#init"] = "1"; } @@ -2185,7 +2200,7 @@ void rotateBuffer(uint8_t rotation, uint8_t ¤tOrientation, TFT_eSprite &sp initSprite(spr, sprCpy.width(), sprCpy.height(), imageParams); sprCpy.pushToSprite(&spr, 0, 0); sprCpy.deleteSprite(); - imageParams.rotatebuffer = 1 - (imageParams.rotatebuffer%2); + imageParams.rotatebuffer = 1 - (imageParams.rotatebuffer % 2); } currentOrientation = rotation; } diff --git a/ESP32_AP-Flasher/src/g5/Group5.h b/ESP32_AP-Flasher/src/g5/Group5.h new file mode 100644 index 00000000..fe6c5f1a --- /dev/null +++ b/ESP32_AP-Flasher/src/g5/Group5.h @@ -0,0 +1,203 @@ +#ifndef __GROUP5__ +#define __GROUP5__ +#include +#include +#include +#include +#ifdef __AVR__ +#include +#endif +// +// Group5 1-bit image compression library +// Written by Larry Bank +// Copyright (c) 2024 BitBank Software, Inc. +// +// Use of this software is governed by the Business Source License +// included in the file ./LICENSE. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// ./APL.txt. +// +// The name "Group5" is derived from the CCITT Group4 standard +// This code is based on a lot of the good ideas from CCITT T.6 +// for FAX image compression, but modified to work in a very +// constrained environment. The Huffman tables for horizontal +// mode have been replaced with a simple 2-bit flag followed by +// short or long counts of a fixed length. The short codes are +// always 3 bits (run lengths 0-7) and the long codes are the +// number of bits needed to encode the width of the image. +// For example, if a 320 pixel wide image is being compressed, +// the longest horizontal run needed is 320, which requires 9 +// bits to encode. The 2 prefix bits have the following meaning: +// 00 = short, short (3+3 bits) +// 01 = short, long (3+N bits) +// 10 = long, short (N+3 bits) +// 11 = long, long (N+N bits) +// The rest of the code works identically to Group4 2D FAX +// +// Caution - this is the maximum number of color changes per line +// The default value is set low to work embedded systems with little RAM +// for font compression, this is plenty since each line of a character should have +// a maximum of 7 color changes +// You can define this in your compiler macros to override the default vlaue +// + + +#define MAX_IMAGE_FLIPS 640 + +#ifndef MAX_IMAGE_FLIPS +#ifdef __AVR__ +#define MAX_IMAGE_FLIPS 32 +#else +#define MAX_IMAGE_FLIPS 512 +#endif // __AVR__ +#endif +// Horizontal prefix bits +enum { + HORIZ_SHORT_SHORT=0, + HORIZ_SHORT_LONG, + HORIZ_LONG_SHORT, + HORIZ_LONG_LONG +}; + +// Return code for encoder and decoder +enum { + G5_SUCCESS = 0, + G5_INVALID_PARAMETER, + G5_DECODE_ERROR, + G5_UNSUPPORTED_FEATURE, + G5_ENCODE_COMPLETE, + G5_DECODE_COMPLETE, + G5_NOT_INITIALIZED, + G5_DATA_OVERFLOW, + G5_MAX_FLIPS_EXCEEDED +}; +// +// Decoder state +// +typedef struct g5_dec_image_tag +{ + int iWidth, iHeight; // image size + int iError; + int y; // last y value drawn + int iVLCSize; + int iHLen; // length of 'long' horizontal codes for this image + int iPitch; // width in bytes of output buffer + uint32_t u32Accum; // fractional scaling accumulator + uint32_t ulBitOff, ulBits; // vlc decode variables + uint8_t *pSrc, *pBuf; // starting & current buffer pointer + int16_t *pCur, *pRef; // current state of current vs reference flips + int16_t CurFlips[MAX_IMAGE_FLIPS]; + int16_t RefFlips[MAX_IMAGE_FLIPS]; +} G5DECIMAGE; + +// Due to unaligned memory causing an exception, we have to do these macros the slow way +#ifdef __AVR__ +// assume PROGMEM as the source of data +inline uint32_t TIFFMOTOLONG(uint8_t *p) +{ + uint32_t u32 = pgm_read_dword(p); + return __builtin_bswap32(u32); +} +#else +#define TIFFMOTOLONG(p) (((uint32_t)(*p)<<24UL) + ((uint32_t)(*(p+1))<<16UL) + ((uint32_t)(*(p+2))<<8UL) + (uint32_t)(*(p+3))) +#endif // __AVR__ + +#define TOP_BIT 0x80000000 +#define MAX_VALUE 0xffffffff +// Must be a 32-bit target processor +#define REGISTER_WIDTH 32 +#define BIGUINT uint32_t + +// +// G5 Encoder +// + +typedef struct pil_buffered_bits +{ +unsigned char *pBuf; // buffer pointer +uint32_t ulBits; // buffered bits +uint32_t ulBitOff; // current bit offset +uint32_t ulDataSize; // available data +} BUFFERED_BITS; + +// +// Encoder state +// +typedef struct g5_enc_image_tag +{ + int iWidth, iHeight; // image size + int iError; + int y; // last y encoded + int iOutSize; + int iDataSize; // generated output size + uint8_t *pOutBuf; + int16_t *pCur, *pRef; // pointers to swap current and reference lines + BUFFERED_BITS bb; + int16_t CurFlips[MAX_IMAGE_FLIPS]; + int16_t RefFlips[MAX_IMAGE_FLIPS]; +} G5ENCIMAGE; + +// 16-bit marker at the start of a BB_FONT file +// (BitBank FontFile) +#define BB_FONT_MARKER 0xBBFF +// 16-bit marker at the start of a BB_BITMAP file +// (BitBank BitmapFile) +#define BB_BITMAP_MARKER 0xBBBF + +// Font info per character (glyph) +typedef struct { + uint16_t bitmapOffset; // Offset to compressed bitmap data for this glyph (starting from the end of the BB_GLYPH[] array) + uint8_t width; // bitmap width in pixels + uint8_t xAdvance; // total width in pixels (bitmap + padding) + uint16_t height; // bitmap height in pixels + int16_t xOffset; // left padding to upper left corner + int16_t yOffset; // padding from baseline to upper left corner (usually negative) +} BB_GLYPH; + +// This structure is stored at the beginning of a BB_FONT file +typedef struct { + uint16_t u16Marker; // 16-bit Marker defining a BB_FONT file + uint16_t first; // first char (ASCII value) + uint16_t last; // last char (ASCII value) + uint16_t height; // total height of font + uint32_t rotation; // store this as 32-bits to not have a struct packing problem + BB_GLYPH glyphs[]; // Array of glyphs (one for each char) +} BB_FONT; + +// This structure defines the start of a compressed bitmap file +typedef struct { + uint16_t u16Marker; // 16-bit marker defining a BB_BITMAP file + uint16_t width; + uint16_t height; + uint16_t size; // compressed data size (not including this 8-byte header) +} BB_BITMAP; + +#ifdef __cplusplus +// +// The G5 classes wrap portable C code which does the actual work +// +class G5ENCODER +{ + public: + int init(int iWidth, int iHeight, uint8_t *pOut, int iOutSize); + int encodeLine(uint8_t *pPixels); + int size(); + + private: + G5ENCIMAGE _g5enc; +}; +class G5DECODER +{ + public: + int init(int iWidth, int iHeight, uint8_t *pData, int iDataSize); + int decodeLine(uint8_t *pOut); + + private: + G5DECIMAGE _g5dec; +}; +#endif // __cplusplus + +#endif // __GROUP5__ diff --git a/ESP32_AP-Flasher/src/g5/g5dec.inl b/ESP32_AP-Flasher/src/g5/g5dec.inl new file mode 100644 index 00000000..07ebd527 --- /dev/null +++ b/ESP32_AP-Flasher/src/g5/g5dec.inl @@ -0,0 +1,338 @@ +// +// Group5 +// A 1-bpp image decoder +// +// Written by Larry Bank +// Copyright (c) 2024 BitBank Software, Inc. +// +// Use of this software is governed by the Business Source License +// included in the file ./LICENSE. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// ./APL.txt. +#include "Group5.h" + +/* + The code tree that follows has: bit_length, decode routine + These codes are for Group 4 (MMR) decoding + + 01 = vertneg1, 11h = vert1, 20h = horiz, 30h = pass, 12h = vert2 + 02 = vertneg2, 13h = vert3, 03 = vertneg3, 90h = trash +*/ + +static const uint8_t code_table[128] = + {0x90, 0, 0x40, 0, /* trash, uncompr mode - codes 0 and 1 */ + 3, 7, /* V(-3) pos = 2 */ + 0x13, 7, /* V(3) pos = 3 */ + 2, 6, 2, 6, /* V(-2) pos = 4,5 */ + 0x12, 6, 0x12, 6, /* V(2) pos = 6,7 */ + 0x30, 4, 0x30, 4, 0x30, 4, 0x30, 4, /* pass pos = 8->F */ + 0x30, 4, 0x30, 4, 0x30, 4, 0x30, 4, + 0x20, 3, 0x20, 3, 0x20, 3, 0x20, 3, /* horiz pos = 10->1F */ + 0x20, 3, 0x20, 3, 0x20, 3, 0x20, 3, + 0x20, 3, 0x20, 3, 0x20, 3, 0x20, 3, + 0x20, 3, 0x20, 3, 0x20, 3, 0x20, 3, +/* V(-1) pos = 20->2F */ + 1, 3, 1, 3, 1, 3, 1, 3, + 1, 3, 1, 3, 1, 3, 1, 3, + 1, 3, 1, 3, 1, 3, 1, 3, + 1, 3, 1, 3, 1, 3, 1, 3, + 0x11, 3, 0x11, 3, 0x11, 3, 0x11, 3, /* V(1) pos = 30->3F */ + 0x11, 3, 0x11, 3, 0x11, 3, 0x11, 3, + 0x11, 3, 0x11, 3, 0x11, 3, 0x11, 3, + 0x11, 3, 0x11, 3, 0x11, 3, 0x11, 3}; + +static int g5_decode_init(G5DECIMAGE *pImage, int iWidth, int iHeight, uint8_t *pData, int iDataSize) +{ + if (pImage == NULL || iWidth < 1 || iHeight < 1 || pData == NULL || iDataSize < 1) + return G5_INVALID_PARAMETER; + + pImage->iVLCSize = iDataSize; + pImage->pSrc = pData; + pImage->ulBitOff = 0; + pImage->y = 0; + pImage->ulBits = TIFFMOTOLONG(pData); // preload the first 32 bits of data + pImage->iWidth = iWidth; + pImage->iHeight = iHeight; + return G5_SUCCESS; + +} /* g5_decode_init() */ + +static void G5DrawLine(G5DECIMAGE *pPage, int16_t *pCurFlips, uint8_t *pOut) +{ + int x, len, run; + uint8_t lBit, rBit, *p; + int iStart = 0, xright = pPage->iWidth; + uint8_t *pDest; + + iStart = 0; + + pDest = pOut; + len = (xright+7)>>3; // number of bytes to generate + for (x=0; x= xright || run == 0) + break; + if ((x + run) > 0) { /* If the run is visible, draw it */ + if (x < 0) { + run += x; /* draw only visible part of run */ + x = 0; + } + if ((x + run) > xright) { /* Don't let it go off right edge */ + run = xright - x; + } + /* Draw this run */ + lBit = 0xff << (8 - (x & 7)); + rBit = 0xff >> ((x + run) & 7); + len = ((x+run)>>3) - (x >> 3); + p = &pDest[x >> 3]; + if (len == 0) { + lBit |= rBit; + *p &= lBit; + } else { + *p++ &= lBit; + while (len > 1) { + *p++ = 0; + len--; + } + *p = rBit; + } + } // visible run + } /* while drawing line */ +} /* G5DrawLine() */ +// +// Initialize internal structures to decode the image +// +static void Decode_Begin(G5DECIMAGE *pPage) +{ + int i, xsize; + int16_t *CurFlips, *RefFlips; + + xsize = pPage->iWidth; + + RefFlips = pPage->RefFlips; + CurFlips = pPage->CurFlips; + + /* Seed the current and reference line with XSIZE for V(0) codes */ + for (i=0; i XSIZE + 3-16-94 */ + CurFlips[i] = RefFlips[i] = 0x7fff; + CurFlips[i+1] = RefFlips[i+1] = 0x7fff; + + pPage->pCur = CurFlips; + pPage->pRef = RefFlips; + pPage->pBuf = pPage->pSrc; + pPage->ulBits = TIFFMOTOLONG(pPage->pSrc); // load 32 bits to start + pPage->ulBitOff = 0; + // Calculate the number of bits needed for a long horizontal code +#ifdef __AVR__ + pPage->iHLen = 16 - __builtin_clz(pPage->iWidth); +#else + pPage->iHLen = 32 - __builtin_clz(pPage->iWidth); +#endif +} /* Decode_Begin() */ +// +// Decode a single line of G5 data (private function) +// +static int DecodeLine(G5DECIMAGE *pPage) +{ + signed int a0, a0_p, b1; + int16_t *pCur, *pRef, *RefFlips, *CurFlips; + int xsize, tot_run=0, tot_run1 = 0; + int32_t sCode; + uint32_t lBits; + uint32_t ulBits, ulBitOff; + uint8_t *pBuf/*, *pBufEnd*/; + uint32_t u32HMask, u32HLen; // horizontal code mask and length + + pCur = CurFlips = pPage->pCur; + pRef = RefFlips = pPage->pRef; + ulBits = pPage->ulBits; + ulBitOff = pPage->ulBitOff; + pBuf = pPage->pBuf; + // pBufEnd = &pPage->pSrc[pPage->iVLCSize]; + u32HLen = pPage->iHLen; + u32HMask = (1 << u32HLen) - 1; + a0 = -1; + xsize = pPage->iWidth; + + while (a0 < xsize) { /* Decode this line */ + if (ulBitOff > (REGISTER_WIDTH-8)) { // need at least 7 unused bits + pBuf += (ulBitOff >> 3); + ulBitOff &= 7; + ulBits = TIFFMOTOLONG(pBuf); + } + if ((int32_t)(ulBits << ulBitOff) < 0) { /* V(0) code is the most frequent case (1 bit) */ + a0 = *pRef++; + ulBitOff++; // length = 1 bit + *pCur++ = a0; + } else { /* Slower method for the less frequence codes */ + lBits = (ulBits >> ((REGISTER_WIDTH - 8) - ulBitOff)) & 0xfe; /* Only the first 7 bits are useful */ + sCode = code_table[lBits]; /* Get the code type as an 8-bit value */ + ulBitOff += code_table[lBits+1]; /* Get the code length */ + switch (sCode) { + case 1: /* V(-1) */ + case 2: /* V(-2) */ + case 3: /* V(-3) */ + a0 = *pRef - sCode; /* A0 = B1 - x */ + *pCur++ = a0; + if (pRef == RefFlips) { + pRef += 2; + } + pRef--; + while (a0 >= *pRef) { + pRef += 2; + } + break; + + case 0x11: /* V(1) */ + case 0x12: /* V(2) */ + case 0x13: /* V(3) */ + a0 = *pRef++; /* A0 = B1 */ + b1 = a0; + a0 += sCode & 7; /* A0 = B1 + x */ + if (b1 != xsize && a0 < xsize) { + while (a0 >= *pRef) { + pRef += 2; + } + } + if (a0 > xsize) { + a0 = xsize; + } + *pCur++ = a0; + break; + + case 0x20: /* Horizontal codes */ + if (ulBitOff > (REGISTER_WIDTH-16)) { // need at least 16 unused bits + pBuf += (ulBitOff >> 3); + ulBitOff &= 7; + ulBits = TIFFMOTOLONG(pBuf); + } + a0_p = a0; + if (a0 < 0) { + a0_p = 0; + } + lBits = (ulBits >> ((REGISTER_WIDTH - 2) - ulBitOff)) & 0x3; // get 2-bit prefix for code type + // There are 4 possible horizontal cases: short/short, short/long, long/short, long/long + // These are encoded in a 2-bit prefix code, followed by 3 bits for short or N bits for long code + // N is the log base 2 of the image width (e.g. 320 pixels requires 9 bits) + ulBitOff += 2; + switch (lBits) { + case HORIZ_SHORT_SHORT: + tot_run = (ulBits >> ((REGISTER_WIDTH - 3) - ulBitOff)) & 0x7; // get 3-bit short length + ulBitOff += 3; + tot_run1 = (ulBits >> ((REGISTER_WIDTH - 3) - ulBitOff)) & 0x7; // get 3-bit short length + ulBitOff += 3; + break; + case HORIZ_SHORT_LONG: + tot_run = (ulBits >> ((REGISTER_WIDTH - 3) - ulBitOff)) & 0x7; // get 3-bit short length + ulBitOff += 3; + tot_run1 = (ulBits >> ((REGISTER_WIDTH - u32HLen) - ulBitOff)) & u32HMask; // get long length + ulBitOff += u32HLen; + break; + case HORIZ_LONG_SHORT: + tot_run = (ulBits >> ((REGISTER_WIDTH - u32HLen) - ulBitOff)) & u32HMask; // get long length + ulBitOff += u32HLen; + tot_run1 = (ulBits >> ((REGISTER_WIDTH - 3) - ulBitOff)) & 0x7; // get 3-bit short length + ulBitOff += 3; + break; + case HORIZ_LONG_LONG: + tot_run = (ulBits >> ((REGISTER_WIDTH - u32HLen) - ulBitOff)) & u32HMask; // get long length + ulBitOff += u32HLen; + if (ulBitOff > (REGISTER_WIDTH-16)) { // need at least 16 unused bits + pBuf += (ulBitOff >> 3); + ulBitOff &= 7; + ulBits = TIFFMOTOLONG(pBuf); + } + tot_run1 = (ulBits >> ((REGISTER_WIDTH - u32HLen) - ulBitOff)) & u32HMask; // get long length + ulBitOff += u32HLen; + break; + } // switch on lBits + a0 = a0_p + tot_run; + *pCur++ = a0; + a0 += tot_run1; + if (a0 < xsize) { + while (a0 >= *pRef) { + pRef += 2; + } + } + *pCur++ = a0; + break; + + case 0x30: /* Pass code */ + pRef++; /* A0 = B2, iRef+=2 */ + a0 = *pRef++; + break; + + default: /* ERROR */ + pPage->iError = G5_DECODE_ERROR; + goto pilreadg5z; + } /* switch */ + } /* Slow climb */ + } + /*--- Convert flips data into run lengths ---*/ + *pCur++ = xsize; /* Terminate the line properly */ + *pCur++ = xsize; +pilreadg5z: + // Save the current VLC decoder state + pPage->ulBits = ulBits; + pPage->ulBitOff = ulBitOff; + pPage->pBuf = pBuf; + return pPage->iError; +} /* DecodeLine() */ +// +// Decompress the VLC data +// +static int g5_decode_line(G5DECIMAGE *pPage, uint8_t *pOut) +{ + int rc; + uint8_t *pBufEnd; + int16_t *t1; + + if (pPage == NULL || pOut == NULL) + return G5_INVALID_PARAMETER; + if (pPage->y >= pPage->iHeight) + return G5_DECODE_COMPLETE; + + pPage->iError = G5_SUCCESS; + + if (pPage->y == 0) { // first time through + Decode_Begin(pPage); + } + pBufEnd = &pPage->pSrc[pPage->iVLCSize]; + + if (pPage->pBuf >= pBufEnd) { // read past the end, error + pPage->iError = G5_DECODE_ERROR; + return G5_DECODE_ERROR; + } + rc = DecodeLine(pPage); + if (rc == G5_SUCCESS) { + // Draw the current line + G5DrawLine(pPage, pPage->pCur, pOut); + /*--- Swap current and reference lines ---*/ + t1 = pPage->pRef; + pPage->pRef = pPage->pCur; + pPage->pCur = t1; + pPage->y++; + if (pPage->y >= pPage->iHeight) { + pPage->iError = G5_DECODE_COMPLETE; + } + } else { + pPage->iError = rc; + } + return pPage->iError; +} /* Decode() */ + diff --git a/ESP32_AP-Flasher/src/g5/g5enc.inl b/ESP32_AP-Flasher/src/g5/g5enc.inl new file mode 100644 index 00000000..e181c7c0 --- /dev/null +++ b/ESP32_AP-Flasher/src/g5/g5enc.inl @@ -0,0 +1,305 @@ +// +// G5 Encoder +// A 1-bpp image encoding library +// +// Written by Larry Bank +// Copyright (c) 2024 BitBank Software, Inc. +// +// Use of this software is governed by the Business Source License +// included in the file ./LICENSE. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// ./APL.txt. +#include "Group5.h" + +/* Number of consecutive 1 bits in a byte from MSB to LSB */ +static uint8_t bitcount[256] = + {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, /* 0-15 */ + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, /* 16-31 */ + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, /* 32-47 */ + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, /* 48-63 */ + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, /* 64-79 */ + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, /* 80-95 */ + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, /* 96-111 */ + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, /* 112-127 */ + 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, /* 128-143 */ + 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, /* 144-159 */ + 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, /* 160-175 */ + 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, /* 176-191 */ + 2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2, /* 192-207 */ + 2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2, /* 208-223 */ + 3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3, /* 224-239 */ + 4,4,4,4,4,4,4,4,5,5,5,5,6,6,7,8}; /* 240-255 */ + +/* Table of vertical codes for G5 encoding */ +/* code followed by length, starting with v(-3) */ +static const uint8_t vtable[14] = + {3,7, /* V(-3) = 0000011 */ + 3,6, /* V(-2) = 000011 */ + 3,3, /* V(-1) = 011 */ + 1,1, /* V(0) = 1 */ + 2,3, /* V(1) = 010 */ + 2,6, /* V(2) = 000010 */ + 2,7}; /* V(3) = 0000010 */ + + +static void G5ENCInsertCode(BUFFERED_BITS *bb, BIGUINT ulCode, int iLen) +{ + if ((bb->ulBitOff + iLen) > REGISTER_WIDTH) { // need to write data + bb->ulBits |= (ulCode >> (bb->ulBitOff + iLen - REGISTER_WIDTH)); // partial bits on first word + *(BIGUINT *)bb->pBuf = __builtin_bswap32(bb->ulBits); + bb->pBuf += sizeof(BIGUINT); + bb->ulBits = ulCode << ((REGISTER_WIDTH*2) - (bb->ulBitOff + iLen)); + bb->ulBitOff += iLen - REGISTER_WIDTH; + } else { + bb->ulBits |= (ulCode << (REGISTER_WIDTH - bb->ulBitOff - iLen)); + bb->ulBitOff += iLen; + } +} /* G5ENCInsertCode() */ +// +// Flush any buffered bits to the output +// +static void G5ENCFlushBits(BUFFERED_BITS *bb) +{ + while (bb->ulBitOff >= 8) + { + *bb->pBuf++ = (unsigned char) (bb->ulBits >> (REGISTER_WIDTH - 8)); + bb->ulBits <<= 8; + bb->ulBitOff -= 8; + } + *bb->pBuf++ = (unsigned char) (bb->ulBits >> (REGISTER_WIDTH - 8)); + bb->ulBitOff = 0; + bb->ulBits = 0; +} /* G5ENCFlushBits() */ +// +// Initialize the compressor +// This must be called before adding data to the output +// +int g5_encode_init(G5ENCIMAGE *pImage, int iWidth, int iHeight, uint8_t *pOut, int iOutSize) +{ + int iError = G5_SUCCESS; + + if (pImage == NULL || iHeight <= 0) + return G5_INVALID_PARAMETER; + pImage->iWidth = iWidth; // image size + pImage->iHeight = iHeight; + pImage->pCur = pImage->CurFlips; + pImage->pRef = pImage->RefFlips; + pImage->pOutBuf = pOut; // optional output buffer + pImage->iOutSize = iOutSize; // output buffer pre-allocated size + pImage->iDataSize = 0; // no data yet + pImage->y = 0; + for (int i=0; iRefFlips[i] = iWidth; + pImage->CurFlips[i] = iWidth; + } + pImage->bb.pBuf = pImage->pOutBuf; + pImage->bb.ulBits = 0; + pImage->bb.ulBitOff = 0; + pImage->iError = iError; + return iError; +} /* g5_encode_init() */ +// +// Internal function to convert uncompressed 1-bit per pixel data +// into the run-end data needed to feed the G5 encoder +// +static int G5ENCEncodeLine(unsigned char *buf, int xsize, int16_t *pDest) +{ +int iCount, xborder; +uint8_t i, c; +int8_t cBits; +int iLen; +int16_t x; +int16_t *pLimit = pDest + (MAX_IMAGE_FLIPS-4); + + xborder = xsize; + iCount = (xsize + 7) >> 3; /* Number of bytes per line */ + cBits = 8; + iLen = 0; /* Current run length */ + x = 0; + + c = *buf++; /* Get the first byte to start */ + iCount--; + while (iCount >=0) { + if (pDest >= pLimit) return G5_MAX_FLIPS_EXCEEDED; + i = bitcount[c]; /* Get the number of consecutive bits */ + iLen += i; /* Add this length to total run length */ + c <<= i; + cBits -= i; /* Minus the number in a byte */ + if (cBits <= 0) + { + iLen += cBits; /* Adjust length */ + cBits = 8; + c = *buf++; /* Get another data byte */ + iCount--; + continue; /* Keep doing white until color change */ + } + c = ~c; /* flip color to count black pixels */ + /* Store the white run length */ + xborder -= iLen; + if (xborder < 0) + { + iLen += xborder; /* Make sure run length is not past end */ + break; + } + x += iLen; + *pDest++ = x; + iLen = 0; +doblack: + i = bitcount[c]; /* Get consecutive bits */ + iLen += i; /* Add to total run length */ + c <<= i; + cBits -= i; + if (cBits <= 0) + { + iLen += cBits; /* Adjust length */ + cBits = 8; + c = *buf++; /* Get another data byte */ + c = ~c; /* Flip color to find black */ + iCount--; + if (iCount < 0) + break; + goto doblack; + } + /* Store the black run length */ + c = ~c; /* Flip color again to find white pixels */ + xborder -= iLen; + if (xborder < 0) + { + iLen += xborder; /* Make sure run length is not past end */ + break; + } + x += iLen; + *pDest++ = x; + iLen = 0; + } /* while */ + + x += iLen; + if (pDest >= pLimit) return G5_MAX_FLIPS_EXCEEDED; + *pDest++ = x; + *pDest++ = x; // Store a few more XSIZE to end the line + *pDest++ = x; // so that the compressor doesn't go past + *pDest++ = x; // the end of the line + return G5_SUCCESS; +} /* G5ENCEncodeLine() */ +// +// Compress a line of pixels and add it to the output +// the input format is expected to be MSB (most significant bit) first +// for example, pixel 0 is in byte 0 at bit 7 (0x80) +// Returns G5ENC_SUCCESS for each line if all is well and G5ENC_IMAGE_COMPLETE +// for the last line +// +int g5_encode_encodeLine(G5ENCIMAGE *pImage, uint8_t *pPixels) +{ +int16_t a0, a0_c, b2, a1; +int dx, run1, run2; +int xsize, iErr, iHighWater; +int iCur, iRef, iLen; +int iHLen; // number of bits for long horizontal codes +int16_t *CurFlips, *RefFlips; +BUFFERED_BITS bb; + + if (pImage == NULL || pPixels == NULL) + return G5_INVALID_PARAMETER; + iHighWater = pImage->iOutSize - 32; + iHLen = 32 - __builtin_clz(pImage->iWidth); + memcpy(&bb, &pImage->bb, sizeof(BUFFERED_BITS)); // keep local copy + CurFlips = pImage->pCur; + RefFlips = pImage->pRef; + xsize = pImage->iWidth; /* For performance reasons */ + + // Convert the incoming line of pixels into run-end data + iErr = G5ENCEncodeLine(pPixels, pImage->iWidth, CurFlips); + if (iErr != G5_SUCCESS) return iErr; // exceeded the maximum number of color changes + /* Encode this line as G5 */ + a0 = a0_c = 0; + iCur = iRef = 0; + while (a0 < xsize) { + b2 = RefFlips[iRef+1]; + a1 = CurFlips[iCur]; + if (b2 < a1) { /* Is b2 to the left of a1? */ + /* yes, do pass mode */ + a0 = b2; + iRef += 2; + G5ENCInsertCode(&bb, 1, 4); /* Pass code = 0001 */ + } else { /* Try vertical and horizontal mode */ + dx = RefFlips[iRef] - a1; /* b1 - a1 */ + if (dx > 3 || dx < -3) { /* Horizontal mode */ + G5ENCInsertCode(&bb, 1, 3); /* Horizontal code = 001 */ + run1 = CurFlips[iCur] - a0; + run2 = CurFlips[iCur+1] - CurFlips[iCur]; + if (run1 < 8) { + if (run2 < 8) { // short, short + G5ENCInsertCode(&bb, HORIZ_SHORT_SHORT, 2); /* short, short = 00 */ + G5ENCInsertCode(&bb, run1, 3); + G5ENCInsertCode(&bb, run2, 3); + } else { // short, long + G5ENCInsertCode(&bb, HORIZ_SHORT_LONG, 2); /* short, long = 01 */ + G5ENCInsertCode(&bb, run1, 3); + G5ENCInsertCode(&bb, run2, iHLen); + } + } else { // first run is long + if (run2 < 8) { // long, short + G5ENCInsertCode(&bb, HORIZ_LONG_SHORT, 2); /* long, short = 10 */ + G5ENCInsertCode(&bb, run1, iHLen); + G5ENCInsertCode(&bb, run2, 3); + } else { // long, long + G5ENCInsertCode(&bb, HORIZ_LONG_LONG, 2); /* long, long = 11 */ + G5ENCInsertCode(&bb, run1, iHLen); + G5ENCInsertCode(&bb, run2, iHLen); + } + } + a0 = CurFlips[iCur+1]; /* a0 = a2 */ + if (a0 != xsize) { + iCur += 2; /* Skip two color flips */ + while (RefFlips[iRef] != xsize && RefFlips[iRef] <= a0) { + iRef += 2; + } + } + } else { /* Vertical mode */ + dx = (dx + 3) * 2; /* Convert to index table */ + G5ENCInsertCode(&bb, vtable[dx], vtable[dx+1]); + a0 = a1; + a0_c = 1-a0_c; + if (a0 != xsize) { + if (iRef != 0) { + iRef -= 2; + } + iRef++; /* Skip a color change in cur and ref */ + iCur++; + while (RefFlips[iRef] <= a0 && RefFlips[iRef] != xsize) { + iRef += 2; + } + } + } /* vertical mode */ + } /* horiz/vert mode */ + } /* while x < xsize */ + iLen = (int)(bb.pBuf-pImage->pOutBuf); + if (iLen >= iHighWater) { // not enough space + pImage->iError = iErr = G5_DATA_OVERFLOW; // we don't have a better error + return iErr; + } + if (pImage->y == pImage->iHeight-1) { // last line of image + G5ENCFlushBits(&bb); // output the final buffered bits + // wrap up final output + pImage->iDataSize = (int)(bb.pBuf-pImage->pOutBuf); + iErr = G5_ENCODE_COMPLETE; + } + pImage->pCur = RefFlips; // swap current and reference lines + pImage->pRef = CurFlips; + pImage->y++; + memcpy(&pImage->bb, &bb, sizeof(bb)); + return iErr; +} /* g5_encode_encodeLine() */ +// +// Returns the number of bytes of G5 created by the encoder +// +int g5_encode_getOutSize(G5ENCIMAGE *pImage) +{ + int iSize = 0; + if (pImage != NULL) + iSize = pImage->iDataSize; + return iSize; +} /* g5_encode_getOutSize() */ diff --git a/ESP32_AP-Flasher/src/makeimage.cpp b/ESP32_AP-Flasher/src/makeimage.cpp index 5371dcaf..92332869 100644 --- a/ESP32_AP-Flasher/src/makeimage.cpp +++ b/ESP32_AP-Flasher/src/makeimage.cpp @@ -15,6 +15,10 @@ #include "ips_display.h" #endif +#include "commstructs.h" +#include "g5/Group5.h" +#include "g5/g5enc.inl" + TFT_eSPI tft = TFT_eSPI(); TFT_eSprite spr = TFT_eSprite(&tft); @@ -80,12 +84,11 @@ uint32_t colorDistance(Color &c1, Color &c2, Error &e1) { } void spr2color(TFT_eSprite &spr, imgParam &imageParams, uint8_t *buffer, size_t buffer_size, bool is_red) { - uint8_t rotate = imageParams.rotate; long bufw = spr.width(), bufh = spr.height(); if (imageParams.rotatebuffer % 2) { - //turn the image 90 or 270 + // turn the image 90 or 270 rotate = (rotate + 3) % 4; rotate = (rotate + (imageParams.rotatebuffer - 1)) % 4; bufw = spr.height(); @@ -286,6 +289,30 @@ void rewriteHeader(File &f_out) { f_out.write(flg); } +uint8_t *g5Compress(uint16_t width, uint16_t height, uint8_t *buffer, uint16_t buffersize, uint16_t &outBufferSize) { + G5ENCIMAGE g5enc; + int rc; + uint8_t *outbuffer = (uint8_t *)ps_malloc(buffersize + 16384); + if (outbuffer == NULL) { + Serial.println("Failed to allocate the output buffer for the G5 encoder"); + return nullptr; + } + + rc = g5_encode_init(&g5enc, width, height, outbuffer, buffersize + 16384); + for (int y = 0; y < height && rc == G5_SUCCESS; y++) { + rc = g5_encode_encodeLine(&g5enc, buffer); + buffer += (width / 8); + } + if (rc == G5_ENCODE_COMPLETE) { + outBufferSize = g5_encode_getOutSize(&g5enc); + } else { + printf("Encode failed! rc=%d\n", rc); + free(outbuffer); + return nullptr; + } + return outbuffer; +} + void spr2buffer(TFT_eSprite &spr, String &fileout, imgParam &imageParams) { long t = millis(); @@ -294,11 +321,11 @@ void spr2buffer(TFT_eSprite &spr, String &fileout, imgParam &imageParams) { if (fileout == "direct") { if (tftOverride == false) { TFT_eSprite spr2 = TFT_eSprite(&tft2); - #ifdef ST7735_NANO_TLSR +#ifdef ST7735_NANO_TLSR tft2.setRotation(1); - #else +#else tft2.setRotation(YellowSense == 1 ? 1 : 3); - #endif +#endif spr2.createSprite(spr.width(), spr.height()); spr2.setColorDepth(spr.getColorDepth()); @@ -307,20 +334,19 @@ void spr2buffer(TFT_eSprite &spr, String &fileout, imgParam &imageParams) { size_t dataSize = spr.width() * spr.height() * (spr.getColorDepth() / 8); memcpy(spriteData2, spriteData, dataSize); - #ifdef HAS_LILYGO_TPANEL - if (spr.getColorDepth() == 16) - { - long dy = spr.height(); - long dx = spr.width(); - - uint16_t* data = static_cast(const_cast(spriteData2)); +#ifdef HAS_LILYGO_TPANEL + if (spr.getColorDepth() == 16) { + long dy = spr.height(); + long dx = spr.width(); - gfx->draw16bitRGBBitmap(0, 0, (uint16_t *)spriteData2, dx, dy); - spr2.deleteSprite(); - } - #else + uint16_t *data = static_cast(const_cast(spriteData2)); + + gfx->draw16bitRGBBitmap(0, 0, (uint16_t *)spriteData2, dx, dy); + spr2.deleteSprite(); + } +#else spr2.pushSprite(0, 0); - #endif +#endif } return; } @@ -339,6 +365,7 @@ void spr2buffer(TFT_eSprite &spr, String &fileout, imgParam &imageParams) { #else uint8_t *buffer = (uint8_t *)malloc(buffer_size); imageParams.zlib = 0; + imageParams.g5 = 0; #endif if (!buffer) { Serial.println("Failed to allocate buffer"); @@ -379,6 +406,68 @@ void spr2buffer(TFT_eSprite &spr, String &fileout, imgParam &imageParams) { free(comp); rewriteHeader(f_out); + } else if (imageParams.g5) { + // handling for G5-compressed image data + + uint8_t headerbuf[6]; + prepareHeader(headerbuf, bufw, bufh, imageParams, buffer_size); + f_out.write(headerbuf, sizeof(headerbuf)); + + uint16_t height = imageParams.height; // spr.height(); + uint16_t width = imageParams.width; + spr.width(); + if (imageParams.hasRed && imageParams.bpp > 1) { + uint8_t *newbuffer = (uint8_t *)ps_realloc(buffer, 2 * buffer_size); + if (newbuffer == NULL) { + Serial.println("Failed to allocate larger buffer for 2bpp G5"); + free(buffer); + f_out.close(); + xSemaphoreGive(fsMutex); + return; + } + buffer = newbuffer; + spr2color(spr, imageParams, buffer + buffer_size, buffer_size, true); + buffer_size *= 2; + // double the height, to do two layers sequentially + if (imageParams.rotatebuffer % 2) { + width *= 2; + } else { + height *= 2; + } + + } + uint16_t outbufferSize = 0; + uint8_t *outBuffer; + bool compressionSuccessful = true; + if (imageParams.rotatebuffer % 2) { + outBuffer = g5Compress(height, width, buffer, buffer_size, outbufferSize); + } else { + outBuffer = g5Compress(width, height, buffer, buffer_size, outbufferSize); + } + if (outBuffer == NULL) { + Serial.println("Failed to compress G5"); + compressionSuccessful = false; + } else { + printf("Compressed %d to %d bytes\n", buffer_size, outbufferSize); + if (outbufferSize > buffer_size) { + printf("That wasn't very useful, falling back to raw\n"); + compressionSuccessful = false; + f_out.seek(0); + } else { + f_out.write(outBuffer, outbufferSize); + } + free(outBuffer); + } + if (!compressionSuccessful) { + // if we failed to compress the image, or the resulting image was larger than a raw file, fallback + imageParams.g5 = false; + if (imageParams.hasRed && imageParams.bpp > 1) { + imageParams.dataType = DATATYPE_IMG_RAW_2BPP; + } else { + imageParams.dataType = DATATYPE_IMG_RAW_1BPP; + } + f_out.write(buffer, buffer_size); + } } else { f_out.write(buffer, buffer_size); if (imageParams.hasRed && imageParams.bpp > 1) { diff --git a/ESP32_AP-Flasher/src/newproto.cpp b/ESP32_AP-Flasher/src/newproto.cpp index 9fd3cb3d..ffade1f4 100644 --- a/ESP32_AP-Flasher/src/newproto.cpp +++ b/ESP32_AP-Flasher/src/newproto.cpp @@ -274,6 +274,7 @@ void prepareExternalDataAvail(struct pendingData* pending, IPAddress remoteIP) { case DATATYPE_IMG_ZLIB: case DATATYPE_IMG_RAW_1BPP: case DATATYPE_IMG_RAW_2BPP: + case DATATYPE_IMG_G5: case DATATYPE_IMG_RAW_3BPP: { char hexmac[17]; mac2hex(pending->targetMac, hexmac); @@ -446,7 +447,7 @@ void processXferComplete(struct espXferComplete* xfc, bool local) { contentFS->remove(dst_path); } if (contentFS->exists(queueItem->filename)) { - if (config.preview && (queueItem->pendingdata.availdatainfo.dataType == DATATYPE_IMG_RAW_3BPP || queueItem->pendingdata.availdatainfo.dataType == DATATYPE_IMG_RAW_2BPP || queueItem->pendingdata.availdatainfo.dataType == DATATYPE_IMG_RAW_1BPP || queueItem->pendingdata.availdatainfo.dataType == DATATYPE_IMG_ZLIB)) { + if (config.preview && (queueItem->pendingdata.availdatainfo.dataType == DATATYPE_IMG_RAW_3BPP || queueItem->pendingdata.availdatainfo.dataType == DATATYPE_IMG_RAW_2BPP || queueItem->pendingdata.availdatainfo.dataType == DATATYPE_IMG_RAW_1BPP || queueItem->pendingdata.availdatainfo.dataType == DATATYPE_IMG_G5 || queueItem->pendingdata.availdatainfo.dataType == DATATYPE_IMG_ZLIB)) { contentFS->rename(queueItem->filename, String(dst_path)); } else { if (queueItem->pendingdata.availdatainfo.dataType != DATATYPE_FW_UPDATE) contentFS->remove(queueItem->filename); @@ -967,7 +968,7 @@ bool queueDataAvail(struct pendingData* pending, bool local) { } newPending.len = taginfo->len; - if ((pending->availdatainfo.dataType == DATATYPE_IMG_RAW_1BPP || pending->availdatainfo.dataType == DATATYPE_IMG_RAW_2BPP || pending->availdatainfo.dataType == DATATYPE_IMG_RAW_3BPP || pending->availdatainfo.dataType == DATATYPE_IMG_ZLIB) && (pending->availdatainfo.dataTypeArgument & 0xF8) == 0x00) { + if ((pending->availdatainfo.dataType == DATATYPE_IMG_RAW_1BPP || pending->availdatainfo.dataType == DATATYPE_IMG_RAW_2BPP || pending->availdatainfo.dataType == DATATYPE_IMG_RAW_3BPP || pending->availdatainfo.dataType == DATATYPE_IMG_ZLIB || pending->availdatainfo.dataType == DATATYPE_IMG_G5) && (pending->availdatainfo.dataTypeArgument & 0xF8) == 0x00) { // in case of an image (no preload), remove already queued images pendingQueue.erase(std::remove_if(pendingQueue.begin(), pendingQueue.end(), [pending](const PendingItem& item) { diff --git a/ESP32_AP-Flasher/src/tag_db.cpp b/ESP32_AP-Flasher/src/tag_db.cpp index 5191f329..925ff1cd 100644 --- a/ESP32_AP-Flasher/src/tag_db.cpp +++ b/ESP32_AP-Flasher/src/tag_db.cpp @@ -334,9 +334,9 @@ void initAPconfig() { config.sleepTime2 = APconfig.containsKey("sleeptime2") ? APconfig["sleeptime2"] : 0; config.ble = APconfig.containsKey("ble") ? APconfig["ble"] : 0; config.discovery = APconfig.containsKey("discovery") ? APconfig["discovery"] : 0; - #ifdef BLE_ONLY +#ifdef BLE_ONLY config.ble = true; - #endif +#endif // default wifi power 8.5 dbM // see https://github.com/espressif/arduino-esp32/blob/master/libraries/WiFi/src/WiFiGeneric.h#L111 config.wifiPower = APconfig.containsKey("wifipower") ? APconfig["wifipower"] : 34; @@ -395,6 +395,7 @@ HwType getHwType(const uint8_t id) { filter["bpp"] = true; filter["shortlut"] = true; filter["zlib_compression"] = true; + filter["g5_compression"] = true; filter["highlight_color"] = true; filter["colortable"] = true; StaticJsonDocument<1000> doc; @@ -416,6 +417,11 @@ HwType getHwType(const uint8_t id) { } else { hwType.zlib = 0; } + if (doc.containsKey("g5_compression")) { + hwType.g5 = strtol(doc["g5_compression"], nullptr, 16); + } else { + hwType.g5 = 0; + } hwType.highlightColor = doc.containsKey("highlight_color") ? doc["highlight_color"].as() : 2; JsonObject colorTable = doc["colortable"]; for (auto kv : colorTable) { diff --git a/ESP32_AP-Flasher/wwwroot/edit.html b/ESP32_AP-Flasher/wwwroot/edit.html index 3e7c7109..2c29dbcf 100644 --- a/ESP32_AP-Flasher/wwwroot/edit.html +++ b/ESP32_AP-Flasher/wwwroot/edit.html @@ -425,7 +425,7 @@ const canvas = $('#previewimg'); canvas.style.display = 'block'; - fetch(path) + fetch(path + "?r=" + Math.random()) .then(response => response.arrayBuffer()) .then(buffer => { @@ -433,6 +433,16 @@ if (tagTypes[hwtype].zlib > 0 && targetDiv.dataset.ver >= tagTypes[hwtype].zlib) { data = window.opener.processZlib(data); } + if (data.length > 0 && tagTypes[hwtype].g5 > 0 && targetDiv.dataset.ver >= tagTypes[hwtype].g5) { + const headerSize = data[0]; + let bufw = (data[2] << 8) | data[1]; + let bufh = (data[4] << 8) | data[3]; + if ((bufw == tagTypes[hwtype].width || bufw == tagTypes[hwtype].height) && (bufh == tagTypes[hwtype].width || bufh == tagTypes[hwtype].height) && (data[5] <= 3)) { + // valid header for g5 compression + if (data[5] == 2) bufh *= 2; + data = window.opener.processG5(data.subarray(headerSize), bufw, bufh); + } + } [canvas.width, canvas.height] = [tagTypes[hwtype].width, tagTypes[hwtype].height] || [0, 0]; if (tagTypes[hwtype].rotatebuffer%2) [canvas.width, canvas.height] = [canvas.height, canvas.width]; diff --git a/ESP32_AP-Flasher/wwwroot/g5decoder.js b/ESP32_AP-Flasher/wwwroot/g5decoder.js new file mode 100644 index 00000000..f54cdf4a --- /dev/null +++ b/ESP32_AP-Flasher/wwwroot/g5decoder.js @@ -0,0 +1,377 @@ +// +// Group5 +// A 1-bpp image decoder +// +// Written by Larry Bank +// Copyright (c) 2024 BitBank Software, Inc. +// +// Use of this software is governed by the Business Source License +// included in the file ./LICENSE. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// ./APL.txt. + +// Converted from C to Javascript by Nic Limper + +// Define constants +const MAX_IMAGE_FLIPS = 640; + +// Horizontal prefix bits +const HORIZ_SHORT_SHORT = 0; +const HORIZ_SHORT_LONG = 1; +const HORIZ_LONG_SHORT = 2; +const HORIZ_LONG_LONG = 3; + +// Return code for encoder and decoder +const G5_SUCCESS = 0; +const G5_INVALID_PARAMETER = 1; +const G5_DECODE_ERROR = 2; +const G5_UNSUPPORTED_FEATURE = 3; +const G5_ENCODE_COMPLETE = 4; +const G5_DECODE_COMPLETE = 5; +const G5_NOT_INITIALIZED = 6; +const G5_DATA_OVERFLOW = 7; +const G5_MAX_FLIPS_EXCEEDED = 8; + +// Utility function equivalent to the TIFFMOTOLONG macro +function TIFFMOTOLONG(p, ix) { + let value = 0; + if (ix < p.length) value |= p[ix] << 24; + if (ix + 1 < p.length) value |= p[ix + 1] << 16; + if (ix + 2 < p.length) value |= p[ix + 2] << 8; + if (ix + 3 < p.length) value |= p[ix + 3]; + return value; +} + +// Constants for bit manipulation +const REGISTER_WIDTH = 32; // Must align with a 32-bit system in C++ + +/* + The code tree that follows has: bit_length, decode routine + These codes are for Group 4 (MMR) decoding + + 01 = vertneg1, 11h = vert1, 20h = horiz, 30h = pass, 12h = vert2 + 02 = vertneg2, 13h = vert3, 03 = vertneg3, 90h = trash +*/ + +const code_table = [ + 0x90, 0, 0x40, 0, // trash, uncompressed mode - codes 0 and 1 + 3, 7, // V(-3) pos = 2 + 0x13, 7, // V(3) pos = 3 + 2, 6, 2, 6, // V(-2) pos = 4,5 + 0x12, 6, 0x12, 6, // V(2) pos = 6,7 + 0x30, 4, 0x30, 4, 0x30, 4, 0x30, 4, // pass pos = 8->F + 0x30, 4, 0x30, 4, 0x30, 4, 0x30, 4, + 0x20, 3, 0x20, 3, 0x20, 3, 0x20, 3, // horiz pos = 10->1F + 0x20, 3, 0x20, 3, 0x20, 3, 0x20, 3, + 0x20, 3, 0x20, 3, 0x20, 3, 0x20, 3, + 0x20, 3, 0x20, 3, 0x20, 3, 0x20, 3, // V(-1) pos = 20->2F + 1, 3, 1, 3, 1, 3, 1, 3, + 1, 3, 1, 3, 1, 3, 1, 3, + 1, 3, 1, 3, 1, 3, 1, 3, + 1, 3, 1, 3, 1, 3, 1, 3, + 0x11, 3, 0x11, 3, 0x11, 3, 0x11, 3, // V(1) pos = 30->3F + 0x11, 3, 0x11, 3, 0x11, 3, 0x11, 3, + 0x11, 3, 0x11, 3, 0x11, 3, 0x11, 3, + 0x11, 3, 0x11, 3, 0x11, 3, 0x11, 3 +]; + +class G5DECIMAGE { + constructor() { + this.iWidth = 0; + this.iHeight = 0; + this.iError = 0; + this.y = 0; + this.iVLCSize = 0; + this.iHLen = 0; + this.iPitch = 0; + this.u32Accum = 0; + this.ulBitOff = 0; + this.ulBits = 0; + this.pSrc = null; // Input buffer + this.pBuf = null; // Current buffer index + this.pBufIndex = 0; + this.pCur = new Int16Array(MAX_IMAGE_FLIPS); // Current state + this.pRef = new Int16Array(MAX_IMAGE_FLIPS); // Reference state + } +} + + +//static int g5_decode_init(G5DECIMAGE *pImage, int iWidth, int iHeight, uint8_t *pData, int iDataSize) +function g5_decode_init(pImage, iWidth, iHeight, pData, iDataSize) { + if ( + pImage == null || + iWidth < 1 || + iHeight < 1 || + pData == null || + iDataSize < 1 + ) { + return G5_INVALID_PARAMETER; + } + + pImage.iVLCSize = iDataSize; + pImage.pSrc = pData; + pImage.ulBitOff = 0; + pImage.y = 0; + pImage.ulBits = TIFFMOTOLONG(pData, 0); // Preload the first 32 bits of data + pImage.iWidth = iWidth; + pImage.iHeight = iHeight; + + return G5_SUCCESS; +} + + +//static void G5DrawLine(G5DECIMAGE *pPage, int16_t *pCurFlips, uint8_t *pOut) +function G5DrawLine(pPage, pCurFlips, pOut) { + const xright = pPage.iWidth; + let pCurIndex = 0; + + // Initialize output to white (0xff) + const len = (xright + 7) >> 3; // Number of bytes to generate + pOut.fill(0xff, 0, len); + + let x = 0; + while (x < xright) { // While the scaled x is within the window bounds + const startX = pCurFlips[pCurIndex++]; // Black starting point + const run = pCurFlips[pCurIndex++] - startX; // Get the black run + + if (startX >= xright || run <= 0) break; + + // Calculate visible run + let visibleX = Math.max(0, startX); + let visibleRun = Math.min(xright, startX + run) - visibleX; + + if (visibleRun > 0) { + const startByte = visibleX >> 3; + const endByte = (visibleX + visibleRun) >> 3; + + const lBit = (0xff << (8 - (visibleX & 7))) & 0xff; // Left bitmask based on the starting x position + const rBit = 0xff >> ((visibleX + visibleRun) & 7); // Right bitmask based on the ending x position + + if (endByte == startByte) { + // If the run fits in a single byte, combine left and right bit masks + pOut[startByte] &= (lBit | rBit); + } else { + // Mask the left-most byte + pOut[startByte] &= lBit; + + // Set intermediate bytes to 0 + for (let i = startByte + 1; i < endByte; i++) { + pOut[i] = 0x00; + } + + // Mask the right-most byte if it's not fully aligned + pOut[endByte] &= rBit; + } + } + } +} + + +// Initialize internal structures to decode the image +// +function Decode_Begin(pPage) { + const xsize = pPage.iWidth; + + // Seed the current and reference lines with xsize for V(0) codes + for (let i = 0; i < MAX_IMAGE_FLIPS - 2; i++) { + pPage.pRef[i] = xsize; + pPage.pCur[i] = xsize; + } + + // Prefill both current and reference lines with 0x7fff to prevent walking off the end + // if the data gets bunged and the current X is > XSIZE + pPage.pCur[MAX_IMAGE_FLIPS - 2] = pPage.pRef[MAX_IMAGE_FLIPS - 2] = 0x7fff; + pPage.pCur[MAX_IMAGE_FLIPS - 1] = pPage.pRef[MAX_IMAGE_FLIPS - 1] = 0x7fff; + + pPage.pBuf = pPage.pSrc; // Start buffer + pPage.pBufIndex = 0; + + // Load 32 bits to start (use a helper function to interpret bytes as a 32-bit integer) + pPage.ulBits = TIFFMOTOLONG(pPage.pSrc, 0); + pPage.ulBitOff = 0; + + // Calculate the number of bits needed for a long horizontal code + pPage.iHLen = 32 - Math.clz32(pPage.iWidth); // clz32 counts leading zeroes in JavaScript +} + + +// Decode a single line of G5 data +// +function DecodeLine(pPage) { + let a0 = -1; + let a0_p, b1; + let pCurIndex = 0, pRefIndex = 0; + const pCur = pPage.pCur; + const pRef = pPage.pRef; + let ulBits = pPage.ulBits; + let ulBitOff = pPage.ulBitOff; + let pBufIndex = pPage.pBufIndex; + const pBuf = pPage.pBuf; + const xsize = pPage.iWidth; + const u32HLen = pPage.iHLen; + const u32HMask = (1 << u32HLen) - 1; + let tot_run, tot_run1; + + while (a0 < xsize) { + if (ulBitOff > (REGISTER_WIDTH - 8)) { + pBufIndex += (ulBitOff >> 3); + ulBitOff &= 7; + ulBits = TIFFMOTOLONG(pBuf, pBufIndex); + } + + if (((ulBits << ulBitOff) & 0x80000000) !== 0) { + a0 = pRef[pRefIndex++]; + pCur[pCurIndex++] = a0; + ulBitOff++; + } else { + const lBits = (ulBits >> (REGISTER_WIDTH - 8 - ulBitOff)) & 0xfe; + const sCode = code_table[lBits]; + ulBitOff += code_table[lBits + 1]; + switch (sCode) { + case 1: case 2: case 3: // V(-1), V(-2), V(-3) + a0 = pRef[pRefIndex] - sCode; // A0 = B1 - x + pCur[pCurIndex++] = a0; + if (pRefIndex == 0) { + pRefIndex += 2; + } + pRefIndex--; + while (a0 >= pRef[pRefIndex]) { + pRefIndex += 2; + } + break; + + case 0x11: case 0x12: case 0x13: // V(1), V(2), V(3) + a0 = pRef[pRefIndex++]; + b1 = a0; + a0 += sCode & 7; + if (b1 !== xsize && a0 < xsize) { + while (a0 >= pRef[pRefIndex]) { + pRefIndex += 2; + } + } + if (a0 > xsize) { + a0 = xsize; + } + pCur[pCurIndex++] = a0; + break; + + case 0x20: // Horizontal codes + if (ulBitOff > (REGISTER_WIDTH - 16)) { + pBufIndex += (ulBitOff >> 3); + ulBitOff &= 7; + ulBits = TIFFMOTOLONG(pBuf, pBufIndex); + } + + a0_p = Math.max(0, a0); + const lBits = (ulBits >> ((REGISTER_WIDTH - 2) - ulBitOff)) & 0x3; + ulBitOff += 2; + + switch (lBits) { + case HORIZ_SHORT_SHORT: + tot_run = (ulBits >> ((REGISTER_WIDTH - 3) - ulBitOff)) & 0x7; + ulBitOff += 3; + tot_run1 = (ulBits >> ((REGISTER_WIDTH - 3) - ulBitOff)) & 0x7; + ulBitOff += 3; + break; + + case HORIZ_SHORT_LONG: + tot_run = (ulBits >> ((REGISTER_WIDTH - 3) - ulBitOff)) & 0x7; + ulBitOff += 3; + tot_run1 = (ulBits >> ((REGISTER_WIDTH - u32HLen) - ulBitOff)) & u32HMask; + ulBitOff += u32HLen; + break; + + case HORIZ_LONG_SHORT: + tot_run = (ulBits >> ((REGISTER_WIDTH - u32HLen) - ulBitOff)) & u32HMask; + ulBitOff += u32HLen; + tot_run1 = (ulBits >> ((REGISTER_WIDTH - 3) - ulBitOff)) & 0x7; + ulBitOff += 3; + break; + + case HORIZ_LONG_LONG: + tot_run = (ulBits >> ((REGISTER_WIDTH - u32HLen) - ulBitOff)) & u32HMask; + ulBitOff += u32HLen; + if (ulBitOff > (REGISTER_WIDTH - 16)) { + pBufIndex += (ulBitOff >> 3); + ulBitOff &= 7; + ulBits = TIFFMOTOLONG(pBuf, pBufIndex); + } + tot_run1 = (ulBits >> ((REGISTER_WIDTH - u32HLen) - ulBitOff)) & u32HMask; + ulBitOff += u32HLen; + break; + } + a0 = a0_p + tot_run; + pCur[pCurIndex++] = a0; + a0 += tot_run1; + + if (a0 < xsize) { + while (a0 >= pRef[pRefIndex]) { + pRefIndex += 2; + } + } + pCur[pCurIndex++] = a0; + break; + + case 0x30: // Pass code + pRefIndex++; + a0 = pRef[pRefIndex++]; + break; + + default: // ERROR + pPage.iError = G5_DECODE_ERROR; + return pPage.iError; + break; + } + } + } + + pCur[pCurIndex++] = xsize; + pCur[pCurIndex++] = xsize; + + pPage.ulBits = ulBits; + pPage.ulBitOff = ulBitOff; + pPage.pBufIndex = pBufIndex; + + return pPage.iError; +} + + +function processG5(data, width, height) { + try { + let decoder = new G5DECIMAGE(); + let initResult = g5_decode_init(decoder, width, height, data, data.length); + + if (initResult !== G5_SUCCESS) { + throw new Error("Initialization failed with code: " + initResult); + } + + Decode_Begin(decoder); + + let outputBuffer = new Uint8Array(height * ((width + 7) >> 3)); // Adjust for byte alignment + + for (let y = 0; y < height; y++) { + let lineBuffer = outputBuffer.subarray(y * ((width + 7) >> 3), (y + 1) * ((width + 7) >> 3)); + decoder.y = y; + let decodeResult = DecodeLine(decoder); + + if (decodeResult !== G5_SUCCESS) { + console.log("Decoding error on line " + y + ": " + decoder.iError); + } + + G5DrawLine(decoder, decoder.pCur, lineBuffer); + const temp = decoder.pRef; + decoder.pRef = decoder.pCur; + decoder.pCur = temp; + } + + return outputBuffer; + } catch (error) { + console.error("Error during G5 decoding:", error.message); + return null; + } +} + + diff --git a/ESP32_AP-Flasher/wwwroot/index.html b/ESP32_AP-Flasher/wwwroot/index.html index a0b8d0f5..5969fba3 100644 --- a/ESP32_AP-Flasher/wwwroot/index.html +++ b/ESP32_AP-Flasher/wwwroot/index.html @@ -7,6 +7,7 @@ Open EPaper Link Access Point + 0 && tagTypes[hwtype].zlib > 0 && $('#tag' + tagmac).dataset.ver >= tagTypes[hwtype].zlib) { data = processZlib(data); } + if (data.length > 0 && tagTypes[hwtype].g5 > 0 && $('#tag' + tagmac).dataset.ver >= tagTypes[hwtype].g5) { + const headerSize = data[0]; + let bufw = (data[2] << 8) | data[1]; + let bufh = (data[4] << 8) | data[3]; + if ((bufw == tagTypes[hwtype].width || bufw == tagTypes[hwtype].height) && (bufh == tagTypes[hwtype].width || bufh == tagTypes[hwtype].height) && (data[5] <= 3)) { + // valid header for g5 compression + if (data[5] == 2) bufh *= 2; + data = processG5(data.subarray(headerSize), bufw, bufh); + } + } [canvas.width, canvas.height] = [tagTypes[hwtype].width, tagTypes[hwtype].height] || [0, 0]; if (tagTypes[hwtype].rotatebuffer % 2) [canvas.width, canvas.height] = [canvas.height, canvas.width]; @@ -1521,6 +1531,7 @@ async function getTagtype(hwtype) { contentids: Object.values(jsonData.contentids ?? []), options: Object.values(jsonData.options ?? []), zlib: parseInt(jsonData.zlib_compression || "0", 16), + g5: parseInt(jsonData.g5_compression || "0", 16), shortlut: parseInt(jsonData.shortlut), busy: false, usetemplate: parseInt(jsonData.usetemplate || "0", 10) @@ -1947,4 +1958,4 @@ function showPreview(previewWindow, element) { console.error('fetch preview image error:', error); }); } -} \ No newline at end of file +} diff --git a/oepl-definitions.h b/oepl-definitions.h index 8238f060..66ea8c9f 100755 --- a/oepl-definitions.h +++ b/oepl-definitions.h @@ -123,8 +123,7 @@ // [uint32_t uncompressed size][2 byte zlib header][zlib compressed image] // image format: [uint8_t header length][uint16_t width][uint16_t height][uint8_t bpp (lower 4)][img data] -#define DATATYPE_IMG_G5_1BPP 0x31 // G5 compressed 1BPP -#define DATATYPE_IMG_G5_2BPP 0x32 // G5 compressed 2BPP +#define DATATYPE_IMG_G5 0x31 // G5 compressed 1BPP #define DATATYPE_UK_SEGMENTED 0x51 // Segmented data for the UK Segmented display type (contained in availableData Reply) #define DATATYPE_EU_SEGMENTED 0x52 // Segmented data for the EU/DE Segmented display type (contained in availableData Reply) diff --git a/resources/tagtypes/00.json b/resources/tagtypes/00.json index 440b55be..d47310c6 100644 --- a/resources/tagtypes/00.json +++ b/resources/tagtypes/00.json @@ -1,5 +1,5 @@ { - "version": 2, + "version": 3, "name": "M2 1.54\"", "width": 152, "height": 152, @@ -10,6 +10,7 @@ "black": [ 0, 0, 0 ], "red": [ 255, 0, 0 ] }, + "g5_compression": "29", "shortlut": 2, "options": [ "button", "customlut" ], "contentids": [ 22, 23, 1, 2, 3, 4, 5, 7, 10, 14, 15, 17, 18, 19, 20, 21, 27 ], diff --git a/resources/tagtypes/01.json b/resources/tagtypes/01.json index 4d4e7ad5..61149af8 100644 --- a/resources/tagtypes/01.json +++ b/resources/tagtypes/01.json @@ -1,5 +1,5 @@ { - "version": 2, + "version": 3, "name": "M2 2.9\"", "width": 296, "height": 128, @@ -10,6 +10,7 @@ "black": [ 0, 0, 0 ], "red": [ 255, 0, 0 ] }, + "g5_compression": "29", "shortlut": 2, "options": [ "button", "customlut" ], "contentids": [ 22, 23, 1, 2, 3, 4, 5, 7, 8, 9, 10, 11, 15, 16, 17, 18, 19, 20, 21, 27 ], diff --git a/resources/tagtypes/02.json b/resources/tagtypes/02.json index 02f9c4b3..c154c312 100644 --- a/resources/tagtypes/02.json +++ b/resources/tagtypes/02.json @@ -1,5 +1,5 @@ { - "version": 4, + "version": 5, "name": "M2 4.2\"", "width": 400, "height": 300, @@ -10,6 +10,7 @@ "black": [ 0, 0, 0 ], "red": [ 255, 0, 0 ] }, + "g5_compression": "29", "shortlut": 1, "options": [ "button" ], "contentids": [ 22, 23, 1, 4, 5, 7, 8, 9, 10, 11, 17, 18, 19, 20, 27 ], diff --git a/resources/tagtypes/03.json b/resources/tagtypes/03.json index ee6ffa6d..378475ac 100644 --- a/resources/tagtypes/03.json +++ b/resources/tagtypes/03.json @@ -1,5 +1,5 @@ { - "version": 4, + "version": 5, "name": "M2 2.2\"", "width": 212, "height": 104, @@ -10,6 +10,7 @@ "black": [ 0, 0, 0 ], "red": [ 255, 0, 0 ] }, + "g5_compression": "29", "shortlut": 2, "options": [ "button", "customlut" ], "contentids": [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 14, 15, 16, 17, 19, 20, 21, 22, 23, 27 ], diff --git a/resources/tagtypes/04.json b/resources/tagtypes/04.json index f96131f4..c8aeae10 100644 --- a/resources/tagtypes/04.json +++ b/resources/tagtypes/04.json @@ -1,5 +1,5 @@ { - "version": 2, + "version": 3, "name": "M2 2.6\"", "width": 296, "height": 152, @@ -10,6 +10,7 @@ "black": [ 0, 0, 0 ], "red": [ 255, 0, 0 ] }, + "g5_compression": "29", "shortlut": 2, "options": [ "button", "customlut" ], "contentids": [ 22, 23, 1, 2, 3, 4, 5, 7, 8, 9, 10, 11, 15, 16, 17, 18, 19, 20, 21, 27 ], diff --git a/resources/tagtypes/06.json b/resources/tagtypes/06.json index 528337d3..74de7fef 100644 --- a/resources/tagtypes/06.json +++ b/resources/tagtypes/06.json @@ -1,5 +1,5 @@ { - "version": 1, + "version": 2, "name": "Opticon 2.2\"", "width": 250, "height": 128, @@ -11,6 +11,7 @@ "red": [ 255, 0, 0 ], "yellow": [ 255, 255, 0 ] }, + "g5_compression": "29", "shortlut": 0, "options": [ "led" ], "contentids": [ 22, 23, 1, 2, 3, 4, 5, 7, 8, 9, 10, 11, 14, 15, 16, 17, 18, 19, 20, 21, 27 ], diff --git a/resources/tagtypes/07.json b/resources/tagtypes/07.json index 44911684..efaca8b2 100644 --- a/resources/tagtypes/07.json +++ b/resources/tagtypes/07.json @@ -1,5 +1,5 @@ { - "version": 1, + "version": 2, "name": "Opticon 2.9\"", "width": 296, "height": 128, @@ -11,6 +11,7 @@ "red": [ 255, 0, 0 ], "yellow": [ 255, 255, 0 ] }, + "g5_compression": "29", "shortlut": 2, "options": [ "led" ], "contentids": [ 22, 23, 1, 2, 3, 4, 5, 7, 8, 9, 10, 11, 14, 15, 16, 17, 18, 19, 20, 21, 27 ], diff --git a/resources/tagtypes/08.json b/resources/tagtypes/08.json index eb81a9d0..bee57db5 100644 --- a/resources/tagtypes/08.json +++ b/resources/tagtypes/08.json @@ -1,5 +1,5 @@ { - "version": 1, + "version": 2, "name": "Opticon 4.2\"", "width": 400, "height": 300, @@ -10,6 +10,7 @@ "black": [ 0, 0, 0 ], "red": [ 255, 0, 0 ] }, + "g5_compression": "29", "shortlut": 1, "options": [ ], "contentids": [ 22, 23, 1, 4, 5, 7, 8, 9, 10, 11, 17, 18, 19, 20, 27 ], diff --git a/resources/tagtypes/09.json b/resources/tagtypes/09.json index bdc48bed..fff4b62e 100644 --- a/resources/tagtypes/09.json +++ b/resources/tagtypes/09.json @@ -1,5 +1,5 @@ { - "version": 1, + "version": 2, "name": "Opticon 7.5\"", "width": 640, "height": 384, @@ -10,6 +10,7 @@ "black": [ 0, 0, 0 ], "red": [ 255, 0, 0] }, + "g5_compression": "29", "shortlut": 1, "options": [ ], "contentids": [ 22, 23, 1, 4, 5, 7, 8, 9, 10, 11, 17, 18, 19, 20, 27 ], diff --git a/resources/tagtypes/11.json b/resources/tagtypes/11.json index 96aa3051..e817b74b 100644 --- a/resources/tagtypes/11.json +++ b/resources/tagtypes/11.json @@ -1,5 +1,5 @@ { - "version": 1, + "version": 2, "name": "M2 2.9\" (UC8151)", "width": 296, "height": 128, @@ -10,6 +10,7 @@ "black": [ 0, 0, 0 ], "red": [ 255, 0, 0 ] }, + "g5_compression": "29", "shortlut": 0, "options": [ "button" ], "contentids": [ 22, 23, 1, 2, 3, 4, 5, 7, 8, 9, 10, 11, 14, 15, 16, 17, 18, 19, 20, 21, 27 ], diff --git a/resources/tagtypes/12.json b/resources/tagtypes/12.json index 81df6747..8cb70b6c 100644 --- a/resources/tagtypes/12.json +++ b/resources/tagtypes/12.json @@ -1,5 +1,5 @@ { - "version": 2, + "version": 3, "name": "M2 4.2\" UC", "width": 400, "height": 300, @@ -10,6 +10,7 @@ "black": [ 0, 0, 0 ], "red": [ 255, 0, 0 ] }, + "g5_compression": "29", "shortlut": 1, "options": [ "button" ], "contentids": [ 22, 23, 1, 4, 5, 7, 8, 9, 10, 11, 17, 18, 19, 20, 27 ], diff --git a/resources/tagtypes/22.json b/resources/tagtypes/22.json index dd7bcb77..0bf7b4a6 100644 --- a/resources/tagtypes/22.json +++ b/resources/tagtypes/22.json @@ -1,5 +1,5 @@ { - "version": 1, + "version": 2, "name": "M2 2.7\"", "width": 264, "height": 176, @@ -10,6 +10,7 @@ "black": [ 0, 0, 0 ], "red": [ 255, 0, 0 ] }, + "g5_compression": "29", "shortlut": 0, "options": [ "button" ], "contentids": [ 0, 1, 2, 3, 4, 5, 7, 8, 9, 10, 11, 17, 18, 19, 20 ], diff --git a/resources/tagtypes/27.json b/resources/tagtypes/27.json index 2fb9f78e..e3fdbe3e 100644 --- a/resources/tagtypes/27.json +++ b/resources/tagtypes/27.json @@ -1,5 +1,5 @@ { - "version": 1, + "version": 2, "name": "ST‐GM29MT1 2.9\"", "width": 296, "height": 128, @@ -9,6 +9,7 @@ "white": [ 255, 255, 255 ], "black": [ 0, 0, 0 ] }, + "g5_compression": "29", "highlight_color": 5, "shortlut": 0, "options": [ "button", "customlut" ],