From 2c2b492c0c1431e185fc4ca2a10bb18cd56eac5a Mon Sep 17 00:00:00 2001 From: Joel Martin Date: Tue, 20 Jul 2010 14:34:44 -0500 Subject: [PATCH] Add Cursor pseudo-encoding support (disabled for now). To change the appearance of the cursor, we use the CSS cursor style and set the url to a data URI scheme. The image data sent via the cursor pseudo-encoding has to be encoded to a CUR format file before being used in the data URI. During Canvas initialization we try and set a simple cursor to see if the browser has support. Opera is missing support for data URI scheme in cursor URLs. Disabled for now until we have a better way of specifying settings overall (too many settings for control bar now). --- README.md | 4 +- docs/TODO | 7 ++- docs/links | 5 ++ include/canvas.js | 114 ++++++++++++++++++++++++++++++++++++++++++++-- include/util.js | 11 +++++ include/vnc.js | 44 +++++++++++++++++- 6 files changed, 175 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 02d7c679..5a26a7de 100644 --- a/README.md +++ b/README.md @@ -178,7 +178,9 @@ In the following table Jaunty is Ubuntu 9.04 and WinXP is Windows XP. is faster than Firefox 3.5, the high variability of web-socket-js performance results in overall performance being lower. Middle mouse clicks and keyboard events need some work to work properly under - Opera. + Opera. Also, Opera does not have support for setting the cursor + style url to a data URI scheme, so cursor pseudo-encoding is + disabled. ### Integration diff --git a/docs/TODO b/docs/TODO index c2bda7ff..95b60cc4 100644 --- a/docs/TODO +++ b/docs/TODO @@ -1,5 +1,8 @@ Short Term: +- Proper Javascript namespacing for Canvas and RFB (using function for + true local variables and functions). + - Timing delta between frames in proxy record log, for playback support (for demo and test). @@ -14,10 +17,6 @@ Short Term: Medium Term: -- Implement Cursor pseudo-encoding (CSS cursor) - http://en.wikipedia.org/wiki/ICO_(file_format) - https://developer.mozilla.org/en/Using_URL_values_for_the_cursor_property - - Viewport and/or scaling support. - Status bar buttons: diff --git a/docs/links b/docs/links index 6e180151..64ea7546 100644 --- a/docs/links +++ b/docs/links @@ -29,6 +29,11 @@ TLS Protocol: Generate self-signed certificate: http://docs.python.org/dev/library/ssl.html#certificates +Cursor appearance/style (for Cursor pseudo-encoding): + http://en.wikipedia.org/wiki/ICO_(file_format) + http://www.daubnet.com/en/file-format-cur + https://developer.mozilla.org/en/Using_URL_values_for_the_cursor_property + Related projects: diff --git a/include/canvas.js b/include/canvas.js index af52227a..8e822c48 100644 --- a/include/canvas.js +++ b/include/canvas.js @@ -27,8 +27,9 @@ var Canvas, Canvas_native; // Everything namespaced inside Canvas Canvas = { -prefer_js : false, -force_canvas : false, +prefer_js : false, // make private +force_canvas : false, // make private +cursor_uri : true, // make private, create getter true_color : false, colourMap : [], @@ -138,7 +139,7 @@ onMouseDisable: function (e) { init: function (id) { - var c, imgTest, arora; + var c, imgTest, tval, i, curTest, curSave; Util.Debug(">> Canvas.init"); Canvas.id = id; @@ -198,6 +199,25 @@ init: function (id) { Canvas._cmapImage = Canvas._cmapImageFill; } + /* + * Determine browser support for setting the cursor via data URI + * scheme + */ + curDat = []; + for (i=0; i < 8 * 8 * 4; i++) { + curDat.push(255); + } + curSave = c.style.cursor; + Canvas.setCursor(curDat, curDat, 2, 2, 8, 8); + if (c.style.cursor) { + Util.Info("Data URI scheme cursor supported"); + } else { + Canvas.cursor_uri = false; + Util.Warn("Data URI scheme cursor not supported"); + } + c.style.cursor = curSave; + + Canvas.colourMap = []; Canvas.prevStyle = ""; Canvas.focused = true; @@ -263,6 +283,11 @@ stop: function () { /* Work around right and middle click browser behaviors */ Util.removeEvent(document, 'click', Canvas.onMouseDisable); Util.removeEvent(document.body, 'contextmenu', Canvas.onMouseDisable); + + // Turn off cursor rendering + if (Canvas.cursor_uri) { + c.style.cursor = "default"; + } }, /* @@ -530,8 +555,89 @@ getKeysym: function(e) { } return keysym; -} +}, +isCursor: function() { + return Canvas.cursor_uri; +}, + +setCursor: function(pixels, mask, hotx, hoty, w, h) { + var cur = [], cmap, IHDRsz, ANDsz, XORsz, url, idx, x, y; + //Util.Debug(">> setCursor, x: " + hotx + ", y: " + hoty + ", w: " + w + ", h: " + h); + + if (!Canvas.cursor_uri) { + Util.Warn("setCursor called but no cursor data URI support"); + return; + } + + cmap = Canvas.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--) { + for (x = 0; x < w; x++) { + idx = y * Math.ceil(w / 8) + Math.floor(x/8); + alpha = (mask[idx] << (x % 8)) & 0x80 ? 255 : 0; + + if (Canvas.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++) { + for (x = 0; x < Math.ceil(w / 8); x++) { + cur.push(0x00); + } + } + + url = "data:image/x-icon;base64," + Base64.encode(cur); + $(Canvas.id).style.cursor = "url(" + url + ") " + hotx + " " + hoty + ", default"; + //Util.Debug("<< setCursor, cur.length: " + cur.length); +} + }; diff --git a/include/util.js b/include/util.js index 98cfeef1..64e3f93d 100644 --- a/include/util.js +++ b/include/util.js @@ -77,6 +77,10 @@ Array.prototype.push16 = function (num) { this.push((num >> 8) & 0xFF, (num ) & 0xFF ); }; +Array.prototype.push16le = function (num) { + this.push((num ) & 0xFF, + (num >> 8) & 0xFF ); +}; Array.prototype.shift32 = function () { @@ -97,6 +101,13 @@ Array.prototype.push32 = function (num) { (num >> 8) & 0xFF, (num ) & 0xFF ); }; +Array.prototype.push32le = function (num) { + this.push((num ) & 0xFF, + (num >> 8) & 0xFF, + (num >> 16) & 0xFF, + (num >> 24) & 0xFF ); +}; + Array.prototype.shiftStr = function (len) { var arr = this.splice(0, len); diff --git a/include/vnc.js b/include/vnc.js index 9f6468f1..030e2b09 100644 --- a/include/vnc.js +++ b/include/vnc.js @@ -85,6 +85,10 @@ encodings : [ // ['compress_hi', -247, 'set_compress_level'] ], +encodingCursor : + ['Cursor', -239, 'set_cursor'], + + setUpdateState: function(externalUpdateState) { RFB.externalUpdateState = externalUpdateState; }, @@ -145,6 +149,14 @@ load: function () { RFB.updateState('fatal', "No working Canvas"); } + // Add Cursor pseudo-encoding if supported +/* + if (Canvas.isCursor()) { + Util.Debug("Adding Cursor pseudo-encoding to encoding list"); + RFB.encodings.push(RFB.encodingCursor); + } +*/ + // Populate encoding lookup tables RFB.encHandlers = {}; RFB.encNames = {}; @@ -1013,10 +1025,40 @@ set_desktopsize : function () { RFB.timing.fbu_rt_start = (new Date()).getTime(); // Send a new non-incremental request RFB.send_array(RFB.fbUpdateRequest(0)); - Util.Debug("<< set_desktopsize"); RFB.FBU.bytes = 0; RFB.FBU.rects -= 1; + + Util.Debug("<< set_desktopsize"); +}, + +set_cursor: function () { + var x, y, w, h, pixelslength, masklength; + //Util.Debug(">> set_cursor"); + x = RFB.FBU.x; // hotspot-x + y = RFB.FBU.y; // hotspot-y + w = RFB.FBU.width; + h = RFB.FBU.height; + + pixelslength = w * h * RFB.fb_Bpp; + masklength = Math.floor((w + 7) / 8) * h; + + if (RFB.RQ.length < (pixelslength + masklength)) { + //Util.Debug("waiting for cursor encoding bytes"); + RFB.FBU.bytes = pixelslength + masklength; + return false; + } + + //Util.Debug(" set_cursor, x: " + x + ", y: " + y + ", w: " + w + ", h: " + h); + + Canvas.setCursor(RFB.RQ.shiftBytes(pixelslength), + RFB.RQ.shiftBytes(masklength), + x, y, w, h); + + RFB.FBU.bytes = 0; + RFB.FBU.rects -= 1; + + //Util.Debug("<< set_cursor"); }, set_jpeg_quality : function () {