Add client-side pixel format conversion support

I (@kelleyk) have updated the original patch to be compatible with
the double-buffering changes made upstream, repaired some test cases,
and cleaned up the patch a little bit.

The author of the original patch is @jimdigriz (Alexander Clouter).
This commit is contained in:
Kevin Kelley 2017-01-09 19:09:02 -08:00
parent 41f476a863
commit 7541906688
8 changed files with 362 additions and 187 deletions

View File

@ -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)

View File

@ -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'));

View File

@ -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) {
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);

View File

@ -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._fb_Bpp = 1;
this._fb_depth = 1;
this._pixelFormat.bpp = 8;
this._pixelFormat.depth = 8;
}
RFB.messages.pixelFormat(this._sock, this._pixelFormat);
} else {
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;

View File

@ -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 () {

View File

@ -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 }];
// 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]];
[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 }];
var rects = [
[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 () {

View File

@ -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
-->
<title>noVNC</title>
@ -199,9 +199,6 @@
<li>
<div class="noVNC_expander">Advanced</div>
<div><ul>
<li>
<label><input id="noVNC_setting_true_color" type="checkbox" checked /> True Color</label>
</li>
<li>
<label><input id="noVNC_setting_cursor" type="checkbox" /> Local Cursor</label>
</li>

View File

@ -10,9 +10,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
-->
<title>noVNC</title>
@ -261,7 +261,6 @@
'encrypt': WebUtil.getConfigVar('encrypt',
(window.location.protocol === "https:")),
'repeaterID': WebUtil.getConfigVar('repeaterID', ''),
'true_color': WebUtil.getConfigVar('true_color', true),
'local_cursor': WebUtil.getConfigVar('cursor', true),
'shared': WebUtil.getConfigVar('shared', true),
'view_only': WebUtil.getConfigVar('view_only', false),
@ -276,7 +275,6 @@
status('Unable to create RFB client -- ' + exc, 'error');
return; // don't continue trying to connect
}
rfb.connect(host, port, password, path);
};
</script>