From d1314d4b3a2dba261ba293ec90c0e3204042734b Mon Sep 17 00:00:00 2001 From: "Alexander E. Patrakov" Date: Sun, 12 Aug 2018 01:43:38 +0800 Subject: [PATCH 1/2] Moved the "pixels + mask -> RGBA" logic to rfb.js As requested by Pierre Ossman - he needs this for supporting other cursor extensions. --- core/rfb.js | 55 +++++++++++++++++++++++++++++++++++++++------ core/util/cursor.js | 19 +++------------- 2 files changed, 51 insertions(+), 23 deletions(-) diff --git a/core/rfb.js b/core/rfb.js index a52c00d2..5d40cdd0 100644 --- a/core/rfb.js +++ b/core/rfb.js @@ -166,7 +166,15 @@ export default class RFB extends EventTargetMixin { this._canvas.tabIndex = -1; this._screen.appendChild(this._canvas); - this._cursor = new Cursor(); + // Cursor + this._cursor = new Cursor(); + this._cursorImage = { + rgbaPixels: [], + hotx: 0, + hoty: 0, + w: 0, + h: 0, + }; // populate encHandlers with bound versions this._encHandlers[encodings.encodingRaw] = RFB.encodingHandlers.RAW.bind(this); @@ -1601,6 +1609,23 @@ export default class RFB extends EventTargetMixin { RFB.messages.xvpOp(this._sock, ver, op); } + _updateCursor(rgba, hotx, hoty, w, h) { + this._cursorImage = { + rgbaPixels: rgba, + hotx: hotx, hoty: hoty, w: w, h: h, + }; + this._refreshCursor(); + } + + _refreshCursor() { + this._cursor.change(this._cursorImage.rgbaPixels, + this._cursorImage.hotx, + this._cursorImage.hoty, + this._cursorImage.w, + this._cursorImage.h + ); + } + static genDES(password, challenge) { const passwd = []; for (let i = 0; i < password.length; i++) { @@ -2521,20 +2546,36 @@ RFB.encodingHandlers = { Cursor() { Log.Debug(">> set_cursor"); - const x = this._FBU.x; // hotspot-x - const y = this._FBU.y; // hotspot-y + const hotx = this._FBU.x; // hotspot-x + const hoty = this._FBU.y; // hotspot-y const w = this._FBU.width; const h = this._FBU.height; const pixelslength = w * h * 4; - const masklength = Math.floor((w + 7) / 8) * h; + const masklength = Math.ceil(w / 8) * h; this._FBU.bytes = pixelslength + masklength; if (this._sock.rQwait("cursor encoding", this._FBU.bytes)) { return false; } - this._cursor.change(this._sock.rQshiftBytes(pixelslength), - this._sock.rQshiftBytes(masklength), - x, y, w, h); + // Decode from BGRX pixels + bit mask to RGBA + const pixels = this._sock.rQshiftBytes(pixelslength); + const mask = this._sock.rQshiftBytes(masklength); + let rgba = new Uint8Array(w * h * 4); + + let pix_idx = 0; + for (let y = 0; y < h; y++) { + for (let x = 0; x < w; x++) { + let mask_idx = y * Math.ceil(w / 8) + Math.floor(x / 8); + let alpha = (mask[mask_idx] << (x % 8)) & 0x80 ? 255 : 0; + rgba[pix_idx ] = pixels[pix_idx + 2]; + rgba[pix_idx + 1] = pixels[pix_idx + 1]; + rgba[pix_idx + 2] = pixels[pix_idx]; + rgba[pix_idx + 3] = alpha; + pix_idx += 4; + } + } + + this._updateCursor(rgba, hotx, hoty, w, h); this._FBU.bytes = 0; this._FBU.rects--; diff --git a/core/util/cursor.js b/core/util/cursor.js index 18aa7beb..7997194f 100644 --- a/core/util/cursor.js +++ b/core/util/cursor.js @@ -79,25 +79,12 @@ export default class Cursor { this._target = null; } - change(pixels, mask, hotx, hoty, w, h) { + change(rgba, hotx, hoty, w, h) { if ((w === 0) || (h === 0)) { this.clear(); return; } - let cur = [] - for (let y = 0; y < h; y++) { - for (let x = 0; x < w; x++) { - let idx = y * Math.ceil(w / 8) + Math.floor(x / 8); - let alpha = (mask[idx] << (x % 8)) & 0x80 ? 255 : 0; - idx = ((w * y) + x) * 4; - cur.push(pixels[idx + 2]); // red - cur.push(pixels[idx + 1]); // green - cur.push(pixels[idx]); // blue - cur.push(alpha); // alpha - } - } - this._position.x = this._position.x + this._hotSpot.x - hotx; this._position.y = this._position.y + this._hotSpot.y - hoty; this._hotSpot.x = hotx; @@ -111,10 +98,10 @@ export default class Cursor { let img; try { // IE doesn't support this - img = new ImageData(new Uint8ClampedArray(cur), w, h); + img = new ImageData(new Uint8ClampedArray(rgba), w, h); } catch (ex) { img = ctx.createImageData(w, h); - img.data.set(new Uint8ClampedArray(cur)); + img.data.set(new Uint8ClampedArray(rgba)); } ctx.clearRect(0, 0, w, h); ctx.putImageData(img, 0, 0); From 4c38179d15ee5011a9144843fc655194409dd506 Mon Sep 17 00:00:00 2001 From: "Alexander E. Patrakov" Date: Sun, 12 Aug 2018 03:17:16 +0800 Subject: [PATCH 2/2] Show dot when there is no visible cursor Disabled by default. --- app/ui.js | 9 ++++++ core/rfb.js | 75 +++++++++++++++++++++++++++++++++++++++-------- docs/API.md | 5 ++++ docs/EMBEDDING.md | 3 ++ vnc.html | 4 +++ 5 files changed, 84 insertions(+), 12 deletions(-) diff --git a/app/ui.js b/app/ui.js index c03f8d37..8486d53a 100644 --- a/app/ui.js +++ b/app/ui.js @@ -161,6 +161,7 @@ const UI = { UI.initSetting('resize', 'off'); UI.initSetting('shared', true); UI.initSetting('view_only', false); + UI.initSetting('show_dot', false); UI.initSetting('path', 'websockify'); UI.initSetting('repeaterID', ''); UI.initSetting('reconnect', false); @@ -347,6 +348,8 @@ const UI = { UI.addSettingChangeHandler('shared'); UI.addSettingChangeHandler('view_only'); UI.addSettingChangeHandler('view_only', UI.updateViewOnly); + UI.addSettingChangeHandler('show_dot'); + UI.addSettingChangeHandler('show_dot', UI.updateShowDotCursor); UI.addSettingChangeHandler('host'); UI.addSettingChangeHandler('port'); UI.addSettingChangeHandler('path'); @@ -1015,6 +1018,7 @@ const UI = { UI.rfb = new RFB(document.getElementById('noVNC_container'), url, { shared: UI.getSetting('shared'), + showDotCursor: UI.getSetting('show_dot'), repeaterID: UI.getSetting('repeaterID'), credentials: { password: password } }); UI.rfb.addEventListener("connect", UI.connectFinished); @@ -1583,6 +1587,11 @@ const UI = { UI.setMouseButton(1); //has it's own logic for hiding/showing }, + updateShowDotCursor() { + if (!UI.rfb) return; + UI.rfb.showDotCursor = UI.getSetting('show_dot'); + }, + updateLogging() { WebUtil.init_logging(UI.getSetting('logging')); }, diff --git a/core/rfb.js b/core/rfb.js index 5d40cdd0..9b59c894 100644 --- a/core/rfb.js +++ b/core/rfb.js @@ -48,6 +48,7 @@ export default class RFB extends EventTargetMixin { this._rfb_credentials = options.credentials || {}; this._shared = 'shared' in options ? !!options.shared : true; this._repeaterID = options.repeaterID || ''; + this._showDotCursor = options.showDotCursor || false; // Internal state this._rfb_connection_state = ''; @@ -168,13 +169,17 @@ export default class RFB extends EventTargetMixin { // Cursor this._cursor = new Cursor(); - this._cursorImage = { - rgbaPixels: [], - hotx: 0, - hoty: 0, - w: 0, - h: 0, - }; + + // XXX: TightVNC 2.8.11 sends no cursor at all until Windows changes + // it. Result: no cursor at all until a window border or an edit field + // is hit blindly. But there are also VNC servers that draw the cursor + // in the framebuffer and don't send the empty local cursor. There is + // no way to satisfy both sides. + // + // The spec is unclear on this "initial cursor" issue. Many other + // viewers (TigerVNC, RealVNC, Remmina) display an arrow as the + // initial cursor instead. + this._cursorImage = RFB.cursors.none; // populate encHandlers with bound versions this._encHandlers[encodings.encodingRaw] = RFB.encodingHandlers.RAW.bind(this); @@ -324,6 +329,12 @@ export default class RFB extends EventTargetMixin { } } + get showDotCursor() { return this._showDotCursor; } + set showDotCursor(show) { + this._showDotCursor = show; + this._refreshCursor(); + } + // ===== PUBLIC METHODS ===== disconnect() { @@ -426,6 +437,7 @@ export default class RFB extends EventTargetMixin { this._target.appendChild(this._screen); this._cursor.attach(this._canvas); + this._refreshCursor(); // Monitor size changes of the screen // FIXME: Use ResizeObserver, or hidden overflow @@ -1617,12 +1629,33 @@ export default class RFB extends EventTargetMixin { this._refreshCursor(); } + _shouldShowDotCursor() { + // Called when this._cursorImage is updated + if (!this._showDotCursor) { + // User does not want to see the dot, so... + return false; + } + + // The dot should not be shown if the cursor is already visible, + // i.e. contains at least one not-fully-transparent pixel. + // So iterate through all alpha bytes in rgba and stop at the + // first non-zero. + for (let i = 3; i < this._cursorImage.rgbaPixels.length; i += 4) { + if (this._cursorImage.rgbaPixels[i]) { + return false; + } + } + + // At this point, we know that the cursor is fully transparent, and + // the user wants to see the dot instead of this. + return true; + } + _refreshCursor() { - this._cursor.change(this._cursorImage.rgbaPixels, - this._cursorImage.hotx, - this._cursorImage.hoty, - this._cursorImage.w, - this._cursorImage.h + const image = this._shouldShowDotCursor() ? RFB.cursors.dot : this._cursorImage; + this._cursor.change(image.rgbaPixels, + image.hotx, image.hoty, + image.w, image.h ); } @@ -2598,3 +2631,21 @@ RFB.encodingHandlers = { } } } + +RFB.cursors = { + none: { + rgbaPixels: new Uint8Array(), + w: 0, h: 0, + hotx: 0, hoty: 0, + }, + + dot: { + rgbaPixels: new Uint8Array([ + 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 255, + 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 0, 255, + 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 255, + ]), + w: 3, h: 3, + hotx: 1, hoty: 1, + } +}; diff --git a/docs/API.md b/docs/API.md index a81da5ca..ae7fb664 100644 --- a/docs/API.md +++ b/docs/API.md @@ -53,6 +53,11 @@ protocol stream. should be sent whenever the container changes dimensions. Disabled by default. +`showDotCursor` + - Is a `boolean` indicating whether a dot cursor should be shown + instead of a zero-sized or fully-transparent cursor if the server + sets such invisible cursor. Disabled by default. + `capabilities` *Read only* - Is an `Object` indicating which optional extensions are available on the server. Some methods may only be called if the corresponding diff --git a/docs/EMBEDDING.md b/docs/EMBEDDING.md index cad80ef3..5399b48b 100644 --- a/docs/EMBEDDING.md +++ b/docs/EMBEDDING.md @@ -61,6 +61,9 @@ query string. Currently the following options are available: * `resize` - How to resize the remote session if it is not the same size as the browser window. Can be one of `off`, `scale` and `remote`. +* `show_dot` - If a dot cursor should be shown when the remote server provides + no local cursor, or provides a fully-transparent (invisible) cursor. + * `logging` - The console log level. Can be one of `error`, `warn`, `info` or `debug`. diff --git a/vnc.html b/vnc.html index 14b85586..d00eec30 100644 --- a/vnc.html +++ b/vnc.html @@ -250,6 +250,10 @@

  • +
  • + +
  • +