canvas.js 17 KB

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