mirror of
https://github.com/novnc/noVNC.git
synced 2026-05-31 17:39:39 +00:00
Instead of R,G,B (red-shift of 0, green-shift of 8, and blue-shift of 16), use the default ordering of B,G,R (red-shift of 16, green-shift of 8, and blue-shift of 0) that tightvncserver uses (and that VMWare's VNC server seems to require). Also, warn in the console if the server does not default to the new format. Fix the tests/canvas.html test. This is a general fix with regards to the rename/refactor of canvas.js into display.js and not specific to the color re-ordering.
571 lines
17 KiB
JavaScript
571 lines
17 KiB
JavaScript
/*
|
|
* noVNC: HTML5 VNC client
|
|
* Copyright (C) 2011 Joel Martin
|
|
* Licensed under LGPL-3 (see LICENSE.txt)
|
|
*
|
|
* See README.md for usage and integration instructions.
|
|
*/
|
|
|
|
/*jslint browser: true, white: false, bitwise: false */
|
|
/*global Util, Base64, changeCursor */
|
|
|
|
function Display(defaults) {
|
|
"use strict";
|
|
|
|
var that = {}, // Public API methods
|
|
conf = {}, // Configuration attributes
|
|
|
|
// Private Display namespace variables
|
|
c_ctx = null,
|
|
c_forceCanvas = false,
|
|
|
|
c_imageData, c_bgrxImage, c_cmapImage,
|
|
|
|
// Predefine function variables (jslint)
|
|
imageDataCreate, imageDataGet, bgrxImageData, cmapImageData,
|
|
bgrxImageFill, cmapImageFill, setFillColor, rescale, flush,
|
|
|
|
c_width = 0,
|
|
c_height = 0,
|
|
|
|
c_prevStyle = "",
|
|
|
|
c_webkit_bug = false,
|
|
c_flush_timer = null;
|
|
|
|
// Configuration attributes
|
|
Util.conf_defaults(conf, that, defaults, [
|
|
['target', 'wo', 'dom', null, 'Canvas element for rendering'],
|
|
['context', 'ro', 'raw', null, 'Canvas 2D context for rendering (read-only)'],
|
|
['logo', 'rw', 'raw', null, 'Logo to display when cleared: {"width": width, "height": height, "data": data}'],
|
|
['true_color', 'rw', 'bool', true, 'Use true-color pixel data'],
|
|
['colourMap', 'rw', 'arr', [], 'Colour map array (when not true-color)'],
|
|
['scale', 'rw', 'float', 1.0, 'Display area scale factor 0.0 - 1.0'],
|
|
['width', 'rw', 'int', null, 'Display area width'],
|
|
['height', 'rw', 'int', null, 'Display area height'],
|
|
|
|
['render_mode', 'ro', 'str', '', 'Canvas rendering mode (read-only)'],
|
|
|
|
['prefer_js', 'rw', 'str', null, 'Prefer Javascript over canvas methods'],
|
|
['cursor_uri', 'rw', 'raw', null, 'Can we render cursor using data URI']
|
|
]);
|
|
|
|
// Override some specific getters/setters
|
|
that.get_context = function () { return c_ctx; };
|
|
|
|
that.set_scale = function(scale) { rescale(scale); };
|
|
|
|
that.set_width = function (val) { that.resize(val, c_height); };
|
|
that.get_width = function() { return c_width; };
|
|
|
|
that.set_height = function (val) { that.resize(c_width, val); };
|
|
that.get_height = function() { return c_height; };
|
|
|
|
that.set_prefer_js = function(val) {
|
|
if (val && c_forceCanvas) {
|
|
Util.Warn("Preferring Javascript to Canvas ops is not supported");
|
|
return false;
|
|
}
|
|
conf.prefer_js = val;
|
|
return true;
|
|
};
|
|
|
|
|
|
|
|
//
|
|
// Private functions
|
|
//
|
|
|
|
// Create the public API interface
|
|
function constructor() {
|
|
Util.Debug(">> Display.constructor");
|
|
|
|
var c, func, imgTest, tval, i, curDat, curSave,
|
|
has_imageData = false, UE = Util.Engine;
|
|
|
|
if (! conf.target) { throw("target must be set"); }
|
|
|
|
if (typeof conf.target === 'string') {
|
|
throw("target must be a DOM element");
|
|
}
|
|
|
|
c = conf.target;
|
|
|
|
if (! c.getContext) { throw("no getContext method"); }
|
|
|
|
if (! c_ctx) { c_ctx = c.getContext('2d'); }
|
|
|
|
Util.Debug("User Agent: " + navigator.userAgent);
|
|
if (UE.gecko) { Util.Debug("Browser: gecko " + UE.gecko); }
|
|
if (UE.webkit) { Util.Debug("Browser: webkit " + UE.webkit); }
|
|
if (UE.trident) { Util.Debug("Browser: trident " + UE.trident); }
|
|
if (UE.presto) { Util.Debug("Browser: presto " + UE.presto); }
|
|
|
|
that.clear();
|
|
|
|
/*
|
|
* Determine browser Canvas feature support
|
|
* and select fastest rendering methods
|
|
*/
|
|
tval = 0;
|
|
try {
|
|
imgTest = c_ctx.getImageData(0, 0, 1,1);
|
|
imgTest.data[0] = 123;
|
|
imgTest.data[3] = 255;
|
|
c_ctx.putImageData(imgTest, 0, 0);
|
|
tval = c_ctx.getImageData(0, 0, 1, 1).data[0];
|
|
if (tval === 123) {
|
|
has_imageData = true;
|
|
}
|
|
} catch (exc1) {}
|
|
|
|
if (has_imageData) {
|
|
Util.Info("Canvas supports imageData");
|
|
c_forceCanvas = false;
|
|
if (c_ctx.createImageData) {
|
|
// If it's there, it's faster
|
|
Util.Info("Using Canvas createImageData");
|
|
conf.render_mode = "createImageData rendering";
|
|
c_imageData = imageDataCreate;
|
|
} else if (c_ctx.getImageData) {
|
|
// I think this is mostly just Opera
|
|
Util.Info("Using Canvas getImageData");
|
|
conf.render_mode = "getImageData rendering";
|
|
c_imageData = imageDataGet;
|
|
}
|
|
Util.Info("Prefering javascript operations");
|
|
if (conf.prefer_js === null) {
|
|
conf.prefer_js = true;
|
|
}
|
|
c_bgrxImage = bgrxImageData;
|
|
c_cmapImage = cmapImageData;
|
|
} else {
|
|
Util.Warn("Canvas lacks imageData, using fillRect (slow)");
|
|
conf.render_mode = "fillRect rendering (slow)";
|
|
c_forceCanvas = true;
|
|
conf.prefer_js = false;
|
|
c_bgrxImage = bgrxImageFill;
|
|
c_cmapImage = cmapImageFill;
|
|
}
|
|
|
|
if (UE.webkit && UE.webkit >= 534.7 && UE.webkit <= 534.9) {
|
|
// Workaround WebKit canvas rendering bug #46319
|
|
conf.render_mode += ", webkit bug workaround";
|
|
Util.Debug("Working around WebKit bug #46319");
|
|
c_webkit_bug = true;
|
|
for (func in {"fillRect":1, "copyImage":1, "bgrxImage":1,
|
|
"cmapImage":1, "blitStringImage":1}) {
|
|
that[func] = (function() {
|
|
var myfunc = that[func]; // Save original function
|
|
//Util.Debug("Wrapping " + func);
|
|
return function() {
|
|
myfunc.apply(this, arguments);
|
|
if (!c_flush_timer) {
|
|
c_flush_timer = setTimeout(flush, 100);
|
|
}
|
|
};
|
|
}());
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Determine browser support for setting the cursor via data URI
|
|
* scheme
|
|
*/
|
|
curDat = [];
|
|
for (i=0; i < 8 * 8 * 4; i += 1) {
|
|
curDat.push(255);
|
|
}
|
|
try {
|
|
curSave = c.style.cursor;
|
|
changeCursor(conf.target, curDat, curDat, 2, 2, 8, 8);
|
|
if (c.style.cursor) {
|
|
if (conf.cursor_uri === null) {
|
|
conf.cursor_uri = true;
|
|
}
|
|
Util.Info("Data URI scheme cursor supported");
|
|
} else {
|
|
if (conf.cursor_uri === null) {
|
|
conf.cursor_uri = false;
|
|
}
|
|
Util.Warn("Data URI scheme cursor not supported");
|
|
}
|
|
c.style.cursor = curSave;
|
|
} catch (exc2) {
|
|
Util.Error("Data URI scheme cursor test exception: " + exc2);
|
|
conf.cursor_uri = false;
|
|
}
|
|
|
|
Util.Debug("<< Display.constructor");
|
|
return that ;
|
|
}
|
|
|
|
rescale = function(factor) {
|
|
var c, tp, x, y,
|
|
properties = ['transform', 'WebkitTransform', 'MozTransform', null];
|
|
c = conf.target;
|
|
tp = properties.shift();
|
|
while (tp) {
|
|
if (typeof c.style[tp] !== 'undefined') {
|
|
break;
|
|
}
|
|
tp = properties.shift();
|
|
}
|
|
|
|
if (tp === null) {
|
|
Util.Debug("No scaling support");
|
|
return;
|
|
}
|
|
|
|
if (factor > 1.0) {
|
|
factor = 1.0;
|
|
} else if (factor < 0.1) {
|
|
factor = 0.1;
|
|
}
|
|
|
|
if (conf.scale === factor) {
|
|
//Util.Debug("Display already scaled to '" + factor + "'");
|
|
return;
|
|
}
|
|
|
|
conf.scale = factor;
|
|
x = c.width - c.width * factor;
|
|
y = c.height - c.height * factor;
|
|
c.style[tp] = "scale(" + conf.scale + ") translate(-" + x + "px, -" + y + "px)";
|
|
};
|
|
|
|
// Force canvas redraw (for webkit bug #46319 workaround)
|
|
flush = function() {
|
|
var old_val;
|
|
//Util.Debug(">> flush");
|
|
old_val = conf.target.style.marginRight;
|
|
conf.target.style.marginRight = "1px";
|
|
c_flush_timer = null;
|
|
setTimeout(function () {
|
|
conf.target.style.marginRight = old_val;
|
|
}, 1);
|
|
};
|
|
|
|
setFillColor = function(color) {
|
|
var bgr, newStyle;
|
|
if (conf.true_color) {
|
|
bgr = color;
|
|
} else {
|
|
bgr = conf.colourMap[color[0]];
|
|
}
|
|
newStyle = "rgb(" + bgr[2] + "," + bgr[1] + "," + bgr[0] + ")";
|
|
if (newStyle !== c_prevStyle) {
|
|
c_ctx.fillStyle = newStyle;
|
|
c_prevStyle = newStyle;
|
|
}
|
|
};
|
|
|
|
|
|
//
|
|
// Public API interface functions
|
|
//
|
|
|
|
that.resize = function(width, height) {
|
|
var c = conf.target;
|
|
|
|
c_prevStyle = "";
|
|
|
|
c.width = width;
|
|
c.height = height;
|
|
|
|
c_width = c.offsetWidth;
|
|
c_height = c.offsetHeight;
|
|
|
|
rescale(conf.scale);
|
|
};
|
|
|
|
that.clear = function() {
|
|
|
|
if (conf.logo) {
|
|
that.resize(conf.logo.width, conf.logo.height);
|
|
that.blitStringImage(conf.logo.data, 0, 0);
|
|
} else {
|
|
that.resize(640, 20);
|
|
c_ctx.clearRect(0, 0, c_width, c_height);
|
|
}
|
|
|
|
// No benefit over default ("source-over") in Chrome and firefox
|
|
//c_ctx.globalCompositeOperation = "copy";
|
|
};
|
|
|
|
that.fillRect = function(x, y, width, height, color) {
|
|
setFillColor(color);
|
|
c_ctx.fillRect(x, y, width, height);
|
|
};
|
|
|
|
that.copyImage = function(old_x, old_y, new_x, new_y, width, height) {
|
|
c_ctx.drawImage(conf.target, old_x, old_y, width, height,
|
|
new_x, new_y, width, height);
|
|
};
|
|
|
|
/*
|
|
* Tile rendering functions optimized for rendering engines.
|
|
*
|
|
* - In Chrome/webkit, Javascript image data array manipulations are
|
|
* faster than direct Canvas fillStyle, fillRect rendering. In
|
|
* gecko, Javascript array handling is much slower.
|
|
*/
|
|
that.getTile = function(x, y, width, height, color) {
|
|
var img, data = [], bgr, red, green, blue, i;
|
|
img = {'x': x, 'y': y, 'width': width, 'height': height,
|
|
'data': data};
|
|
if (conf.prefer_js) {
|
|
if (conf.true_color) {
|
|
bgr = color;
|
|
} else {
|
|
bgr = conf.colourMap[color[0]];
|
|
}
|
|
// Keep in BGR order because bgrxImage will flip it
|
|
red = bgr[2];
|
|
green = bgr[1];
|
|
blue = bgr[0];
|
|
for (i = 0; i < (width * height * 4); i+=4) {
|
|
data[i ] = blue;
|
|
data[i + 1] = green;
|
|
data[i + 2] = red;
|
|
}
|
|
} else {
|
|
that.fillRect(x, y, width, height, color);
|
|
}
|
|
return img;
|
|
};
|
|
|
|
that.setSubTile = function(img, x, y, w, h, color) {
|
|
var data, p, bgr, red, green, blue, width, j, i, xend, yend;
|
|
if (conf.prefer_js) {
|
|
data = img.data;
|
|
width = img.width;
|
|
if (conf.true_color) {
|
|
bgr = color;
|
|
} else {
|
|
bgr = conf.colourMap[color[0]];
|
|
}
|
|
// Keep in BGR order because bgrxImage will flip it
|
|
red = bgr[2];
|
|
green = bgr[1];
|
|
blue = bgr[0];
|
|
xend = x + w;
|
|
yend = y + h;
|
|
for (j = y; j < yend; j += 1) {
|
|
for (i = x; i < xend; i += 1) {
|
|
p = (i + (j * width) ) * 4;
|
|
data[p ] = blue;
|
|
data[p + 1] = green;
|
|
data[p + 2] = red;
|
|
}
|
|
}
|
|
} else {
|
|
that.fillRect(img.x + x, img.y + y, w, h, color);
|
|
}
|
|
};
|
|
|
|
that.putTile = function(img) {
|
|
if (conf.prefer_js) {
|
|
c_bgrxImage(img.x, img.y, img.width, img.height, img.data, 0);
|
|
}
|
|
// else: No-op, under gecko already done by setSubTile
|
|
};
|
|
|
|
imageDataGet = function(width, height) {
|
|
return c_ctx.getImageData(0, 0, width, height);
|
|
};
|
|
imageDataCreate = function(width, height) {
|
|
return c_ctx.createImageData(width, height);
|
|
};
|
|
|
|
bgrxImageData = function(x, y, width, height, arr, offset) {
|
|
var img, i, j, data;
|
|
img = c_imageData(width, height);
|
|
data = img.data;
|
|
for (i=0, j=offset; i < (width * height * 4); i=i+4, j=j+4) {
|
|
data[i ] = arr[j + 2];
|
|
data[i + 1] = arr[j + 1];
|
|
data[i + 2] = arr[j ];
|
|
data[i + 3] = 255; // Set Alpha
|
|
}
|
|
c_ctx.putImageData(img, x, y);
|
|
};
|
|
|
|
// really slow fallback if we don't have imageData
|
|
bgrxImageFill = function(x, y, width, height, arr, offset) {
|
|
var i, j, sx = 0, sy = 0;
|
|
for (i=0, j=offset; i < (width * height); i+=1, j+=4) {
|
|
that.fillRect(x+sx, y+sy, 1, 1, [arr[j], arr[j+1], arr[j+2]]);
|
|
sx += 1;
|
|
if ((sx % width) === 0) {
|
|
sx = 0;
|
|
sy += 1;
|
|
}
|
|
}
|
|
};
|
|
|
|
cmapImageData = function(x, y, width, height, arr, offset) {
|
|
var img, i, j, data, bgr, cmap;
|
|
img = c_imageData(width, height);
|
|
data = img.data;
|
|
cmap = conf.colourMap;
|
|
for (i=0, j=offset; i < (width * height * 4); i+=4, j+=1) {
|
|
bgr = cmap[arr[j]];
|
|
data[i ] = bgr[2];
|
|
data[i + 1] = bgr[1];
|
|
data[i + 2] = bgr[0];
|
|
data[i + 3] = 255; // Set Alpha
|
|
}
|
|
c_ctx.putImageData(img, x, y);
|
|
};
|
|
|
|
cmapImageFill = function(x, y, width, height, arr, offset) {
|
|
var i, j, sx = 0, sy = 0, cmap;
|
|
cmap = conf.colourMap;
|
|
for (i=0, j=offset; i < (width * height); i+=1, j+=1) {
|
|
that.fillRect(x+sx, y+sy, 1, 1, [arr[j]]);
|
|
sx += 1;
|
|
if ((sx % width) === 0) {
|
|
sx = 0;
|
|
sy += 1;
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
that.blitImage = function(x, y, width, height, arr, offset) {
|
|
if (conf.true_color) {
|
|
c_bgrxImage(x, y, width, height, arr, offset);
|
|
} else {
|
|
c_cmapImage(x, y, width, height, arr, offset);
|
|
}
|
|
};
|
|
|
|
that.blitStringImage = function(str, x, y) {
|
|
var img = new Image();
|
|
img.onload = function () { c_ctx.drawImage(img, x, y); };
|
|
img.src = str;
|
|
};
|
|
|
|
that.changeCursor = function(pixels, mask, hotx, hoty, w, h) {
|
|
if (conf.cursor_uri === false) {
|
|
Util.Warn("changeCursor called but no cursor data URI support");
|
|
return;
|
|
}
|
|
|
|
if (conf.true_color) {
|
|
changeCursor(conf.target, pixels, mask, hotx, hoty, w, h);
|
|
} else {
|
|
changeCursor(conf.target, pixels, mask, hotx, hoty, w, h, conf.colourMap);
|
|
}
|
|
};
|
|
|
|
that.defaultCursor = function() {
|
|
conf.target.style.cursor = "default";
|
|
};
|
|
|
|
return constructor(); // Return the public API interface
|
|
|
|
} // End of Display()
|
|
|
|
|
|
/* Set CSS cursor property using data URI encoded cursor file */
|
|
function changeCursor(target, pixels, mask, hotx, hoty, w, h, cmap) {
|
|
"use strict";
|
|
var cur = [], rgb, IHDRsz, RGBsz, ANDsz, XORsz, url, idx, alpha, x, y;
|
|
//Util.Debug(">> changeCursor, x: " + hotx + ", y: " + hoty + ", w: " + w + ", h: " + h);
|
|
|
|
// Push multi-byte little-endian values
|
|
cur.push16le = function (num) {
|
|
this.push((num ) & 0xFF,
|
|
(num >> 8) & 0xFF );
|
|
};
|
|
cur.push32le = function (num) {
|
|
this.push((num ) & 0xFF,
|
|
(num >> 8) & 0xFF,
|
|
(num >> 16) & 0xFF,
|
|
(num >> 24) & 0xFF );
|
|
};
|
|
|
|
IHDRsz = 40;
|
|
RGBsz = w * h * 4;
|
|
XORsz = Math.ceil( (w * h) / 8.0 );
|
|
ANDsz = Math.ceil( (w * h) / 8.0 );
|
|
|
|
// Main header
|
|
cur.push16le(0); // 0: Reserved
|
|
cur.push16le(2); // 2: .CUR type
|
|
cur.push16le(1); // 4: Number of images, 1 for non-animated ico
|
|
|
|
// Cursor #1 header (ICONDIRENTRY)
|
|
cur.push(w); // 6: width
|
|
cur.push(h); // 7: height
|
|
cur.push(0); // 8: colors, 0 -> true-color
|
|
cur.push(0); // 9: reserved
|
|
cur.push16le(hotx); // 10: hotspot x coordinate
|
|
cur.push16le(hoty); // 12: hotspot y coordinate
|
|
cur.push32le(IHDRsz + RGBsz + XORsz + ANDsz);
|
|
// 14: cursor data byte size
|
|
cur.push32le(22); // 18: offset of cursor data in the file
|
|
|
|
|
|
// Cursor #1 InfoHeader (ICONIMAGE/BITMAPINFO)
|
|
cur.push32le(IHDRsz); // 22: Infoheader size
|
|
cur.push32le(w); // 26: Cursor width
|
|
cur.push32le(h*2); // 30: XOR+AND height
|
|
cur.push16le(1); // 34: number of planes
|
|
cur.push16le(32); // 36: bits per pixel
|
|
cur.push32le(0); // 38: Type of compression
|
|
|
|
cur.push32le(XORsz + ANDsz); // 43: Size of Image
|
|
// Gimp leaves this as 0
|
|
|
|
cur.push32le(0); // 46: reserved
|
|
cur.push32le(0); // 50: reserved
|
|
cur.push32le(0); // 54: reserved
|
|
cur.push32le(0); // 58: reserved
|
|
|
|
// 62: color data (RGBQUAD icColors[])
|
|
for (y = h-1; y >= 0; y -= 1) {
|
|
for (x = 0; x < w; x += 1) {
|
|
idx = y * Math.ceil(w / 8) + Math.floor(x/8);
|
|
alpha = (mask[idx] << (x % 8)) & 0x80 ? 255 : 0;
|
|
|
|
if (cmap) {
|
|
idx = (w * y) + x;
|
|
rgb = cmap[pixels[idx]];
|
|
cur.push(rgb[2]); // blue
|
|
cur.push(rgb[1]); // green
|
|
cur.push(rgb[0]); // red
|
|
cur.push(alpha); // alpha
|
|
} else {
|
|
idx = ((w * y) + x) * 4;
|
|
cur.push(pixels[idx + 2]); // blue
|
|
cur.push(pixels[idx + 1]); // green
|
|
cur.push(pixels[idx ]); // red
|
|
cur.push(alpha); // alpha
|
|
}
|
|
}
|
|
}
|
|
|
|
// XOR/bitmask data (BYTE icXOR[])
|
|
// (ignored, just needs to be right size)
|
|
for (y = 0; y < h; y += 1) {
|
|
for (x = 0; x < Math.ceil(w / 8); x += 1) {
|
|
cur.push(0x00);
|
|
}
|
|
}
|
|
|
|
// AND/bitmask data (BYTE icAND[])
|
|
// (ignored, just needs to be right size)
|
|
for (y = 0; y < h; y += 1) {
|
|
for (x = 0; x < Math.ceil(w / 8); x += 1) {
|
|
cur.push(0x00);
|
|
}
|
|
}
|
|
|
|
url = "data:image/x-icon;base64," + Base64.encode(cur);
|
|
target.style.cursor = "url(" + url + ") " + hotx + " " + hoty + ", default";
|
|
//Util.Debug("<< changeCursor, cur.length: " + cur.length);
|
|
}
|