UNPKG

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
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