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)

660 lines 26.1 kB
import { ColorUtils } from '../../color/color-utils.js'; import { ArrayUtils } from '../../common/array-utils.js'; import { InputBuffer } from '../../common/input-buffer.js'; import { LibError } from '../../error/lib-error.js'; import { MemoryImage } from '../../image/image.js'; import { VP8LBitReader } from './vp8l-bit-reader.js'; import { VP8LColorCache } from './vp8l-color-cache.js'; import { VP8LImageTransformType } from './vp8l-image-transform-type.js'; import { VP8LTransform } from './vp8l-transform.js'; import { WebPFormat } from './webp-format.js'; import { HuffmanTree } from './webp-huffman-tree.js'; import { HuffmanTreeGroup } from './webp-huffman-tree-group.js'; import { StringUtils } from '../../common/string-utils.js'; import { ExifData } from '../../exif/exif-data.js'; export class VP8L { get webp() { return this._webp; } constructor(input, webp) { this._lastPixel = 0; this._lastRow = 0; this._colorCacheSize = 0; this._huffmanMask = 0; this._huffmanSubsampleBits = 0; this._huffmanXsize = 0; this._numHtreeGroups = 0; this._htreeGroups = []; this._transforms = []; this._transformsSeen = 0; this._input = input; this._webp = webp; this._br = new VP8LBitReader(input); } readTransform(transformSize) { let ok = true; const type = this._br.readBits(2); if ((this._transformsSeen & (1 << type)) !== 0) { return false; } this._transformsSeen |= 1 << type; const transform = new VP8LTransform(); this._transforms.push(transform); transform.type = type; transform.xsize = transformSize[0]; transform.ysize = transformSize[1]; switch (transform.type) { case VP8LImageTransformType.predictor: case VP8LImageTransformType.crossColor: transform.bits = this._br.readBits(3) + 2; transform.data = this.decodeImageStream(VP8L.subSampleSize(transform.xsize, transform.bits), VP8L.subSampleSize(transform.ysize, transform.bits), false); break; case VP8LImageTransformType.colorIndexing: { const numColors = this._br.readBits(8) + 1; const bits = numColors > 16 ? 0 : numColors > 4 ? 1 : numColors > 2 ? 2 : 3; transformSize[0] = VP8L.subSampleSize(transform.xsize, bits); transform.bits = bits; transform.data = this.decodeImageStream(numColors, 1, false); ok = this.expandColorMap(numColors, transform); break; } case VP8LImageTransformType.subtractGreen: break; default: throw new LibError('Invalid WebP transform type: $type'); } return ok; } extractPalettedAlphaRows(row) { const numRows = row - this._lastRow; const pIn = new InputBuffer({ buffer: this._pixels8, offset: this._webp.width * this._lastRow, }); if (numRows > 0) { this.applyInverseTransformsAlpha(numRows, pIn); } this._lastRow = row; } applyInverseTransformsAlpha(numRows, rows) { const startRow = this._lastRow; const endRow = startRow + numRows; const rowsOut = new InputBuffer({ buffer: this._opaque, offset: this._ioWidth * startRow, }); this._transforms[0].colorIndexInverseTransformAlpha(startRow, endRow, rows, rowsOut); } processRows(row) { const rows = this._webp.width * this._lastRow; const numRows = row - this._lastRow; if (numRows <= 0) { return; } this.applyInverseTransforms(numRows, rows); for (let y = 0, pi = this._argbCache, dy = this._lastRow; y < numRows; ++y, ++dy) { for (let x = 0; x < this._webp.width; ++x, ++pi) { const c = this._pixels[pi]; const r = ColorUtils.uint32ToRed(c); const g = ColorUtils.uint32ToGreen(c); const b = ColorUtils.uint32ToBlue(c); const a = ColorUtils.uint32ToAlpha(c); this._image.setPixelRgba(x, dy, b, g, r, a); } } this._lastRow = row; } applyInverseTransforms(numRows, rows) { let n = this._transforms.length; const cachePixs = this._webp.width * numRows; const startRow = this._lastRow; const endRow = startRow + numRows; let rowsIn = rows; const rowsOut = this._argbCache; ArrayUtils.copyRange(this._pixels, rowsIn, this._pixels, rowsOut, cachePixs); while (n-- > 0) { this._transforms[n].inverseTransform(startRow, endRow, this._pixels, rowsIn, this._pixels, rowsOut); rowsIn = rowsOut; } } readHuffmanCodes(xSize, ySize, colorCacheBits, allowRecursion) { let huffmanImage = undefined; let numHtreeGroups = 1; if (allowRecursion && this._br.readBits(1) !== 0) { const huffmanPrecision = this._br.readBits(3) + 2; const huffmanXsize = VP8L.subSampleSize(xSize, huffmanPrecision); const huffmanYsize = VP8L.subSampleSize(ySize, huffmanPrecision); const huffmanPixs = huffmanXsize * huffmanYsize; huffmanImage = this.decodeImageStream(huffmanXsize, huffmanYsize, false); this._huffmanSubsampleBits = huffmanPrecision; for (let i = 0; i < huffmanPixs; ++i) { const group = (huffmanImage[i] >>> 8) & 0xffff; huffmanImage[i] = group; if (group >= numHtreeGroups) { numHtreeGroups = group + 1; } } } const htreeGroups = ArrayUtils.generate(numHtreeGroups, () => new HuffmanTreeGroup()); for (let i = 0; i < numHtreeGroups; ++i) { for (let j = 0; j < VP8L.huffmanCodesPerMetaCode; ++j) { let alphabetSize = VP8L.alphabetSize[j]; if (j === 0 && colorCacheBits > 0) { alphabetSize += 1 << colorCacheBits; } if (!this.readHuffmanCode(alphabetSize, htreeGroups[i].htrees[j])) { return false; } } } this._huffmanImage = huffmanImage; this._numHtreeGroups = numHtreeGroups; this._htreeGroups = htreeGroups; return true; } readHuffmanCode(alphabetSize, tree) { let ok = false; const simpleCode = this._br.readBits(1); if (simpleCode !== 0) { const symbols = [0, 0]; const codes = [0, 0]; const codeLengths = [0, 0]; const numSymbols = this._br.readBits(1) + 1; const firstSymbolLenCode = this._br.readBits(1); symbols[0] = this._br.readBits(firstSymbolLenCode === 0 ? 1 : 8); codes[0] = 0; codeLengths[0] = numSymbols - 1; if (numSymbols === 2) { symbols[1] = this._br.readBits(8); codes[1] = 1; codeLengths[1] = numSymbols - 1; } ok = tree.buildExplicit(codeLengths, codes, symbols, alphabetSize, numSymbols); } else { const codeLengthCodeLengths = new Int32Array(VP8L.numCodeLengthCodes); const numCodes = this._br.readBits(4) + 4; if (numCodes > VP8L.numCodeLengthCodes) { return false; } const codeLengths = new Int32Array(alphabetSize); for (let i = 0; i < numCodes; ++i) { codeLengthCodeLengths[VP8L.codeLengthCodeOrder[i]] = this._br.readBits(3); } ok = this.readHuffmanCodeLengths(codeLengthCodeLengths, alphabetSize, codeLengths); if (ok) { ok = tree.buildImplicit(codeLengths, alphabetSize); } } return ok; } readHuffmanCodeLengths(codeLengthCodeLengths, numSymbols, codeLengths) { let symbol = 0; let maxSymbol = 0; let prevCodeLen = VP8L.defaultCodeLength; const tree = new HuffmanTree(); if (!tree.buildImplicit(codeLengthCodeLengths, VP8L.numCodeLengthCodes)) { return false; } if (this._br.readBits(1) !== 0) { const lengthNBits = 2 + 2 * this._br.readBits(3); maxSymbol = 2 + this._br.readBits(lengthNBits); if (maxSymbol > numSymbols) { return false; } } else { maxSymbol = numSymbols; } symbol = 0; while (symbol < numSymbols) { let codeLen = 0; if (maxSymbol-- === 0) { break; } this._br.fillBitWindow(); codeLen = tree.readSymbol(this._br); if (codeLen < VP8L.codeLengthLiterals) { codeLengths[symbol++] = codeLen; if (codeLen !== 0) { prevCodeLen = codeLen; } } else { const usePrev = codeLen === VP8L.codeLengthRepeatCode; const slot = codeLen - VP8L.codeLengthLiterals; const extraBits = VP8L.codeLengthExtraBits[slot]; const repeatOffset = VP8L.codeLengthRepeatOffsets[slot]; let repeat = this._br.readBits(extraBits) + repeatOffset; if (symbol + repeat > numSymbols) { return false; } else { const length = usePrev ? prevCodeLen : 0; while (repeat-- > 0) { codeLengths[symbol++] = length; } } } } return true; } getCopyDistance(distanceSymbol) { if (distanceSymbol < 4) { return distanceSymbol + 1; } const extraBits = (distanceSymbol - 2) >>> 1; const offset = (2 + (distanceSymbol & 1)) << extraBits; return offset + this._br.readBits(extraBits) + 1; } getCopyLength(lengthSymbol) { return this.getCopyDistance(lengthSymbol); } planeCodeToDistance(xsize, planeCode) { if (planeCode > VP8L.codeToPlaneCodes) { return planeCode - VP8L.codeToPlaneCodes; } else { const distCode = VP8L.codeToPlane[planeCode - 1]; const yoffset = distCode >>> 4; const xoffset = 8 - (distCode & 0xf); const dist = yoffset * xsize + xoffset; return dist >= 1 ? dist : 1; } } expandColorMap(numColors, transform) { const finalNumColors = 1 << (8 >>> transform.bits); const newColorMap = new Uint32Array(finalNumColors); const data = new Uint8Array(transform.data.buffer); const newData = new Uint8Array(newColorMap.buffer); newColorMap[0] = transform.data[0]; let len = 4 * numColors; let i = 0; for (i = 4; i < len; ++i) { newData[i] = (data[i] + newData[i - 4]) & 0xff; } for (len = 4 * finalNumColors; i < len; ++i) { newData[i] = 0; } transform.data = newColorMap; return true; } getMetaIndex(image, xsize, bits, x, y) { if (bits === 0) { return 0; } return image[xsize * (y >>> bits) + (x >>> bits)]; } getHtreeGroupForPos(x, y) { const metaIndex = this.getMetaIndex(this._huffmanImage, this._huffmanXsize, this._huffmanSubsampleBits, x, y); return this._htreeGroups[metaIndex]; } allocateInternalBuffers32b() { const numPixels = this._webp.width * this._webp.height; const cacheTopPixels = this._webp.width; const cachePixels = this._webp.width * VP8L.numArgbCacheRows; const totalNumPixels = numPixels + cacheTopPixels + cachePixels; const pixels32 = new Uint32Array(totalNumPixels); this._pixels = pixels32; this._pixels8 = new Uint8Array(pixels32.buffer); this._argbCache = numPixels + cacheTopPixels; return true; } allocateInternalBuffers8b() { const totalNumPixels = this._webp.width * this._webp.height; this._argbCache = 0; const n = totalNumPixels + (4 - (totalNumPixels % 4)); this._pixels8 = new Uint8Array(n); this._pixels = new Uint32Array(this._pixels8.buffer); return true; } decodeImageStream(xsize, ysize, isLevel0) { let transformXsize = xsize; let transformYsize = ysize; let colorCacheBits = 0; if (isLevel0) { while (this._br.readBits(1) !== 0) { const sizes = [transformXsize, transformYsize]; if (!this.readTransform(sizes)) { throw new LibError('Invalid Transform'); } transformXsize = sizes[0]; transformYsize = sizes[1]; } } if (this._br.readBits(1) !== 0) { colorCacheBits = this._br.readBits(4); const ok = colorCacheBits >= 1 && colorCacheBits <= VP8L.maxCacheBits; if (!ok) { throw new LibError('Invalid Color Cache'); } } if (!this.readHuffmanCodes(transformXsize, transformYsize, colorCacheBits, isLevel0)) { throw new LibError('Invalid Huffman Codes'); } if (colorCacheBits > 0) { this._colorCacheSize = 1 << colorCacheBits; this._colorCache = new VP8LColorCache(colorCacheBits); } else { this._colorCacheSize = 0; } this._webp.width = transformXsize; this._webp.height = transformYsize; const numBits = this._huffmanSubsampleBits; this._huffmanXsize = VP8L.subSampleSize(transformXsize, numBits); this._huffmanMask = numBits === 0 ? ~0 : (1 << numBits) - 1; if (isLevel0) { this._lastPixel = 0; return undefined; } const totalSize = transformXsize * transformYsize; const data = new Uint32Array(totalSize); if (!this.decodeImageData(data, transformXsize, transformYsize, transformYsize, undefined)) { throw new LibError('Failed to decode image data.'); } this._lastPixel = 0; return data; } decodeImageData(data, width, height, lastRow, processFunc) { let row = Math.trunc(this._lastPixel / width); let col = this._lastPixel % width; let htreeGroup = this.getHtreeGroupForPos(col, row); let src = this._lastPixel; let lastCached = src; const srcEnd = width * height; const srcLast = width * lastRow; const lenCodeLimit = VP8L.numLiteralCodes + VP8L.numLengthCodes; const colorCacheLimit = lenCodeLimit + this._colorCacheSize; const colorCache = this._colorCacheSize > 0 ? this._colorCache : undefined; const mask = this._huffmanMask; while (!this._br.isEOS && src < srcLast) { if ((col & mask) === 0) { htreeGroup = this.getHtreeGroupForPos(col, row); } this._br.fillBitWindow(); const code = htreeGroup.htrees[VP8L.green].readSymbol(this._br); if (code < VP8L.numLiteralCodes) { const red = htreeGroup.htrees[VP8L.red].readSymbol(this._br); const green = code; this._br.fillBitWindow(); const blue = htreeGroup.htrees[VP8L.blue].readSymbol(this._br); const alpha = htreeGroup.htrees[VP8L.alpha].readSymbol(this._br); const c = ColorUtils.rgbaToUint32(blue, green, red, alpha); data[src] = c; ++src; ++col; if (col >= width) { col = 0; ++row; if (row % VP8L.numArgbCacheRows === 0 && processFunc !== undefined) { processFunc.call(this, row); } if (colorCache !== undefined) { while (lastCached < src) { colorCache.insert(data[lastCached]); lastCached++; } } } } else if (code < lenCodeLimit) { const lengthSym = code - VP8L.numLiteralCodes; const length = this.getCopyLength(lengthSym); const distSymbol = htreeGroup.htrees[VP8L.dist].readSymbol(this._br); this._br.fillBitWindow(); const distCode = this.getCopyDistance(distSymbol); const dist = this.planeCodeToDistance(width, distCode); if (src < dist || srcEnd - src < length) { return false; } else { const dst = src - dist; for (let i = 0; i < length; ++i) { data[src + i] = data[dst + i]; } src += length; } col += length; while (col >= width) { col -= width; ++row; if (row % VP8L.numArgbCacheRows === 0 && processFunc !== undefined) { processFunc.call(this, row); } } if (src < srcLast) { if ((col & mask) !== 0) { htreeGroup = this.getHtreeGroupForPos(col, row); } if (colorCache !== undefined) { while (lastCached < src) { colorCache.insert(data[lastCached]); lastCached++; } } } } else if (code < colorCacheLimit) { const key = code - lenCodeLimit; while (lastCached < src) { colorCache.insert(data[lastCached]); lastCached++; } data[src] = colorCache.lookup(key); ++src; ++col; if (col >= width) { col = 0; ++row; if (row % VP8L.numArgbCacheRows === 0 && processFunc !== undefined) { processFunc.call(this, row); } while (lastCached < src) { colorCache.insert(data[lastCached]); lastCached++; } } } else { return false; } } if (processFunc !== undefined) { processFunc.call(this, row); } if (this._br.isEOS && src < srcEnd) { return false; } this._lastPixel = src; return true; } is8bOptimizable() { if (this._colorCacheSize > 0) { return false; } for (let i = 0; i < this._numHtreeGroups; ++i) { const htrees = this._htreeGroups[i].htrees; if (htrees[VP8L.red].numNodes > 1) { return false; } if (htrees[VP8L.blue].numNodes > 1) { return false; } if (htrees[VP8L.alpha].numNodes > 1) { return false; } } return true; } extractAlphaRows(row) { const numRows = row - this._lastRow; if (numRows <= 0) { return; } this.applyInverseTransforms(numRows, this._webp.width * this._lastRow); const width = this._webp.width; const cachePixs = width * numRows; const di = width * this._lastRow; const src = new InputBuffer({ buffer: this._pixels, offset: this._argbCache, }); for (let i = 0; i < cachePixs; ++i) { this._opaque[di + i] = (src.get(i) >>> 8) & 0xff; } this._lastRow = row; } decodeAlphaData(width, height, lastRow) { let row = Math.trunc(this._lastPixel / width); let col = this._lastPixel % width; let htreeGroup = this.getHtreeGroupForPos(col, row); let pos = this._lastPixel; const end = width * height; const last = width * lastRow; const lenCodeLimit = VP8L.numLiteralCodes + VP8L.numLengthCodes; const mask = this._huffmanMask; while (!this._br.isEOS && pos < last) { if ((col & mask) === 0) { htreeGroup = this.getHtreeGroupForPos(col, row); } this._br.fillBitWindow(); const code = htreeGroup.htrees[VP8L.green].readSymbol(this._br); if (code < VP8L.numLiteralCodes) { this._pixels8[pos] = code; ++pos; ++col; if (col >= width) { col = 0; ++row; if (row % VP8L.numArgbCacheRows === 0) { this.extractPalettedAlphaRows(row); } } } else if (code < lenCodeLimit) { const lengthSym = code - VP8L.numLiteralCodes; const length = this.getCopyLength(lengthSym); const distSymbol = htreeGroup.htrees[VP8L.dist].readSymbol(this._br); this._br.fillBitWindow(); const distCode = this.getCopyDistance(distSymbol); const dist = this.planeCodeToDistance(width, distCode); if (pos >= dist && end - pos >= length) { for (let i = 0; i < length; ++i) { this._pixels8[pos + i] = this._pixels8[pos + i - dist]; } } else { this._lastPixel = pos; return true; } pos += length; col += length; while (col >= width) { col -= width; ++row; if (row % VP8L.numArgbCacheRows === 0) { this.extractPalettedAlphaRows(row); } } if (pos < last && (col & mask) !== 0) { htreeGroup = this.getHtreeGroupForPos(col, row); } } else { return false; } } this.extractPalettedAlphaRows(row); this._lastPixel = pos; return true; } decodeHeader() { const signature = this._br.readBits(8); if (signature !== VP8L.vp8lMagicByte) { return false; } this._webp.format = WebPFormat.lossless; this._webp.width = this._br.readBits(14) + 1; this._webp.height = this._br.readBits(14) + 1; this._webp.hasAlpha = this._br.readBits(1) !== 0; const version = this._br.readBits(3); if (version !== VP8L.vp8lVersion) { return false; } return true; } decode() { this._lastPixel = 0; if (!this.decodeHeader()) { return undefined; } this.decodeImageStream(this._webp.width, this._webp.height, true); this.allocateInternalBuffers32b(); this._image = new MemoryImage({ width: this._webp.width, height: this._webp.height, numChannels: 4, }); if (!this.decodeImageData(this._pixels, this._webp.width, this._webp.height, this._webp.height, this.processRows)) { return undefined; } if (this._webp.exifData.length > 0) { const input = new InputBuffer({ buffer: StringUtils.getCodePoints(this._webp.exifData), }); this._image.exifData = ExifData.fromInputBuffer(input); } return this._image; } static subSampleSize(size, samplingBits) { return (size + (1 << samplingBits) - 1) >>> samplingBits; } } VP8L.green = 0; VP8L.red = 1; VP8L.blue = 2; VP8L.alpha = 3; VP8L.dist = 4; VP8L.numArgbCacheRows = 16; VP8L.numCodeLengthCodes = 19; VP8L.codeLengthCodeOrder = [ 17, 18, 0, 1, 2, 3, 4, 5, 16, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, ]; VP8L.codeToPlaneCodes = 120; VP8L.codeToPlane = [ 0x18, 0x07, 0x17, 0x19, 0x28, 0x06, 0x27, 0x29, 0x16, 0x1a, 0x26, 0x2a, 0x38, 0x05, 0x37, 0x39, 0x15, 0x1b, 0x36, 0x3a, 0x25, 0x2b, 0x48, 0x04, 0x47, 0x49, 0x14, 0x1c, 0x35, 0x3b, 0x46, 0x4a, 0x24, 0x2c, 0x58, 0x45, 0x4b, 0x34, 0x3c, 0x03, 0x57, 0x59, 0x13, 0x1d, 0x56, 0x5a, 0x23, 0x2d, 0x44, 0x4c, 0x55, 0x5b, 0x33, 0x3d, 0x68, 0x02, 0x67, 0x69, 0x12, 0x1e, 0x66, 0x6a, 0x22, 0x2e, 0x54, 0x5c, 0x43, 0x4d, 0x65, 0x6b, 0x32, 0x3e, 0x78, 0x01, 0x77, 0x79, 0x53, 0x5d, 0x11, 0x1f, 0x64, 0x6c, 0x42, 0x4e, 0x76, 0x7a, 0x21, 0x2f, 0x75, 0x7b, 0x31, 0x3f, 0x63, 0x6d, 0x52, 0x5e, 0x00, 0x74, 0x7c, 0x41, 0x4f, 0x10, 0x20, 0x62, 0x6e, 0x30, 0x73, 0x7d, 0x51, 0x5f, 0x40, 0x72, 0x7e, 0x61, 0x6f, 0x50, 0x71, 0x7f, 0x60, 0x70, ]; VP8L.codeLengthLiterals = 16; VP8L.codeLengthRepeatCode = 16; VP8L.codeLengthExtraBits = [2, 3, 7]; VP8L.codeLengthRepeatOffsets = [3, 3, 11]; VP8L.argbBlack = 0xff000000; VP8L.maxCacheBits = 11; VP8L.huffmanCodesPerMetaCode = 5; VP8L.defaultCodeLength = 8; VP8L.maxAllowedCodeLength = 15; VP8L.numLiteralCodes = 256; VP8L.numLengthCodes = 24; VP8L.numDistanceCodes = 40; VP8L.codeLengthCodes = 19; VP8L.alphabetSize = [ VP8L.numLiteralCodes + VP8L.numLengthCodes, VP8L.numLiteralCodes, VP8L.numLiteralCodes, VP8L.numLiteralCodes, VP8L.numDistanceCodes, ]; VP8L.vp8lMagicByte = 0x2f; VP8L.vp8lVersion = 0; //# sourceMappingURL=vp8l.js.map