display.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568
  1. /*
  2. * noVNC: HTML5 VNC client
  3. * Copyright (C) 2011 Joel Martin
  4. * Licensed under LGPL-3 (see LICENSE.txt)
  5. *
  6. * See README.md for usage and integration instructions.
  7. */
  8. /*jslint browser: true, white: false, bitwise: false */
  9. /*global Util, Base64, changeCursor */
  10. function Display(defaults) {
  11. "use strict";
  12. var that = {}, // Public API methods
  13. conf = {}, // Configuration attributes
  14. // Private Display namespace variables
  15. c_ctx = null,
  16. c_forceCanvas = false,
  17. c_imageData, c_rgbxImage, c_cmapImage,
  18. // Predefine function variables (jslint)
  19. imageDataCreate, imageDataGet, rgbxImageData, cmapImageData,
  20. rgbxImageFill, cmapImageFill, setFillColor, rescale, flush,
  21. c_width = 0,
  22. c_height = 0,
  23. c_prevStyle = "",
  24. c_webkit_bug = false,
  25. c_flush_timer = null;
  26. // Configuration attributes
  27. Util.conf_defaults(conf, that, defaults, [
  28. ['target', 'wo', 'dom', null, 'Canvas element for rendering'],
  29. ['context', 'ro', 'raw', null, 'Canvas 2D context for rendering (read-only)'],
  30. ['logo', 'rw', 'raw', null, 'Logo to display when cleared: {"width": width, "height": height, "data": data}'],
  31. ['true_color', 'rw', 'bool', true, 'Use true-color pixel data'],
  32. ['colourMap', 'rw', 'arr', [], 'Colour map array (when not true-color)'],
  33. ['scale', 'rw', 'float', 1.0, 'Display area scale factor 0.0 - 1.0'],
  34. ['width', 'rw', 'int', null, 'Display area width'],
  35. ['height', 'rw', 'int', null, 'Display area height'],
  36. ['render_mode', 'ro', 'str', '', 'Canvas rendering mode (read-only)'],
  37. ['prefer_js', 'rw', 'str', null, 'Prefer Javascript over canvas methods'],
  38. ['cursor_uri', 'rw', 'raw', null, 'Can we render cursor using data URI']
  39. ]);
  40. // Override some specific getters/setters
  41. that.get_context = function () { return c_ctx; };
  42. that.set_scale = function(scale) { rescale(scale); };
  43. that.set_width = function (val) { that.resize(val, c_height); };
  44. that.get_width = function() { return c_width; };
  45. that.set_height = function (val) { that.resize(c_width, val); };
  46. that.get_height = function() { return c_height; };
  47. that.set_prefer_js = function(val) {
  48. if (val && c_forceCanvas) {
  49. Util.Warn("Preferring Javascript to Canvas ops is not supported");
  50. return false;
  51. }
  52. conf.prefer_js = val;
  53. return true;
  54. };
  55. //
  56. // Private functions
  57. //
  58. // Create the public API interface
  59. function constructor() {
  60. Util.Debug(">> Display.constructor");
  61. var c, func, imgTest, tval, i, curDat, curSave,
  62. has_imageData = false, UE = Util.Engine;
  63. if (! conf.target) { throw("target must be set"); }
  64. if (typeof conf.target === 'string') {
  65. throw("target must be a DOM element");
  66. }
  67. c = conf.target;
  68. if (! c.getContext) { throw("no getContext method"); }
  69. if (! c_ctx) { c_ctx = c.getContext('2d'); }
  70. Util.Debug("User Agent: " + navigator.userAgent);
  71. if (UE.gecko) { Util.Debug("Browser: gecko " + UE.gecko); }
  72. if (UE.webkit) { Util.Debug("Browser: webkit " + UE.webkit); }
  73. if (UE.trident) { Util.Debug("Browser: trident " + UE.trident); }
  74. if (UE.presto) { Util.Debug("Browser: presto " + UE.presto); }
  75. that.clear();
  76. /*
  77. * Determine browser Canvas feature support
  78. * and select fastest rendering methods
  79. */
  80. tval = 0;
  81. try {
  82. imgTest = c_ctx.getImageData(0, 0, 1,1);
  83. imgTest.data[0] = 123;
  84. imgTest.data[3] = 255;
  85. c_ctx.putImageData(imgTest, 0, 0);
  86. tval = c_ctx.getImageData(0, 0, 1, 1).data[0];
  87. if (tval === 123) {
  88. has_imageData = true;
  89. }
  90. } catch (exc1) {}
  91. if (has_imageData) {
  92. Util.Info("Canvas supports imageData");
  93. c_forceCanvas = false;
  94. if (c_ctx.createImageData) {
  95. // If it's there, it's faster
  96. Util.Info("Using Canvas createImageData");
  97. conf.render_mode = "createImageData rendering";
  98. c_imageData = imageDataCreate;
  99. } else if (c_ctx.getImageData) {
  100. // I think this is mostly just Opera
  101. Util.Info("Using Canvas getImageData");
  102. conf.render_mode = "getImageData rendering";
  103. c_imageData = imageDataGet;
  104. }
  105. Util.Info("Prefering javascript operations");
  106. if (conf.prefer_js === null) {
  107. conf.prefer_js = true;
  108. }
  109. c_rgbxImage = rgbxImageData;
  110. c_cmapImage = cmapImageData;
  111. } else {
  112. Util.Warn("Canvas lacks imageData, using fillRect (slow)");
  113. conf.render_mode = "fillRect rendering (slow)";
  114. c_forceCanvas = true;
  115. conf.prefer_js = false;
  116. c_rgbxImage = rgbxImageFill;
  117. c_cmapImage = cmapImageFill;
  118. }
  119. if (UE.webkit && UE.webkit >= 534.7 && UE.webkit <= 534.9) {
  120. // Workaround WebKit canvas rendering bug #46319
  121. conf.render_mode += ", webkit bug workaround";
  122. Util.Debug("Working around WebKit bug #46319");
  123. c_webkit_bug = true;
  124. for (func in {"fillRect":1, "copyImage":1, "rgbxImage":1,
  125. "cmapImage":1, "blitStringImage":1}) {
  126. that[func] = (function() {
  127. var myfunc = that[func]; // Save original function
  128. //Util.Debug("Wrapping " + func);
  129. return function() {
  130. myfunc.apply(this, arguments);
  131. if (!c_flush_timer) {
  132. c_flush_timer = setTimeout(flush, 100);
  133. }
  134. };
  135. }());
  136. }
  137. }
  138. /*
  139. * Determine browser support for setting the cursor via data URI
  140. * scheme
  141. */
  142. curDat = [];
  143. for (i=0; i < 8 * 8 * 4; i += 1) {
  144. curDat.push(255);
  145. }
  146. try {
  147. curSave = c.style.cursor;
  148. changeCursor(conf.target, curDat, curDat, 2, 2, 8, 8);
  149. if (c.style.cursor) {
  150. if (conf.cursor_uri === null) {
  151. conf.cursor_uri = true;
  152. }
  153. Util.Info("Data URI scheme cursor supported");
  154. } else {
  155. if (conf.cursor_uri === null) {
  156. conf.cursor_uri = false;
  157. }
  158. Util.Warn("Data URI scheme cursor not supported");
  159. }
  160. c.style.cursor = curSave;
  161. } catch (exc2) {
  162. Util.Error("Data URI scheme cursor test exception: " + exc2);
  163. conf.cursor_uri = false;
  164. }
  165. Util.Debug("<< Display.constructor");
  166. return that ;
  167. }
  168. rescale = function(factor) {
  169. var c, tp, x, y,
  170. properties = ['transform', 'WebkitTransform', 'MozTransform', null];
  171. c = conf.target;
  172. tp = properties.shift();
  173. while (tp) {
  174. if (typeof c.style[tp] !== 'undefined') {
  175. break;
  176. }
  177. tp = properties.shift();
  178. }
  179. if (tp === null) {
  180. Util.Debug("No scaling support");
  181. return;
  182. }
  183. if (factor > 1.0) {
  184. factor = 1.0;
  185. } else if (factor < 0.1) {
  186. factor = 0.1;
  187. }
  188. if (conf.scale === factor) {
  189. //Util.Debug("Display already scaled to '" + factor + "'");
  190. return;
  191. }
  192. conf.scale = factor;
  193. x = c.width - c.width * factor;
  194. y = c.height - c.height * factor;
  195. c.style[tp] = "scale(" + conf.scale + ") translate(-" + x + "px, -" + y + "px)";
  196. };
  197. // Force canvas redraw (for webkit bug #46319 workaround)
  198. flush = function() {
  199. var old_val;
  200. //Util.Debug(">> flush");
  201. old_val = conf.target.style.marginRight;
  202. conf.target.style.marginRight = "1px";
  203. c_flush_timer = null;
  204. setTimeout(function () {
  205. conf.target.style.marginRight = old_val;
  206. }, 1);
  207. };
  208. setFillColor = function(color) {
  209. var rgb, newStyle;
  210. if (conf.true_color) {
  211. rgb = color;
  212. } else {
  213. rgb = conf.colourMap[color[0]];
  214. }
  215. newStyle = "rgb(" + rgb[0] + "," + rgb[1] + "," + rgb[2] + ")";
  216. if (newStyle !== c_prevStyle) {
  217. c_ctx.fillStyle = newStyle;
  218. c_prevStyle = newStyle;
  219. }
  220. };
  221. //
  222. // Public API interface functions
  223. //
  224. that.resize = function(width, height) {
  225. var c = conf.target;
  226. c_prevStyle = "";
  227. c.width = width;
  228. c.height = height;
  229. c_width = c.offsetWidth;
  230. c_height = c.offsetHeight;
  231. rescale(conf.scale);
  232. };
  233. that.clear = function() {
  234. if (conf.logo) {
  235. that.resize(conf.logo.width, conf.logo.height);
  236. that.blitStringImage(conf.logo.data, 0, 0);
  237. } else {
  238. that.resize(640, 20);
  239. c_ctx.clearRect(0, 0, c_width, c_height);
  240. }
  241. // No benefit over default ("source-over") in Chrome and firefox
  242. //c_ctx.globalCompositeOperation = "copy";
  243. };
  244. that.fillRect = function(x, y, width, height, color) {
  245. setFillColor(color);
  246. c_ctx.fillRect(x, y, width, height);
  247. };
  248. that.copyImage = function(old_x, old_y, new_x, new_y, width, height) {
  249. c_ctx.drawImage(conf.target, old_x, old_y, width, height,
  250. new_x, new_y, width, height);
  251. };
  252. /*
  253. * Tile rendering functions optimized for rendering engines.
  254. *
  255. * - In Chrome/webkit, Javascript image data array manipulations are
  256. * faster than direct Canvas fillStyle, fillRect rendering. In
  257. * gecko, Javascript array handling is much slower.
  258. */
  259. that.getTile = function(x, y, width, height, color) {
  260. var img, data = [], rgb, red, green, blue, i;
  261. img = {'x': x, 'y': y, 'width': width, 'height': height,
  262. 'data': data};
  263. if (conf.prefer_js) {
  264. if (conf.true_color) {
  265. rgb = color;
  266. } else {
  267. rgb = conf.colourMap[color[0]];
  268. }
  269. red = rgb[0];
  270. green = rgb[1];
  271. blue = rgb[2];
  272. for (i = 0; i < (width * height * 4); i+=4) {
  273. data[i ] = red;
  274. data[i + 1] = green;
  275. data[i + 2] = blue;
  276. }
  277. } else {
  278. that.fillRect(x, y, width, height, color);
  279. }
  280. return img;
  281. };
  282. that.setSubTile = function(img, x, y, w, h, color) {
  283. var data, p, rgb, red, green, blue, width, j, i, xend, yend;
  284. if (conf.prefer_js) {
  285. data = img.data;
  286. width = img.width;
  287. if (conf.true_color) {
  288. rgb = color;
  289. } else {
  290. rgb = conf.colourMap[color[0]];
  291. }
  292. red = rgb[0];
  293. green = rgb[1];
  294. blue = rgb[2];
  295. xend = x + w;
  296. yend = y + h;
  297. for (j = y; j < yend; j += 1) {
  298. for (i = x; i < xend; i += 1) {
  299. p = (i + (j * width) ) * 4;
  300. data[p ] = red;
  301. data[p + 1] = green;
  302. data[p + 2] = blue;
  303. }
  304. }
  305. } else {
  306. that.fillRect(img.x + x, img.y + y, w, h, color);
  307. }
  308. };
  309. that.putTile = function(img) {
  310. if (conf.prefer_js) {
  311. c_rgbxImage(img.x, img.y, img.width, img.height, img.data, 0);
  312. }
  313. // else: No-op, under gecko already done by setSubTile
  314. };
  315. imageDataGet = function(width, height) {
  316. return c_ctx.getImageData(0, 0, width, height);
  317. };
  318. imageDataCreate = function(width, height) {
  319. return c_ctx.createImageData(width, height);
  320. };
  321. rgbxImageData = function(x, y, width, height, arr, offset) {
  322. var img, i, j, data;
  323. img = c_imageData(width, height);
  324. data = img.data;
  325. for (i=0, j=offset; i < (width * height * 4); i=i+4, j=j+4) {
  326. data[i ] = arr[j ];
  327. data[i + 1] = arr[j + 1];
  328. data[i + 2] = arr[j + 2];
  329. data[i + 3] = 255; // Set Alpha
  330. }
  331. c_ctx.putImageData(img, x, y);
  332. };
  333. // really slow fallback if we don't have imageData
  334. rgbxImageFill = function(x, y, width, height, arr, offset) {
  335. var i, j, sx = 0, sy = 0;
  336. for (i=0, j=offset; i < (width * height); i+=1, j+=4) {
  337. that.fillRect(x+sx, y+sy, 1, 1, [arr[j], arr[j+1], arr[j+2]]);
  338. sx += 1;
  339. if ((sx % width) === 0) {
  340. sx = 0;
  341. sy += 1;
  342. }
  343. }
  344. };
  345. cmapImageData = function(x, y, width, height, arr, offset) {
  346. var img, i, j, data, rgb, cmap;
  347. img = c_imageData(width, height);
  348. data = img.data;
  349. cmap = conf.colourMap;
  350. for (i=0, j=offset; i < (width * height * 4); i+=4, j+=1) {
  351. rgb = cmap[arr[j]];
  352. data[i ] = rgb[0];
  353. data[i + 1] = rgb[1];
  354. data[i + 2] = rgb[2];
  355. data[i + 3] = 255; // Set Alpha
  356. }
  357. c_ctx.putImageData(img, x, y);
  358. };
  359. cmapImageFill = function(x, y, width, height, arr, offset) {
  360. var i, j, sx = 0, sy = 0, cmap;
  361. cmap = conf.colourMap;
  362. for (i=0, j=offset; i < (width * height); i+=1, j+=1) {
  363. that.fillRect(x+sx, y+sy, 1, 1, [arr[j]]);
  364. sx += 1;
  365. if ((sx % width) === 0) {
  366. sx = 0;
  367. sy += 1;
  368. }
  369. }
  370. };
  371. that.blitImage = function(x, y, width, height, arr, offset) {
  372. if (conf.true_color) {
  373. c_rgbxImage(x, y, width, height, arr, offset);
  374. } else {
  375. c_cmapImage(x, y, width, height, arr, offset);
  376. }
  377. };
  378. that.blitStringImage = function(str, x, y) {
  379. var img = new Image();
  380. img.onload = function () { c_ctx.drawImage(img, x, y); };
  381. img.src = str;
  382. };
  383. that.changeCursor = function(pixels, mask, hotx, hoty, w, h) {
  384. if (conf.cursor_uri === false) {
  385. Util.Warn("changeCursor called but no cursor data URI support");
  386. return;
  387. }
  388. if (conf.true_color) {
  389. changeCursor(conf.target, pixels, mask, hotx, hoty, w, h);
  390. } else {
  391. changeCursor(conf.target, pixels, mask, hotx, hoty, w, h, conf.colourMap);
  392. }
  393. };
  394. that.defaultCursor = function() {
  395. conf.target.style.cursor = "default";
  396. };
  397. return constructor(); // Return the public API interface
  398. } // End of Display()
  399. /* Set CSS cursor property using data URI encoded cursor file */
  400. function changeCursor(target, pixels, mask, hotx, hoty, w, h, cmap) {
  401. "use strict";
  402. var cur = [], rgb, IHDRsz, RGBsz, ANDsz, XORsz, url, idx, alpha, x, y;
  403. //Util.Debug(">> changeCursor, x: " + hotx + ", y: " + hoty + ", w: " + w + ", h: " + h);
  404. // Push multi-byte little-endian values
  405. cur.push16le = function (num) {
  406. this.push((num ) & 0xFF,
  407. (num >> 8) & 0xFF );
  408. };
  409. cur.push32le = function (num) {
  410. this.push((num ) & 0xFF,
  411. (num >> 8) & 0xFF,
  412. (num >> 16) & 0xFF,
  413. (num >> 24) & 0xFF );
  414. };
  415. IHDRsz = 40;
  416. RGBsz = w * h * 4;
  417. XORsz = Math.ceil( (w * h) / 8.0 );
  418. ANDsz = Math.ceil( (w * h) / 8.0 );
  419. // Main header
  420. cur.push16le(0); // 0: Reserved
  421. cur.push16le(2); // 2: .CUR type
  422. cur.push16le(1); // 4: Number of images, 1 for non-animated ico
  423. // Cursor #1 header (ICONDIRENTRY)
  424. cur.push(w); // 6: width
  425. cur.push(h); // 7: height
  426. cur.push(0); // 8: colors, 0 -> true-color
  427. cur.push(0); // 9: reserved
  428. cur.push16le(hotx); // 10: hotspot x coordinate
  429. cur.push16le(hoty); // 12: hotspot y coordinate
  430. cur.push32le(IHDRsz + RGBsz + XORsz + ANDsz);
  431. // 14: cursor data byte size
  432. cur.push32le(22); // 18: offset of cursor data in the file
  433. // Cursor #1 InfoHeader (ICONIMAGE/BITMAPINFO)
  434. cur.push32le(IHDRsz); // 22: Infoheader size
  435. cur.push32le(w); // 26: Cursor width
  436. cur.push32le(h*2); // 30: XOR+AND height
  437. cur.push16le(1); // 34: number of planes
  438. cur.push16le(32); // 36: bits per pixel
  439. cur.push32le(0); // 38: Type of compression
  440. cur.push32le(XORsz + ANDsz); // 43: Size of Image
  441. // Gimp leaves this as 0
  442. cur.push32le(0); // 46: reserved
  443. cur.push32le(0); // 50: reserved
  444. cur.push32le(0); // 54: reserved
  445. cur.push32le(0); // 58: reserved
  446. // 62: color data (RGBQUAD icColors[])
  447. for (y = h-1; y >= 0; y -= 1) {
  448. for (x = 0; x < w; x += 1) {
  449. idx = y * Math.ceil(w / 8) + Math.floor(x/8);
  450. alpha = (mask[idx] << (x % 8)) & 0x80 ? 255 : 0;
  451. if (cmap) {
  452. idx = (w * y) + x;
  453. rgb = cmap[pixels[idx]];
  454. cur.push(rgb[2]); // blue
  455. cur.push(rgb[1]); // green
  456. cur.push(rgb[0]); // red
  457. cur.push(alpha); // alpha
  458. } else {
  459. idx = ((w * y) + x) * 4;
  460. cur.push(pixels[idx + 2]); // blue
  461. cur.push(pixels[idx + 1]); // green
  462. cur.push(pixels[idx ]); // red
  463. cur.push(alpha); // alpha
  464. }
  465. }
  466. }
  467. // XOR/bitmask data (BYTE icXOR[])
  468. // (ignored, just needs to be right size)
  469. for (y = 0; y < h; y += 1) {
  470. for (x = 0; x < Math.ceil(w / 8); x += 1) {
  471. cur.push(0x00);
  472. }
  473. }
  474. // AND/bitmask data (BYTE icAND[])
  475. // (ignored, just needs to be right size)
  476. for (y = 0; y < h; y += 1) {
  477. for (x = 0; x < Math.ceil(w / 8); x += 1) {
  478. cur.push(0x00);
  479. }
  480. }
  481. url = "data:image/x-icon;base64," + Base64.encode(cur);
  482. target.style.cursor = "url(" + url + ") " + hotx + " " + hoty + ", default";
  483. //Util.Debug("<< changeCursor, cur.length: " + cur.length);
  484. }