From 4d4b29a14f88c9c56fc45a17cfd59c4c856d4afc Mon Sep 17 00:00:00 2001 From: Daniel Hammerschmidt Date: Mon, 8 Sep 2025 22:44:41 +0200 Subject: [PATCH 1/4] Add noVNC_setting_crop_rect to show only a region of the fb --- app/ui.js | 31 ++++++++++---- core/rfb.js | 114 +++++++++++++++++++++++++++++++++++++++++++++++----- vnc.html | 4 ++ 3 files changed, 130 insertions(+), 19 deletions(-) diff --git a/app/ui.js b/app/ui.js index 2542e0591..302b432eb 100644 --- a/app/ui.js +++ b/app/ui.js @@ -179,6 +179,7 @@ const UI = { UI.initSetting('autoconnect', false); UI.initSetting('view_clip', false); UI.initSetting('resize', 'off'); + UI.initSetting('crop_rect'); UI.initSetting('quality', 6); UI.initSetting('compression', 2); UI.initSetting('shared', true); @@ -360,6 +361,8 @@ const UI = { UI.addSettingChangeHandler('resize'); UI.addSettingChangeHandler('resize', UI.applyResizeMode); UI.addSettingChangeHandler('resize', UI.updateViewClip); + UI.addSettingChangeHandler('crop_rect'); + UI.addSettingChangeHandler('crop_rect', UI.updateCropRect); UI.addSettingChangeHandler('quality'); UI.addSettingChangeHandler('quality', UI.updateQuality); UI.addSettingChangeHandler('compression'); @@ -464,6 +467,17 @@ const UI = { .classList.remove('noVNC_open'); }, + showConnectedStatus(e) { + let msg; + if (UI.getSetting('encrypt')) { + msg = _("Connected (encrypted) to ") + UI.desktopName; + } else { + msg = _("Connected (unencrypted) to ") + UI.desktopName; + } + msg += ' [' + UI.rfb.cropRect + ']'; + UI.showStatus(msg); + }, + showStatus(text, statusType, time) { const statusElem = document.getElementById('noVNC_status'); @@ -1095,9 +1109,11 @@ const UI = { UI.rfb.addEventListener("clipboard", UI.clipboardReceive); UI.rfb.addEventListener("bell", UI.bell); UI.rfb.addEventListener("desktopname", UI.updateDesktopName); + UI.rfb.addEventListener("croprectchanged", UI.showConnectedStatus); UI.rfb.clipViewport = UI.getSetting('view_clip'); UI.rfb.scaleViewport = UI.getSetting('resize') === 'scale'; UI.rfb.resizeSession = UI.getSetting('resize') === 'remote'; + UI.rfb.cropRect = UI.getSetting('crop_rect'); UI.rfb.qualityLevel = parseInt(UI.getSetting('quality')); UI.rfb.compressionLevel = parseInt(UI.getSetting('compression')); UI.rfb.showDotCursor = UI.getSetting('show_dot'); @@ -1144,14 +1160,7 @@ const UI = { connectFinished(e) { UI.connected = true; UI.inhibitReconnect = false; - - let msg; - if (UI.getSetting('encrypt')) { - msg = _("Connected (encrypted) to ") + UI.desktopName; - } else { - msg = _("Connected (unencrypted) to ") + UI.desktopName; - } - UI.showStatus(msg); + UI.showConnectedStatus(); UI.updateVisualState('connected'); // Do this last because it can only be used on rendered elements @@ -1438,6 +1447,12 @@ const UI = { viewDragButton.disabled = !UI.rfb.clippingViewport; }, + updateCropRect() { + if (!UI.connected) return; + + UI.rfb.cropRect = UI.getSetting('crop_rect'); + }, + /* ------^------- * /VIEWDRAG * ============== diff --git a/core/rfb.js b/core/rfb.js index 80011e4a1..993fe7bd4 100644 --- a/core/rfb.js +++ b/core/rfb.js @@ -87,6 +87,15 @@ const extendedClipboardActionPeek = 1 << 26; const extendedClipboardActionNotify = 1 << 27; const extendedClipboardActionProvide = 1 << 28; +// [GEOMETRY SPECIFICATIONS](https://www.x.org/releases/X11R7.7/doc/man/man7/X.7.xhtml#heading7) +const geometryRegExp = new RegExp([ + '^', + '(?0|[1-9][0-9]*)', 'x', '(?0|[1-9][0-9]*)', + '(?:', '[+](?0|[1-9][0-9]*)', '|', '[-](?0|[1-9][0-9]*)', ')', + '(?:', '[+](?0|[1-9][0-9]*)', '|', '[-](?0|[1-9][0-9]*)', ')', + '$', +].join('')); + export default class RFB extends EventTargetMixin { constructor(target, urlOrChannel, options) { if (!target) { @@ -136,6 +145,19 @@ export default class RFB extends EventTargetMixin { this._fbWidth = 0; this._fbHeight = 0; + this._cropRect = { + geometry: undefined, + width: 0, + height: 0, + left: 0, + right: undefined, + top: 0, + bottom: undefined, + // we keep a redundant copy of fbWidth and fbHeight here + // to avoid conflicts with existing code + fbWidth: undefined, + fbHeight: undefined, + }; this._fbName = ""; @@ -356,6 +378,45 @@ export default class RFB extends EventTargetMixin { } } + get cropRect() { + const { width, height, left, right, top, bottom, fbWidth, fbHeight } = this._cropRect; + return `${width}x${height}${ + !right ? `+${left}` : `-${right}` + }${ + !bottom ? `+${top}` : `-${bottom}` + }${ + width === fbWidth && height === fbHeight ? '' : ` (${fbWidth}x${fbHeight})` + }`; + } + set cropRect(geometry) { + const rect = Object.assign( this._cropRect, { geometry } ); + const { fbWidth, fbHeight } = rect; + if (geometry && (geometry = geometry.match(geometryRegExp)?.groups)) { + Object.assign(rect, Object.fromEntries( + Object.entries(geometry).map(([k, v]) => ([k, v === undefined ? undefined : +v])) + )); + } else { // empty or invalid geometry + Object.assign(rect, { + width: 0, + height: 0, + left: 0, + right: undefined, + top: 0, + bottom: undefined, + }); + } + const { width, height, left, top } = this._updateCropRect(fbWidth, fbHeight); + if (width && height) { + this._resize(width, height); + if (this._rfbConnectionState === 'connected') { + RFB.messages.fbUpdateRequest(this._sock, false, left, top, width, height); + this.dispatchEvent(new CustomEvent('croprectchanged', { + detail: this.cropRect, + })); + } + } + } + get resizeSession() { return this._resizeSession; } set resizeSession(resize) { this._resizeSession = resize; @@ -783,6 +844,35 @@ export default class RFB extends EventTargetMixin { this._fixScrollbars(); } + _updateCropRect(fbWidth, fbHeight) { + const rect = this._cropRect; + const { fbWidth: prevFbWidth, fbHeight: prevFbHeight, geometry } = rect; + let { width, height, left, right, top, bottom } = Object.assign(rect, { fbWidth, fbHeight }); + function compute(width, left, right, maxWidth) { + if (width === 0 || width > maxWidth) { width = maxWidth; } + if (right === undefined) { + if (left + width > maxWidth) { + left = maxWidth - width; + } + } else { + if (right + width > maxWidth) { + right = 0; + left = 0; + } else { + left = maxWidth - (right + width); + } + } + return [ width, left, right ]; + } + [ width, left, right ] = compute(width, left, right, fbWidth); + [ height, top, bottom ] = compute(height, top, bottom, fbHeight); + Object.assign(rect, { width, height, left, right, top, bottom }); + if (prevFbWidth !== fbWidth || prevFbHeight !== fbHeight) { + this.cropRect = geometry; + } + return rect; + } + // Requests a change of remote desktop size. This message is an extension // and may only be sent if we have received an ExtendedDesktopSize message _requestRemoteResize() { @@ -2141,8 +2231,9 @@ export default class RFB extends EventTargetMixin { if (this._sock.rQwait("server initialization", 24)) { return false; } /* Screen size */ - const width = this._sock.rQshift16(); - const height = this._sock.rQshift16(); + const fbWidth = this._sock.rQshift16(); + const fbHeight = this._sock.rQshift16(); + const { width, height, left, top } = this._updateCropRect(fbWidth, fbHeight); /* PIXEL_FORMAT */ const bpp = this._sock.rQshift8(); @@ -2219,7 +2310,7 @@ export default class RFB extends EventTargetMixin { RFB.messages.pixelFormat(this._sock, this._fbDepth, true); this._sendEncodings(); - RFB.messages.fbUpdateRequest(this._sock, false, 0, 0, this._fbWidth, this._fbHeight); + RFB.messages.fbUpdateRequest(this._sock, false, left, top, width, height); this._updateConnectionState('connected'); return true; @@ -2569,8 +2660,8 @@ export default class RFB extends EventTargetMixin { case 0: // FramebufferUpdate ret = this._framebufferUpdate(); if (ret && !this._enabledContinuousUpdates) { - RFB.messages.fbUpdateRequest(this._sock, true, 0, 0, - this._fbWidth, this._fbHeight); + const { left, top, width, height } = this._cropRect; + RFB.messages.fbUpdateRequest(this._sock, true, left, top, width, height); } return ret; @@ -2641,8 +2732,9 @@ export default class RFB extends EventTargetMixin { if (this._sock.rQwait("rect header", 12)) { return false; } /* New FramebufferUpdate */ - this._FBU.x = this._sock.rQshift16(); - this._FBU.y = this._sock.rQshift16(); + const { left: offsetX, top: offsetY } = this._cropRect; + this._FBU.x = this._sock.rQshift16() - offsetX; + this._FBU.y = this._sock.rQshift16() - offsetY; this._FBU.width = this._sock.rQshift16(); this._FBU.height = this._sock.rQshift16(); this._FBU.encoding = this._sock.rQshift32(); @@ -2683,7 +2775,7 @@ export default class RFB extends EventTargetMixin { return this._handleDesktopName(); case encodings.pseudoEncodingDesktopSize: - this._resize(this._FBU.width, this._FBU.height); + this._updateCropRect(this._FBU.width, this._FBU.height); return true; case encodings.pseudoEncodingExtendedDesktopSize: @@ -2994,9 +3086,9 @@ export default class RFB extends EventTargetMixin { _updateContinuousUpdates() { if (!this._enabledContinuousUpdates) { return; } - - RFB.messages.enableContinuousUpdates(this._sock, true, 0, 0, - this._fbWidth, this._fbHeight); + // TODO: test required + const { left, top, width, height } = this._cropRect; + RFB.messages.enableContinuousUpdates(this._sock, true, left, top, width, height); } // Handle resize-messages from the server diff --git a/vnc.html b/vnc.html index 82cacd580..27f38ac4e 100644 --- a/vnc.html +++ b/vnc.html @@ -235,6 +235,10 @@

no
VNC

+
  • + + +

  • Advanced
    From b9528f1adb65e1d19c6fc72980628c8ec0b7cd6c Mon Sep 17 00:00:00 2001 From: Daniel Hammerschmidt Date: Sun, 14 Sep 2025 14:41:31 +0200 Subject: [PATCH 2/4] Add rfb.cropRect to vnc_lite.html --- vnc_lite.html | 1 + 1 file changed, 1 insertion(+) diff --git a/vnc_lite.html b/vnc_lite.html index 79d481460..945a80649 100644 --- a/vnc_lite.html +++ b/vnc_lite.html @@ -165,6 +165,7 @@ // Set parameters that can be changed on an active connection rfb.viewOnly = readQueryVariable('view_only', false); rfb.scaleViewport = readQueryVariable('scale', false); + rfb.cropRect = readQueryVariable('crop_rect', ''); From d379338cae42dfd9b3f5a7da614ff843ba245dff Mon Sep 17 00:00:00 2001 From: Daniel Hammerschmidt Date: Mon, 15 Sep 2025 19:33:22 +0200 Subject: [PATCH 3/4] Use right and bottom in geometry if set zero () (falsy) --- core/rfb.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/rfb.js b/core/rfb.js index 993fe7bd4..0328d044f 100644 --- a/core/rfb.js +++ b/core/rfb.js @@ -381,9 +381,9 @@ export default class RFB extends EventTargetMixin { get cropRect() { const { width, height, left, right, top, bottom, fbWidth, fbHeight } = this._cropRect; return `${width}x${height}${ - !right ? `+${left}` : `-${right}` + right ?? false ? `+${left}` : `-${right}` }${ - !bottom ? `+${top}` : `-${bottom}` + bottom ?? false ? `+${top}` : `-${bottom}` }${ width === fbWidth && height === fbHeight ? '' : ` (${fbWidth}x${fbHeight})` }`; From e8bde27810f369ab71fbbaded15da5d487b9fb4a Mon Sep 17 00:00:00 2001 From: Daniel Hammerschmidt Date: Mon, 15 Sep 2025 21:00:34 +0200 Subject: [PATCH 4/4] Adopt documentation from TigerVNC and make offsets optional --- core/rfb.js | 4 ++++ docs/EMBEDDING.md | 7 +++++++ 2 files changed, 11 insertions(+) diff --git a/core/rfb.js b/core/rfb.js index 0328d044f..d98706e03 100644 --- a/core/rfb.js +++ b/core/rfb.js @@ -88,11 +88,14 @@ const extendedClipboardActionNotify = 1 << 27; const extendedClipboardActionProvide = 1 << 28; // [GEOMETRY SPECIFICATIONS](https://www.x.org/releases/X11R7.7/doc/man/man7/X.7.xhtml#heading7) +// [X0VNCSERVER −Geometry](https://tigervnc.org/doc/x0vncserver.html#:~:text=is%2060.-,%E2%88%92Geometry%20geometry) const geometryRegExp = new RegExp([ '^', '(?0|[1-9][0-9]*)', 'x', '(?0|[1-9][0-9]*)', + '(?:', '(?:', '[+](?0|[1-9][0-9]*)', '|', '[-](?0|[1-9][0-9]*)', ')', '(?:', '[+](?0|[1-9][0-9]*)', '|', '[-](?0|[1-9][0-9]*)', ')', + ')?', '$', ].join('')); @@ -851,6 +854,7 @@ export default class RFB extends EventTargetMixin { function compute(width, left, right, maxWidth) { if (width === 0 || width > maxWidth) { width = maxWidth; } if (right === undefined) { + left ??= 0; if (left + width > maxWidth) { left = maxWidth - width; } diff --git a/docs/EMBEDDING.md b/docs/EMBEDDING.md index 9e927d0d3..5d203ecd7 100644 --- a/docs/EMBEDDING.md +++ b/docs/EMBEDDING.md @@ -85,6 +85,13 @@ 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`. +* `crop_rect` - This option specifies the remote framebuffer area that will be + shown in the noVNC client. The format is widthxheight+xoffset+yoffset, where + ‘+’ signs can be replaced with ‘−’ signs to specify offsets from the right + and/or from the bottom of the screen. Offsets are optional, +0+0 is assumed + by default (top left corner). If the argument is empty, full screen is shown + to VNC clients (this is the default). See -Geometry parameter of TigerVNC. + * `quality` - The session JPEG quality level. Can be `0` to `9`. * `compression` - The session compression level. Can be `0` to `9`.