/* * KasmVNC: HTML5 VNC client * Copyright (C) 2020 Kasm Technologies * Copyright (C) 2019 The noVNC Authors * (c) 2012 Michael Tinglof, Joe Balaz, Les Piech (Mercuri.ca) * Licensed under MPL 2.0 (see LICENSE.txt) * * See README.md for usage and integration instructions. * */ import * as Log from '../util/logging.js'; import Inflator from "../inflator.js"; import { hashUInt8Array } from '../util/int.js'; export default class TightDecoder { constructor(display) { this._ctl = null; this._filter = null; this._numColors = 0; this._palette = new Uint8Array(1024); // 256 * 4 (max palette size * max bytes-per-pixel) this._len = 0; this._enableQOI = false; this._displayGlobal = display; this._lastTransparentRectHash = ''; this._lastTransparentRectInfo = ''; this._zlibs = []; for (let i = 0; i < 4; i++) { this._zlibs[i] = new Inflator(); } this._itzlib = new Inflator(); } // ===== PROPERTIES ===== get enableQOI() { return this._enableQOI; } set enableQOI(enabled) { if(this._enableQOI === enabled) { return; } if (enabled) { this._enableQOI = this._enableQOIWorkers(); } else { this._enableQOI = false; this._disableQOIWorkers(); } } // ===== Public Methods ===== decodeRect(x, y, width, height, sock, display, depth, frame_id) { if (this._ctl === null) { if (sock.rQwait("TIGHT compression-control", 1)) { return false; } this._ctl = sock.rQshift8(); // Reset streams if the server requests it for (let i = 0; i < 4; i++) { if ((this._ctl >> i) & 1) { this._zlibs[i].reset(); Log.Info("Reset zlib stream " + i); } } // Figure out filter this._ctl = this._ctl >> 4; } let ret; if (this._ctl === 0x08) { ret = this._fillRect(x, y, width, height, sock, display, depth, frame_id); } else if (this._ctl === 0x09) { ret = this._jpegRect(x, y, width, height, sock, display, depth, frame_id); } else if (this._ctl === 0x0A) { ret = this._pngRect(x, y, width, height, sock, display, depth, frame_id); } else if ((this._ctl & 0x08) == 0) { ret = this._basicRect(this._ctl, x, y, width, height, sock, display, depth, frame_id); } else if (this._ctl === 0x0B) { ret = this._webpRect(x, y, width, height, sock, display, depth, frame_id); } else if (this._ctl === 0x0C) { ret = this._qoiRect(x, y, width, height, sock, display, depth, frame_id); } else if (this._ctl === 0x0D) { ret = this._itRect(x, y, width, height, sock, display, depth, frame_id); } else { throw new Error("Illegal tight compression received (ctl: " + this._ctl + ")"); } if (ret) { this._ctl = null; } return ret; } // ===== Private Methods ===== _fillRect(x, y, width, height, sock, display, depth, frame_id) { if (sock.rQwait("TIGHT", 3)) { return false; } const rQi = sock.rQi; const rQ = sock.rQ; display.fillRect(x, y, width, height, [rQ[rQi], rQ[rQi + 1], rQ[rQi + 2]], frame_id, false); sock.rQskipBytes(3); return true; } _jpegRect(x, y, width, height, sock, display, depth, frame_id) { let data = this._readData(sock); if (data === null) { return false; } display.imageRect(x, y, width, height, "image/jpeg", data, frame_id); return true; } _webpRect(x, y, width, height, sock, display, depth, frame_id) { let data = this._readData(sock); if (data === null) { return false; } display.imageRect(x, y, width, height, "image/webp", data, frame_id); return true; } _processRectQ() { while (this._availableWorkers.length > 0 && this._qoiRects.length > 0) { let i = this._availableWorkers.pop(); let worker = this._workers[i]; let rect = this._qoiRects.shift(); var image = new ArrayBuffer(rect.data.length); new Uint8Array(image).set(new Uint8Array(rect.data)); worker.postMessage({ x: rect.x, y: rect.y, width: rect.width, height: rect.height, depth: rect.depth, frame_id: rect.frame_id, image: image }, [image]); } } _qoiRect(x, y, width, height, sock, display, depth, frame_id) { let data = this._readData(sock); if (data === null) { return false; } if (this._enableQOI) { let dataClone = new Uint8Array(data); let item = {x: x,y: y,width: width,height: height,data: dataClone,depth: depth, frame_id: frame_id}; if (this._qoiRects.length < 1000) { this._qoiRects.push(item); this._processRectQ(); } else { Log.Warn("QOI queue exceeded limit."); this._qoiRects.splice(0, 500); } } return true; } _itRect(x, y, width, height, sock, display, depth, frame_id) { let data = this._readData(sock); if (data === null) { return false; } //filter out consecutive redundant data let h = hashUInt8Array(data); let info = `${x}.${y}.${width}.${height}` if (!(h === this._lastTransparentRectHash && info === this._lastTransparentRectInfo)) { const r = data[0]; const g = data[1]; const b = data[2]; const a = data[3]; const uncompressedSize = Math.floor(width * height / 2 + 1); this._itzlib.reset(); this._itzlib.setInput(data.slice(4)); data = this._itzlib.inflate(uncompressedSize); this._itzlib.setInput(null); // unpack let rgba = new Uint8Array(width * height * 4 + 4); for (let i = 0, d = 0; i < uncompressedSize; i++, d += 8) { let p = data[i]; rgba[d + 0] = r; rgba[d + 1] = g; rgba[d + 2] = b; rgba[d + 3] = a * ((p & 15) << 4) / 255; rgba[d + 4] = r; rgba[d + 5] = g; rgba[d + 6] = b; rgba[d + 7] = a * (p & 240) / 255; } let img = new ImageData(new Uint8ClampedArray(rgba.buffer, 0, width * height * 4), width, height); display.transparentRect(x, y, width, height, img, frame_id, h); this._lastTransparentRectHash = h; this._lastTransparentRectInfo = info; } else { display.dummyRect(x, y, width, height, frame_id); } return true; } _pngRect(x, y, width, height, sock, display, depth, frame_id) { throw new Error("PNG received in standard Tight rect"); } _basicRect(ctl, x, y, width, height, sock, display, depth, frame_id) { if (this._filter === null) { if (ctl & 0x4) { if (sock.rQwait("TIGHT", 1)) { return false; } this._filter = sock.rQshift8(); } else { // Implicit CopyFilter this._filter = 0; } } let streamId = ctl & 0x3; let ret; switch (this._filter) { case 0: // CopyFilter ret = this._copyFilter(streamId, x, y, width, height, sock, display, depth, frame_id); break; case 1: // PaletteFilter ret = this._paletteFilter(streamId, x, y, width, height, sock, display, depth, frame_id); break; case 2: // GradientFilter ret = this._gradientFilter(streamId, x, y, width, height, sock, display, depth, frame_id); break; default: throw new Error("Illegal tight filter received (ctl: " + this._filter + ")"); } if (ret) { this._filter = null; } return ret; } _copyFilter(streamId, x, y, width, height, sock, display, depth, frame_id) { const uncompressedSize = width * height * 3; let data; if (uncompressedSize === 0) { return true; } if (uncompressedSize < 12) { if (sock.rQwait("TIGHT", uncompressedSize)) { return false; } data = sock.rQshiftBytes(uncompressedSize); } else { data = this._readData(sock); if (data === null) { return false; } this._zlibs[streamId].setInput(data); data = this._zlibs[streamId].inflate(uncompressedSize); this._zlibs[streamId].setInput(null); } let rgbx = new Uint8Array(width * height * 4); for (let i = 0, j = 0; i < width * height * 4; i += 4, j += 3) { rgbx[i] = data[j]; rgbx[i + 1] = data[j + 1]; rgbx[i + 2] = data[j + 2]; rgbx[i + 3] = 255; // Alpha } display.blitImage(x, y, width, height, rgbx, 0, frame_id, false); return true; } _paletteFilter(streamId, x, y, width, height, sock, display, depth, frame_id) { if (this._numColors === 0) { if (sock.rQwait("TIGHT palette", 1)) { return false; } const numColors = sock.rQpeek8() + 1; const paletteSize = numColors * 3; if (sock.rQwait("TIGHT palette", 1 + paletteSize)) { return false; } this._numColors = numColors; sock.rQskipBytes(1); sock.rQshiftTo(this._palette, paletteSize); } const bpp = (this._numColors <= 2) ? 1 : 8; const rowSize = Math.floor((width * bpp + 7) / 8); const uncompressedSize = rowSize * height; let data; if (uncompressedSize === 0) { return true; } if (uncompressedSize < 12) { if (sock.rQwait("TIGHT", uncompressedSize)) { return false; } data = sock.rQshiftBytes(uncompressedSize); } else { data = this._readData(sock); if (data === null) { return false; } this._zlibs[streamId].setInput(data); data = this._zlibs[streamId].inflate(uncompressedSize); this._zlibs[streamId].setInput(null); } // Convert indexed (palette based) image data to RGB if (this._numColors == 2) { this._monoRect(x, y, width, height, data, this._palette, display, frame_id); } else { this._paletteRect(x, y, width, height, data, this._palette, display, frame_id); } this._numColors = 0; return true; } _monoRect(x, y, width, height, data, palette, display, frame_id) { // Convert indexed (palette based) image data to RGB // TODO: reduce number of calculations inside loop const dest = this._getScratchBuffer(width * height * 4); const w = Math.floor((width + 7) / 8); const w1 = Math.floor(width / 8); for (let y = 0; y < height; y++) { let dp, sp, x; for (x = 0; x < w1; x++) { for (let b = 7; b >= 0; b--) { dp = (y * width + x * 8 + 7 - b) * 4; sp = (data[y * w + x] >> b & 1) * 3; dest[dp] = palette[sp]; dest[dp + 1] = palette[sp + 1]; dest[dp + 2] = palette[sp + 2]; dest[dp + 3] = 255; } } for (let b = 7; b >= 8 - width % 8; b--) { dp = (y * width + x * 8 + 7 - b) * 4; sp = (data[y * w + x] >> b & 1) * 3; dest[dp] = palette[sp]; dest[dp + 1] = palette[sp + 1]; dest[dp + 2] = palette[sp + 2]; dest[dp + 3] = 255; } } display.blitImage(x, y, width, height, dest, 0, frame_id, false); } _paletteRect(x, y, width, height, data, palette, display, frame_id) { // Convert indexed (palette based) image data to RGB const dest = this._getScratchBuffer(width * height * 4); const total = width * height * 4; for (let i = 0, j = 0; i < total; i += 4, j++) { const sp = data[j] * 3; dest[i] = palette[sp]; dest[i + 1] = palette[sp + 1]; dest[i + 2] = palette[sp + 2]; dest[i + 3] = 255; } display.blitImage(x, y, width, height, dest, 0, frame_id, false); } _gradientFilter(streamId, x, y, width, height, sock, display, depth, frame_id) { throw new Error("Gradient filter not implemented"); } _readData(sock) { if (this._len === 0) { if (sock.rQwait("TIGHT", 3)) { return null; } let byte; byte = sock.rQshift8(); this._len = byte & 0x7f; if (byte & 0x80) { byte = sock.rQshift8(); this._len |= (byte & 0x7f) << 7; if (byte & 0x80) { byte = sock.rQshift8(); this._len |= byte << 14; } } } if (sock.rQwait("TIGHT", this._len)) { return null; } let data = sock.rQshiftBytes(this._len); this._len = 0; return data; } _getScratchBuffer(size) { if (!this._scratchBuffer || (this._scratchBuffer.length < size)) { this._scratchBuffer = new Uint8Array(size); } return this._scratchBuffer; } async _disableQOIWorkers() { if (this._workers) { this._enableQOI = false; this._availableWorkers = null; this._qoiRects = null; this._rectQlooping = null; for await (let i of Array.from(Array(this._threads).keys())) { this._workers[i].terminate(); delete this._workers[i]; } this._workers = null; } } _enableQOIWorkers() { let fullPath = window.location.pathname; let path = fullPath.substring(0, fullPath.lastIndexOf('/')+1); if ((window.navigator.hardwareConcurrency) && (window.navigator.hardwareConcurrency >= 4)) { this._threads = 16; } else { this._threads = 8; } this._workers = []; this._availableWorkers = []; this._qoiRects = []; this._rectQlooping = false; for (let i = 0; i < this._threads; i++) { this._workers.push(new Worker("core/decoders/qoi/decoder.js")); this._workers[i].onmessage = (evt) => { this._availableWorkers.push(i); switch(evt.data.result) { case 0: evt.data.freemem = null; let data = new Uint8ClampedArray(evt.data.data); let img = new ImageData(data, evt.data.img.width, evt.data.img.height, {colorSpace: evt.data.img.colorSpace}); this._displayGlobal.blitQoi( evt.data.x, evt.data.y, evt.data.width, evt.data.height, img, 0, evt.data.frame_id, false ); this._processRectQ(); // Send data back for garbage collection this._workers[i].postMessage({freemem: evt.data.data}); break; case 1: Log.Info("QOI Worker is now available."); break; case 2: Log.Info("Error on worker: " + evt.error); break; } }; } for (let i = 0; i < this._threads; i++) { this._workers[i].postMessage({path:path}); } return true; } }