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)
513 lines • 19.7 kB
JavaScript
import { InputBuffer } from '../common/input-buffer.js';
import { ArrayUtils } from '../common/array-utils.js';
import { GifColorMap } from './gif/gif-color-map.js';
import { GifImageDesc } from './gif/gif-image-desc.js';
import { GifInfo } from './gif/gif-info.js';
import { MemoryImage } from '../image/image.js';
import { ColorUint8 } from '../color/color-uint8.js';
import { ImageFormat } from './image-format.js';
export class GifDecoder {
get format() {
return ImageFormat.gif;
}
get numFrames() {
return this._info !== undefined ? this._info.numFrames : 0;
}
constructor(bytes) {
this._repeat = 0;
this._bitsPerPixel = 0;
this._currentShiftDWord = 0;
this._currentShiftState = 0;
this._stackPtr = 0;
this._lastCode = 0;
this._maxCode1 = 0;
this._runningBits = 0;
this._runningCode = 0;
this._eofCode = 0;
this._clearCode = 0;
this._transparentFlag = 0;
this._disposalMethod = 0;
this._transparent = 0;
this._duration = 0;
if (bytes !== undefined) {
this.startDecode(bytes);
}
}
static getPrefixChar(prefix, code, clearCode) {
let c = code;
let i = 0;
while (c > clearCode && i++ <= GifDecoder._lzMaxCode) {
if (c > GifDecoder._lzMaxCode) {
return GifDecoder._noSuchCode;
}
c = prefix[c];
}
return c;
}
static updateImage(image, y, colorMap, line) {
if (colorMap !== undefined) {
const width = line.length;
for (let x = 0; x < width; ++x) {
image.setPixelRgb(x, y, line[x], 0, 0);
}
}
}
getInfo() {
if (this._input === undefined) {
return false;
}
const tag = this._input.readString(GifDecoder._stampSize);
if (tag !== GifDecoder._gif87Stamp && tag !== GifDecoder._gif89Stamp) {
return false;
}
const width = this._input.readUint16();
const height = this._input.readUint16();
const b = this._input.read();
const colorResolution = (((b & 0x70) + 1) >>> 4) + 1;
const bitsPerPixel = (b & 0x07) + 1;
const backgroundColor = new ColorUint8(new Uint8Array([this._input.read()]));
this._input.skip(1);
let globalColorMap = undefined;
if ((b & 0x80) !== 0) {
globalColorMap = new GifColorMap(1 << bitsPerPixel);
for (let i = 0; i < globalColorMap.numColors; ++i) {
const r = this._input.read();
const g = this._input.read();
const b = this._input.read();
globalColorMap.setColor(i, r, g, b);
}
}
const isGif89 = tag === GifDecoder._gif89Stamp;
this._info = new GifInfo({
width: width,
height: height,
colorResolution: colorResolution,
backgroundColor: backgroundColor,
globalColorMap: globalColorMap,
isGif89: isGif89,
});
return true;
}
skipImage() {
if (this._input === undefined || this._input.isEOS) {
return undefined;
}
const gifImage = new GifImageDesc(this._input);
this._input.skip(1);
this.skipRemainder();
return gifImage;
}
skipRemainder() {
if (this._input === undefined || this._input.isEOS) {
return true;
}
let b = this._input.read();
while (b !== 0 && !this._input.isEOS) {
this._input.skip(b);
if (this._input.isEOS) {
return true;
}
b = this._input.read();
}
return true;
}
readApplicationExt(input) {
const blockSize = input.read();
const tag = input.readString(blockSize);
if (tag === 'NETSCAPE2.0') {
const b1 = input.read();
const b2 = input.read();
if (b1 === 0x03 && b2 === 0x01) {
this._repeat = input.readUint16();
}
}
else {
this.skipRemainder();
}
}
readGraphicsControlExt(input) {
input.read();
const b = input.read();
this._duration = input.readUint16();
this._transparent = input.read();
input.read();
this._disposalMethod = (b >>> 2) & 0x7;
this._transparentFlag = b & 0x1;
const recordType = input.peek(1).get(0);
if (recordType === GifDecoder._imageDescRecordType) {
input.skip(1);
const gifImage = this.skipImage();
if (gifImage === undefined) {
return;
}
gifImage.duration = this._duration;
gifImage.disposal = this._disposalMethod;
if (this._transparentFlag !== 0) {
if (gifImage.colorMap === undefined &&
this._info.globalColorMap !== undefined) {
gifImage.colorMap = GifColorMap.from(this._info.globalColorMap);
}
if (gifImage.colorMap !== undefined) {
gifImage.colorMap.transparent = this._transparent;
}
}
this._info.frames.push(gifImage);
}
}
getLine(line) {
this._pixelCount = this._pixelCount - line.length;
if (!this.decompressLine(line)) {
return false;
}
if (this._pixelCount === 0) {
this.skipRemainder();
}
return true;
}
decompressLine(line) {
if (this._stackPtr > GifDecoder._lzMaxCode) {
return false;
}
const lineLen = line.length;
let i = 0;
if (this._stackPtr !== 0) {
while (this._stackPtr !== 0 && i < lineLen) {
line[i++] = this._stack[--this._stackPtr];
}
}
let currentPrefix = undefined;
while (i < lineLen) {
this._currentCode = this.decompressInput();
if (this._currentCode === undefined) {
return false;
}
if (this._currentCode === this._eofCode) {
return false;
}
if (this._currentCode === this._clearCode) {
for (let j = 0; j <= GifDecoder._lzMaxCode; j++) {
this._prefix[j] = GifDecoder._noSuchCode;
}
this._runningCode = this._eofCode + 1;
this._runningBits = this._bitsPerPixel + 1;
this._maxCode1 = 1 << this._runningBits;
this._lastCode = GifDecoder._noSuchCode;
}
else {
if (this._currentCode < this._clearCode) {
line[i++] = this._currentCode;
}
else {
if (this._prefix[this._currentCode] === GifDecoder._noSuchCode) {
if (this._currentCode === this._runningCode - 2) {
currentPrefix = this._lastCode;
const prefixChar = GifDecoder.getPrefixChar(this._prefix, this._lastCode, this._clearCode);
this._stack[this._stackPtr++] = prefixChar;
this._suffix[this._runningCode - 2] = prefixChar;
}
else {
return false;
}
}
else {
currentPrefix = this._currentCode;
}
let j = 0;
while (j++ <= GifDecoder._lzMaxCode &&
currentPrefix > this._clearCode &&
currentPrefix <= GifDecoder._lzMaxCode) {
this._stack[this._stackPtr++] = this._suffix[currentPrefix];
currentPrefix = this._prefix[currentPrefix];
}
if (j >= GifDecoder._lzMaxCode ||
currentPrefix > GifDecoder._lzMaxCode) {
return false;
}
this._stack[this._stackPtr++] = currentPrefix;
while (this._stackPtr !== 0 && i < lineLen) {
line[i++] = this._stack[--this._stackPtr];
}
}
if (this._lastCode !== GifDecoder._noSuchCode &&
this._prefix[this._runningCode - 2] === GifDecoder._noSuchCode) {
this._prefix[this._runningCode - 2] = this._lastCode;
if (this._currentCode === this._runningCode - 2) {
this._suffix[this._runningCode - 2] = GifDecoder.getPrefixChar(this._prefix, this._lastCode, this._clearCode);
}
else {
this._suffix[this._runningCode - 2] = GifDecoder.getPrefixChar(this._prefix, this._currentCode, this._clearCode);
}
}
this._lastCode = this._currentCode;
}
}
return true;
}
decompressInput() {
if (this._runningBits > GifDecoder._lzBits) {
return undefined;
}
while (this._currentShiftState < this._runningBits) {
const nextByte = this.bufferedInput();
this._currentShiftDWord |= nextByte << this._currentShiftState;
this._currentShiftState += 8;
}
const code = this._currentShiftDWord & GifDecoder._codeMasks[this._runningBits];
this._currentShiftDWord >>>= this._runningBits;
this._currentShiftState -= this._runningBits;
if (this._runningCode < GifDecoder._lzMaxCode + 2 &&
++this._runningCode > this._maxCode1 &&
this._runningBits < GifDecoder._lzBits) {
this._maxCode1 <<= 1;
this._runningBits++;
}
return code;
}
bufferedInput() {
let nextByte = 0;
if (this._buffer[0] === 0) {
this._buffer[0] = this._input.read();
if (this._buffer[0] === 0) {
return undefined;
}
const from = this._input.readRange(this._buffer[0]).toUint8Array();
ArrayUtils.copyRange(from, 0, this._buffer, 1, this._buffer[0]);
nextByte = this._buffer[1];
this._buffer[1] = 2;
this._buffer[0]--;
}
else {
nextByte = this._buffer[this._buffer[1]++];
this._buffer[0]--;
}
return nextByte;
}
initDecode() {
this._buffer = new Uint8Array(256);
this._stack = new Uint8Array(GifDecoder._lzMaxCode);
this._suffix = new Uint8Array(GifDecoder._lzMaxCode + 1);
this._prefix = new Uint32Array(GifDecoder._lzMaxCode + 1);
}
decodeImage(gifImage) {
if (this._input === undefined || this._info === undefined) {
return undefined;
}
if (this._buffer === undefined) {
this.initDecode();
}
this._bitsPerPixel = this._input.read();
this._clearCode = 1 << this._bitsPerPixel;
this._eofCode = this._clearCode + 1;
this._runningCode = this._eofCode + 1;
this._runningBits = this._bitsPerPixel + 1;
this._maxCode1 = 1 << this._runningBits;
this._stackPtr = 0;
this._lastCode = GifDecoder._noSuchCode;
this._currentShiftState = 0;
this._currentShiftDWord = 0;
this._buffer[0] = 0;
this._prefix.fill(GifDecoder._noSuchCode, 0, this._prefix.length);
const width = gifImage.width;
const height = gifImage.height;
if (gifImage.x + width > this._info.width ||
gifImage.y + height > this._info.height) {
return undefined;
}
const colorMap = gifImage.colorMap !== undefined
? gifImage.colorMap
: this._info.globalColorMap;
this._pixelCount = width * height;
const image = new MemoryImage({
width: width,
height: height,
numChannels: 1,
palette: colorMap.getPalette(),
});
const line = new Uint8Array(width);
if (gifImage.interlaced) {
const row = gifImage.y;
for (let i = 0, j = 0; i < 4; ++i) {
for (let y = row + GifDecoder._interlacedOffset[i]; y < row + height; y += GifDecoder._interlacedJump[i], ++j) {
if (!this.getLine(line)) {
return image;
}
GifDecoder.updateImage(image, y, colorMap, line);
}
}
}
else {
for (let y = 0; y < height; ++y) {
if (!this.getLine(line)) {
return image;
}
GifDecoder.updateImage(image, y, colorMap, line);
}
}
return image;
}
isValidFile(bytes) {
this._input = new InputBuffer({
buffer: bytes,
});
return this.getInfo();
}
startDecode(bytes) {
this._input = new InputBuffer({
buffer: bytes,
});
if (!this.getInfo()) {
return undefined;
}
try {
while (!this._input.isEOS) {
const recordType = this._input.read();
switch (recordType) {
case GifDecoder._imageDescRecordType: {
const gifImage = this.skipImage();
if (gifImage === undefined) {
return this._info;
}
gifImage.duration = this._duration;
gifImage.disposal = this._disposalMethod;
if (this._transparentFlag !== 0) {
if (gifImage.colorMap === undefined &&
this._info.globalColorMap !== undefined) {
gifImage.colorMap = GifColorMap.from(this._info.globalColorMap);
}
if (gifImage.colorMap !== undefined) {
gifImage.colorMap.transparent = this._transparent;
}
}
this._info.frames.push(gifImage);
break;
}
case GifDecoder._extensionRecordType: {
const extCode = this._input.read();
if (extCode === GifDecoder._applicationExt) {
this.readApplicationExt(this._input);
}
else if (extCode === GifDecoder._graphicControlExt) {
this.readGraphicsControlExt(this._input);
}
else {
this.skipRemainder();
}
break;
}
case GifDecoder._terminateRecordType: {
return this._info;
}
default:
break;
}
}
}
catch (error) {
}
return this._info;
}
decode(opt) {
var _a;
const bytes = opt.bytes;
if (this.startDecode(bytes) === undefined || this._info === undefined) {
return undefined;
}
if (this._info.numFrames === 1 || opt.frameIndex !== undefined) {
return this.decodeFrame((_a = opt.frameIndex) !== null && _a !== void 0 ? _a : 0);
}
let firstImage = undefined;
let lastImage = undefined;
for (let i = 0; i < this._info.numFrames; ++i) {
const frame = this._info.frames[i];
const image = this.decodeFrame(i);
if (image === undefined) {
return undefined;
}
image.frameDuration = frame.duration * 10;
if (firstImage === undefined || lastImage === undefined) {
firstImage = image;
lastImage = image;
image.loopCount = this._repeat;
continue;
}
if (image.width === lastImage.width &&
image.height === lastImage.height &&
frame.x === 0 &&
frame.y === 0 &&
frame.disposal === 2) {
lastImage = image;
firstImage.addFrame(lastImage);
continue;
}
const colorMap = frame.colorMap !== undefined
? frame.colorMap
: this._info.globalColorMap;
const nextImage = new MemoryImage({
width: lastImage.width,
height: lastImage.height,
numChannels: 1,
palette: colorMap.getPalette(),
});
if (frame.disposal === 2) {
nextImage.clear(colorMap.getColor(this._info.backgroundColor.r));
}
else if (frame.disposal !== 3) {
if (frame.colorMap !== undefined) {
const lp = lastImage.palette;
const remapColors = new Map();
for (let ci = 0; ci < colorMap.numColors; ++ci) {
const nc = colorMap.findColor(lp.getRed(ci), lp.getGreen(ci), lp.getBlue(ci), lp.getAlpha(ci));
remapColors.set(ci, nc);
}
const nextBytes = nextImage.toUint8Array();
const lastBytes = lastImage.toUint8Array();
for (let i = 0, l = nextBytes.length; i < l; ++i) {
const lc = lastBytes[i];
const nc = remapColors.get(lc);
if (nc !== -1) {
nextBytes[i] = nc;
}
}
}
}
nextImage.frameDuration = image.frameDuration;
for (const p of image) {
if (p.a !== 0) {
nextImage.setPixel(p.x + frame.x, p.y + frame.y, p);
}
}
firstImage.addFrame(nextImage);
lastImage = nextImage;
}
return firstImage;
}
decodeFrame(frameIndex) {
if (this._input === undefined || this._info === undefined) {
return undefined;
}
if (frameIndex >= this._info.frames.length || frameIndex < 0) {
return undefined;
}
const gifImage = this._info.frames[frameIndex];
this._input.offset = gifImage.inputPosition;
return this.decodeImage(this._info.frames[frameIndex]);
}
}
GifDecoder._stampSize = 6;
GifDecoder._gif87Stamp = 'GIF87a';
GifDecoder._gif89Stamp = 'GIF89a';
GifDecoder._imageDescRecordType = 0x2c;
GifDecoder._extensionRecordType = 0x21;
GifDecoder._terminateRecordType = 0x3b;
GifDecoder._graphicControlExt = 0xf9;
GifDecoder._applicationExt = 0xff;
GifDecoder._lzMaxCode = 4095;
GifDecoder._lzBits = 12;
GifDecoder._noSuchCode = 4098;
GifDecoder._codeMasks = [
0x0000, 0x0001, 0x0003, 0x0007, 0x000f, 0x001f, 0x003f, 0x007f, 0x00ff,
0x01ff, 0x03ff, 0x07ff, 0x0fff,
];
GifDecoder._interlacedOffset = [0, 4, 2, 1];
GifDecoder._interlacedJump = [8, 8, 4, 2];
//# sourceMappingURL=gif-decoder.js.map