2 * noVNC: HTML5 VNC client
3 * Copyright (C) 2012 Joel Martin
4 * Copyright (C) 2015 Samuel Mannehed for Cendio AB
5 * Licensed under MPL 2.0 (see LICENSE.txt)
7 * See README.md for usage and integration instructions.
10 /*jslint browser: true, white: false */
11 /*global Util, Base64, changeCursor */
13 import { Engine
, browserSupportsCursorURIs as cursorURIsSupported
} from './util/browsers.js';
14 import { set_defaults
, make_properties
} from './util/properties.js';
15 import * as Log
from './util/logging.js';
16 import Base64
from "./base64.js";
18 export default function Display(defaults
) {
20 this._c_forceCanvas
= false;
22 this._renderQ
= []; // queue drawing actions for in-oder rendering
23 this._flushing
= false;
25 // the full frame buffer (logical canvas) size
29 this._prevDrawStyle
= "";
31 this._tile16x16
= null;
35 set_defaults(this, defaults
, {
41 "onFlush": function () {},
44 Log
.Debug(">> Display.constructor");
48 throw new Error("Target must be set");
51 if (typeof this._target
=== 'string') {
52 throw new Error('target must be a DOM element');
55 if (!this._target
.getContext
) {
56 throw new Error("no getContext method");
59 this._targetCtx
= this._target
.getContext('2d');
61 // the visible canvas viewport (i.e. what actually gets seen)
62 this._viewportLoc
= { 'x': 0, 'y': 0, 'w': this._target
.width
, 'h': this._target
.height
};
64 // The hidden canvas, where we do the actual rendering
65 this._backbuffer
= document
.createElement('canvas');
66 this._drawCtx
= this._backbuffer
.getContext('2d');
68 this._damageBounds
= { left
:0, top
:0,
69 right
: this._backbuffer
.width
,
70 bottom
: this._backbuffer
.height
};
72 Log
.Debug("User Agent: " + navigator
.userAgent
);
73 if (Engine
.gecko
) { Log
.Debug("Browser: gecko " + Engine
.gecko
); }
74 if (Engine
.webkit
) { Log
.Debug("Browser: webkit " + Engine
.webkit
); }
75 if (Engine
.trident
) { Log
.Debug("Browser: trident " + Engine
.trident
); }
76 if (Engine
.presto
) { Log
.Debug("Browser: presto " + Engine
.presto
); }
80 // Check canvas features
81 if ('createImageData' in this._drawCtx
) {
82 this._render_mode
= 'canvas rendering';
84 throw new Error("Canvas does not support createImageData");
87 if (this._prefer_js
=== null) {
88 Log
.Info("Prefering javascript operations");
89 this._prefer_js
= true;
92 // Determine browser support for setting the cursor via data URI scheme
93 if (this._cursor_uri
|| this._cursor_uri
=== null ||
94 this._cursor_uri
=== undefined) {
95 this._cursor_uri
= cursorURIsSupported();
98 Log
.Debug("<< Display.constructor");
101 var SUPPORTS_IMAGEDATA_CONSTRUCTOR
= false;
103 new ImageData(new Uint8ClampedArray(4), 1, 1);
104 SUPPORTS_IMAGEDATA_CONSTRUCTOR
= true;
109 Display
.prototype = {
111 viewportChangePos: function (deltaX
, deltaY
) {
112 var vp
= this._viewportLoc
;
113 deltaX
= Math
.floor(deltaX
);
114 deltaY
= Math
.floor(deltaY
);
116 if (!this._viewport
) {
117 deltaX
= -vp
.w
; // clamped later of out of bounds
121 var vx2
= vp
.x
+ vp
.w
- 1;
122 var vy2
= vp
.y
+ vp
.h
- 1;
126 if (deltaX
< 0 && vp
.x
+ deltaX
< 0) {
129 if (vx2
+ deltaX
>= this._fb_width
) {
130 deltaX
-= vx2
+ deltaX
- this._fb_width
+ 1;
133 if (vp
.y
+ deltaY
< 0) {
136 if (vy2
+ deltaY
>= this._fb_height
) {
137 deltaY
-= (vy2
+ deltaY
- this._fb_height
+ 1);
140 if (deltaX
=== 0 && deltaY
=== 0) {
143 Log
.Debug("viewportChange deltaX: " + deltaX
+ ", deltaY: " + deltaY
);
148 this._damage(vp
.x
, vp
.y
, vp
.w
, vp
.h
);
153 viewportChangeSize: function(width
, height
) {
155 if (!this._viewport
||
156 typeof(width
) === "undefined" ||
157 typeof(height
) === "undefined") {
159 Log
.Debug("Setting viewport to full display region");
160 width
= this._fb_width
;
161 height
= this._fb_height
;
164 if (width
> this._fb_width
) {
165 width
= this._fb_width
;
167 if (height
> this._fb_height
) {
168 height
= this._fb_height
;
171 var vp
= this._viewportLoc
;
172 if (vp
.w
!== width
|| vp
.h
!== height
) {
176 var canvas
= this._target
;
177 canvas
.width
= width
;
178 canvas
.height
= height
;
180 // The position might need to be updated if we've grown
181 this.viewportChangePos(0, 0);
183 this._damage(vp
.x
, vp
.y
, vp
.w
, vp
.h
);
186 // Update the visible size of the target canvas
187 this._rescale(this._scale
);
192 return x
/ this._scale
+ this._viewportLoc
.x
;
196 return y
/ this._scale
+ this._viewportLoc
.y
;
199 resize: function (width
, height
) {
200 this._prevDrawStyle
= "";
202 this._fb_width
= width
;
203 this._fb_height
= height
;
205 var canvas
= this._backbuffer
;
206 if (canvas
.width
!== width
|| canvas
.height
!== height
) {
208 // We have to save the canvas data since changing the size will clear it
210 if (canvas
.width
> 0 && canvas
.height
> 0) {
211 saveImg
= this._drawCtx
.getImageData(0, 0, canvas
.width
, canvas
.height
);
214 if (canvas
.width
!== width
) {
215 canvas
.width
= width
;
217 if (canvas
.height
!== height
) {
218 canvas
.height
= height
;
222 this._drawCtx
.putImageData(saveImg
, 0, 0);
226 // Readjust the viewport as it may be incorrectly sized
228 var vp
= this._viewportLoc
;
229 this.viewportChangeSize(vp
.w
, vp
.h
);
230 this.viewportChangePos(0, 0);
233 // Track what parts of the visible canvas that need updating
234 _damage: function(x
, y
, w
, h
) {
235 if (x
< this._damageBounds
.left
) {
236 this._damageBounds
.left
= x
;
238 if (y
< this._damageBounds
.top
) {
239 this._damageBounds
.top
= y
;
241 if ((x
+ w
) > this._damageBounds
.right
) {
242 this._damageBounds
.right
= x
+ w
;
244 if ((y
+ h
) > this._damageBounds
.bottom
) {
245 this._damageBounds
.bottom
= y
+ h
;
249 // Update the visible canvas with the contents of the
251 flip: function(from_queue
) {
252 if (this._renderQ
.length
!== 0 && !from_queue
) {
257 var x
, y
, vx
, vy
, w
, h
;
259 x
= this._damageBounds
.left
;
260 y
= this._damageBounds
.top
;
261 w
= this._damageBounds
.right
- x
;
262 h
= this._damageBounds
.bottom
- y
;
264 vx
= x
- this._viewportLoc
.x
;
265 vy
= y
- this._viewportLoc
.y
;
278 if ((vx
+ w
) > this._viewportLoc
.w
) {
279 w
= this._viewportLoc
.w
- vx
;
281 if ((vy
+ h
) > this._viewportLoc
.h
) {
282 h
= this._viewportLoc
.h
- vy
;
285 if ((w
> 0) && (h
> 0)) {
286 // FIXME: We may need to disable image smoothing here
287 // as well (see copyImage()), but we haven't
288 // noticed any problem yet.
289 this._targetCtx
.drawImage(this._backbuffer
,
294 this._damageBounds
.left
= this._damageBounds
.top
= 65535;
295 this._damageBounds
.right
= this._damageBounds
.bottom
= 0;
301 this.resize(this._logo
.width
, this._logo
.height
);
302 this.imageRect(0, 0, this._logo
.type
, this._logo
.data
);
304 this.resize(240, 20);
305 this._drawCtx
.clearRect(0, 0, this._fb_width
, this._fb_height
);
310 pending: function() {
311 return this._renderQ
.length
> 0;
315 if (this._renderQ
.length
=== 0) {
318 this._flushing
= true;
322 fillRect: function (x
, y
, width
, height
, color
, from_queue
) {
323 if (this._renderQ
.length
!== 0 && !from_queue
) {
333 this._setFillColor(color
);
334 this._drawCtx
.fillRect(x
, y
, width
, height
);
335 this._damage(x
, y
, width
, height
);
339 copyImage: function (old_x
, old_y
, new_x
, new_y
, w
, h
, from_queue
) {
340 if (this._renderQ
.length
!== 0 && !from_queue
) {
351 // Due to this bug among others [1] we need to disable the image-smoothing to
352 // avoid getting a blur effect when copying data.
354 // 1. https://bugzilla.mozilla.org/show_bug.cgi?id=1194719
356 // We need to set these every time since all properties are reset
357 // when the the size is changed
358 this._drawCtx
.mozImageSmoothingEnabled
= false;
359 this._drawCtx
.webkitImageSmoothingEnabled
= false;
360 this._drawCtx
.msImageSmoothingEnabled
= false;
361 this._drawCtx
.imageSmoothingEnabled
= false;
363 this._drawCtx
.drawImage(this._backbuffer
,
366 this._damage(new_x
, new_y
, w
, h
);
370 imageRect: function(x
, y
, mime
, arr
) {
371 var img
= new Image();
372 img
.src
= "data: " + mime
+ ";base64," + Base64
.encode(arr
);
381 // start updating a tile
382 startTile: function (x
, y
, width
, height
, color
) {
385 if (width
=== 16 && height
=== 16) {
386 this._tile
= this._tile16x16
;
388 this._tile
= this._drawCtx
.createImageData(width
, height
);
391 if (this._prefer_js
) {
393 if (this._true_color
) {
396 bgr
= this._colourMap
[color
[0]];
402 var data
= this._tile
.data
;
403 for (var i
= 0; i
< width
* height
* 4; i
+= 4) {
410 this.fillRect(x
, y
, width
, height
, color
, true);
414 // update sub-rectangle of the current tile
415 subTile: function (x
, y
, w
, h
, color
) {
416 if (this._prefer_js
) {
418 if (this._true_color
) {
421 bgr
= this._colourMap
[color
[0]];
429 var data
= this._tile
.data
;
430 var width
= this._tile
.width
;
431 for (var j
= y
; j
< yend
; j
++) {
432 for (var i
= x
; i
< xend
; i
++) {
433 var p
= (i
+ (j
* width
)) * 4;
441 this.fillRect(this._tile_x
+ x
, this._tile_y
+ y
, w
, h
, color
, true);
445 // draw the current tile to the screen
446 finishTile: function () {
447 if (this._prefer_js
) {
448 this._drawCtx
.putImageData(this._tile
, this._tile_x
, this._tile_y
);
449 this._damage(this._tile_x
, this._tile_y
,
450 this._tile
.width
, this._tile
.height
);
452 // else: No-op -- already done by setSubTile
455 blitImage: function (x
, y
, width
, height
, arr
, offset
, from_queue
) {
456 if (this._renderQ
.length
!== 0 && !from_queue
) {
457 // NB(directxman12): it's technically more performant here to use preallocated arrays,
458 // but it's a lot of extra work for not a lot of payoff -- if we're using the render queue,
459 // this probably isn't getting called *nearly* as much
460 var new_arr
= new Uint8Array(width
* height
* 4);
461 new_arr
.set(new Uint8Array(arr
.buffer
, 0, new_arr
.length
));
470 } else if (this._true_color
) {
471 this._bgrxImageData(x
, y
, width
, height
, arr
, offset
);
473 this._cmapImageData(x
, y
, width
, height
, arr
, offset
);
477 blitRgbImage: function (x
, y
, width
, height
, arr
, offset
, from_queue
) {
478 if (this._renderQ
.length
!== 0 && !from_queue
) {
479 // NB(directxman12): it's technically more performant here to use preallocated arrays,
480 // but it's a lot of extra work for not a lot of payoff -- if we're using the render queue,
481 // this probably isn't getting called *nearly* as much
482 var new_arr
= new Uint8Array(width
* height
* 3);
483 new_arr
.set(new Uint8Array(arr
.buffer
, 0, new_arr
.length
));
492 } else if (this._true_color
) {
493 this._rgbImageData(x
, y
, width
, height
, arr
, offset
);
496 this._cmapImageData(x
, y
, width
, height
, arr
, offset
);
500 blitRgbxImage: function (x
, y
, width
, height
, arr
, offset
, from_queue
) {
501 if (this._renderQ
.length
!== 0 && !from_queue
) {
502 // NB(directxman12): it's technically more performant here to use preallocated arrays,
503 // but it's a lot of extra work for not a lot of payoff -- if we're using the render queue,
504 // this probably isn't getting called *nearly* as much
505 var new_arr
= new Uint8Array(width
* height
* 4);
506 new_arr
.set(new Uint8Array(arr
.buffer
, 0, new_arr
.length
));
516 this._rgbxImageData(x
, y
, width
, height
, arr
, offset
);
520 drawImage: function (img
, x
, y
) {
521 this._drawCtx
.drawImage(img
, x
, y
);
522 this._damage(x
, y
, img
.width
, img
.height
);
525 changeCursor: function (pixels
, mask
, hotx
, hoty
, w
, h
) {
526 if (this._cursor_uri
=== false) {
527 Log
.Warn("changeCursor called but no cursor data URI support");
531 if (this._true_color
) {
532 Display
.changeCursor(this._target
, pixels
, mask
, hotx
, hoty
, w
, h
);
534 Display
.changeCursor(this._target
, pixels
, mask
, hotx
, hoty
, w
, h
, this._colourMap
);
538 defaultCursor: function () {
539 this._target
.style
.cursor
= "default";
542 disableLocalCursor: function () {
543 this._target
.style
.cursor
= "none";
546 clippingDisplay: function () {
547 var vp
= this._viewportLoc
;
548 return this._fb_width
> vp
.w
|| this._fb_height
> vp
.h
;
551 // Overridden getters/setters
552 set_scale: function (scale
) {
553 this._rescale(scale
);
556 set_viewport: function (viewport
) {
557 this._viewport
= viewport
;
558 // May need to readjust the viewport dimensions
559 var vp
= this._viewportLoc
;
560 this.viewportChangeSize(vp
.w
, vp
.h
);
561 this.viewportChangePos(0, 0);
564 get_width: function () {
565 return this._fb_width
;
567 get_height: function () {
568 return this._fb_height
;
571 autoscale: function (containerWidth
, containerHeight
, downscaleOnly
) {
572 var vp
= this._viewportLoc
;
573 var targetAspectRatio
= containerWidth
/ containerHeight
;
574 var fbAspectRatio
= vp
.w
/ vp
.h
;
577 if (fbAspectRatio
>= targetAspectRatio
) {
578 scaleRatio
= containerWidth
/ vp
.w
;
580 scaleRatio
= containerHeight
/ vp
.h
;
583 if (scaleRatio
> 1.0 && downscaleOnly
) {
587 this._rescale(scaleRatio
);
591 _rescale: function (factor
) {
592 this._scale
= factor
;
593 var vp
= this._viewportLoc
;
595 // NB(directxman12): If you set the width directly, or set the
596 // style width to a number, the canvas is cleared.
597 // However, if you set the style width to a string
598 // ('NNNpx'), the canvas is scaled without clearing.
599 var width
= Math
.round(factor
* vp
.w
) + 'px';
600 var height
= Math
.round(factor
* vp
.h
) + 'px';
602 if ((this._target
.style
.width
!== width
) ||
603 (this._target
.style
.height
!== height
)) {
604 this._target
.style
.width
= width
;
605 this._target
.style
.height
= height
;
609 _setFillColor: function (color
) {
611 if (this._true_color
) {
614 bgr
= this._colourMap
[color
];
617 var newStyle
= 'rgb(' + bgr
[2] + ',' + bgr
[1] + ',' + bgr
[0] + ')';
618 if (newStyle
!== this._prevDrawStyle
) {
619 this._drawCtx
.fillStyle
= newStyle
;
620 this._prevDrawStyle
= newStyle
;
624 _rgbImageData: function (x
, y
, width
, height
, arr
, offset
) {
625 var img
= this._drawCtx
.createImageData(width
, height
);
627 for (var i
= 0, j
= offset
; i
< width
* height
* 4; i
+= 4, j
+= 3) {
629 data
[i
+ 1] = arr
[j
+ 1];
630 data
[i
+ 2] = arr
[j
+ 2];
631 data
[i
+ 3] = 255; // Alpha
633 this._drawCtx
.putImageData(img
, x
, y
);
634 this._damage(x
, y
, img
.width
, img
.height
);
637 _bgrxImageData: function (x
, y
, width
, height
, arr
, offset
) {
638 var img
= this._drawCtx
.createImageData(width
, height
);
640 for (var i
= 0, j
= offset
; i
< width
* height
* 4; i
+= 4, j
+= 4) {
641 data
[i
] = arr
[j
+ 2];
642 data
[i
+ 1] = arr
[j
+ 1];
643 data
[i
+ 2] = arr
[j
];
644 data
[i
+ 3] = 255; // Alpha
646 this._drawCtx
.putImageData(img
, x
, y
);
647 this._damage(x
, y
, img
.width
, img
.height
);
650 _rgbxImageData: function (x
, y
, width
, height
, arr
, offset
) {
651 // NB(directxman12): arr must be an Type Array view
653 if (SUPPORTS_IMAGEDATA_CONSTRUCTOR
) {
654 img
= new ImageData(new Uint8ClampedArray(arr
.buffer
, arr
.byteOffset
, width
* height
* 4), width
, height
);
656 img
= this._drawCtx
.createImageData(width
, height
);
657 img
.data
.set(new Uint8ClampedArray(arr
.buffer
, arr
.byteOffset
, width
* height
* 4));
659 this._drawCtx
.putImageData(img
, x
, y
);
660 this._damage(x
, y
, img
.width
, img
.height
);
663 _cmapImageData: function (x
, y
, width
, height
, arr
, offset
) {
664 var img
= this._drawCtx
.createImageData(width
, height
);
666 var cmap
= this._colourMap
;
667 for (var i
= 0, j
= offset
; i
< width
* height
* 4; i
+= 4, j
++) {
668 var bgr
= cmap
[arr
[j
]];
670 data
[i
+ 1] = bgr
[1];
671 data
[i
+ 2] = bgr
[0];
672 data
[i
+ 3] = 255; // Alpha
674 this._drawCtx
.putImageData(img
, x
, y
);
675 this._damage(x
, y
, img
.width
, img
.height
);
678 _renderQ_push: function (action
) {
679 this._renderQ
.push(action
);
680 if (this._renderQ
.length
=== 1) {
681 // If this can be rendered immediately it will be, otherwise
682 // the scanner will wait for the relevant event
683 this._scan_renderQ();
687 _resume_renderQ: function() {
688 // "this" is the object that is ready, not the
690 this.removeEventListener('load', this._noVNC_display
._resume_renderQ
);
691 this._noVNC_display
._scan_renderQ();
694 _scan_renderQ: function () {
696 while (ready
&& this._renderQ
.length
> 0) {
697 var a
= this._renderQ
[0];
703 this.copyImage(a
.old_x
, a
.old_y
, a
.x
, a
.y
, a
.width
, a
.height
, true);
706 this.fillRect(a
.x
, a
.y
, a
.width
, a
.height
, a
.color
, true);
709 this.blitImage(a
.x
, a
.y
, a
.width
, a
.height
, a
.data
, 0, true);
712 this.blitRgbImage(a
.x
, a
.y
, a
.width
, a
.height
, a
.data
, 0, true);
715 this.blitRgbxImage(a
.x
, a
.y
, a
.width
, a
.height
, a
.data
, 0, true);
718 if (a
.img
.complete
) {
719 this.drawImage(a
.img
, a
.x
, a
.y
);
721 a
.img
._noVNC_display
= this;
722 a
.img
.addEventListener('load', this._resume_renderQ
);
723 // We need to wait for this image to 'load'
724 // to keep things in-order
731 this._renderQ
.shift();
735 if (this._renderQ
.length
=== 0 && this._flushing
) {
736 this._flushing
= false;
742 make_properties(Display
, [
743 ['target', 'wo', 'dom'], // Canvas element for rendering
744 ['context', 'ro', 'raw'], // Canvas 2D context for rendering (read-only)
745 ['logo', 'rw', 'raw'], // Logo to display when cleared: {"width": w, "height": h, "type": mime-type, "data": data}
746 ['true_color', 'rw', 'bool'], // Use true-color pixel data
747 ['colourMap', 'rw', 'arr'], // Colour map array (when not true-color)
748 ['scale', 'rw', 'float'], // Display area scale factor 0.0 - 1.0
749 ['viewport', 'rw', 'bool'], // Use viewport clipping
750 ['width', 'ro', 'int'], // Display area width
751 ['height', 'ro', 'int'], // Display area height
753 ['render_mode', 'ro', 'str'], // Canvas rendering mode (read-only)
755 ['prefer_js', 'rw', 'str'], // Prefer Javascript over canvas methods
756 ['cursor_uri', 'rw', 'raw'], // Can we render cursor using data URI
758 ['onFlush', 'rw', 'func'], // onFlush(): A flush request has finished
762 Display
.changeCursor = function (target
, pixels
, mask
, hotx
, hoty
, w0
, h0
, cmap
) {
766 h
= w
; // increase h to make it square
768 w
= h
; // increase w to make it square
773 // Push multi-byte little-endian values
774 cur
.push16le = function (num
) {
775 this.push(num
& 0xFF, (num
>> 8) & 0xFF);
777 cur
.push32le = function (num
) {
778 this.push(num
& 0xFF,
785 var RGBsz
= w
* h
* 4;
786 var XORsz
= Math
.ceil((w
* h
) / 8.0);
787 var ANDsz
= Math
.ceil((w
* h
) / 8.0);
789 cur
.push16le(0); // 0: Reserved
790 cur
.push16le(2); // 2: .CUR type
791 cur
.push16le(1); // 4: Number of images, 1 for non-animated ico
793 // Cursor #1 header (ICONDIRENTRY)
794 cur
.push(w
); // 6: width
795 cur
.push(h
); // 7: height
796 cur
.push(0); // 8: colors, 0 -> true-color
797 cur
.push(0); // 9: reserved
798 cur
.push16le(hotx
); // 10: hotspot x coordinate
799 cur
.push16le(hoty
); // 12: hotspot y coordinate
800 cur
.push32le(IHDRsz
+ RGBsz
+ XORsz
+ ANDsz
);
801 // 14: cursor data byte size
802 cur
.push32le(22); // 18: offset of cursor data in the file
804 // Cursor #1 InfoHeader (ICONIMAGE/BITMAPINFO)
805 cur
.push32le(IHDRsz
); // 22: InfoHeader size
806 cur
.push32le(w
); // 26: Cursor width
807 cur
.push32le(h
* 2); // 30: XOR+AND height
808 cur
.push16le(1); // 34: number of planes
809 cur
.push16le(32); // 36: bits per pixel
810 cur
.push32le(0); // 38: Type of compression
812 cur
.push32le(XORsz
+ ANDsz
);
814 cur
.push32le(0); // 46: reserved
815 cur
.push32le(0); // 50: reserved
816 cur
.push32le(0); // 54: reserved
817 cur
.push32le(0); // 58: reserved
819 // 62: color data (RGBQUAD icColors[])
821 for (y
= h
- 1; y
>= 0; y
--) {
822 for (x
= 0; x
< w
; x
++) {
823 if (x
>= w0
|| y
>= h0
) {
825 cur
.push(0); // green
827 cur
.push(0); // alpha
829 var idx
= y
* Math
.ceil(w0
/ 8) + Math
.floor(x
/ 8);
830 var alpha
= (mask
[idx
] << (x
% 8)) & 0x80 ? 255 : 0;
833 var rgb
= cmap
[pixels
[idx
]];
834 cur
.push(rgb
[2]); // blue
835 cur
.push(rgb
[1]); // green
836 cur
.push(rgb
[0]); // red
837 cur
.push(alpha
); // alpha
839 idx
= ((w0
* y
) + x
) * 4;
840 cur
.push(pixels
[idx
]); // blue
841 cur
.push(pixels
[idx
+ 1]); // green
842 cur
.push(pixels
[idx
+ 2]); // red
843 cur
.push(alpha
); // alpha
849 // XOR/bitmask data (BYTE icXOR[])
850 // (ignored, just needs to be the right size)
851 for (y
= 0; y
< h
; y
++) {
852 for (x
= 0; x
< Math
.ceil(w
/ 8); x
++) {
857 // AND/bitmask data (BYTE icAND[])
858 // (ignored, just needs to be the right size)
859 for (y
= 0; y
< h
; y
++) {
860 for (x
= 0; x
< Math
.ceil(w
/ 8); x
++) {
865 var url
= 'data:image/x-icon;base64,' + Base64
.encode(cur
);
866 target
.style
.cursor
= 'url(' + url
+ ')' + hotx
+ ' ' + hoty
+ ', default';