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)

728 lines 28.4 kB
import { ArrayUtils } from '../common/array-utils.js'; import { BitUtils } from '../common/bit-utils.js'; import { MapUtils } from '../common/map-utils.js'; import { MathUtils } from '../common/math-utils.js'; import { OutputBuffer } from '../common/output-buffer.js'; import { WebPBitWriter } from './webp/webp-bit-writer.js'; import { WebPClSymbol } from './webp/webp-cl-symbol.js'; export class WebPEncoder { constructor() { this._supportsAnimation = false; } get supportsAnimation() { return this._supportsAnimation; } encodeVP8L(image, width, height) { const out = new OutputBuffer(); const hasAlpha = image.numChannels >= 4; const header = (width - 1) | ((height - 1) << 14) | ((hasAlpha ? 1 : 0) << 28); out.writeByte(0x2f); out.writeByte(header & 0xff); out.writeByte((header >> 8) & 0xff); out.writeByte((header >> 16) & 0xff); out.writeByte((header >> 24) & 0xff); const predSizeBits = 5; const predBlockSize = 1 << predSizeBits; const predBlockW = Math.trunc((width + predBlockSize - 1) / predBlockSize); const predBlockH = Math.trunc((height + predBlockSize - 1) / predBlockSize); const numPixels = width * height; const g = new Uint8Array(numPixels); const r = new Uint8Array(numPixels); const b = new Uint8Array(numPixels); const a = new Uint8Array(numPixels); let i = 0; for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { const p = image.getPixel(x, y); g[i] = MathUtils.clampInt255(p.g); r[i] = MathUtils.clampInt255(p.r); b[i] = MathUtils.clampInt255(p.b); a[i] = hasAlpha ? MathUtils.clampInt255(p.a) : 255; i++; } } this.applySubtractGreenTransform(r, g, b, numPixels); const predModes = this.selectPredictorModes(r, g, b, width, height, predBlockW, predBlockH, predBlockSize); this.applyPredictorTransform(r, g, b, a, width, height, predBlockW, predBlockSize, predModes); const bw = new WebPBitWriter(); bw.writeBits(1, 1); bw.writeBits(2, 2); bw.writeBits(1, 1); bw.writeBits(0, 2); bw.writeBits(predSizeBits - 2, 3); this.writePredictorSubImage(bw, predBlockW, predBlockH, predModes); bw.writeBits(0, 1); bw.writeBits(0, 1); bw.writeBits(0, 1); const tokenIsLit = []; const tokenLitIdx = []; const tokenLen = []; const tokenDist = []; const maxChain = 64; const maxMatchLen = 4096; const hashChain = new Map(); const addToHash = (pos) => { const key = (g[pos] << 24) | (r[pos] << 16) | (b[pos] << 8) | a[pos]; const list = MapUtils.putIfAbsent(hashChain, key, () => []); if (list.length >= maxChain) { list.splice(0, 1); } list.push(pos); }; let j = 0; while (j < numPixels) { const key = (g[j] << 24) | (r[j] << 16) | (b[j] << 8) | a[j]; let bestLen = 0; let bestDist = 0; const candidates = hashChain.get(key); if (j > 0 && candidates !== undefined) { for (let ci = candidates.length - 1; ci >= 0; ci--) { const c = candidates[ci]; const dist = j - c; if (dist > 1048456) break; let len = 1; while (len < maxMatchLen && j + len < numPixels && g[j + len] === g[c + len] && r[j + len] === r[c + len] && b[j + len] === b[c + len] && a[j + len] === a[c + len]) { len++; } if (len > bestLen || (len === bestLen && dist < bestDist)) { bestLen = len; bestDist = dist; } } } if (bestLen >= 3) { tokenIsLit.push(false); tokenLen.push(bestLen); tokenDist.push(bestDist); for (let k = 0; k < bestLen; k++) { addToHash(j + k); } j += bestLen; } else { tokenIsLit.push(true); tokenLitIdx.push(j); addToHash(j); j++; } } const greenFreq = new Array(280).fill(0); const redFreq = new Array(256).fill(0); const blueFreq = new Array(256).fill(0); const alphaFreq = new Array(256).fill(0); const distFreq = new Array(40).fill(0); let litPtr = 0; let refPtr = 0; for (const isLit of tokenIsLit) { if (isLit) { const idx = tokenLitIdx[litPtr++]; greenFreq[g[idx]]++; redFreq[r[idx]]++; blueFreq[b[idx]]++; alphaFreq[a[idx]]++; } else { const len = tokenLen[refPtr]; const dist = tokenDist[refPtr]; refPtr++; greenFreq[this.lengthSymbol(len)]++; const planeCode = this.distToPlaneCode(width, dist); distFreq[this.prefixCode(planeCode)]++; } } const greenCl = this.buildHuffmanCodeLengths(greenFreq, 280); const redCl = this.buildHuffmanCodeLengths(redFreq, 256); const blueCl = this.buildHuffmanCodeLengths(blueFreq, 256); const alphaCl = this.buildHuffmanCodeLengths(alphaFreq, 256); const distCl = this.buildHuffmanCodeLengths(distFreq, 40); this.writeHuffmanCode(bw, 280, greenCl); this.writeHuffmanCode(bw, 256, redCl); this.writeHuffmanCode(bw, 256, blueCl); this.writeHuffmanCode(bw, 256, alphaCl); this.writeHuffmanCode(bw, 40, distCl); const greenCodes = this.canonicalCodes(Int32Array.from(greenCl), 280); const redCodes = this.canonicalCodes(Int32Array.from(redCl), 256); const blueCodes = this.canonicalCodes(Int32Array.from(blueCl), 256); const alphaCodes = this.canonicalCodes(Int32Array.from(alphaCl), 256); const distCodes = this.canonicalCodes(Int32Array.from(distCl), 40); litPtr = 0; refPtr = 0; for (const isLit of tokenIsLit) { if (isLit) { const idx = tokenLitIdx[litPtr++]; bw.writeBits(greenCodes[g[idx]], greenCl[g[idx]]); bw.writeBits(redCodes[r[idx]], redCl[r[idx]]); bw.writeBits(blueCodes[b[idx]], blueCl[b[idx]]); bw.writeBits(alphaCodes[a[idx]], alphaCl[a[idx]]); } else { const len = tokenLen[refPtr]; const dist = tokenDist[refPtr]; refPtr++; const lSym = this.lengthSymbol(len); bw.writeBits(greenCodes[lSym], greenCl[lSym]); let le = this.lengthExtra(len); if (le.extraBits > 0) { bw.writeBits(le.extraValue, le.extraBits); } const planeCode = this.distToPlaneCode(width, dist); const dSym = this.prefixCode(planeCode); bw.writeBits(distCodes[dSym], distCl[dSym]); le = this.prefixExtra(planeCode); if (le.extraBits > 0) { bw.writeBits(le.extraValue, le.extraBits); } } } bw.flush(); out.writeBytes(bw.getBytes()); return out.getBytes(); } applySubtractGreenTransform(r, g, b, numPixels) { for (let i = 0; i < numPixels; i++) { r[i] = (r[i] - g[i]) & 0xff; b[i] = (b[i] - g[i]) & 0xff; } } selectPredictorModes(r, g, b, width, height, blockW, blockH, blockSize) { const candidates = [1, 2, 7, 11]; const modes = new Array(blockW * blockH).fill(11); for (let by = 0; by < blockH; by++) { for (let bx = 0; bx < blockW; bx++) { const x0 = bx * blockSize; const y0 = by * blockSize; const x1 = MathUtils.clamp(x0 + blockSize, 0, width); const y1 = MathUtils.clamp(y0 + blockSize, 0, height); let bestMode = 11; let bestCost = 0x7fffffff; for (const m of candidates) { let cost = 0; for (let y = y0; y < y1; y++) { for (let x = x0; x < x1; x++) { const idx = y * width + x; let pR = 0; let pG = 0; let pB = 0; if (y === 0 && x === 0) { pR = 0; pG = 0; pB = 0; } else if (y === 0) { const li = idx - 1; pR = r[li]; pG = g[li]; pB = b[li]; } else if (x === 0) { const ti = idx - width; pR = r[ti]; pG = g[ti]; pB = b[ti]; } else { const li = idx - 1; const ti = idx - width; switch (m) { case 1: pR = r[li]; pG = g[li]; pB = b[li]; break; case 2: pR = r[ti]; pG = g[ti]; pB = b[ti]; break; case 7: pR = (r[li] + r[ti]) >> 1; pG = (g[li] + g[ti]) >> 1; pB = (b[li] + b[ti]) >> 1; break; default: { const tli = ti - 1; const sl = Math.abs(r[li] - r[tli]) + Math.abs(g[li] - g[tli]) + Math.abs(b[li] - b[tli]); const st = Math.abs(r[ti] - r[tli]) + Math.abs(g[ti] - g[tli]) + Math.abs(b[ti] - b[tli]); if (sl <= st) { pR = r[ti]; pG = g[ti]; pB = b[ti]; } else { pR = r[li]; pG = g[li]; pB = b[li]; } } } } const dr = (r[idx] - pR) & 0xff; const dg = (g[idx] - pG) & 0xff; const db = (b[idx] - pB) & 0xff; cost += dr < 128 ? dr : 256 - dr; cost += dg < 128 ? dg : 256 - dg; cost += db < 128 ? db : 256 - db; } } if (cost < bestCost) { bestCost = cost; bestMode = m; } } modes[by * blockW + bx] = bestMode; } } return modes; } applyPredictorTransform(r, g, b, a, width, height, blockW, blockSize, modes) { const origR = Uint8Array.from(r); const origG = Uint8Array.from(g); const origB = Uint8Array.from(b); const origA = Uint8Array.from(a); for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { const i = y * width + x; let pR = 0; let pG = 0; let pB = 0; let pA = 0; if (y === 0 && x === 0) { pA = 255; pR = 0; pG = 0; pB = 0; } else if (y === 0) { const li = i - 1; pR = origR[li]; pG = origG[li]; pB = origB[li]; pA = origA[li]; } else if (x === 0) { const ti = i - width; pR = origR[ti]; pG = origG[ti]; pB = origB[ti]; pA = origA[ti]; } else { const li = i - 1; const ti = i - width; const shift = BitUtils.bitLength(blockSize) - 1; const mode = modes[(y >> shift) * blockW + (x >> shift)]; switch (mode) { case 1: pR = origR[li]; pG = origG[li]; pB = origB[li]; pA = origA[li]; break; case 2: pR = origR[ti]; pG = origG[ti]; pB = origB[ti]; pA = origA[ti]; break; case 7: pR = (origR[li] + origR[ti]) >> 1; pG = (origG[li] + origG[ti]) >> 1; pB = (origB[li] + origB[ti]) >> 1; pA = (origA[li] + origA[ti]) >> 1; break; default: { const tli = ti - 1; const sl = Math.abs(origR[li] - origR[tli]) + Math.abs(origG[li] - origG[tli]) + Math.abs(origB[li] - origB[tli]) + Math.abs(origA[li] - origA[tli]); const st = Math.abs(origR[ti] - origR[tli]) + Math.abs(origG[ti] - origG[tli]) + Math.abs(origB[ti] - origB[tli]) + Math.abs(origA[ti] - origA[tli]); if (sl <= st) { pR = origR[ti]; pG = origG[ti]; pB = origB[ti]; pA = origA[ti]; } else { pR = origR[li]; pG = origG[li]; pB = origB[li]; pA = origA[li]; } } } } r[i] = (origR[i] - pR) & 0xff; g[i] = (origG[i] - pG) & 0xff; b[i] = (origB[i] - pB) & 0xff; a[i] = (origA[i] - pA) & 0xff; } } } writePredictorSubImage(bw, blockW, blockH, modes) { const n = blockW * blockH; const greenFreq = new Array(280).fill(0); for (const m of modes) { greenFreq[m]++; } const greenCl = this.buildHuffmanCodeLengths(greenFreq, 280); const greenCodes = this.canonicalCodes(Int32Array.from(greenCl), 280); bw.writeBits(0, 1); this.writeHuffmanCode(bw, 280, greenCl); bw.writeBits(1, 1); bw.writeBits(0, 1); bw.writeBits(0, 1); bw.writeBits(0, 1); bw.writeBits(1, 1); bw.writeBits(0, 1); bw.writeBits(0, 1); bw.writeBits(0, 1); bw.writeBits(1, 1); bw.writeBits(0, 1); bw.writeBits(1, 1); bw.writeBits(255, 8); bw.writeBits(1, 1); bw.writeBits(0, 1); bw.writeBits(0, 1); bw.writeBits(0, 1); for (let i = 0; i < n; i++) { const m = modes[i]; bw.writeBits(greenCodes[m], greenCl[m]); } } lengthSymbol(length) { if (length <= 4) { return 255 + length; } const msb = this.log2Floor(length - 1); const half = ((length - 1) >> (msb - 1)) & 1; return 256 + 2 * msb + half; } lengthExtra(length) { if (length <= 4) { return { extraBits: 0, extraValue: 0 }; } const msb = this.log2Floor(length - 1); const half = ((length - 1) >> (msb - 1)) & 1; const eb = msb - 1; const base = (2 + half) << eb; return { extraBits: eb, extraValue: length - 1 - base, }; } distToPlaneCode(width, dist) { const yoff = Math.trunc(dist / width); const xoff = dist - yoff * width; if (xoff <= 8 && yoff < 8) { return WebPEncoder.planeLut[yoff * 16 + 8 - xoff] + 1; } else if (xoff > width - 8 && yoff < 7) { return WebPEncoder.planeLut[(yoff + 1) * 16 + 8 + width - xoff] + 1; } return dist + 120; } prefixCode(v) { const val = v - 1; if (val < 4) { return val; } const msb = this.log2Floor(val); const half = (val >> (msb - 1)) & 1; return 2 * msb + half; } prefixExtra(v) { const val = v - 1; if (val < 4) { return { extraBits: 0, extraValue: 0 }; } const msb = this.log2Floor(val); const half = (val >> (msb - 1)) & 1; const eb = msb - 1; const base = (2 + half) << eb; return { extraBits: eb, extraValue: val - base, }; } log2Floor(v) { let _v = v; let log = 0; while (_v > 1) { _v >>= 1; log++; } return log; } buildHuffmanCodeLengths(freq, alphabetSize, maxBits = 15) { const cl = new Array(alphabetSize).fill(0); const syms = []; for (let k = 0; k < alphabetSize; k++) { if (freq[k] > 0) { syms.push(k); } } if (syms.length === 0) { cl[0] = 1; return cl; } if (syms.length === 1) { cl[syms[0]] = 1; return cl; } const maxNodes = 2 * syms.length; const nodeFreq = new Array(maxNodes).fill(0); const nodeLeft = new Array(maxNodes).fill(-1); const nodeRight = new Array(maxNodes).fill(-1); for (let countMin = 1;; countMin *= 2) { for (let k = 0; k < syms.length; k++) { nodeFreq[k] = freq[syms[k]]; if (nodeFreq[k] < countMin) nodeFreq[k] = countMin; } let nextNode = syms.length; const pq = ArrayUtils.generate(syms.length, (k) => k); pq.sort((x, y) => MathUtils.cmp(nodeFreq[x], nodeFreq[y])); while (pq.length > 1) { const x = pq.splice(0, 1)[0]; const y = pq.splice(0, 1)[0]; const id = nextNode++; nodeFreq[id] = nodeFreq[x] + nodeFreq[y]; nodeLeft[id] = x; nodeRight[id] = y; let pos = 0; while (pos < pq.length && nodeFreq[pq[pos]] <= nodeFreq[id]) { pos++; } pq.splice(pos, 0, id); } const stackNodes = [pq[0]]; const stackDepths = [0]; let currentMaxBits = 0; while (stackNodes.length !== 0) { const nodeId = stackNodes.pop(); const depth = stackDepths.pop(); if (nodeLeft[nodeId] === -1) { cl[syms[nodeId]] = depth; if (depth > currentMaxBits) currentMaxBits = depth; } else { stackNodes.push(nodeLeft[nodeId]); stackNodes.push(nodeRight[nodeId]); stackDepths.push(depth + 1); stackDepths.push(depth + 1); } } if (currentMaxBits <= maxBits) { break; } } return cl; } writeHuffmanCode(bw, alphabetSize, codeLengths) { const used = []; for (let k = 0; k < alphabetSize; k++) { if (codeLengths[k] > 0) { used.push(k); } } if (used.length <= 2 && (used.length === 0 || used[used.length - 1] <= 255)) { bw.writeBits(1, 1); if (used.length === 0) { bw.writeBits(0, 1); bw.writeBits(0, 1); bw.writeBits(0, 1); return; } bw.writeBits(used.length - 1, 1); const sym0 = used[0]; if (sym0 <= 1) { bw.writeBits(0, 1); bw.writeBits(sym0, 1); } else { bw.writeBits(1, 1); bw.writeBits(sym0, 8); } if (used.length === 2) { bw.writeBits(used[1], 8); } else if (used.length === 1) { codeLengths[sym0] = 0; } return; } const clSymbols = this.buildRleSequence(codeLengths, alphabetSize); const clFreq = new Array(19).fill(0); for (const s of clSymbols) { clFreq[s.symbol]++; } const clCl = this.buildHuffmanCodeLengths(clFreq, 19, 7); const clCodes = this.canonicalCodes(Int32Array.from(clCl), 19); const kCodeLengthOrder = [ 17, 18, 0, 1, 2, 3, 4, 5, 16, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, ]; let numClCl = 4; for (let k = 18; k >= 4; k--) { if (clCl[kCodeLengthOrder[k]] !== 0) { numClCl = k + 1; break; } } bw.writeBits(0, 1); bw.writeBits(numClCl - 4, 4); for (let k = 0; k < numClCl; k++) { bw.writeBits(clCl[kCodeLengthOrder[k]], 3); } bw.writeBits(0, 1); for (const s of clSymbols) { bw.writeBits(clCodes[s.symbol], clCl[s.symbol]); if (s.extraBits > 0) { bw.writeBits(s.extraValue, s.extraBits); } } } buildRleSequence(codeLengths, alphabetSize) { const result = []; let i = 0; while (i < alphabetSize) { const cl = codeLengths[i]; if (cl === 0) { let count = 0; while (i + count < alphabetSize && codeLengths[i + count] === 0) { count++; } let rem = count; while (rem > 0) { if (rem >= 11) { const n = MathUtils.clamp(rem, 11, 138); result.push(new WebPClSymbol(18, 7, n - 11)); rem -= n; } else if (rem >= 3) { const n = MathUtils.clamp(rem, 3, 10); result.push(new WebPClSymbol(17, 3, n - 3)); rem -= n; } else { result.push(new WebPClSymbol(0, 0, 0)); rem--; } } i += count; } else { result.push(new WebPClSymbol(cl, 0, 0)); i++; while (i < alphabetSize && codeLengths[i] === cl) { let count = 0; while (i + count < alphabetSize && codeLengths[i + count] === cl && count < 6) { count++; } if (count >= 3) { result.push(new WebPClSymbol(16, 2, count - 3)); i += count; } else { for (let k = 0; k < count; k++) { result.push(new WebPClSymbol(cl, 0, 0)); } i += count; } } } } return result; } canonicalCodes(codeLengths, numSymbols) { const codes = new Array(numSymbols).fill(0); let maxLen = 0; for (let k = 0; k < numSymbols; k++) { if (codeLengths[k] > maxLen) maxLen = codeLengths[k]; } if (maxLen === 0) return codes; const blCount = new Array(maxLen + 1).fill(0); for (let k = 0; k < numSymbols; k++) { if (codeLengths[k] > 0) blCount[codeLengths[k]]++; } blCount[0] = 0; const nextCode = new Array(maxLen + 1).fill(0); let code = 0; for (let bits = 1; bits <= maxLen; bits++) { code = (code + blCount[bits - 1]) << 1; nextCode[bits] = code; } for (let k = 0; k < numSymbols; k++) { const len = codeLengths[k]; if (len > 0) { codes[k] = this.reverseBits(nextCode[len], len); nextCode[len]++; } } return codes; } reverseBits(value, numBits) { let _value = value; let result = 0; for (let k = 0; k < numBits; k++) { result = (result << 1) | (_value & 1); _value >>= 1; } return result; } tag(s) { const bytes = new Uint8Array(s.length); for (let k = 0; k < s.length; k++) { bytes[k] = s.charCodeAt(k); } return bytes; } encode(opt) { const width = opt.image.width; const height = opt.image.height; const vp8lData = this.encodeVP8L(opt.image, width, height); const out = new OutputBuffer(); const paddedLen = vp8lData.length + (MathUtils.isEven(vp8lData.length) ? 0 : 1); const fileSize = 4 + 8 + paddedLen; out.writeBytes(this.tag('RIFF')); out.writeUint32(fileSize); out.writeBytes(this.tag('WEBP')); out.writeBytes(this.tag('VP8L')); out.writeUint32(vp8lData.length); out.writeBytes(vp8lData); if (!MathUtils.isEven(vp8lData.length)) { out.writeByte(0); } return out.getBytes(); } } WebPEncoder.planeLut = [ 96, 73, 55, 39, 23, 13, 5, 1, 255, 255, 255, 255, 255, 255, 255, 255, 101, 78, 58, 42, 26, 16, 8, 2, 0, 3, 9, 17, 27, 43, 59, 79, 102, 86, 62, 46, 32, 20, 10, 6, 4, 7, 11, 21, 33, 47, 63, 87, 105, 90, 70, 52, 37, 28, 18, 14, 12, 15, 19, 29, 38, 53, 71, 91, 110, 99, 82, 66, 48, 35, 30, 24, 22, 25, 31, 36, 49, 67, 83, 100, 115, 108, 94, 76, 64, 50, 44, 40, 34, 41, 45, 51, 65, 77, 95, 109, 118, 113, 103, 92, 80, 68, 60, 56, 54, 57, 61, 69, 81, 93, 104, 114, 119, 116, 111, 106, 97, 88, 84, 74, 72, 75, 85, 89, 98, 107, 112, 117, ]; //# sourceMappingURL=webp-encoder.js.map