/* * noVNC: HTML5 VNC client * Copyright (C) 2010 Joel Martin * Licensed under LGPL-3 (see LICENSE.txt) * * See README.md for usage and integration instructions. */ "use strict"; /*jslint browser: true, white: false, bitwise: false */ /*global window, Util, Base64 */ function Canvas(conf) { conf = conf || {}; // Configuration var that = {}, // Public API interface // Private Canvas namespace variables c_forceCanvas = false, c_width = 0, c_height = 0, c_prevStyle = "", c_keyPress = null, c_mouseButton = null, c_mouseMove = null; // Configuration settings function cdef(v, type, defval, desc) { Util.conf_default(conf, that, v, type, defval, desc); } // Capability settings, default can be overridden cdef('prefer_js', 'raw', null, 'Prefer Javascript over canvas methods'); cdef('cursor_uri', 'raw', null, 'Can we render cursor using data URI'); cdef('target', 'dom', null, 'Canvas element for VNC viewport'); cdef('focusContainer', 'dom', document, 'DOM element that traps keyboard input'); cdef('true_color', 'bool', true, 'Request true color pixel data'); cdef('focused', 'bool', true, 'Capture and send key strokes'); cdef('colourMap', 'raw', [], 'Colour map array (not true color)'); cdef('scale', 'float', 1, 'VNC viewport scale factor'); cdef('render_mode', 'str', '', 'Canvas rendering mode (read-only)'); // Override some specific getters/setters 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; }; that.get_colourMap = function(idx) { if (typeof idx === 'undefined') { return conf.colourMap; } else { return conf.colourMap[idx]; } }; that.set_colourMap = function(val, idx) { if (typeof idx === 'undefined') { conf.colourMap = val; } else { conf.colourMap[idx] = val; } }; that.set_render_mode = function () { throw("render_mode is read-only"); }; // Add some other getters/setters that.get_width = function() { return c_width; }; that.get_height = function() { return c_height; }; // // Private functions // // Create the public API interface function constructor() { Util.Debug(">> Canvas.init"); var c, ctx, imgTest, tval, i, curDat, curSave, has_imageData = false, UE = Util.Engine; if (! conf.target) { throw("target must be set"); } if (typeof conf.target === 'string') { conf.target = window.$(conf.target); } c = conf.target; if (! c.getContext) { throw("no getContext method"); } if (! conf.ctx) { conf.ctx = c.getContext('2d'); } ctx = conf.ctx; if (UE.gecko) { Util.Debug("Browser: gecko " + UE.gecko); } if (UE.webkit) { Util.Debug("Browser: webkit " + UE.webkit); } if (UE.trident) { Util.Debug("Browser: webkit " + UE.trident); } if (UE.presto) { Util.Debug("Browser: webkit " + UE.presto); } that.clear(); /* * Determine browser Canvas feature support * and select fastest rendering methods */ tval = 0; try { imgTest = ctx.getImageData(0, 0, 1,1); imgTest.data[0] = 123; imgTest.data[3] = 255; ctx.putImageData(imgTest, 0, 0); tval = 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 (ctx.createImageData) { // If it's there, it's faster Util.Info("Using Canvas createImageData"); conf.render_mode = "createImageData rendering"; that.imageData = that.imageDataCreate; } else if (ctx.getImageData) { // I think this is mostly just Opera Util.Info("Using Canvas getImageData"); conf.render_mode = "getImageData rendering"; that.imageData = that.imageDataGet; } Util.Info("Prefering javascript operations"); if (conf.prefer_js === null) { conf.prefer_js = true; } that.rgbxImage = that.rgbxImageData; that.cmapImage = that.cmapImageData; } else { Util.Warn("Canvas lacks imageData, using fillRect (slow)"); conf.render_mode = "fillRect rendering (slow)"; c_forceCanvas = true; conf.prefer_js = false; that.rgbxImage = that.rgbxImageFill; that.cmapImage = that.cmapImageFill; } /* * 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; that.changeCursor(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; } conf.focused = true; Util.Debug("<< Canvas.init"); return that ; } /* Translate DOM key event to keysym value */ function getKeysym(e) { var evt, keysym; evt = (e ? e : window.event); /* Remap modifier and special keys */ switch ( evt.keyCode ) { case 8 : keysym = 0xFF08; break; // BACKSPACE case 9 : keysym = 0xFF09; break; // TAB case 13 : keysym = 0xFF0D; break; // ENTER case 27 : keysym = 0xFF1B; break; // ESCAPE case 45 : keysym = 0xFF63; break; // INSERT case 46 : keysym = 0xFFFF; break; // DELETE case 36 : keysym = 0xFF50; break; // HOME case 35 : keysym = 0xFF57; break; // END case 33 : keysym = 0xFF55; break; // PAGE_UP case 34 : keysym = 0xFF56; break; // PAGE_DOWN case 37 : keysym = 0xFF51; break; // LEFT case 38 : keysym = 0xFF52; break; // UP case 39 : keysym = 0xFF53; break; // RIGHT case 40 : keysym = 0xFF54; break; // DOWN case 112 : keysym = 0xFFBE; break; // F1 case 113 : keysym = 0xFFBF; break; // F2 case 114 : keysym = 0xFFC0; break; // F3 case 115 : keysym = 0xFFC1; break; // F4 case 116 : keysym = 0xFFC2; break; // F5 case 117 : keysym = 0xFFC3; break; // F6 case 118 : keysym = 0xFFC4; break; // F7 case 119 : keysym = 0xFFC5; break; // F8 case 120 : keysym = 0xFFC6; break; // F9 case 121 : keysym = 0xFFC7; break; // F10 case 122 : keysym = 0xFFC8; break; // F11 case 123 : keysym = 0xFFC9; break; // F12 case 16 : keysym = 0xFFE1; break; // SHIFT case 17 : keysym = 0xFFE3; break; // CONTROL //case 18 : keysym = 0xFFE7; break; // Left Meta (Mac Option) case 18 : keysym = 0xFFE9; break; // Left ALT (Mac Command) default : keysym = evt.keyCode; break; } /* Remap symbols */ switch (keysym) { case 186 : keysym = 59; break; // ; (IE) case 187 : keysym = 61; break; // = (IE) case 188 : keysym = 44; break; // , (Mozilla, IE) case 109 : // - (Mozilla) if (Util.Engine.gecko) { keysym = 45; } break; case 189 : keysym = 45; break; // - (IE) case 190 : keysym = 46; break; // . (Mozilla, IE) case 191 : keysym = 47; break; // / (Mozilla, IE) case 192 : keysym = 96; break; // ` (Mozilla, IE) case 219 : keysym = 91; break; // [ (Mozilla, IE) case 220 : keysym = 92; break; // \ (Mozilla, IE) case 221 : keysym = 93; break; // ] (Mozilla, IE) case 222 : keysym = 39; break; // ' (Mozilla, IE) } /* Remap shifted and unshifted keys */ if (!!evt.shiftKey) { switch (keysym) { case 48 : keysym = 41 ; break; // ) (shifted 0) case 49 : keysym = 33 ; break; // ! (shifted 1) case 50 : keysym = 64 ; break; // @ (shifted 2) case 51 : keysym = 35 ; break; // # (shifted 3) case 52 : keysym = 36 ; break; // $ (shifted 4) case 53 : keysym = 37 ; break; // % (shifted 5) case 54 : keysym = 94 ; break; // ^ (shifted 6) case 55 : keysym = 38 ; break; // & (shifted 7) case 56 : keysym = 42 ; break; // * (shifted 8) case 57 : keysym = 40 ; break; // ( (shifted 9) case 59 : keysym = 58 ; break; // : (shifted `) case 61 : keysym = 43 ; break; // + (shifted ;) case 44 : keysym = 60 ; break; // < (shifted ,) case 45 : keysym = 95 ; break; // _ (shifted -) case 46 : keysym = 62 ; break; // > (shifted .) case 47 : keysym = 63 ; break; // ? (shifted /) case 96 : keysym = 126; break; // ~ (shifted `) case 91 : keysym = 123; break; // { (shifted [) case 92 : keysym = 124; break; // | (shifted \) case 93 : keysym = 125; break; // } (shifted ]) case 39 : keysym = 34 ; break; // " (shifted ') } } else if ((keysym >= 65) && (keysym <=90)) { /* Remap unshifted A-Z */ keysym += 32; } return keysym; } function onMouseButton(e, down) { var evt, pos, bmask; if (! conf.focused) { return true; } evt = (e ? e : window.event); pos = Util.getEventPosition(e, conf.target, conf.scale); bmask = 1 << evt.button; //Util.Debug('mouse ' + pos.x + "," + pos.y + " down: " + down + " bmask: " + bmask); if (c_mouseButton) { c_mouseButton(pos.x, pos.y, down, bmask); } Util.stopEvent(e); return false; } function onMouseDown(e) { onMouseButton(e, 1); } function onMouseUp(e) { onMouseButton(e, 0); } function onMouseWheel(e) { var evt, pos, bmask, wheelData; evt = (e ? e : window.event); pos = Util.getEventPosition(e, conf.target, conf.scale); wheelData = evt.detail ? evt.detail * -1 : evt.wheelDelta / 40; if (wheelData > 0) { bmask = 1 << 3; } else { bmask = 1 << 4; } //Util.Debug('mouse scroll by ' + wheelData + ':' + pos.x + "," + pos.y); if (c_mouseButton) { c_mouseButton(pos.x, pos.y, 1, bmask); c_mouseButton(pos.x, pos.y, 0, bmask); } Util.stopEvent(e); return false; } function onMouseMove(e) { var evt, pos; evt = (e ? e : window.event); pos = Util.getEventPosition(e, conf.target, conf.scale); //Util.Debug('mouse ' + evt.which + '/' + evt.button + ' up:' + pos.x + "," + pos.y); if (c_mouseMove) { c_mouseMove(pos.x, pos.y); } } function onKeyDown(e) { //Util.Debug("keydown: " + getKeysym(e)); if (! conf.focused) { return true; } if (c_keyPress) { c_keyPress(getKeysym(e), 1); } Util.stopEvent(e); return false; } function onKeyUp(e) { //Util.Debug("keyup: " + getKeysym(e)); if (! conf.focused) { return true; } if (c_keyPress) { c_keyPress(getKeysym(e), 0); } Util.stopEvent(e); return false; } function onMouseDisable(e) { var evt, pos; if (! conf.focused) { return true; } evt = (e ? e : window.event); pos = Util.getEventPosition(e, conf.target, conf.scale); /* Stop propagation if inside canvas area */ if ((pos.x >= 0) && (pos.y >= 0) && (pos.x < c_width) && (pos.y < c_height)) { //Util.Debug("mouse event disabled"); Util.stopEvent(e); return false; } //Util.Debug("mouse event not disabled"); return true; } // // Public API interface functions // that.getContext = function () { return conf.ctx; }; that.start = function(keyPressFunc, mouseButtonFunc, mouseMoveFunc) { var c; Util.Debug(">> Canvas.start"); c = conf.target; c_keyPress = keyPressFunc || null; c_mouseButton = mouseButtonFunc || null; c_mouseMove = mouseMoveFunc || null; Util.addEvent(conf.focusContainer, 'keydown', onKeyDown); Util.addEvent(conf.focusContainer, 'keyup', onKeyUp); Util.addEvent(c, 'mousedown', onMouseDown); Util.addEvent(c, 'mouseup', onMouseUp); Util.addEvent(c, 'mousemove', onMouseMove); Util.addEvent(c, (Util.Engine.gecko) ? 'DOMMouseScroll' : 'mousewheel', onMouseWheel); /* Work around right and middle click browser behaviors */ Util.addEvent(conf.focusContainer, 'click', onMouseDisable); Util.addEvent(conf.focusContainer.body, 'contextmenu', onMouseDisable); Util.Debug("<< Canvas.start"); }; 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 (conf.scale === factor) { //Util.Debug("Canvas 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)"; }; that.resize = function(width, height, true_color) { var c = conf.target; if (typeof true_color !== "undefined") { conf.true_color = true_color; } c_prevStyle = ""; c.width = width; c.height = height; c_width = c.offsetWidth; c_height = c.offsetHeight; that.rescale(conf.scale); }; that.clear = function() { that.resize(640, 20); conf.ctx.clearRect(0, 0, c_width, c_height); }; that.stop = function() { var c = conf.target; Util.removeEvent(conf.focusContainer, 'keydown', onKeyDown); Util.removeEvent(conf.focusContainer, 'keyup', onKeyUp); Util.removeEvent(c, 'mousedown', onMouseDown); Util.removeEvent(c, 'mouseup', onMouseUp); Util.removeEvent(c, 'mousemove', onMouseMove); Util.removeEvent(c, (Util.Engine.gecko) ? 'DOMMouseScroll' : 'mousewheel', onMouseWheel); /* Work around right and middle click browser behaviors */ Util.removeEvent(conf.focusContainer, 'click', onMouseDisable); Util.removeEvent(conf.focusContainer.body, 'contextmenu', onMouseDisable); // Turn off cursor rendering if (conf.cursor_uri) { c.style.cursor = "default"; } }; that.setFillColor = function(color) { var rgb, newStyle; if (conf.true_color) { rgb = color; } else { rgb = conf.colourMap[color[0]]; } newStyle = "rgb(" + rgb[0] + "," + rgb[1] + "," + rgb[2] + ")"; if (newStyle !== c_prevStyle) { conf.ctx.fillStyle = newStyle; c_prevStyle = newStyle; } }; that.fillRect = function(x, y, width, height, color) { that.setFillColor(color); conf.ctx.fillRect(x, y, width, height); }; that.copyImage = function(old_x, old_y, new_x, new_y, width, height) { conf.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 = [], p, rgb, red, green, blue, j, i; img = {'x': x, 'y': y, 'width': width, 'height': height, 'data': data}; if (conf.prefer_js) { if (conf.true_color) { rgb = color; } else { rgb = conf.colourMap[color[0]]; } red = rgb[0]; green = rgb[1]; blue = rgb[2]; for (i = 0; i < (width * height * 4); i+=4) { data[i ] = red; data[i + 1] = green; data[i + 2] = blue; } } else { that.fillRect(x, y, width, height, color); } return img; }; that.setSubTile = function(img, x, y, w, h, color) { var data, p, rgb, red, green, blue, width, j, i, xend, yend; if (conf.prefer_js) { data = img.data; width = img.width; if (conf.true_color) { rgb = color; } else { rgb = conf.colourMap[color[0]]; } red = rgb[0]; green = rgb[1]; blue = rgb[2]; 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 ] = red; data[p + 1] = green; data[p + 2] = blue; } } } else { that.fillRect(img.x + x, img.y + y, w, h, color); } }; that.putTile = function(img) { if (conf.prefer_js) { that.rgbxImage(img.x, img.y, img.width, img.height, img.data, 0); } else { // No-op, under gecko already done by setSubTile } }; that.imageDataGet = function(width, height) { return conf.ctx.getImageData(0, 0, width, height); }; that.imageDataCreate = function(width, height) { return conf.ctx.createImageData(width, height); }; that.rgbxImageData = function(x, y, width, height, arr, offset) { var img, i, j, data; img = that.imageData(width, height); data = img.data; for (i=0, j=offset; i < (width * height * 4); i=i+4, j=j+4) { data[i + 0] = arr[j + 0]; data[i + 1] = arr[j + 1]; data[i + 2] = arr[j + 2]; data[i + 3] = 255; // Set Alpha } conf.ctx.putImageData(img, x, y); }; // really slow fallback if we don't have imageData that.rgbxImageFill = 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+0], arr[j+1], arr[j+2]]); sx += 1; if ((sx % width) === 0) { sx = 0; sy += 1; } } }; that.cmapImageData = function(x, y, width, height, arr, offset) { var img, i, j, data, rgb, cmap; img = that.imageData(width, height); data = img.data; cmap = conf.colourMap; for (i=0, j=offset; i < (width * height * 4); i+=4, j+=1) { rgb = cmap[arr[j]]; data[i + 0] = rgb[0]; data[i + 1] = rgb[1]; data[i + 2] = rgb[2]; data[i + 3] = 255; // Set Alpha } conf.ctx.putImageData(img, x, y); }; that.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) { that.rgbxImage(x, y, width, height, arr, offset); } else { that.cmapImage(x, y, width, height, arr, offset); } }; that.blitStringImage = function(str, x, y) { var img = new Image(); img.onload = function () { conf.ctx.drawImage(img, x, y); }; img.src = str; }; that.changeCursor = function(pixels, mask, hotx, hoty, w, h) { var cur = [], cmap, rgb, IHDRsz, ANDsz, XORsz, url, idx, alpha, x, y; //Util.Debug(">> changeCursor, x: " + hotx + ", y: " + hoty + ", w: " + w + ", h: " + h); if (conf.cursor_uri === false) { Util.Warn("changeCursor called but no cursor data URI support"); return; } // 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 ); }; cmap = conf.colourMap; IHDRsz = 40; ANDsz = w * h * 4; XORsz = Math.ceil( (w * h) / 8.0 ); // Main header cur.push16le(0); // Reserved cur.push16le(2); // .CUR type cur.push16le(1); // Number of images, 1 for non-animated ico // Cursor #1 header cur.push(w); // width cur.push(h); // height cur.push(0); // colors, 0 -> true-color cur.push(0); // reserved cur.push16le(hotx); // hotspot x coordinate cur.push16le(hoty); // hotspot y coordinate cur.push32le(IHDRsz + XORsz + ANDsz); // cursor data byte size cur.push32le(22); // offset of cursor data in the file // Cursor #1 InfoHeader cur.push32le(IHDRsz); // Infoheader size cur.push32le(w); // Cursor width cur.push32le(h*2); // XOR+AND height cur.push16le(1); // number of planes cur.push16le(32); // bits per pixel cur.push32le(0); // Type of compression cur.push32le(XORsz + ANDsz); // Size of Image cur.push32le(0); cur.push32le(0); cur.push32le(0); cur.push32le(0); // XOR/color data 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 (conf.true_color) { idx = ((w * y) + x) * 4; cur.push(pixels[idx + 2]); // blue cur.push(pixels[idx + 1]); // green cur.push(pixels[idx + 0]); // red cur.push(alpha); // red } else { 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 } } } // AND/bitmask data (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); conf.target.style.cursor = "url(" + url + ") " + hotx + " " + hoty + ", default"; //Util.Debug("<< changeCursor, cur.length: " + cur.length); }; return constructor(); // Return the public API interface } // End of Canvas()