Added support for ZRLE encoding in VNC.
This commit is contained in:
parent
a85c85fb5f
commit
0578f3edb8
|
|
@ -0,0 +1,242 @@
|
||||||
|
/*
|
||||||
|
* noVNC: HTML5 VNC client
|
||||||
|
* Copyright (C) 2021 The noVNC Authors
|
||||||
|
* Licensed under MPL 2.0 (see LICENSE.txt)
|
||||||
|
*
|
||||||
|
* See README.md for usage and integration instructions.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
import Inflator from "../inflator.js";
|
||||||
|
|
||||||
|
export default class ZRLEDecoder {
|
||||||
|
constructor() {
|
||||||
|
this._data = false;
|
||||||
|
this._compressedLength = null;
|
||||||
|
this._uncompressed = null;
|
||||||
|
this._tileBuffer = new Uint8ClampedArray(64 * 64 * 4);
|
||||||
|
this._zlib = new Inflator();
|
||||||
|
this._clearDataBuffer();
|
||||||
|
}
|
||||||
|
|
||||||
|
_clearDataBuffer() {
|
||||||
|
this._dataBuffer = null;
|
||||||
|
this._dataBufferPtr = 0;
|
||||||
|
this._dataBufferSize = 1 + (1024 * 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
_fillDataBuffer() {
|
||||||
|
let fillSize = this._dataBufferSize;
|
||||||
|
while (true) {
|
||||||
|
try {
|
||||||
|
this._dataBuffer = this._zlib.inflate(fillSize, true);
|
||||||
|
this._dataBufferPtr = 0;
|
||||||
|
this._dataBufferSize = this._dataBuffer.length;
|
||||||
|
break;
|
||||||
|
} catch (e) {
|
||||||
|
if (fillSize == 1) { // Something's wrong if we can't fill even 1 byte
|
||||||
|
throw (e);
|
||||||
|
}
|
||||||
|
fillSize = Math.ceil(fillSize / 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_inflateFromStream(bytes) {
|
||||||
|
if (this._dataBuffer == null) {
|
||||||
|
this._dataBuffer = new Uint8Array(this._dataBufferSize);
|
||||||
|
this._fillDataBuffer();
|
||||||
|
}
|
||||||
|
let ret = new Uint8Array(bytes), pos = 0;
|
||||||
|
while (bytes > 0) {
|
||||||
|
let sliceLen = bytes > (this._dataBufferSize - this._dataBufferPtr) ? this._dataBufferSize - this._dataBufferPtr : bytes;
|
||||||
|
ret.set(this._dataBuffer.slice(this._dataBufferPtr, this._dataBufferPtr + sliceLen), pos);
|
||||||
|
pos += sliceLen;
|
||||||
|
this._dataBufferPtr += sliceLen;
|
||||||
|
bytes -= sliceLen;
|
||||||
|
if (bytes > 0 && this._dataBufferPtr == this._dataBufferSize) {
|
||||||
|
this._fillDataBuffer();
|
||||||
|
this._dataBufferPtr = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
_rleRun() {
|
||||||
|
let r = 0, runLength = 1;
|
||||||
|
do {
|
||||||
|
r = this._inflateFromStream(1)[0];
|
||||||
|
runLength += r;
|
||||||
|
} while (r == 255);
|
||||||
|
return runLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
_blitTile(blitpos, blitlen, color) {
|
||||||
|
let bp = blitpos * 4;
|
||||||
|
let ep = bp + (blitlen * 4);
|
||||||
|
let p = bp;
|
||||||
|
for (; p < ep;) {
|
||||||
|
this._tileBuffer[p] = color[0];
|
||||||
|
this._tileBuffer[p + 1] = color[1];
|
||||||
|
this._tileBuffer[p + 2] = color[2];
|
||||||
|
this._tileBuffer[p + 3] = 255;
|
||||||
|
p += 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_colorFromPalette(palette, index, bpp) {
|
||||||
|
let idx = bpp * index;
|
||||||
|
return [palette[idx], palette[idx + 1], palette[idx + 2]];
|
||||||
|
}
|
||||||
|
|
||||||
|
_testBit(cb, bit) {
|
||||||
|
return (cb & (1 << bit)) === 0 ? 0 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
decodeRect(x, y, width, height, sock, display, depth) {
|
||||||
|
if (this._compressedLength === null) {
|
||||||
|
this._clearDataBuffer();
|
||||||
|
|
||||||
|
// Wait for compressed data length
|
||||||
|
if (sock.rQwait("ZRLE", 4)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
this._compressedLength = sock.rQshift32();
|
||||||
|
if (this._compressedLength < this._dataBufferSize) {
|
||||||
|
// Try to choose a better data buffer size in powers of 2
|
||||||
|
this._dataBufferSize = 1 + Math.pow(2, Math.floor(Math.log(this._compressedLength) / Math.log(2)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this._compressedLength !== null && this._data === false) {
|
||||||
|
// Wait for compressed data
|
||||||
|
if (sock.rQwait("ZRLE", this._compressedLength)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
this._data = true;
|
||||||
|
let data = sock.rQshiftBytes(this._compressedLength);
|
||||||
|
this._zlib.setInput(data);
|
||||||
|
}
|
||||||
|
if (this._data === true) {
|
||||||
|
let bytesPerPixel = (depth / 8) > 3 ? 3 : Math.round(depth / 8);
|
||||||
|
let totalTilesX = Math.ceil(width / 64);
|
||||||
|
let totalTilesY = Math.ceil(height / 64);
|
||||||
|
let rx = 0, ry = 0;
|
||||||
|
for (let ty = 1; ty <= totalTilesY; ty++) {
|
||||||
|
rx = 0;
|
||||||
|
for (let tx = 1; tx <= totalTilesX; tx++) {
|
||||||
|
let tileWidth = (tx == totalTilesX) ? width - ((totalTilesX - 1) * 64) : 64;
|
||||||
|
let tileHeight = (ty == totalTilesY) ? height - ((totalTilesY - 1) * 64) : 64;
|
||||||
|
let tileTotalPixels = tileWidth * tileHeight;
|
||||||
|
let px = x + rx, py = y + ry;
|
||||||
|
|
||||||
|
let subencoding = this._inflateFromStream(1)[0];
|
||||||
|
if (subencoding == 0) { // Raw pixel data
|
||||||
|
let bytes = tileWidth * tileHeight * bytesPerPixel;
|
||||||
|
let data = this._inflateFromStream(bytes);
|
||||||
|
for (let src = 0, dst = 0; src < bytes; src += 3, dst += 4) {
|
||||||
|
this._tileBuffer[dst] = data[src];
|
||||||
|
this._tileBuffer[dst + 1] = data[src + 1];
|
||||||
|
this._tileBuffer[dst + 2] = data[src + 2];
|
||||||
|
this._tileBuffer[dst + 3] = 255;
|
||||||
|
}
|
||||||
|
display.blitImage(px, py, tileWidth, tileHeight, this._tileBuffer, 0, false);
|
||||||
|
}
|
||||||
|
if (subencoding == 1) { // Solid tile (single color)
|
||||||
|
let pixel = this._inflateFromStream(bytesPerPixel);
|
||||||
|
display.fillRect(px, py, tileWidth, tileHeight, [pixel[0], pixel[1], pixel[2]], false);
|
||||||
|
}
|
||||||
|
if (subencoding >= 2 && subencoding <= 16) { // Packed palette
|
||||||
|
let bytes = subencoding * bytesPerPixel;
|
||||||
|
let paletteData = this._inflateFromStream(bytes);
|
||||||
|
let packedPixelBytes, bitsPerPixel, pixelsPerByte;
|
||||||
|
switch (subencoding) {
|
||||||
|
case 2:
|
||||||
|
packedPixelBytes = Math.floor((tileWidth + 7) / 8) * tileHeight;
|
||||||
|
bitsPerPixel = 1;
|
||||||
|
pixelsPerByte = 8;
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
case 4:
|
||||||
|
packedPixelBytes = Math.floor((tileWidth + 3) / 4) * tileHeight;
|
||||||
|
bitsPerPixel = 2;
|
||||||
|
pixelsPerByte = 4;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
packedPixelBytes = Math.floor((tileWidth + 1) / 2) * tileHeight;
|
||||||
|
bitsPerPixel = 4;
|
||||||
|
pixelsPerByte = 2;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let strideWidth = (Math.ceil(tileWidth / 8) * 8) / pixelsPerByte;
|
||||||
|
let pixelData = this._inflateFromStream(packedPixelBytes), pixel = 0, tilePos = 0, cb = pixelData[0];
|
||||||
|
for (let tileY = 0; tileY < tileHeight; tileY++) {
|
||||||
|
cb = pixelData[strideWidth * tileY];
|
||||||
|
for (let tileX = 0, bitIdx = 0, byteIdx = strideWidth * tileY; tileX < tileWidth; tileX++) {
|
||||||
|
switch (bitsPerPixel) {
|
||||||
|
case 1:
|
||||||
|
pixel = this._testBit(cb, 8 - bitIdx);
|
||||||
|
bitIdx++;
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
pixel = (this._testBit(cb, 6 - bitIdx))
|
||||||
|
+ (this._testBit(cb, 7 - bitIdx) << 1);
|
||||||
|
bitIdx += 2;
|
||||||
|
break;
|
||||||
|
case 4:
|
||||||
|
pixel = this._testBit(cb, 4 - bitIdx)
|
||||||
|
+ (this._testBit(cb, 5 - bitIdx) << 1)
|
||||||
|
+ (this._testBit(cb, 6 - bitIdx) << 2)
|
||||||
|
+ (this._testBit(cb, 7 - bitIdx) << 3);
|
||||||
|
bitIdx += 4;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (bitIdx == 8) {
|
||||||
|
byteIdx += 1;
|
||||||
|
cb = pixelData[byteIdx];
|
||||||
|
bitIdx = 0;
|
||||||
|
}
|
||||||
|
this._blitTile(tilePos, 1, [paletteData[pixel * 3], paletteData[pixel * 3 + 1], paletteData[pixel * 3 + 2]]);
|
||||||
|
tilePos++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
display.blitImage(px, py, tileWidth, tileHeight, this._tileBuffer, 0, false);
|
||||||
|
}
|
||||||
|
if (subencoding == 128) { // Plain RLE
|
||||||
|
let tilePos = 0;
|
||||||
|
while (tilePos < tileTotalPixels) {
|
||||||
|
let pixel = this._inflateFromStream(bytesPerPixel);
|
||||||
|
let runLength = this._rleRun();
|
||||||
|
this._blitTile(tilePos, runLength, pixel);
|
||||||
|
tilePos += runLength;
|
||||||
|
}
|
||||||
|
display.blitImage(px, py, tileWidth, tileHeight, this._tileBuffer, 0, false);
|
||||||
|
}
|
||||||
|
if (subencoding >= 130) { // Palette RLE
|
||||||
|
let paletteBytes = (subencoding - 128) * bytesPerPixel;
|
||||||
|
let palette = this._inflateFromStream(paletteBytes);
|
||||||
|
let tilePos = 0;
|
||||||
|
while (tilePos < tileTotalPixels) {
|
||||||
|
let paletteIndex = this._inflateFromStream(1)[0];
|
||||||
|
let runLength = 1;
|
||||||
|
if (paletteIndex > 127) {
|
||||||
|
let color = this._colorFromPalette(palette, paletteIndex - 128, bytesPerPixel);
|
||||||
|
runLength = this._rleRun();
|
||||||
|
this._blitTile(tilePos, runLength, color);
|
||||||
|
} else {
|
||||||
|
let color = this._colorFromPalette(palette, paletteIndex, bytesPerPixel);
|
||||||
|
this._blitTile(tilePos, runLength, color);
|
||||||
|
}
|
||||||
|
tilePos += runLength;
|
||||||
|
}
|
||||||
|
display.blitImage(px, py, tileWidth, tileHeight, this._tileBuffer, 0, false);
|
||||||
|
}
|
||||||
|
rx += 64; // next tile
|
||||||
|
}
|
||||||
|
ry += 64; // next row
|
||||||
|
}
|
||||||
|
this._zlib.setInput(null);
|
||||||
|
this._compressedLength = null;
|
||||||
|
this._data = false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -12,6 +12,7 @@ export const encodings = {
|
||||||
encodingRRE: 2,
|
encodingRRE: 2,
|
||||||
encodingHextile: 5,
|
encodingHextile: 5,
|
||||||
encodingTight: 7,
|
encodingTight: 7,
|
||||||
|
encodingZRLE: 16,
|
||||||
encodingTightPNG: -260,
|
encodingTightPNG: -260,
|
||||||
|
|
||||||
pseudoEncodingQualityLevel9: -23,
|
pseudoEncodingQualityLevel9: -23,
|
||||||
|
|
@ -39,6 +40,7 @@ export function encodingName(num) {
|
||||||
case encodings.encodingHextile: return "Hextile";
|
case encodings.encodingHextile: return "Hextile";
|
||||||
case encodings.encodingTight: return "Tight";
|
case encodings.encodingTight: return "Tight";
|
||||||
case encodings.encodingTightPNG: return "TightPNG";
|
case encodings.encodingTightPNG: return "TightPNG";
|
||||||
|
case encodings.encodingZRLE: return "ZRLE";
|
||||||
default: return "[unknown encoding " + num + "]";
|
default: return "[unknown encoding " + num + "]";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ export default class Inflate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inflate(expected) {
|
inflate(expected, allowIncomplete) {
|
||||||
// resize our output buffer if it's too small
|
// resize our output buffer if it's too small
|
||||||
// (we could just use multiple chunks, but that would cause an extra
|
// (we could just use multiple chunks, but that would cause an extra
|
||||||
// allocation each time to flatten the chunks)
|
// allocation each time to flatten the chunks)
|
||||||
|
|
@ -54,7 +54,9 @@ export default class Inflate {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.strm.next_out != expected) {
|
if (this.strm.next_out != expected) {
|
||||||
throw new Error("Incomplete zlib block");
|
if (allowIncomplete == null || allowIncomplete === false) {
|
||||||
|
throw new Error("Incomplete zlib block");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Uint8Array(this.strm.output.buffer, 0, this.strm.next_out);
|
return new Uint8Array(this.strm.output.buffer, 0, this.strm.next_out);
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ import RREDecoder from "./decoders/rre.js";
|
||||||
import HextileDecoder from "./decoders/hextile.js";
|
import HextileDecoder from "./decoders/hextile.js";
|
||||||
import TightDecoder from "./decoders/tight.js";
|
import TightDecoder from "./decoders/tight.js";
|
||||||
import TightPNGDecoder from "./decoders/tightpng.js";
|
import TightPNGDecoder from "./decoders/tightpng.js";
|
||||||
|
import ZRLEDecoder from "./decoders/zrle.js";
|
||||||
|
|
||||||
// How many seconds to wait for a disconnect to finish
|
// How many seconds to wait for a disconnect to finish
|
||||||
const DISCONNECT_TIMEOUT = 3;
|
const DISCONNECT_TIMEOUT = 3;
|
||||||
|
|
@ -218,6 +219,7 @@ export default class RFB extends EventTargetMixin {
|
||||||
this._decoders[encodings.encodingHextile] = new HextileDecoder();
|
this._decoders[encodings.encodingHextile] = new HextileDecoder();
|
||||||
this._decoders[encodings.encodingTight] = new TightDecoder();
|
this._decoders[encodings.encodingTight] = new TightDecoder();
|
||||||
this._decoders[encodings.encodingTightPNG] = new TightPNGDecoder();
|
this._decoders[encodings.encodingTightPNG] = new TightPNGDecoder();
|
||||||
|
this._decoders[encodings.encodingZRLE] = new ZRLEDecoder();
|
||||||
|
|
||||||
// NB: nothing that needs explicit teardown should be done
|
// NB: nothing that needs explicit teardown should be done
|
||||||
// before this point, since this can throw an exception
|
// before this point, since this can throw an exception
|
||||||
|
|
@ -1772,6 +1774,7 @@ export default class RFB extends EventTargetMixin {
|
||||||
if (this._fbDepth == 24) {
|
if (this._fbDepth == 24) {
|
||||||
encs.push(encodings.encodingTight);
|
encs.push(encodings.encodingTight);
|
||||||
encs.push(encodings.encodingTightPNG);
|
encs.push(encodings.encodingTightPNG);
|
||||||
|
encs.push(encodings.encodingZRLE);
|
||||||
encs.push(encodings.encodingHextile);
|
encs.push(encodings.encodingHextile);
|
||||||
encs.push(encodings.encodingRRE);
|
encs.push(encodings.encodingRRE);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue