diff --git a/README.md b/README.md index 31ec370c..e3330833 100644 --- a/README.md +++ b/README.md @@ -140,6 +140,7 @@ WebSockets to TCP socket proxy. There is a python proxy included * UI and Icons : Pierre Ossman, Chris Gordon * Original Logo : Michael Sersen * tight encoding : Michael Tinglof (Mercuri.ca) + * pixel format conversion : [Alexander Clouter](http://www.digriz.org.uk/) * Included libraries: * as3crypto : Henri Torgemane (code.google.com/p/as3crypto) diff --git a/app/ui.js b/app/ui.js index 0e789c04..52ecd88c 100644 --- a/app/ui.js +++ b/app/ui.js @@ -197,7 +197,6 @@ var UI; UI.initSetting('host', window.location.hostname); UI.initSetting('port', port); UI.initSetting('encrypt', (window.location.protocol === "https:")); - UI.initSetting('true_color', true); UI.initSetting('cursor', !Util.isTouchDevice); UI.initSetting('resize', 'off'); UI.initSetting('shared', true); @@ -451,7 +450,6 @@ var UI; updateVisualState: function() { //Util.Debug(">> updateVisualState"); document.getElementById('noVNC_setting_encrypt').disabled = UI.connected; - document.getElementById('noVNC_setting_true_color').disabled = UI.connected; if (Util.browserSupportsCursorURIs()) { document.getElementById('noVNC_setting_cursor').disabled = UI.connected; } else { @@ -825,7 +823,6 @@ var UI; settingsApply: function() { //Util.Debug(">> settingsApply"); UI.saveSetting('encrypt'); - UI.saveSetting('true_color'); if (Util.browserSupportsCursorURIs()) { UI.saveSetting('cursor'); } @@ -876,7 +873,6 @@ var UI; UI.openControlbar(); UI.updateSetting('encrypt'); - UI.updateSetting('true_color'); if (Util.browserSupportsCursorURIs()) { UI.updateSetting('cursor'); } else { @@ -1060,7 +1056,6 @@ var UI; UI.closeConnectPanel(); UI.rfb.set_encrypt(UI.getSetting('encrypt')); - UI.rfb.set_true_color(UI.getSetting('true_color')); UI.rfb.set_local_cursor(UI.getSetting('cursor')); UI.rfb.set_shared(UI.getSetting('shared')); UI.rfb.set_view_only(UI.getSetting('view_only')); diff --git a/core/display.js b/core/display.js index ac2e1e5f..5a6df0e6 100644 --- a/core/display.js +++ b/core/display.js @@ -456,7 +456,12 @@ // else: No-op -- already done by setSubTile }, - blitImage: function (x, y, width, height, arr, offset, from_queue) { + // N.B.: You *cannot* call this during a test in RGBX-order true-color + // mode, or PhantomJS dies with a Uint8ClampeddArray error. + // N.B.: If you know that your data will always be RGB (that is, isRgb==true + // and this._true_color==true), then you should be calling blitRgbxImage() + // instead of blitImage() to avoid the extra branches. + blitImage: function (x, y, width, height, arr, offset, isRgb, 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, @@ -470,9 +475,14 @@ 'y': y, 'width': width, 'height': height, + 'isRgb': isRgb, }); } else if (this._true_color) { - this._bgrxImageData(x, y, width, height, arr, offset); + if (isRgb) { + this._rgbxImageData(x, y, width, height, arr, offset); + } else { + this._bgrxImageData(x, y, width, height, arr, offset); + } } else { this._cmapImageData(x, y, width, height, arr, offset); } @@ -501,6 +511,8 @@ } }, + // this is different from the above in that it assumes the data is always rgbx format, instead + // of dealing with the possibility of cmap data blitRgbxImage: function (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, @@ -716,10 +728,7 @@ this.fillRect(a.x, a.y, a.width, a.height, a.color, true); break; case 'blit': - this.blitImage(a.x, a.y, a.width, a.height, a.data, 0, true); - break; - case 'blitRgb': - this.blitRgbImage(a.x, a.y, a.width, a.height, a.data, 0, true); + this.blitImage(a.x, a.y, a.width, a.height, a.data, 0, a.rgb, true); break; case 'blitRgbx': this.blitRgbxImage(a.x, a.y, a.width, a.height, a.data, 0, true); diff --git a/core/rfb.js b/core/rfb.js index c6e19731..db983864 100644 --- a/core/rfb.js +++ b/core/rfb.js @@ -88,6 +88,8 @@ this._supportsContinuousUpdates = false; this._enabledContinuousUpdates = false; + this._convert_color = false; + // Frame buffer update state this._FBU = { rects: 0, @@ -105,14 +107,14 @@ zlib: [] // TIGHT zlib streams }; - this._fb_Bpp = 4; - this._fb_depth = 3; + this._pixelFormat = {}; this._fb_width = 0; this._fb_height = 0; this._fb_name = ""; this._destBuff = null; - this._paletteBuff = new Uint8Array(1024); // 256 * 4 (max palette size * max bytes-per-pixel) + this._paletteRawBuff = new Uint8Array(1024); // 256 * 4 (max palette size * max bytes-per-pixel) + this._paletteConvertedBuff = new Uint8Array(1024); // 256 * 4 (max palette size * rgbx bytes-per-pixel) this._rre_chunk_sz = 100; @@ -148,7 +150,6 @@ 'target': 'null', // VNC display rendering Canvas object 'focusContainer': document, // DOM element that captures keyboard input 'encrypt': false, // Use TLS/SSL/wss encryption - 'true_color': true, // Request true color pixel data 'local_cursor': false, // Request locally rendered cursor 'shared': true, // Request shared mode 'view_only': false, // Disable client mouse/keyboard @@ -414,6 +415,7 @@ this._mouse_buttonMask = 0; this._mouse_arr = []; this._rfb_tightvnc = false; + this._convert_color = false; // Clear the per connection encoding stats var i; @@ -1021,17 +1023,17 @@ this._destBuff = new Uint8Array(this._fb_width * this._fb_height * 4); /* PIXEL_FORMAT */ - var bpp = this._sock.rQshift8(); - var depth = this._sock.rQshift8(); - var big_endian = this._sock.rQshift8(); - var true_color = this._sock.rQshift8(); + this._pixelFormat.bpp = this._sock.rQshift8(); + this._pixelFormat.depth = this._sock.rQshift8(); + this._pixelFormat.big_endian = (this._sock.rQshift8() !== 0) ? true : false; + this._pixelFormat.true_color = (this._sock.rQshift8() !== 0) ? true : false; - var red_max = this._sock.rQshift16(); - var green_max = this._sock.rQshift16(); - var blue_max = this._sock.rQshift16(); - var red_shift = this._sock.rQshift8(); - var green_shift = this._sock.rQshift8(); - var blue_shift = this._sock.rQshift8(); + this._pixelFormat.red_max = this._sock.rQshift16(); + this._pixelFormat.green_max = this._sock.rQshift16(); + this._pixelFormat.blue_max = this._sock.rQshift16(); + this._pixelFormat.red_shift = this._sock.rQshift8(); + this._pixelFormat.green_shift = this._sock.rQshift8(); + this._pixelFormat.blue_shift = this._sock.rQshift8(); this._sock.rQskipBytes(3); // padding // NB(directxman12): we don't want to call any callbacks or print messages until @@ -1069,53 +1071,84 @@ // NB(directxman12): these are down here so that we don't run them multiple times // if we backtrack Util.Info("Screen: " + this._fb_width + "x" + this._fb_height + - ", bpp: " + bpp + ", depth: " + depth + - ", big_endian: " + big_endian + - ", true_color: " + true_color + - ", red_max: " + red_max + - ", green_max: " + green_max + - ", blue_max: " + blue_max + - ", red_shift: " + red_shift + - ", green_shift: " + green_shift + - ", blue_shift: " + blue_shift); - - if (big_endian !== 0) { - Util.Warn("Server native endian is not little endian"); - } - - if (red_shift !== 16) { - Util.Warn("Server native red-shift is not 16"); - } - - if (blue_shift !== 0) { - Util.Warn("Server native blue-shift is not 0"); - } + ", bpp: " + this._pixelFormat.bpp + ", depth: " + this._pixelFormat.depth + + ", big_endian: " + this._pixelFormat.big_endian + + ", true_color: " + this._pixelFormat.true_color + + ", red_max: " + this._pixelFormat.red_max + + ", green_max: " + this._pixelFormat.green_max + + ", blue_max: " + this._pixelFormat.blue_max + + ", red_shift: " + this._pixelFormat.red_shift + + ", green_shift: " + this._pixelFormat.green_shift + + ", blue_shift: " + this._pixelFormat.blue_shift); // we're past the point where we could backtrack, so it's safe to call this this._onDesktopName(this, this._fb_name); - if (this._true_color && this._fb_name === "Intel(r) AMT KVM") { - Util.Warn("Intel AMT KVM only supports 8/16 bit depths. Disabling true color"); - this._true_color = false; + if (this._fb_name === "Intel(r) AMT KVM") { + Util.Warn("Intel AMT KVM only supports 8/16 bit depths, using server pixel format"); + this._convert_color = true; } - this._display.set_true_color(this._true_color); + if (this._convert_color) + this._display.set_true_color(this._pixelFormat.true_color); this._display.resize(this._fb_width, this._fb_height); this._onFBResize(this, this._fb_width, this._fb_height); if (!this._view_only) { this._keyboard.grab(); } if (!this._view_only) { this._mouse.grab(); } - if (this._true_color) { - this._fb_Bpp = 4; - this._fb_depth = 3; + // only send if not native, and we think the server will honor the conversion + if (!this._convert_color) { + if (this._pixelFormat.big_endian !== false || + this._pixelFormat.red_max !== 255 || + this._pixelFormat.green_max !== 255 || + this._pixelFormat.blue_max !== 255 || + this._pixelFormat.red_shift !== 16 || + this._pixelFormat.green_shift !== 8 || + this._pixelFormat.blue_shift !== 0 || + !(this._pixelFormat.bpp === 32 && + this._pixelFormat.depth === 24 && + this._pixelFormat.true_color === true) || + !(this._pixelFormat.bpp === 8 && + this._pixelFormat.depth === 8 && + this._pixelFormat.true_color === false)) { + this._pixelFormat.big_endian = false; + this._pixelFormat.red_max = 255; + this._pixelFormat.green_max = 255; + this._pixelFormat.blue_max = 255; + this._pixelFormat.red_shift = 16; + this._pixelFormat.green_shift = 8; + this._pixelFormat.blue_shift = 0; + if (this._pixelFormat.true_color) { + this._pixelFormat.bpp = 32; + this._pixelFormat.depth = 24; + } else { + this._pixelFormat.bpp = 8; + this._pixelFormat.depth = 8; + } + RFB.messages.pixelFormat(this._sock, this._pixelFormat); } else { - this._fb_Bpp = 1; - this._fb_depth = 1; + Util.Warn("Server pixel format matches our preferred native, disabling color conversion"); + this._convert_color = false; + } } - RFB.messages.pixelFormat(this._sock, this._fb_Bpp, this._fb_depth, this._true_color); - RFB.messages.clientEncodings(this._sock, this._encodings, this._local_cursor, this._true_color); + this._pixelFormat.Bpp = this._pixelFormat.bpp / 8; + this._pixelFormat.Bdepth = Math.ceil(this._pixelFormat.depth / 8); + + if (this._pixelFormat.bpp < this._pixelFormat.depth) { + return this._fail('server claims greater depth than bpp'); + } + + var max_depth = Math.ceil(Math.log(this._pixelFormat.red_max)/Math.LN2) + + Math.ceil(Math.log(this._pixelFormat.green_max)/Math.LN2) + + Math.ceil(Math.log(this._pixelFormat.blue_max)/Math.LN2); + + if (this._pixelFormat.true_color && this._pixelFormat.depth > max_depth) { + return this._fail('server claims greater depth than sum of RGB maximums'); + } + + RFB.messages.clientEncodings(this._sock, this._encodings, this._local_cursor, this._pixelFormat.true_color); RFB.messages.fbUpdateRequest(this._sock, false, 0, 0, this._fb_width, this._fb_height); this._timing.fbu_rt_start = (new Date()).getTime(); @@ -1437,14 +1470,97 @@ RFB.messages.enableContinuousUpdates(this._sock, true, 0, 0, this._fb_width, this._fb_height); - } + }, + + // like _convert_color, but always outputs bgr, and for only one pixel + _convert_one_color: function (arr, offset, Bpp) { + if (Bpp === undefined) { + Bpp = this._pixelFormat.Bpp; + } + + if (offset === undefined) { + offset = 0; + } + + if (!this._convert_color || + // HACK? Xtightvnc needs this and I have no idea why + (this._FBU.encoding === 0x07 && this._pixelFormat.depth === 24)) { + if (Bpp === 4) { + return [arr[offset + 0], arr[offset + 1], arr[offset + 2], arr[offset + 3]]; + } else if (Bpp === 3) { + return [arr[offset + 2], arr[offset + 1], arr[offset + 0]]; + } else { + Util.Error('convert color disabled, but Bpp is not 3 or 4!'); + } + } + + var bgr = new Array(3); + + var redMult = 256/(this._pixelFormat.red_max + 1); + var greenMult = 256/(this._pixelFormat.red_max + 1); + var blueMult = 256/(this._pixelFormat.blue_max + 1); + + var pix = 0; + for (var k = 0; k < Bpp; k++) { + if (this._pixelFormat.big_endian) { + pix = (pix << 8) | arr[k + offset]; + } else { + pix = (arr[k + offset] << (k*8)) | pix; + } + } + + bgr[2] = ((pix >>> this._pixelFormat.red_shift) & this._pixelFormat.red_max) * redMult; + bgr[1] = ((pix >>> this._pixelFormat.green_shift) & this._pixelFormat.green_max) * greenMult; + bgr[0] = ((pix >>> this._pixelFormat.blue_shift) & this._pixelFormat.blue_max) * blueMult; + + return bgr; + }, + + // takes a byte stream in the pixel format, and outputs rgbx into the output buffer + _convert_color_and_copy: function (out_arr, in_arr, Bpp) { + if (Bpp === undefined) { + Bpp = this._pixelFormat.Bpp; + } + + if (!this._convert_color || + // HACK? Xtightvnc needs this and I have no idea why + (this._FBU.encoding === 0x07 && this._pixelFormat.depth === 24)) { + if (Bpp !== 4 && Bpp !== 3) { + Util.Error('convert color disabled, but Bpp is not 3 or 4!'); + } else { + out_arr.set(in_arr); + return; + } + } + + var redMult = 256/(this._pixelFormat.red_max + 1); + var greenMult = 256/(this._pixelFormat.red_max + 1); + var blueMult = 256/(this._pixelFormat.blue_max + 1); + + for (var i = 0, j = 0; i < in_arr.length; i += Bpp, j += 4) { + var pix = 0; + + for (var k = 0; k < Bpp; k++) { + if (this._pixelFormat.big_endian) { + pix = (pix << 8) | in_arr[i + k]; + } else { + pix = (in_arr[i + k] << (k*8)) | pix; + } + } + + out_arr[j] = ((pix >>> this._pixelFormat.red_shift) & this._pixelFormat.red_max) * redMult; + out_arr[j + 1] = ((pix >>> this._pixelFormat.green_shift) & this._pixelFormat.green_max) * greenMult; + out_arr[j + 2] = ((pix >>> this._pixelFormat.blue_shift) & this._pixelFormat.blue_max) * blueMult; + out_arr[j + 3] = 255; + } + }, }; Util.make_properties(RFB, [ ['target', 'wo', 'dom'], // VNC display rendering Canvas object ['focusContainer', 'wo', 'dom'], // DOM element that captures keyboard input ['encrypt', 'rw', 'bool'], // Use TLS/SSL/wss encryption - ['true_color', 'rw', 'bool'], // Request true color pixel data + ['convert_color', 'rw', 'bool'], // Client will not honor request for native color ['local_cursor', 'rw', 'bool'], // Request locally rendered cursor ['shared', 'rw', 'bool'], // Request shared mode ['view_only', 'rw', 'bool'], // Disable client mouse/keyboard @@ -1670,7 +1786,7 @@ sock.flush(); }, - pixelFormat: function (sock, bpp, depth, true_color) { + pixelFormat: function (sock, pf) { var buff = sock._sQ; var offset = sock._sQlen; @@ -1680,23 +1796,23 @@ buff[offset + 2] = 0; // padding buff[offset + 3] = 0; // padding - buff[offset + 4] = bpp * 8; // bits-per-pixel - buff[offset + 5] = depth * 8; // depth - buff[offset + 6] = 0; // little-endian - buff[offset + 7] = true_color ? 1 : 0; // true-color + buff[offset + 4] = pf.bpp; // bits-per-pixel + buff[offset + 5] = pf.depth; // depth + buff[offset + 6] = pf.big_endian ? 1 : 0; // big-endian + buff[offset + 7] = pf.true_color ? 1 : 0; // true-color - buff[offset + 8] = 0; // red-max - buff[offset + 9] = 255; // red-max + buff[offset + 8] = (pf.red_max >> 8) & 0xFF; // red-max + buff[offset + 9] = pf.red_max & 0xFF; // red-max - buff[offset + 10] = 0; // green-max - buff[offset + 11] = 255; // green-max + buff[offset + 10] = (pf.green_max >> 8) & 0xFF; // green-max + buff[offset + 11] = pf.green_max & 0xFF; // green-max - buff[offset + 12] = 0; // blue-max - buff[offset + 13] = 255; // blue-max + buff[offset + 12] = (pf.blue_max >> 8) & 0xFF; // blue-max + buff[offset + 13] = (pf.blue_max) & 0xFF; // blue-max - buff[offset + 14] = 16; // red-shift - buff[offset + 15] = 8; // green-shift - buff[offset + 16] = 0; // blue-shift + buff[offset + 14] = pf.red_shift; // red-shift + buff[offset + 15] = pf.green_shift; // green-shift + buff[offset + 16] = pf.blue_shift; // blue-shift buff[offset + 17] = 0; // padding buff[offset + 18] = 0; // padding @@ -1782,19 +1898,21 @@ this._FBU.lines = this._FBU.height; } - this._FBU.bytes = this._FBU.width * this._fb_Bpp; // at least a line + this._FBU.bytes = this._FBU.width * this._pixelFormat.Bpp; // at least a line if (this._sock.rQwait("RAW", this._FBU.bytes)) { return false; } var cur_y = this._FBU.y + (this._FBU.height - this._FBU.lines); var curr_height = Math.min(this._FBU.lines, - Math.floor(this._sock.rQlen() / (this._FBU.width * this._fb_Bpp))); - this._display.blitImage(this._FBU.x, cur_y, this._FBU.width, - curr_height, this._sock.get_rQ(), - this._sock.get_rQi()); - this._sock.rQskipBytes(this._FBU.width * curr_height * this._fb_Bpp); + Math.floor(this._sock.rQlen() / (this._FBU.width * this._pixelFormat.Bpp))); + + // NB(directxman12): renderQ_push automatically clones the data is we have to push + // to the render queue + this._convert_color_and_copy(this._destBuff, this._sock.rQshiftBytes(curr_height * this._FBU.width * this._pixelFormat.Bpp)); + this._display.blitImage(this._FBU.x, cur_y, this._FBU.width, curr_height, this._destBuff, 0, this._convert_color || this._pixelFormat.Bpp === 3, false); + this._FBU.lines -= curr_height; if (this._FBU.lines > 0) { - this._FBU.bytes = this._FBU.width * this._fb_Bpp; // At least another line + this._FBU.bytes = this._FBU.width * this._pixelFormat.Bpp; // At least another line } else { this._FBU.rects--; this._FBU.bytes = 0; @@ -1818,15 +1936,15 @@ RRE: function () { var color; if (this._FBU.subrects === 0) { - this._FBU.bytes = 4 + this._fb_Bpp; - if (this._sock.rQwait("RRE", 4 + this._fb_Bpp)) { return false; } + this._FBU.bytes = 4 + this._pixelFormat.Bpp; + if (this._sock.rQwait("RRE", 4 + this._pixelFormat.Bpp)) { return false; } this._FBU.subrects = this._sock.rQshift32(); - color = this._sock.rQshiftBytes(this._fb_Bpp); // Background + color = this._convert_one_color(this._sock.rQshiftBytes(this._pixelFormat.Bpp)); // Background this._display.fillRect(this._FBU.x, this._FBU.y, this._FBU.width, this._FBU.height, color); } - while (this._FBU.subrects > 0 && this._sock.rQlen() >= (this._fb_Bpp + 8)) { - color = this._sock.rQshiftBytes(this._fb_Bpp); + while (this._FBU.subrects > 0 && this._sock.rQlen() >= (this._pixelFormat.Bpp + 8)) { + color = this._convert_one_color(this._sock.rQshiftBytes(this._pixelFormat.Bpp)); var x = this._sock.rQshift16(); var y = this._sock.rQshift16(); var width = this._sock.rQshift16(); @@ -1837,7 +1955,7 @@ if (this._FBU.subrects > 0) { var chunk = Math.min(this._rre_chunk_sz, this._FBU.subrects); - this._FBU.bytes = (this._fb_Bpp + 8) * chunk; + this._FBU.bytes = (this._pixelFormat.Bpp + 8) * chunk; } else { this._FBU.rects--; this._FBU.bytes = 0; @@ -1878,20 +1996,20 @@ // Figure out how much we are expecting if (subencoding & 0x01) { // Raw - this._FBU.bytes += w * h * this._fb_Bpp; + this._FBU.bytes += w * h * this._pixelFormat.Bpp; } else { if (subencoding & 0x02) { // Background - this._FBU.bytes += this._fb_Bpp; + this._FBU.bytes += this._pixelFormat.Bpp; } if (subencoding & 0x04) { // Foreground - this._FBU.bytes += this._fb_Bpp; + this._FBU.bytes += this._pixelFormat.Bpp; } if (subencoding & 0x08) { // AnySubrects this._FBU.bytes++; // Since we aren't shifting it off if (this._sock.rQwait("hextile subrects header", this._FBU.bytes)) { return false; } subrects = rQ[rQi + this._FBU.bytes - 1]; // Peek if (subencoding & 0x10) { // SubrectsColoured - this._FBU.bytes += subrects * (this._fb_Bpp + 2); + this._FBU.bytes += subrects * (this._pixelFormat.Bpp + 2); } else { this._FBU.bytes += subrects * 2; } @@ -1911,26 +2029,19 @@ this._display.fillRect(x, y, w, h, this._FBU.background); } } else if (this._FBU.subencoding & 0x01) { // Raw - this._display.blitImage(x, y, w, h, rQ, rQi); + // NB(directxman12): renderQ_push automatically clones the data is we have to push + // to the render queue + this._convert_color_and_copy(this._destBuff, new Uint8Array(rQ.buffer, rQi, this._FBU.bytes - 1)); + this._display.blitImage(x, y, w, h, this._destBuff, 0, this._convert_color || this._pixelFormat.Bpp === 3, false); rQi += this._FBU.bytes - 1; } else { if (this._FBU.subencoding & 0x02) { // Background - if (this._fb_Bpp == 1) { - this._FBU.background = rQ[rQi]; - } else { - // fb_Bpp is 4 - this._FBU.background = [rQ[rQi], rQ[rQi + 1], rQ[rQi + 2], rQ[rQi + 3]]; - } - rQi += this._fb_Bpp; + this._FBU.background = this._convert_one_color(rQ, rQi); + rQi += this._pixelFormat.Bpp; } if (this._FBU.subencoding & 0x04) { // Foreground - if (this._fb_Bpp == 1) { - this._FBU.foreground = rQ[rQi]; - } else { - // this._fb_Bpp is 4 - this._FBU.foreground = [rQ[rQi], rQ[rQi + 1], rQ[rQi + 2], rQ[rQi + 3]]; - } - rQi += this._fb_Bpp; + this._FBU.foreground = this._convert_one_color(rQ, rQi); + rQi += this._pixelFormat.Bpp; } this._display.startTile(x, y, w, h, this._FBU.background); @@ -1941,13 +2052,8 @@ for (var s = 0; s < subrects; s++) { var color; if (this._FBU.subencoding & 0x10) { // SubrectsColoured - if (this._fb_Bpp === 1) { - color = rQ[rQi]; - } else { - // _fb_Bpp is 4 - color = [rQ[rQi], rQ[rQi + 1], rQ[rQi + 2], rQ[rQi + 3]]; - } - rQi += this._fb_Bpp; + color = this._convert_one_color(rQ, rQi); + rQi += this._pixelFormat.Bpp; } else { color = this._FBU.foreground; } @@ -1994,10 +2100,8 @@ }, display_tight: function (isTightPNG) { - if (this._fb_depth === 1) { - this._fail("Internal error", - "Tight protocol handler only implements " + - "true color mode"); + if (this._pixelFormat.Bdepth === 1) { + this._fail("Internal error", "Tight protocol handler only implements true color mode"); } this._FBU.bytes = 1; // compression-control byte @@ -2115,7 +2219,7 @@ var handlePalette = function () { var numColors = rQ[rQi + 2] + 1; - var paletteSize = numColors * this._fb_depth; + var paletteSize = numColors * this._pixelFormat.Bdepth; this._FBU.bytes += paletteSize; if (this._sock.rQwait("TIGHT palette " + cmode, this._FBU.bytes)) { return false; } @@ -2149,8 +2253,8 @@ // Shift ctl, filter id, num colors, palette entries, and clength off this._sock.rQskipBytes(3); - //var palette = this._sock.rQshiftBytes(paletteSize); - this._sock.rQshiftTo(this._paletteBuff, paletteSize); + this._sock.rQshiftTo(this._paletteRawBuff, paletteSize); + this._convert_color_and_copy(this._paletteConvertedBuff, this._paletteRawBuff, this._pixelFormat.Bdepth); this._sock.rQskipBytes(cl_header); if (raw) { @@ -2162,10 +2266,10 @@ // Convert indexed (palette based) image data to RGB var rgbx; if (numColors == 2) { - rgbx = indexedToRGBX2Color(data, this._paletteBuff, this._FBU.width, this._FBU.height); + rgbx = indexedToRGBX2Color(data, this._paletteConvertedBuff, this._FBU.width, this._FBU.height); this._display.blitRgbxImage(this._FBU.x, this._FBU.y, this._FBU.width, this._FBU.height, rgbx, 0, false); } else { - rgbx = indexedToRGBX(data, this._paletteBuff, this._FBU.width, this._FBU.height); + rgbx = indexedToRGBX(data, this._paletteConvertedBuff, this._FBU.width, this._FBU.height); this._display.blitRgbxImage(this._FBU.x, this._FBU.y, this._FBU.width, this._FBU.height, rgbx, 0, false); } @@ -2175,7 +2279,7 @@ var handleCopy = function () { var raw = false; - var uncompressedSize = this._FBU.width * this._FBU.height * this._fb_depth; + var uncompressedSize = this._FBU.width * this._FBU.height * this._pixelFormat.Bdepth; if (uncompressedSize < 12) { raw = true; cl_header = 0; @@ -2208,7 +2312,8 @@ data = decompress(this._sock.rQshiftBytes(cl_data), uncompressedSize); } - this._display.blitRgbImage(this._FBU.x, this._FBU.y, this._FBU.width, this._FBU.height, data, 0, false); + this._convert_color_and_copy(this._destBuff, data, this._pixelFormat.Bdepth); + this._display.blitImage(this._FBU.x, this._FBU.y, this._FBU.width, this._FBU.height, this._destBuff, 0, this._convert_color || this._pixelFormat.Bpp === 3, false); return true; }.bind(this); @@ -2239,7 +2344,7 @@ switch (cmode) { // fill use fb_depth because TPIXELs drop the padding byte case "fill": // TPIXEL - this._FBU.bytes += this._fb_depth; + this._FBU.bytes += this._pixelFormat.Bdepth; break; case "jpeg": // max clength this._FBU.bytes += 3; @@ -2260,8 +2365,9 @@ switch (cmode) { case "fill": // skip ctl byte - this._display.fillRect(this._FBU.x, this._FBU.y, this._FBU.width, this._FBU.height, [rQ[rQi + 3], rQ[rQi + 2], rQ[rQi + 1]], false); - this._sock.rQskipBytes(4); + var color = this._convert_one_color(rQ, rQi + 1, this._pixelFormat.Bdepth); + this._display.fillRect(this._FBU.x, this._FBU.y, this._FBU.width, this._FBU.height, color, false); + this._sock.rQskipBytes(this._pixelFormat.Bdepth + 1); break; case "png": case "jpeg": @@ -2407,7 +2513,7 @@ var w = this._FBU.width; var h = this._FBU.height; - var pixelslength = w * h * this._fb_Bpp; + var pixelslength = w * h * this._pixelFormat.Bpp; var masklength = Math.floor((w + 7) / 8) * h; this._FBU.bytes = pixelslength + masklength; diff --git a/tests/test.display.js b/tests/test.display.js index 5f4eed12..8ec79d55 100644 --- a/tests/test.display.js +++ b/tests/test.display.js @@ -392,19 +392,14 @@ describe('Display/Canvas Helper', function () { data[i * 4 + 2] = checked_data[i * 4]; data[i * 4 + 3] = checked_data[i * 4 + 3]; } + display.blitImage(0, 0, 4, 4, data, 0); display.flip(); expect(display).to.have.displayed(checked_data); }); - it('should support drawing RGB blit images with true color via #blitRgbImage', function () { - var data = []; - for (var i = 0; i < 16; i++) { - data[i * 3] = checked_data[i * 4]; - data[i * 3 + 1] = checked_data[i * 4 + 1]; - data[i * 3 + 2] = checked_data[i * 4 + 2]; - } - display.blitRgbImage(0, 0, 4, 4, data, 0); + it('should support drawing RGBX blit images with true color via #blitImage', function () { + display.blitImage(0, 0, 4, 4, checked_data, 0, true); display.flip(); expect(display).to.have.displayed(checked_data); }); @@ -490,18 +485,19 @@ describe('Display/Canvas Helper', function () { expect(display.get_onFlush()).to.have.been.calledOnce; }); - it('should draw a blit image on type "blit"', function () { + it('should draw a blit image on type "blit" with "rgb" set to false', function () { display.blitImage = sinon.spy(); - display._renderQ_push({ type: 'blit', x: 3, y: 4, width: 5, height: 6, data: [7, 8, 9] }); + display._renderQ_push({ type: 'blit', x: 3, y: 4, width: 5, height: 6, data: [7, 8, 9], rgb: false }); expect(display.blitImage).to.have.been.calledOnce; - expect(display.blitImage).to.have.been.calledWith(3, 4, 5, 6, [7, 8, 9], 0); + expect(display.blitImage).to.have.been.calledWith(3, 4, 5, 6, [7, 8, 9], 0, false); }); - it('should draw a blit RGB image on type "blitRgb"', function () { - display.blitRgbImage = sinon.spy(); - display._renderQ_push({ type: 'blitRgb', x: 3, y: 4, width: 5, height: 6, data: [7, 8, 9] }); - expect(display.blitRgbImage).to.have.been.calledOnce; - expect(display.blitRgbImage).to.have.been.calledWith(3, 4, 5, 6, [7, 8, 9], 0); + + it('should draw a blit RGBX image on type "blit" with "rgb" set to true', function () { + display.blitImage = sinon.spy(); + display._renderQ_push({ type: 'blit', x: 3, y: 4, width: 5, height: 6, data: [7, 8, 9, 10], rgb: true }); + expect(display.blitImage).to.have.been.calledOnce; + expect(display.blitImage).to.have.been.calledWith(3, 4, 5, 6, [7, 8, 9, 10], 0, true); }); it('should copy a region on type "copy"', function () { diff --git a/tests/test.rfb.js b/tests/test.rfb.js index 229cfe58..9c98df9b 100644 --- a/tests/test.rfb.js +++ b/tests/test.rfb.js @@ -1103,8 +1103,6 @@ describe('Remote Frame Buffer Protocol Client', function() { expect(client._fb_height).to.equal(84); }); - // NB(sross): we just warn, not fail, for endian-ness and shifts, so we don't test them - it('should set the framebuffer name and call the callback', function () { client.set_onDesktopName(sinon.spy()); send_server_init({ name: 'some name' }, client); @@ -1134,14 +1132,6 @@ describe('Remote Frame Buffer Protocol Client', function() { expect(client._rfb_connection_state).to.equal('connected'); }); - it('should set the true color mode on the display to the configuration variable', function () { - client.set_true_color(false); - sinon.spy(client._display, 'set_true_color'); - send_server_init({ true_color: 1 }, client); - expect(client._display.set_true_color).to.have.been.calledOnce; - expect(client._display.set_true_color).to.have.been.calledWith(false); - }); - it('should call the resize callback and resize the display', function () { client.set_onFBResize(sinon.spy()); sinon.spy(client._display, 'resize'); @@ -1163,29 +1153,35 @@ describe('Remote Frame Buffer Protocol Client', function() { expect(client._mouse.grab).to.have.been.calledOnce; }); - it('should set the BPP and depth to 4 and 3 respectively if in true color mode', function () { - client.set_true_color(true); - send_server_init({}, client); - expect(client._fb_Bpp).to.equal(4); - expect(client._fb_depth).to.equal(3); + it('should set the BPP and depth to 4 and 3 respectively if server can send native (true color)', function () { + send_server_init({ true_color: 1, bpp: 8, depth: 8 }, client); + expect(client._pixelFormat.Bpp).to.equal(4); + expect(client._pixelFormat.Bdepth).to.equal(3); }); - it('should set the BPP and depth to 1 and 1 respectively if not in true color mode', function () { - client.set_true_color(false); - send_server_init({}, client); - expect(client._fb_Bpp).to.equal(1); - expect(client._fb_depth).to.equal(1); + it('should set the BPP and depth to 2 and 2 respectively if server cannot send native (true color)', function () { + client.set_convert_color(true); + send_server_init({ true_color: 1, bpp: 16, depth: 15 }, client); + expect(client._pixelFormat.Bpp).to.equal(2); + expect(client._pixelFormat.Bdepth).to.equal(2); + }); + + it('should set the BPP and depth to 1 and 1 respectively if server cannot send native (not true color)', function () { + client.set_convert_color(true); + send_server_init({ true_color: 0, bpp: 8, depth: 8 }, client); + expect(client._pixelFormat.Bpp).to.equal(1); + expect(client._pixelFormat.Bdepth).to.equal(1); }); // TODO(directxman12): test the various options in this configuration matrix it('should reply with the pixel format, client encodings, and initial update request', function () { - client.set_true_color(true); client.set_local_cursor(false); // we skip the cursor encoding var expected = {_sQ: new Uint8Array(34 + 4 * (client._encodings.length - 1)), _sQlen: 0, flush: function () {}}; - RFB.messages.pixelFormat(expected, 4, 3, true); + var pf = { bpp: 32, depth: 24, big_endian: false, true_color: true, red_max: 255, green_max: 255, blue_max: 255, red_shift: 16, green_shift: 8, blue_shift: 0 }; + RFB.messages.pixelFormat(expected, pf); RFB.messages.clientEncodings(expected, client._encodings, false, true); RFB.messages.fbUpdateRequest(expected, false, 0, 0, 27, 32); @@ -1197,6 +1193,7 @@ describe('Remote Frame Buffer Protocol Client', function() { send_server_init({}, client); expect(client._rfb_connection_state).to.equal('connected'); }); + }); }); @@ -1224,13 +1221,14 @@ describe('Remote Frame Buffer Protocol Client', function() { client._fb_name = 'some device'; client._fb_width = 640; client._fb_height = 20; + client._pixelFormat.Bpp = 4; }); var target_data_arr = [ - 0xff, 0x00, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, - 0x00, 0xff, 0x00, 255, 0xff, 0x00, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, - 0xee, 0x00, 0xff, 255, 0x00, 0xee, 0xff, 255, 0xaa, 0xee, 0xff, 255, 0xab, 0xee, 0xff, 255, - 0xee, 0x00, 0xff, 255, 0x00, 0xee, 0xff, 255, 0xaa, 0xee, 0xff, 255, 0xab, 0xee, 0xff, 255 + 0xf8, 0x00, 0x00, 255, 0x00, 0xf8, 0x00, 255, 0x00, 0x00, 0xf8, 255, 0x00, 0x00, 0xf8, 255, + 0x00, 0xf8, 0x00, 255, 0xf8, 0x00, 0x00, 255, 0x00, 0x00, 0xf8, 255, 0x00, 0x00, 0xf8, 255, + 0xe8, 0x00, 0xf8, 255, 0x00, 0xe8, 0xf8, 255, 0xa8, 0xe8, 0xf8, 255, 0xa8, 0xe8, 0xf8, 255, + 0xe8, 0x00, 0xf8, 255, 0x00, 0xe8, 0xf8, 255, 0xa8, 0xe8, 0xf8, 255, 0xa8, 0xe8, 0xf8, 255 ]; var target_data; @@ -1379,24 +1377,98 @@ describe('Remote Frame Buffer Protocol Client', function() { client._fb_width = 4; client._fb_height = 4; client._display.resize(4, 4); - client._fb_Bpp = 4; + client._pixelFormat.Bpp = 4; + client._destBuff = new Uint8Array(client._fb_width * client._fb_height * 4); }); - it('should handle the RAW encoding', function () { + // warning: the fbupdates *overlap* so you have to send all rects for the numbers + // to even make sense; this means (ironically) no iterative building of your tests + describe('should handle the RAW encoding', function () { + it('should handle 24bit depth (RGBX888) @ 32bpp [native]', function () { + client._convert_color = true; + client._pixelFormat.big_endian = false; + client._pixelFormat.red_shift = 0; + client._pixelFormat.red_max = 255; + client._pixelFormat.green_shift = 8; + client._pixelFormat.green_max = 255; + client._pixelFormat.blue_shift = 16; + client._pixelFormat.blue_max = 255; + var info = [{ x: 0, y: 0, width: 2, height: 2, encoding: 0x00 }, + { x: 2, y: 0, width: 2, height: 2, encoding: 0x00 }, + { x: 0, y: 2, width: 4, height: 1, encoding: 0x00 }, + { x: 0, y: 3, width: 4, height: 1, encoding: 0x00 }]; + var rects = [ + [0xf8, 0x00, 0x00, 0, 0x00, 0xf8, 0x00, 0, 0x00, 0xf8, 0x00, 0, 0xf8, 0x00, 0x00, 0], + [0x00, 0x00, 0xf8, 0, 0x00, 0x00, 0xf8, 0, 0x00, 0x00, 0xf8, 0, 0x00, 0x00, 0xf8, 0], + [0xe8, 0x00, 0xf8, 0, 0x00, 0xe8, 0xf8, 0, 0xa8, 0xe8, 0xf8, 0, 0xa8, 0xe8, 0xf8, 0], + [0xe8, 0x00, 0xf8, 0, 0x00, 0xe8, 0xf8, 0, 0xa8, 0xe8, 0xf8, 0, 0xa8, 0xe8, 0xf8, 0]]; + send_fbu_msg(info, rects, client); + expect(client._display).to.have.displayed(target_data); + }); + + it('should handle 24bit depth (BGRX888) @ 32bpp', function () { var info = [{ x: 0, y: 0, width: 2, height: 2, encoding: 0x00 }, { x: 2, y: 0, width: 2, height: 2, encoding: 0x00 }, { x: 0, y: 2, width: 4, height: 1, encoding: 0x00 }, { x: 0, y: 3, width: 4, height: 1, encoding: 0x00 }]; - // data is in bgrx var rects = [ - [0x00, 0x00, 0xff, 0, 0x00, 0xff, 0x00, 0, 0x00, 0xff, 0x00, 0, 0x00, 0x00, 0xff, 0], - [0xff, 0x00, 0x00, 0, 0xff, 0x00, 0x00, 0, 0xff, 0x00, 0x00, 0, 0xff, 0x00, 0x00, 0], - [0xff, 0x00, 0xee, 0, 0xff, 0xee, 0x00, 0, 0xff, 0xee, 0xaa, 0, 0xff, 0xee, 0xab, 0], - [0xff, 0x00, 0xee, 0, 0xff, 0xee, 0x00, 0, 0xff, 0xee, 0xaa, 0, 0xff, 0xee, 0xab, 0]]; + [0x00, 0x00, 0xf8, 0, 0x00, 0xf8, 0x00, 0, 0x00, 0xf8, 0x00, 0, 0x00, 0x00, 0xf8, 0], + [0xf8, 0x00, 0x00, 0, 0xf8, 0x00, 0x00, 0, 0xf8, 0x00, 0x00, 0, 0xf8, 0x00, 0x00, 0], + [0xf8, 0x00, 0xe8, 0, 0xf8, 0xe8, 0x00, 0, 0xf8, 0xe8, 0xa8, 0, 0xf8, 0xe8, 0xa8, 0], + [0xf8, 0x00, 0xe8, 0, 0xf8, 0xe8, 0x00, 0, 0xf8, 0xe8, 0xa8, 0, 0xf8, 0xe8, 0xa8, 0]]; send_fbu_msg(info, rects, client); expect(client._display).to.have.displayed(target_data); }); + // for wisdom: perl -e '($w, $r, $g, $b) = @ARGV; $W=2**$w; $nb = $b*($W/256); $ng = $g*($W/256); $nr = $r*($W/256); printf "%f:%f:%f %04x\n", $nr, $ng, $nb, unpack("S", pack("n", ($nr << (2*$w)) | ($ng << (1*$w)) | ($nb << (0*$w))))' 5 0 248 0 + it('should handle 15bit depth (BGR555) @ 16bpp', function () { + client._convert_color = true; + client._pixelFormat.big_endian = false; + client._pixelFormat.Bpp = 2; + client._pixelFormat.red_shift = 10; + client._pixelFormat.red_max = 31; + client._pixelFormat.green_shift = 5; + client._pixelFormat.green_max = 31; + client._pixelFormat.blue_shift = 0; + client._pixelFormat.blue_max = 31; + var info = [{ x: 0, y: 0, width: 2, height: 2, encoding: 0x00 }, + { x: 2, y: 0, width: 2, height: 2, encoding: 0x00 }, + { x: 0, y: 2, width: 4, height: 1, encoding: 0x00 }, + { x: 0, y: 3, width: 4, height: 1, encoding: 0x00 }]; + var rects = [ + [0x00, 0x7c, 0xe0, 0x03, 0xe0, 0x03, 0x00, 0x7c], + [0x1f, 0x00, 0x1f, 0x00, 0x1f, 0x00, 0x1f, 0x00], + [0x1f, 0x74, 0xbf, 0x03, 0xbf, 0x57, 0xbf, 0x57], + [0x1f, 0x74, 0xbf, 0x03, 0xbf, 0x57, 0xbf, 0x57]]; + send_fbu_msg(info, rects, client); + expect(client._display).to.have.displayed(target_data); + }); + + it('should handle 15bit depth (BGR555) @ 16bpp big-endian', function () { + client._convert_color = true; + client._pixelFormat.big_endian = false; + client._pixelFormat.Bpp = 2; + client._pixelFormat.big_endian = true; + client._pixelFormat.red_shift = 10; + client._pixelFormat.red_max = 31; + client._pixelFormat.green_shift = 5; + client._pixelFormat.green_max = 31; + client._pixelFormat.blue_shift = 0; + client._pixelFormat.blue_max = 31; + var info = [{ x: 0, y: 0, width: 2, height: 2, encoding: 0x00 }, + { x: 2, y: 0, width: 2, height: 2, encoding: 0x00 }, + { x: 0, y: 2, width: 4, height: 1, encoding: 0x00 }, + { x: 0, y: 3, width: 4, height: 1, encoding: 0x00 }]; + var rects = [ + [0x7c, 0x00, 0x03, 0xe0, 0x03, 0xe0, 0x7c, 0x00], + [0x00, 0x1f, 0x00, 0x1f, 0x00, 0x1f, 0x00, 0x1f], + [0x74, 0x1f, 0x03, 0xbf, 0x57, 0xbf, 0x57, 0xbf], + [0x74, 0x1f, 0x03, 0xbf, 0x57, 0xbf, 0x57, 0xbf]]; + send_fbu_msg(info, rects, client); + expect(client._display).to.have.displayed(target_data); + }); + }); + it('should handle the COPYRECT encoding', function () { // seed some initial data to copy client._display.blitRgbxImage(0, 0, 4, 2, new Uint8Array(target_data_check_arr.slice(0, 32)), 0); @@ -1426,7 +1498,7 @@ describe('Remote Frame Buffer Protocol Client', function() { push16(rect, 2); // width: 2 push16(rect, 2); // height: 2 rect.push(0xff); // becomes ff0000ff --> #0000FF color - rect.push(0x00); + rect.push(0x00); // becomes 0000ffff --> #0000FF color rect.push(0x00); rect.push(0xff); push16(rect, 2); // x: 2 @@ -1450,7 +1522,8 @@ describe('Remote Frame Buffer Protocol Client', function() { client._fb_width = 4; client._fb_height = 4; client._display.resize(4, 4); - client._fb_Bpp = 4; + client._pixelFormat.Bpp = 4; + client._destBuff = new Uint8Array(client._fb_width * client._fb_height * 4); }); it('should handle a tile with fg, bg specified, normal subrects', function () { diff --git a/vnc.html b/vnc.html index 5e241a2a..733a3299 100644 --- a/vnc.html +++ b/vnc.html @@ -11,9 +11,9 @@ This file is licensed under the 2-Clause BSD license (see LICENSE.txt). Connect parameters are provided in query string: - http://example.com/?host=HOST&port=PORT&encrypt=1&true_color=1 + http://example.com/?host=HOST&port=PORT&encrypt=1 or the fragment: - http://example.com/#host=HOST&port=PORT&encrypt=1&true_color=1 + http://example.com/#host=HOST&port=PORT&encrypt=1 -->