edp445/node_modules/@skyra/gifenc/dist/lib/LZWEncoder.js

353 lines
12 KiB
JavaScript

"use strict";
/**
* LZWEncoder
*
* Authors
* - Kevin Weiner (original Java version - kweiner@fmsware.com)
* - Thibault Imbert (AS3 version - bytearray.org)
* - Johan Nordberg (JS version - code@johan-nordberg.com)
* - Antonio Román (TS version - kyradiscord@gmail.com)
*
* Acknowledgements
* - GIFCOMPR.C - GIF Image compression routines
* - Lempel-Ziv compression based on 'compress'. GIF modifications by
* - David Rowley (mgardi@watdcsu.waterloo.edu)
* GIF Image compression - modified 'compress'
* Based on: compress.c - File compression ala IEEE Computer, June 1984.
* By Authors:
* - Spencer W. Thomas (decvax!harpo!utah-cs!utah-gr!thomas)
* - Jim McKie (decvax!mcvax!jim)
* - Steve Davies (decvax!vax135!petsd!peora!srd)
* - Ken Turkowski (decvax!decwrl!turtlevax!ken)
* - James A. Woods (decvax!ihnp4!ames!jaw)
* - Joe Orost (decvax!vax135!petsd!joe)
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.LZWEncoder = void 0;
const EOF = -1;
const BITS = 12;
const HASH_SIZE = 5003; // 80% occupancy
const masks = new Uint16Array([
0x0000, 0x0001, 0x0003, 0x0007, 0x000f, 0x001f, 0x003f, 0x007f, 0x00ff, 0x01ff, 0x03ff, 0x07ff, 0x0fff, 0x1fff, 0x3fff, 0x7fff, 0xffff
]);
/**
* @summary
* Algorithm: use open addressing double hashing (no chaining) on the prefix code / next character combination.
*
* We do a variant of Knuth's algorithm D (vol. 3, sec. 6.4) along with G. Knott's relatively-prime secondary probe.
* Here, the modular division first probe is gives way to a faster exclusive-or manipulation. Also do block compression
* with an adaptive reset, whereby the code table is cleared when the compression ratio decreases, but after the table
* fills. The variable-length output codes are re-sized at this point, and a special CLEAR code is generated for the
* decompression.
*
* **Late addition**: construct the table according to file size for noticeable speed improvement on small files. Please
* direct questions about this implementation to ames!jaw.
*/
class LZWEncoder {
/**
* Constructs a {@link LZWEncoder} instance.
* @param width The width of the image.
* @param height The height of the image.
* @param pixels The pixel data in RGB format.
* @param colorDepth The color depth.
*/
constructor(width, height, pixels, colorDepth) {
/**
* The GIF image's width, between `1` and `65536`.
*/
Object.defineProperty(this, "width", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
/**
* The GIF image's height, between `1` and `65536`.
*/
Object.defineProperty(this, "height", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "pixels", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "initCodeSize", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "currentAccumulator", {
enumerable: true,
configurable: true,
writable: true,
value: 0
});
Object.defineProperty(this, "currentBits", {
enumerable: true,
configurable: true,
writable: true,
value: 0
});
Object.defineProperty(this, "currentPixel", {
enumerable: true,
configurable: true,
writable: true,
value: 0
});
Object.defineProperty(this, "accumulator", {
enumerable: true,
configurable: true,
writable: true,
value: 0
});
Object.defineProperty(this, "firstUnusedEntry", {
enumerable: true,
configurable: true,
writable: true,
value: 0
}); // first unused entry
Object.defineProperty(this, "maximumCode", {
enumerable: true,
configurable: true,
writable: true,
value: 0
});
Object.defineProperty(this, "remaining", {
enumerable: true,
configurable: true,
writable: true,
value: 0
});
Object.defineProperty(this, "bitSize", {
enumerable: true,
configurable: true,
writable: true,
value: 0
});
// block compression parameters -- after all codes are used up,
// and compression rate changes, start over.
Object.defineProperty(this, "clearFlag", {
enumerable: true,
configurable: true,
writable: true,
value: false
});
Object.defineProperty(this, "globalInitialBits", {
enumerable: true,
configurable: true,
writable: true,
value: 0
});
Object.defineProperty(this, "clearCode", {
enumerable: true,
configurable: true,
writable: true,
value: 0
});
Object.defineProperty(this, "endOfFrameCode", {
enumerable: true,
configurable: true,
writable: true,
value: 0
});
Object.defineProperty(this, "accumulators", {
enumerable: true,
configurable: true,
writable: true,
value: new Uint8Array(256)
});
Object.defineProperty(this, "hashes", {
enumerable: true,
configurable: true,
writable: true,
value: new Int32Array(HASH_SIZE)
});
Object.defineProperty(this, "codes", {
enumerable: true,
configurable: true,
writable: true,
value: new Int32Array(HASH_SIZE)
});
this.width = width;
this.height = height;
this.pixels = pixels;
this.initCodeSize = Math.max(2, colorDepth);
}
/**
* Encodes the image into the output.
* @param output The byte buffer to write to.
*/
encode(output) {
output.writeByte(this.initCodeSize); // write "initial code size" byte
this.remaining = this.width * this.height; // reset navigation variables
this.currentPixel = 0;
this.compress(this.initCodeSize + 1, output); // compress and write the pixel data
output.writeByte(0); // write block terminator
}
/**
* Compresses the GIF data.
* @param initialBits The initial bits for the compression.
* @param output The byte buffer to write to.
*/
compress(initialBits, output) {
// Set up the globals: globalInitialBits - initial number of bits
this.globalInitialBits = initialBits;
// Set up the necessary values
this.clearFlag = false;
this.bitSize = this.globalInitialBits;
this.maximumCode = this.getMaximumCode(this.bitSize);
this.clearCode = 1 << (initialBits - 1);
this.endOfFrameCode = this.clearCode + 1;
this.firstUnusedEntry = this.clearCode + 2;
// Clear packet
this.accumulator = 0;
let code = this.nextPixel();
let hash = 80048;
const hashShift = 4;
const hashSizeRegion = HASH_SIZE;
this.resetHashRange(hashSizeRegion);
this.processOutput(this.clearCode, output);
let c;
outerLoop: while ((c = this.nextPixel()) !== EOF) {
hash = (c << BITS) + code;
// XOR hashing:
let i = (c << hashShift) ^ code;
if (this.hashes[i] === hash) {
code = this.codes[i];
continue;
}
if (this.hashes[i] >= 0) {
// Non-empty slot, perform secondary hash (after G. Knott):
let dispose = hashSizeRegion - i;
if (i === 0)
dispose = 1;
do {
if ((i -= dispose) < 0)
i += hashSizeRegion;
if (this.hashes[i] === hash) {
code = this.codes[i];
continue outerLoop;
}
} while (this.hashes[i] >= 0);
}
this.processOutput(code, output);
code = c;
if (this.firstUnusedEntry < 1 << BITS) {
// code -> hash-table
this.codes[i] = this.firstUnusedEntry++;
this.hashes[i] = hash;
}
else {
this.clearCodeTable(output);
}
}
// Put out the final code:
this.processOutput(code, output);
this.processOutput(this.endOfFrameCode, output);
}
/**
* Adds a character to the end of the current packet, and if it is at 254 characters, it flushes the packet to disk
* via {@link LZWEncoder.flushPacket}.
* @param c The character code to add.
* @param output The byte buffer to write to.
*/
addCharacter(c, output) {
this.accumulators[this.accumulator++] = c;
if (this.accumulator >= 254)
this.flushPacket(output);
}
/**
* Clears out the hash table for block compress.
* @param output The byte buffer to write to.
*/
clearCodeTable(output) {
this.resetHashRange(HASH_SIZE);
this.firstUnusedEntry = this.clearCode + 2;
this.clearFlag = true;
this.processOutput(this.clearCode, output);
}
/**
* Resets the hash table given an amount of hashes.
* @param hashSize The amount of hashes to reset.
*/
resetHashRange(hashSize) {
this.hashes.fill(-1, 0, hashSize);
}
/**
* Flushes the packet to disk, and reset the accumulator.
* @param output The byte buffer to write to.
*/
flushPacket(output) {
if (this.accumulator > 0) {
output.writeByte(this.accumulator);
output.writeBytes(this.accumulators, 0, this.accumulator);
this.accumulator = 0;
}
}
/**
* Gets the maximum representable number for a given amount of bits.
* @param size The bit size to get the number from.
* @returns The maximum code given a number of bits.
* @example
* ```typescript
* getMaximumCode(6);
* // ➡ 0b0011_1111
* ```
*/
getMaximumCode(size) {
return (1 << size) - 1;
}
/**
* Gets the next pixel from the image.
* @returns The next pixel from the image.
*/
nextPixel() {
if (this.remaining === 0)
return EOF;
--this.remaining;
const pixel = this.pixels[this.currentPixel++];
return pixel & 0xff;
}
processOutput(code, outs) {
this.currentAccumulator &= masks[this.currentBits];
this.currentAccumulator = this.currentBits > 0 ? (this.currentAccumulator |= code << this.currentBits) : code;
this.currentBits += this.bitSize;
while (this.currentBits >= 8) {
this.addCharacter(this.currentAccumulator & 0xff, outs);
this.currentAccumulator >>= 8;
this.currentBits -= 8;
}
// If the next entry is going to be too big for the code size,
// then increase it, if possible.
if (this.firstUnusedEntry > this.maximumCode || this.clearFlag) {
if (this.clearFlag) {
this.maximumCode = this.getMaximumCode((this.bitSize = this.globalInitialBits));
this.clearFlag = false;
}
else {
++this.bitSize;
if (this.bitSize === BITS)
this.maximumCode = 1 << BITS;
else
this.maximumCode = this.getMaximumCode(this.bitSize);
}
}
if (code === this.endOfFrameCode) {
// At EOF, write the rest of the buffer.
while (this.currentBits > 0) {
this.addCharacter(this.currentAccumulator & 0xff, outs);
this.currentAccumulator >>= 8;
this.currentBits -= 8;
}
this.flushPacket(outs);
}
}
}
exports.LZWEncoder = LZWEncoder;
//# sourceMappingURL=LZWEncoder.js.map