Merge pull request #404 from OpenEPaperLink/G5-compress

Added G5 encoding
This commit is contained in:
Jelmer
2024-12-08 15:59:29 +01:00
committed by GitHub
27 changed files with 1426 additions and 56 deletions

View File

@@ -39,6 +39,7 @@ struct imgParam {
uint8_t preloadlut;
uint8_t zlib;
uint8_t g5;
};
void spr2buffer(TFT_eSprite &spr, String &fileout, imgParam &imageParams);

View File

@@ -93,6 +93,7 @@ struct HwType {
uint8_t bpp;
uint8_t shortlut;
uint8_t zlib;
uint8_t g5;
uint16_t highlightColor;
std::vector<Color> colortable;
};

View File

@@ -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<double>());
if (rain > 0) {
drawString(spr, String(rain) + "mm", dag * column1 + loc["rain"][0].as<int>(), loc["rain"][1], day[2], TC_DATUM, (rain > 10 ? imageParams.highlightColor : TFT_BLACK));
}
} else {
double fRain = daily["precipitation_sum"][dag].as<double>();
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<int>(), 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<double>());
if (rain > 0) {
drawString(spr, String(rain) + "mm", dag * column1 + loc["rain"][0].as<int>(), loc["rain"][1], day[2], TC_DATUM, (rain > 10 ? imageParams.highlightColor : TFT_BLACK));
}
} else {
double fRain = daily["precipitation_sum"][dag].as<double>();
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<int>(), loc["rain"][1], day[2], TC_DATUM, (fRain > 0.5 ? imageParams.highlightColor : TFT_BLACK));
}
}
}
drawString(spr, String(tmin) + " ", dag * column1 + day[0].as<int>(), 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 &currentOrientation, 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;
}

View File

@@ -0,0 +1,203 @@
#ifndef __GROUP5__
#define __GROUP5__
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <stdio.h>
#ifdef __AVR__
#include <avr/pgmspace.h>
#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__

View File

@@ -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<len; x++) {
pOut[x] = 0xff; // start with white and only draw the black runs
}
x = 0;
while (x < xright) { // while the scaled x is within the window bounds
x = *pCurFlips++; // black starting point
run = *pCurFlips++ - x; // get the black run
x -= iStart;
if (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<MAX_IMAGE_FLIPS-2; i++) {
RefFlips[i] = xsize;
CurFlips[i] = xsize;
}
/* Prefill both current and reference lines with 7fff to prevent it from
walking off the end if the data gets bunged and the current X is > 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() */

View File

@@ -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; i<MAX_IMAGE_FLIPS; i++) {
pImage->RefFlips[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() */

View File

@@ -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<uint16_t*>(const_cast<void*>(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<uint16_t *>(const_cast<void *>(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) {

View File

@@ -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) {

View File

@@ -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<uint16_t>() : 2;
JsonObject colorTable = doc["colortable"];
for (auto kv : colorTable) {

View File

@@ -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];

View File

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

View File

@@ -7,6 +7,7 @@
<title>Open EPaper Link Access Point</title>
<script src="main.js" defer></script>
<script src="g5decoder.js?2"></script>
<link rel="stylesheet" href="main.css" type="text/css" />
<!--<link rel="icon" type="image/vnd.icon" href="favicon.ico">-->
<link rel="stylesheet"

View File

@@ -1207,6 +1207,16 @@ function drawCanvas(buffer, canvas, hwtype, tagmac, doRotate) {
if (data.length > 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);
});
}
}
}

View File

@@ -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)

View File

@@ -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 ],

View File

@@ -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 ],

View File

@@ -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 ],

View File

@@ -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 ],

View File

@@ -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 ],

View File

@@ -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 ],

View File

@@ -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 ],

View File

@@ -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 ],

View File

@@ -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 ],

View File

@@ -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 ],

View File

@@ -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 ],

View File

@@ -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 ],

View File

@@ -1,5 +1,5 @@
{
"version": 1,
"version": 2,
"name": "STGM29MT1 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" ],