summaryrefslogtreecommitdiff
path: root/core
diff options
context:
space:
mode:
authorpdlan <pengdinglan@gmail.com>2021-11-20 13:22:52 -0500
committerpdlan <pengdinglan@gmail.com>2021-11-26 03:59:19 -0500
commit7f8416014751a0ad67357f5457aaedfd9be9a964 (patch)
tree3bae1c711899ff36e2c0d097c59e4ffdc2dedc15 /core
parentbfb6ac259d3176b916ab6353619cb420f8daa71e (diff)
downloadnovnc-7f8416014751a0ad67357f5457aaedfd9be9a964.tar.gz
Add RealVNC's JPEG encoding
Add support for RealVNC's JPEG encoding. Add tests for JPEGDecoder. Fix the corner case of caching Huffman or quantization tables.
Diffstat (limited to 'core')
-rw-r--r--core/decoders/jpeg.js141
-rw-r--r--core/encodings.js2
-rw-r--r--core/rfb.js3
3 files changed, 146 insertions, 0 deletions
diff --git a/core/decoders/jpeg.js b/core/decoders/jpeg.js
new file mode 100644
index 0000000..e1f2bdf
--- /dev/null
+++ b/core/decoders/jpeg.js
@@ -0,0 +1,141 @@
+/*
+ * noVNC: HTML5 VNC client
+ * Copyright (C) 2019 The noVNC Authors
+ * Licensed under MPL 2.0 (see LICENSE.txt)
+ *
+ * See README.md for usage and integration instructions.
+ *
+ */
+
+export default class JPEGDecoder {
+ constructor() {
+ // RealVNC will reuse the quantization tables
+ // and Huffman tables, so we need to cache them.
+ this._quantTables = [];
+ this._huffmanTables = [];
+ this._cachedQuantTables = [];
+ this._cachedHuffmanTables = [];
+
+ this._jpegLength = 0;
+ this._segments = [];
+ }
+
+ decodeRect(x, y, width, height, sock, display, depth) {
+ // A rect of JPEG encodings is simply a JPEG file
+ if (!this._parseJPEG(sock.rQslice(0))) {
+ return false;
+ }
+ const data = sock.rQshiftBytes(this._jpegLength);
+ if (this._quantTables.length != 0 && this._huffmanTables.length != 0) {
+ // If there are quantization tables and Huffman tables in the JPEG
+ // image, we can directly render it.
+ display.imageRect(x, y, width, height, "image/jpeg", data);
+ return true;
+ } else {
+ // Otherwise we need to insert cached tables.
+ const sofIndex = this._segments.findIndex(
+ x => x[1] == 0xC0 || x[1] == 0xC2
+ );
+ if (sofIndex == -1) {
+ throw new Error("Illegal JPEG image without SOF");
+ }
+ let segments = this._segments.slice(0, sofIndex);
+ segments = segments.concat(this._quantTables.length ?
+ this._quantTables :
+ this._cachedQuantTables);
+ segments.push(this._segments[sofIndex]);
+ segments = segments.concat(this._huffmanTables.length ?
+ this._huffmanTables :
+ this._cachedHuffmanTables,
+ this._segments.slice(sofIndex + 1));
+ let length = 0;
+ for (let i = 0; i < segments.length; i++) {
+ length += segments[i].length;
+ }
+ const data = new Uint8Array(length);
+ length = 0;
+ for (let i = 0; i < segments.length; i++) {
+ data.set(segments[i], length);
+ length += segments[i].length;
+ }
+ display.imageRect(x, y, width, height, "image/jpeg", data);
+ return true;
+ }
+ }
+
+ _parseJPEG(buffer) {
+ if (this._quantTables.length != 0) {
+ this._cachedQuantTables = this._quantTables;
+ }
+ if (this._huffmanTables.length != 0) {
+ this._cachedHuffmanTables = this._huffmanTables;
+ }
+ this._quantTables = [];
+ this._huffmanTables = [];
+ this._segments = [];
+ let i = 0;
+ let bufferLength = buffer.length;
+ while (true) {
+ let j = i;
+ if (j + 2 > bufferLength) {
+ return false;
+ }
+ if (buffer[j] != 0xFF) {
+ throw new Error("Illegal JPEG marker received (byte: " +
+ buffer[j] + ")");
+ }
+ const type = buffer[j+1];
+ j += 2;
+ if (type == 0xD9) {
+ this._jpegLength = j;
+ this._segments.push(buffer.slice(i, j));
+ return true;
+ } else if (type == 0xDA) {
+ // start of scan
+ let hasFoundEndOfScan = false;
+ for (let k = j + 3; k + 1 < bufferLength; k++) {
+ if (buffer[k] == 0xFF && buffer[k+1] != 0x00 &&
+ !(buffer[k+1] >= 0xD0 && buffer[k+1] <= 0xD7)) {
+ j = k;
+ hasFoundEndOfScan = true;
+ break;
+ }
+ }
+ if (!hasFoundEndOfScan) {
+ return false;
+ }
+ this._segments.push(buffer.slice(i, j));
+ i = j;
+ continue;
+ } else if (type >= 0xD0 && type < 0xD9 || type == 0x01) {
+ // No length after marker
+ this._segments.push(buffer.slice(i, j));
+ i = j;
+ continue;
+ }
+ if (j + 2 > bufferLength) {
+ return false;
+ }
+ const length = (buffer[j] << 8) + buffer[j+1] - 2;
+ if (length < 0) {
+ throw new Error("Illegal JPEG length received (length: " +
+ length + ")");
+ }
+ j += 2;
+ if (j + length > bufferLength) {
+ return false;
+ }
+ j += length;
+ const segment = buffer.slice(i, j);
+ if (type == 0xC4) {
+ // Huffman tables
+ this._huffmanTables.push(segment);
+ } else if (type == 0xDB) {
+ // Quantization tables
+ this._quantTables.push(segment);
+ }
+ this._segments.push(segment);
+ i = j;
+ }
+ }
+}
diff --git a/core/encodings.js b/core/encodings.js
index 51c0992..f16d033 100644
--- a/core/encodings.js
+++ b/core/encodings.js
@@ -13,6 +13,7 @@ export const encodings = {
encodingHextile: 5,
encodingTight: 7,
encodingTightPNG: -260,
+ encodingJPEG: 21,
pseudoEncodingQualityLevel9: -23,
pseudoEncodingQualityLevel0: -32,
@@ -39,6 +40,7 @@ export function encodingName(num) {
case encodings.encodingHextile: return "Hextile";
case encodings.encodingTight: return "Tight";
case encodings.encodingTightPNG: return "TightPNG";
+ case encodings.encodingJPEG: return "JPEG";
default: return "[unknown encoding " + num + "]";
}
}
diff --git a/core/rfb.js b/core/rfb.js
index bc52f4a..1583103 100644
--- a/core/rfb.js
+++ b/core/rfb.js
@@ -33,6 +33,7 @@ import HextileDecoder from "./decoders/hextile.js";
import TightDecoder from "./decoders/tight.js";
import TightPNGDecoder from "./decoders/tightpng.js";
import ZRLEDecoder from "./decoders/zrle.js";
+import JPEGDecoder from "./decoders/jpeg.js";
// How many seconds to wait for a disconnect to finish
const DISCONNECT_TIMEOUT = 3;
@@ -220,6 +221,7 @@ export default class RFB extends EventTargetMixin {
this._decoders[encodings.encodingTight] = new TightDecoder();
this._decoders[encodings.encodingTightPNG] = new TightPNGDecoder();
this._decoders[encodings.encodingZRLE] = new ZRLEDecoder();
+ this._decoders[encodings.encodingJPEG] = new JPEGDecoder();
// NB: nothing that needs explicit teardown should be done
// before this point, since this can throw an exception
@@ -1775,6 +1777,7 @@ export default class RFB extends EventTargetMixin {
encs.push(encodings.encodingTight);
encs.push(encodings.encodingTightPNG);
encs.push(encodings.encodingZRLE);
+ encs.push(encodings.encodingJPEG);
encs.push(encodings.encodingHextile);
encs.push(encodings.encodingRRE);
}