From ae87ac19605a1180bfa15fa0d79a05f07719fe1c Mon Sep 17 00:00:00 2001 From: Nic Limper Date: Sat, 7 Dec 2024 20:46:25 +0100 Subject: [PATCH] added javascript g5 decoder for previews --- ESP32_AP-Flasher/wwwroot/g5decoder.js | 392 ++++++++++++++++++++++++++ ESP32_AP-Flasher/wwwroot/index.html | 1 + ESP32_AP-Flasher/wwwroot/main.js | 8 +- 3 files changed, 400 insertions(+), 1 deletion(-) create mode 100644 ESP32_AP-Flasher/wwwroot/g5decoder.js diff --git a/ESP32_AP-Flasher/wwwroot/g5decoder.js b/ESP32_AP-Flasher/wwwroot/g5decoder.js new file mode 100644 index 00000000..e1146f16 --- /dev/null +++ b/ESP32_AP-Flasher/wwwroot/g5decoder.js @@ -0,0 +1,392 @@ +// +// 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 (pPage.y > 580) console.log("line " + visibleX + "-" + visibleRun); + + 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; + if (pPage.y > 580) console.log("ix: " + pBufIndex + " bit " + ulBitOff); + ulBits = TIFFMOTOLONG(pBuf, pBufIndex); + } + if (pPage.y > 580) console.log(ulBits.toString(2).padStart(32, '0') + " offset " + ulBitOff); + + if (((ulBits << ulBitOff) & 0x80000000) !== 0) { + if (pPage.y > 580) console.log("1> a0: " + pRef[pRefIndex] + " prefindex " + pRefIndex); + 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]; + if (pPage.y > 580) console.log("s: " + sCode); + switch (sCode) { + case 1: case 2: case 3: // V(-1), V(-2), V(-3) + a0 = pRef[pRefIndex] - sCode; // A0 = B1 - x + if (pPage.y > 580) console.log("01> a0: " + pRef[pRefIndex] + " scode " + sCode + " prefindex " + pRefIndex); + pCur[pCurIndex++] = a0; + if (pRefIndex == 0) { + pRefIndex += 2; + if (pPage.y > 580) console.log("01> =0 new prefindex " + pRefIndex); + } + pRefIndex--; + while (a0 >= pRef[pRefIndex]) { + pRefIndex += 2; + if (pPage.y > 580) console.log("01> >= new prefindex " + pRefIndex); + } + if (pPage.y > 580) console.log("01> new prefindex " + pRefIndex); + 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; + } + if (pPage.y > 580) console.log("H: " + tot_run + " - " + tot_run1); + 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 + console.log("scode " + sCode); + 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++) { + console.log(y + " " + decoder.pBufIndex + " " + data.length); + 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); + } + + if (y > 580) console.log("draw line " + y); + G5DrawLine(decoder, decoder.pCur, lineBuffer); + const temp = decoder.pRef; + decoder.pRef = decoder.pCur; + decoder.pCur = temp; + } + + console.log("Decoding complete."); + 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) { + console.log("calling G5 decoder"); + console.log(tagTypes[hwtype].width); + data = processG5(data, tagTypes[hwtype].height, tagTypes[hwtype].width * 2); + } [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 +1526,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 +1953,4 @@ function showPreview(previewWindow, element) { console.error('fetch preview image error:', error); }); } -} \ No newline at end of file +}