Files
2024-12-07 21:59:39 +01:00

378 lines
12 KiB
JavaScript

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