image-in-browser
Version:
Package for encoding / decoding images, transforming images, applying filters, drawing primitives on images on the client side (no need for server Node.js)
375 lines • 14.2 kB
JavaScript
import { NeuralQuantizer } from '../image/neural-quantizer.js';
import { OutputBuffer } from '../common/output-buffer.js';
import { StringUtils } from '../common/string-utils.js';
import { QuantizerType } from '../image/quantizer-type.js';
import { OctreeQuantizer } from '../image/octree-quantizer.js';
import { Filter } from '../filter/filter.js';
import { LibError } from '../error/lib-error.js';
import { DitherKernel } from '../filter/dither-kernel.js';
import { BinaryQuantizer } from '../image/binary-quantizer.js';
export class GifEncoder {
get supportsAnimation() {
return this._supportsAnimation;
}
constructor(opt) {
var _a, _b, _c, _d, _e;
this._curAccum = 0;
this._curBits = 0;
this._nBits = 0;
this._initBits = 0;
this._eofCode = 0;
this._maxCode = 0;
this._clearCode = 0;
this._freeEnt = 0;
this._clearFlag = false;
this._blockSize = 0;
this._supportsAnimation = true;
this._delay = (_a = opt === null || opt === void 0 ? void 0 : opt.delay) !== null && _a !== void 0 ? _a : 80;
this._repeat = (_b = opt === null || opt === void 0 ? void 0 : opt.repeat) !== null && _b !== void 0 ? _b : 0;
this._numColors = 256;
this._quantizerType = QuantizerType.neural;
this._samplingFactor = (_c = opt === null || opt === void 0 ? void 0 : opt.samplingFactor) !== null && _c !== void 0 ? _c : 10;
this._dither = (_d = opt === null || opt === void 0 ? void 0 : opt.dither) !== null && _d !== void 0 ? _d : DitherKernel.floydSteinberg;
this._ditherSerpentine = (_e = opt === null || opt === void 0 ? void 0 : opt.ditherSerpentine) !== null && _e !== void 0 ? _e : false;
this._encodedFrames = 0;
}
addImage(image, width, height) {
if (!image.hasPalette) {
throw new LibError('GIF can only encode palette images.');
}
const palette = image.palette;
const numColors = palette.numColors;
const out = this._outputBuffer;
out.writeByte(GifEncoder._imageDescRecordType);
out.writeUint16(0);
out.writeUint16(0);
out.writeUint16(width);
out.writeUint16(height);
const paletteBytes = palette.toUint8Array();
out.writeByte(0x87);
const numChannels = palette.numChannels;
if (numChannels === 3) {
out.writeBytes(paletteBytes);
}
else if (numChannels === 4) {
for (let i = 0, pi = 0; i < numColors; ++i, pi += 4) {
out.writeByte(paletteBytes[pi]);
out.writeByte(paletteBytes[pi + 1]);
out.writeByte(paletteBytes[pi + 2]);
}
}
else if (numChannels === 1 || numChannels === 2) {
for (let i = 0, pi = 0; i < numColors; ++i, pi += numChannels) {
const g = paletteBytes[pi];
out.writeByte(g);
out.writeByte(g);
out.writeByte(g);
}
}
for (let i = numColors; i < 256; ++i) {
out.writeByte(0);
out.writeByte(0);
out.writeByte(0);
}
this.encodeLZW(image);
}
encodeLZW(image) {
this._curAccum = 0;
this._curBits = 0;
this._blockSize = 0;
this._block = new Uint8Array(256);
const initCodeSize = 8;
this._outputBuffer.writeByte(initCodeSize);
const hTab = new Int32Array(GifEncoder._hSize);
const codeTab = new Int32Array(GifEncoder._hSize);
const pIter = image[Symbol.iterator]();
let pIterRes = pIter.next();
this._initBits = initCodeSize + 1;
this._nBits = this._initBits;
this._maxCode = (1 << this._nBits) - 1;
this._clearCode = 1 << (this._initBits - 1);
this._eofCode = this._clearCode + 1;
this._clearFlag = false;
this._freeEnt = this._clearCode + 2;
let pFinished = false;
const nextPixel = () => {
if (pFinished) {
return GifEncoder._eof;
}
const r = Math.trunc(pIterRes.value.index);
if (((pIterRes = pIter.next()), pIterRes.done)) {
pFinished = true;
}
return r;
};
let ent = nextPixel();
let hShift = 0;
for (let fCode = GifEncoder._hSize; fCode < 65536; fCode *= 2) {
hShift++;
}
hShift = 8 - hShift;
const hSizeReg = GifEncoder._hSize;
for (let i = 0; i < hSizeReg; ++i) {
hTab[i] = -1;
}
this.output(this._clearCode);
let outerLoop = true;
while (outerLoop) {
outerLoop = false;
let c = nextPixel();
while (c !== GifEncoder._eof) {
const fcode = (c << GifEncoder._bits) + ent;
let i = (c << hShift) ^ ent;
if (hTab[i] === fcode) {
ent = codeTab[i];
c = nextPixel();
continue;
}
else if (hTab[i] >= 0) {
let disp = hSizeReg - i;
if (i === 0) {
disp = 1;
}
do {
if ((i -= disp) < 0) {
i += hSizeReg;
}
if (hTab[i] === fcode) {
ent = codeTab[i];
outerLoop = true;
break;
}
} while (hTab[i] >= 0);
if (outerLoop) {
break;
}
}
this.output(ent);
ent = c;
if (this._freeEnt < 1 << GifEncoder._bits) {
codeTab[i] = this._freeEnt++;
hTab[i] = fcode;
}
else {
for (let i = 0; i < GifEncoder._hSize; ++i) {
hTab[i] = -1;
}
this._freeEnt = this._clearCode + 2;
this._clearFlag = true;
this.output(this._clearCode);
}
c = nextPixel();
}
}
this.output(ent);
this.output(this._eofCode);
this._outputBuffer.writeByte(0);
}
output(code) {
this._curAccum &= GifEncoder._masks[this._curBits];
if (this._curBits > 0) {
this._curAccum |= code << this._curBits;
}
else {
this._curAccum = code;
}
this._curBits += this._nBits;
while (this._curBits >= 8) {
this.addToBlock(this._curAccum & 0xff);
this._curAccum >>>= 8;
this._curBits -= 8;
}
if (this._freeEnt > this._maxCode || this._clearFlag) {
if (this._clearFlag) {
this._nBits = this._initBits;
this._maxCode = (1 << this._nBits) - 1;
this._clearFlag = false;
}
else {
++this._nBits;
if (this._nBits === GifEncoder._bits) {
this._maxCode = 1 << GifEncoder._bits;
}
else {
this._maxCode = (1 << this._nBits) - 1;
}
}
}
if (code === this._eofCode) {
while (this._curBits > 0) {
this.addToBlock(this._curAccum & 0xff);
this._curAccum >>>= 8;
this._curBits -= 8;
}
this.writeBlock();
}
}
writeBlock() {
if (this._blockSize > 0) {
this._outputBuffer.writeByte(this._blockSize);
this._outputBuffer.writeBytes(this._block, this._blockSize);
this._blockSize = 0;
}
}
addToBlock(c) {
this._block[this._blockSize++] = c;
if (this._blockSize >= 254) {
this.writeBlock();
}
}
writeApplicationExt() {
this._outputBuffer.writeByte(GifEncoder._extensionRecordType);
this._outputBuffer.writeByte(GifEncoder._applicationExt);
this._outputBuffer.writeByte(11);
const appCodeUnits = StringUtils.getCodePoints('NETSCAPE2.0');
this._outputBuffer.writeBytes(appCodeUnits);
this._outputBuffer.writeBytes(new Uint8Array([0x03, 0x01]));
this._outputBuffer.writeUint16(this._repeat);
this._outputBuffer.writeByte(0);
}
writeGraphicsCtrlExt(image) {
var _a;
this._outputBuffer.writeByte(GifEncoder._extensionRecordType);
this._outputBuffer.writeByte(GifEncoder._graphicControlExt);
this._outputBuffer.writeByte(4);
let transparentIndex = 0;
let hasTransparency = 0;
const palette = image.palette;
const nc = palette.numChannels;
const pa = nc - 1;
if (nc === 4 || nc === 2) {
const p = palette.toUint8Array();
const l = palette.numColors;
for (let i = 0, pi = pa; i < l; ++i, pi += nc) {
const a = p[pi];
if (a === 0) {
hasTransparency = 1;
transparentIndex = i;
break;
}
}
}
const dispose = 2;
const fields = 0 |
(dispose << 2) |
0 |
hasTransparency;
this._outputBuffer.writeByte(fields);
this._outputBuffer.writeUint16((_a = this._lastImageDuration) !== null && _a !== void 0 ? _a : this._delay);
this._outputBuffer.writeByte(transparentIndex);
this._outputBuffer.writeByte(0);
}
writeHeader(width, height) {
const idCodeUnits = StringUtils.getCodePoints(GifEncoder._gif89Id);
this._outputBuffer.writeBytes(idCodeUnits);
this._outputBuffer.writeUint16(width);
this._outputBuffer.writeUint16(height);
this._outputBuffer.writeByte(0);
this._outputBuffer.writeByte(0);
this._outputBuffer.writeByte(0);
}
finish() {
let bytes = undefined;
if (this._outputBuffer === undefined) {
return bytes;
}
if (this._encodedFrames === 0) {
this.writeHeader(this._width, this._height);
this.writeApplicationExt();
}
this.writeGraphicsCtrlExt(this._lastImage);
this.addImage(this._lastImage, this._width, this._height);
this._outputBuffer.writeByte(GifEncoder._terminateRecordType);
this._lastImage = undefined;
this._lastColorMap = undefined;
this._encodedFrames = 0;
bytes = this._outputBuffer.getBytes();
this._outputBuffer = undefined;
return bytes;
}
addFrame(image, duration) {
if (this._outputBuffer === undefined) {
this._outputBuffer = new OutputBuffer();
if (!image.hasPalette) {
if (this._quantizerType === QuantizerType.neural) {
this._lastColorMap = new NeuralQuantizer(image, this._numColors, this._samplingFactor);
}
else if (this._quantizerType === QuantizerType.octree) {
this._lastColorMap = new OctreeQuantizer(image, this._numColors);
}
else {
this._lastColorMap = new BinaryQuantizer();
}
this._lastImage = Filter.ditherImage({
image: image,
quantizer: this._lastColorMap,
kernel: this._dither,
serpentine: this._ditherSerpentine,
});
}
else {
this._lastImage = image;
}
this._lastImageDuration = duration;
this._width = image.width;
this._height = image.height;
return;
}
if (this._encodedFrames === 0) {
this.writeHeader(this._width, this._height);
this.writeApplicationExt();
}
this.writeGraphicsCtrlExt(this._lastImage);
this.addImage(this._lastImage, this._width, this._height);
this._encodedFrames++;
if (!image.hasPalette) {
if (this._quantizerType === QuantizerType.neural) {
this._lastColorMap = new NeuralQuantizer(image, this._numColors, this._samplingFactor);
}
else if (this._quantizerType === QuantizerType.octree) {
this._lastColorMap = new OctreeQuantizer(image, this._numColors);
}
else {
this._lastColorMap = new BinaryQuantizer();
}
this._lastImage = Filter.ditherImage({
image: image,
quantizer: this._lastColorMap,
kernel: this._dither,
serpentine: this._ditherSerpentine,
});
}
else {
this._lastImage = image;
}
this._lastImageDuration = duration;
}
encode(opt) {
var _a;
const image = opt.image;
const singleFrame = (_a = opt.singleFrame) !== null && _a !== void 0 ? _a : false;
if (!image.hasAnimation || singleFrame) {
this.addFrame(image);
return this.finish();
}
this._repeat = image.loopCount;
for (const f of image.frames) {
this.addFrame(f, Math.trunc(f.frameDuration / 10));
}
return this.finish();
}
}
GifEncoder._gif89Id = 'GIF89a';
GifEncoder._imageDescRecordType = 0x2c;
GifEncoder._extensionRecordType = 0x21;
GifEncoder._terminateRecordType = 0x3b;
GifEncoder._applicationExt = 0xff;
GifEncoder._graphicControlExt = 0xf9;
GifEncoder._eof = -1;
GifEncoder._bits = 12;
GifEncoder._hSize = 5003;
GifEncoder._masks = [
0x0000, 0x0001, 0x0003, 0x0007, 0x000f, 0x001f, 0x003f, 0x007f, 0x00ff,
0x01ff, 0x03ff, 0x07ff, 0x0fff, 0x1fff, 0x3fff, 0x7fff, 0xffff,
];
//# sourceMappingURL=gif-encoder.js.map