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