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