diff --git a/core/decoders/zrle.js b/core/decoders/zrle.js new file mode 100644 index 00000000..d4d87a81 --- /dev/null +++ b/core/decoders/zrle.js @@ -0,0 +1,194 @@ +/* + * noVNC: HTML5 VNC client + * Copyright (C) 2012 Joel Martin + * Copyright (C) 2018 Samuel Mannehed for Cendio AB + * Copyright (C) 2018 Pierre Ossman for Cendio AB + * Copyright (C) 2018 Maxim Furtuna for Skysilk inc. + * Licensed under MPL 2.0 (see LICENSE.txt) + * + * See README.md for usage and integration instructions. + * + */ + +import Inflate from "../inflator.js"; + +const ZRLE_TILE_WIDTH = 64; +const ZRLE_TILE_HEIGHT = 64; + + +export default class ZRLEDecoder { + constructor() { + this._length = 0; + this._offset = 0; + this._inflator = new Inflate(); + + this._tileBuffer = new Uint8Array(ZRLE_TILE_WIDTH * ZRLE_TILE_HEIGHT * 3); + } + + decodeRect(x, y, width, height, sock, display, depth) { + if (this._length === 0) { + if (sock.rQwait("ZLib data length", 4)) { + return false; + } + + this._length = sock.rQshift32(); + } + if (sock.rQwait("Zlib data", this._length)) { + return false; + } + const data = sock.rQshiftBytes(this._length); + + const tiles_x = Math.ceil(width / ZRLE_TILE_WIDTH); + const tiles_y = Math.ceil(height / ZRLE_TILE_HEIGHT); + const total_tiles = tiles_x * tiles_y; + + //this._inflator.reset(); + this._uncompressed = this._inflator.inflate(data, true, width * height * 3 + total_tiles); + + for (let ty = y; ty < y + height; ty += ZRLE_TILE_HEIGHT) { + let th = Math.min(ZRLE_TILE_HEIGHT, y + height - ty); + + for (let tx = x; tx < x + width; tx += ZRLE_TILE_WIDTH) { + let tw = Math.min(ZRLE_TILE_WIDTH, x + width - tx); + + const tileSize = tw * th; + + const subencoding = this._uncompressed[this._offset++]; + if (subencoding === 0) { + // raw data + const data = this._readPixels(tileSize); + display.blitBgrImage(tx, ty, tw, th, data, 0, false); + + } else if (subencoding === 1) { + // solid + const background = this._readPixels(1); + display.fillRect(tx, ty, tw, th, [background[2], background[1], background[0]]); + + } else if (subencoding >= 2 && subencoding <= 16) { + // palette types + const data = this._decodePaletteTile(subencoding, tileSize); + display.blitBgrImage(tx, ty, tw, th, data, 0, false); + + } else if (subencoding === 128) { + // run-length encoding + const data = this._decodeRLETile(tileSize); + display.blitBgrImage(tx, ty, tw, th, data, 0, false); + + } else if (subencoding >= 130 && subencoding <= 255) { + const data = this._decodeRLEPaletteTile(subencoding - 128, tileSize); + display.blitBgrImage(tx, ty, tw, th, data, 0, false); + } else { + throw new Error('Unknown subencoding: ' + subencoding); + } + } + } + this._uncompressed = null; + this._length = 0; + this._offset = 0; + return true; + } + + _getBitsPerPixelInPalette(paletteSize) { + if (paletteSize <= 2) { + return 1; + } else if (paletteSize <= 4) { + return 2; + } else if (paletteSize <= 16) { + return 4; + } + } + + _readPixels(pixels) { + const size = pixels * 3; + const data = new Uint8Array(this._uncompressed.buffer, this._offset, size); + this._offset += size; + return data; + } + + _decodePaletteTile(paletteSize, tileSize) { + const data = this._tileBuffer; + + // palette + const palette = this._readPixels(paletteSize); + + const bitsPerPixel = this._getBitsPerPixelInPalette(paletteSize); + const mask = (1 << bitsPerPixel) - 1; + const encodedLength = Math.ceil(tileSize * bitsPerPixel / 8); + + let offset = 0; + + for (let j = 0; j < encodedLength; j++) { + let encoded = this._uncompressed[this._offset]; + for (let i = 0; i < 8; i += bitsPerPixel) { + const indexInPalette = encoded & mask; + encoded = encoded >> bitsPerPixel; + + data[offset] = palette[indexInPalette * 3]; + data[offset + 1] = palette[indexInPalette * 3 + 1]; + data[offset + 2] = palette[indexInPalette * 3 + 2]; + + offset += 3; + } + this._offset++; + } + + return data; + } + + _decodeRLETile(tileSize) { + const data = this._tileBuffer; + let i = 0; + while (i < tileSize) { + const pixel = this._readPixels(1); + const length = this._readRLELength(); + for (let j = 0; j < length; j++) { + data[i * 3] = pixel[0]; + data[i * 3 + 1] = pixel[1]; + data[i * 3 + 2] = pixel[2]; + i++; + } + } + return data; + } + + _decodeRLEPaletteTile(paletteSize, tileSize) { + const data = this._tileBuffer; + + // palette + const palette = this._readPixels(paletteSize); + + let offset = 0; + while (offset < tileSize) { + let indexInPalette = this._uncompressed[this._offset++]; + let length = 1; + if (indexInPalette >= 128) { + indexInPalette -= 128; + length = this._readRLELength(); + } + if (indexInPalette > paletteSize) { + throw new Error('Too big index in palette: ' + indexInPalette + ', palette size: ' + paletteSize); + } + if (offset + length > tileSize) { + throw new Error('Too big rle length in palette mode: ' + length + ', allowed length is: ' + (tileSize - offset)); + } + for (let j = 0; j < length; j++) { + data[offset * 3] = palette[indexInPalette * 3]; + data[offset * 3 + 1] = palette[indexInPalette * 3 + 1]; + data[offset * 3 + 2] = palette[indexInPalette * 3 + 2]; + offset++; + } + //offset += length; + } + return data; + } + + _readRLELength() { + let length = 0; + let current = 0; + do { + current = this._uncompressed[this._offset++]; + length += current; + } while (current === 255); + return length + 1; + } +} diff --git a/core/display.js b/core/display.js index 1528384d..7dd869c0 100644 --- a/core/display.js +++ b/core/display.js @@ -475,6 +475,26 @@ export default class Display { } } + blitBgrImage(x, y, width, height, arr, offset, from_queue) { + if (this._renderQ.length !== 0 && !from_queue) { + // NB(directxman12): it's technically more performant here to use preallocated arrays, + // but it's a lot of extra work for not a lot of payoff -- if we're using the render queue, + // this probably isn't getting called *nearly* as much + const new_arr = new Uint8Array(width * height * 3); + new_arr.set(new Uint8Array(arr.buffer, 0, new_arr.length)); + this._renderQ_push({ + 'type': 'blitBgr', + 'data': new_arr, + 'x': x, + 'y': y, + 'width': width, + 'height': height, + }); + } else { + this._bgrImageData(x, y, width, height, arr, offset); + } + } + blitRgbxImage(x, y, width, height, arr, offset, from_queue) { if (this._renderQ.length !== 0 && !from_queue) { // NB(directxman12): it's technically more performant here to use preallocated arrays, @@ -576,6 +596,19 @@ export default class Display { this._damage(x, y, img.width, img.height); } + _bgrImageData(x, y, width, height, arr, offset) { + const img = this._drawCtx.createImageData(width, height); + const data = img.data; + for (let i = 0, j = offset; i < width * height * 4; i += 4, j += 3) { + data[i] = arr[j + 2]; + data[i + 1] = arr[j + 1]; + data[i + 2] = arr[j]; + data[i + 3] = 255; // Alpha + } + this._drawCtx.putImageData(img, x, y); + this._damage(x, y, img.width, img.height); + } + _rgbxImageData(x, y, width, height, arr, offset) { // NB(directxman12): arr must be an Type Array view let img; @@ -625,6 +658,9 @@ export default class Display { case 'blitRgb': this.blitRgbImage(a.x, a.y, a.width, a.height, a.data, 0, true); break; + case 'blitBgr': + this.blitBgrImage(a.x, a.y, a.width, a.height, a.data, 0, true); + break; case 'blitRgbx': this.blitRgbxImage(a.x, a.y, a.width, a.height, a.data, 0, true); break; diff --git a/core/encodings.js b/core/encodings.js index 9fd38d58..79923413 100644 --- a/core/encodings.js +++ b/core/encodings.js @@ -12,6 +12,8 @@ export const encodings = { encodingRRE: 2, encodingHextile: 5, encodingTight: 7, + encodingZRLE: 16, + encodingZYWRLE: 17, encodingTightPNG: -260, pseudoEncodingQualityLevel9: -23, @@ -36,6 +38,8 @@ export function encodingName(num) { case encodings.encodingHextile: return "Hextile"; case encodings.encodingTight: return "Tight"; case encodings.encodingTightPNG: return "TightPNG"; + case encodings.encodingZRLE: return "ZRLE"; + case encodings.encodingZYWRLE: return "ZYWRLE"; default: return "[unknown encoding " + num + "]"; } } diff --git a/core/rfb.js b/core/rfb.js index a608be4f..7ba0c16c 100644 --- a/core/rfb.js +++ b/core/rfb.js @@ -28,6 +28,7 @@ import RREDecoder from "./decoders/rre.js"; import HextileDecoder from "./decoders/hextile.js"; import TightDecoder from "./decoders/tight.js"; import TightPNGDecoder from "./decoders/tightpng.js"; +import ZRLEDecoder from "./decoders/zrle.js"; // How many seconds to wait for a disconnect to finish const DISCONNECT_TIMEOUT = 3; @@ -163,6 +164,8 @@ export default class RFB extends EventTargetMixin { this._decoders[encodings.encodingHextile] = new HextileDecoder(); this._decoders[encodings.encodingTight] = new TightDecoder(); this._decoders[encodings.encodingTightPNG] = new TightPNGDecoder(); + this._decoders[encodings.encodingZRLE] = new ZRLEDecoder(); + this._decoders[encodings.encodingZYWRLE] = new ZRLEDecoder(); // NB: nothing that needs explicit teardown should be done // before this point, since this can throw an exception @@ -1217,6 +1220,8 @@ export default class RFB extends EventTargetMixin { if (this._fb_depth == 24) { encs.push(encodings.encodingTight); encs.push(encodings.encodingTightPNG); + encs.push(encodings.encodingZRLE); + encs.push(encodings.encodingZYWRLE); encs.push(encodings.encodingHextile); encs.push(encodings.encodingRRE); }