]> git.proxmox.com Git - mirror_novnc.git/blob - include/display.js
Always generate square cursor images in changeCursor (fix portrait mode issue).
[mirror_novnc.git] / include / display.js
1 /*
2 * noVNC: HTML5 VNC client
3 * Copyright (C) 2012 Joel Martin
4 * Licensed under MPL 2.0 (see LICENSE.txt)
5 *
6 * See README.md for usage and integration instructions.
7 */
8
9 /*jslint browser: true, white: false, bitwise: false */
10 /*global Util, Base64, changeCursor */
11
12 function Display(defaults) {
13 "use strict";
14
15 var that = {}, // Public API methods
16 conf = {}, // Configuration attributes
17
18 // Private Display namespace variables
19 c_ctx = null,
20 c_forceCanvas = false,
21
22 // Queued drawing actions for in-order rendering
23 renderQ = [],
24
25 // Predefine function variables (jslint)
26 imageDataGet, rgbImageData, bgrxImageData, cmapImageData,
27 setFillColor, rescale, scan_renderQ,
28
29 // The full frame buffer (logical canvas) size
30 fb_width = 0,
31 fb_height = 0,
32 // The visible "physical canvas" viewport
33 viewport = {'x': 0, 'y': 0, 'w' : 0, 'h' : 0 },
34 cleanRect = {'x1': 0, 'y1': 0, 'x2': -1, 'y2': -1},
35
36 c_prevStyle = "",
37 tile = null,
38 tile16x16 = null,
39 tile_x = 0,
40 tile_y = 0;
41
42
43 // Configuration attributes
44 Util.conf_defaults(conf, that, defaults, [
45 ['target', 'wo', 'dom', null, 'Canvas element for rendering'],
46 ['context', 'ro', 'raw', null, 'Canvas 2D context for rendering (read-only)'],
47 ['logo', 'rw', 'raw', null, 'Logo to display when cleared: {"width": width, "height": height, "data": data}'],
48 ['true_color', 'rw', 'bool', true, 'Use true-color pixel data'],
49 ['colourMap', 'rw', 'arr', [], 'Colour map array (when not true-color)'],
50 ['scale', 'rw', 'float', 1.0, 'Display area scale factor 0.0 - 1.0'],
51 ['viewport', 'rw', 'bool', false, 'Use a viewport set with viewportChange()'],
52 ['width', 'rw', 'int', null, 'Display area width'],
53 ['height', 'rw', 'int', null, 'Display area height'],
54
55 ['render_mode', 'ro', 'str', '', 'Canvas rendering mode (read-only)'],
56
57 ['prefer_js', 'rw', 'str', null, 'Prefer Javascript over canvas methods'],
58 ['cursor_uri', 'rw', 'raw', null, 'Can we render cursor using data URI']
59 ]);
60
61 // Override some specific getters/setters
62 that.get_context = function () { return c_ctx; };
63
64 that.set_scale = function(scale) { rescale(scale); };
65
66 that.set_width = function (val) { that.resize(val, fb_height); };
67 that.get_width = function() { return fb_width; };
68
69 that.set_height = function (val) { that.resize(fb_width, val); };
70 that.get_height = function() { return fb_height; };
71
72
73
74 //
75 // Private functions
76 //
77
78 // Create the public API interface
79 function constructor() {
80 Util.Debug(">> Display.constructor");
81
82 var c, func, i, curDat, curSave,
83 has_imageData = false, UE = Util.Engine;
84
85 if (! conf.target) { throw("target must be set"); }
86
87 if (typeof conf.target === 'string') {
88 throw("target must be a DOM element");
89 }
90
91 c = conf.target;
92
93 if (! c.getContext) { throw("no getContext method"); }
94
95 if (! c_ctx) { c_ctx = c.getContext('2d'); }
96
97 Util.Debug("User Agent: " + navigator.userAgent);
98 if (UE.gecko) { Util.Debug("Browser: gecko " + UE.gecko); }
99 if (UE.webkit) { Util.Debug("Browser: webkit " + UE.webkit); }
100 if (UE.trident) { Util.Debug("Browser: trident " + UE.trident); }
101 if (UE.presto) { Util.Debug("Browser: presto " + UE.presto); }
102
103 that.clear();
104
105 // Check canvas features
106 if ('createImageData' in c_ctx) {
107 conf.render_mode = "canvas rendering";
108 } else {
109 throw("Canvas does not support createImageData");
110 }
111 if (conf.prefer_js === null) {
112 Util.Info("Prefering javascript operations");
113 conf.prefer_js = true;
114 }
115
116 // Initialize cached tile imageData
117 tile16x16 = c_ctx.createImageData(16, 16);
118
119 /*
120 * Determine browser support for setting the cursor via data URI
121 * scheme
122 */
123 curDat = [];
124 for (i=0; i < 8 * 8 * 4; i += 1) {
125 curDat.push(255);
126 }
127 try {
128 curSave = c.style.cursor;
129 changeCursor(conf.target, curDat, curDat, 2, 2, 8, 8);
130 if (c.style.cursor) {
131 if (conf.cursor_uri === null) {
132 conf.cursor_uri = true;
133 }
134 Util.Info("Data URI scheme cursor supported");
135 } else {
136 if (conf.cursor_uri === null) {
137 conf.cursor_uri = false;
138 }
139 Util.Warn("Data URI scheme cursor not supported");
140 }
141 c.style.cursor = curSave;
142 } catch (exc2) {
143 Util.Error("Data URI scheme cursor test exception: " + exc2);
144 conf.cursor_uri = false;
145 }
146
147 Util.Debug("<< Display.constructor");
148 return that ;
149 }
150
151 rescale = function(factor) {
152 var c, tp, x, y,
153 properties = ['transform', 'WebkitTransform', 'MozTransform', null];
154 c = conf.target;
155 tp = properties.shift();
156 while (tp) {
157 if (typeof c.style[tp] !== 'undefined') {
158 break;
159 }
160 tp = properties.shift();
161 }
162
163 if (tp === null) {
164 Util.Debug("No scaling support");
165 return;
166 }
167
168
169 if (typeof(factor) === "undefined") {
170 factor = conf.scale;
171 } else if (factor > 1.0) {
172 factor = 1.0;
173 } else if (factor < 0.1) {
174 factor = 0.1;
175 }
176
177 if (conf.scale === factor) {
178 //Util.Debug("Display already scaled to '" + factor + "'");
179 return;
180 }
181
182 conf.scale = factor;
183 x = c.width - c.width * factor;
184 y = c.height - c.height * factor;
185 c.style[tp] = "scale(" + conf.scale + ") translate(-" + x + "px, -" + y + "px)";
186 };
187
188 setFillColor = function(color) {
189 var bgr, newStyle;
190 if (conf.true_color) {
191 bgr = color;
192 } else {
193 bgr = conf.colourMap[color[0]];
194 }
195 newStyle = "rgb(" + bgr[2] + "," + bgr[1] + "," + bgr[0] + ")";
196 if (newStyle !== c_prevStyle) {
197 c_ctx.fillStyle = newStyle;
198 c_prevStyle = newStyle;
199 }
200 };
201
202
203 //
204 // Public API interface functions
205 //
206
207 // Shift and/or resize the visible viewport
208 that.viewportChange = function(deltaX, deltaY, width, height) {
209 var c = conf.target, v = viewport, cr = cleanRect,
210 saveImg = null, saveStyle, x1, y1, vx2, vy2, w, h;
211
212 if (!conf.viewport) {
213 Util.Debug("Setting viewport to full display region");
214 deltaX = -v.w; // Clamped later if out of bounds
215 deltaY = -v.h; // Clamped later if out of bounds
216 width = fb_width;
217 height = fb_height;
218 }
219
220 if (typeof(deltaX) === "undefined") { deltaX = 0; }
221 if (typeof(deltaY) === "undefined") { deltaY = 0; }
222 if (typeof(width) === "undefined") { width = v.w; }
223 if (typeof(height) === "undefined") { height = v.h; }
224
225 // Size change
226
227 if (width > fb_width) { width = fb_width; }
228 if (height > fb_height) { height = fb_height; }
229
230 if ((v.w !== width) || (v.h !== height)) {
231 // Change width
232 if ((width < v.w) && (cr.x2 > v.x + width -1)) {
233 cr.x2 = v.x + width - 1;
234 }
235 v.w = width;
236
237 // Change height
238 if ((height < v.h) && (cr.y2 > v.y + height -1)) {
239 cr.y2 = v.y + height - 1;
240 }
241 v.h = height;
242
243
244 if (v.w > 0 && v.h > 0 && c.width > 0 && c.height > 0) {
245 saveImg = c_ctx.getImageData(0, 0,
246 (c.width < v.w) ? c.width : v.w,
247 (c.height < v.h) ? c.height : v.h);
248 }
249
250 c.width = v.w;
251 c.height = v.h;
252
253 if (saveImg) {
254 c_ctx.putImageData(saveImg, 0, 0);
255 }
256 }
257
258 vx2 = v.x + v.w - 1;
259 vy2 = v.y + v.h - 1;
260
261
262 // Position change
263
264 if ((deltaX < 0) && ((v.x + deltaX) < 0)) {
265 deltaX = - v.x;
266 }
267 if ((vx2 + deltaX) >= fb_width) {
268 deltaX -= ((vx2 + deltaX) - fb_width + 1);
269 }
270
271 if ((v.y + deltaY) < 0) {
272 deltaY = - v.y;
273 }
274 if ((vy2 + deltaY) >= fb_height) {
275 deltaY -= ((vy2 + deltaY) - fb_height + 1);
276 }
277
278 if ((deltaX === 0) && (deltaY === 0)) {
279 //Util.Debug("skipping viewport change");
280 return;
281 }
282 Util.Debug("viewportChange deltaX: " + deltaX + ", deltaY: " + deltaY);
283
284 v.x += deltaX;
285 vx2 += deltaX;
286 v.y += deltaY;
287 vy2 += deltaY;
288
289 // Update the clean rectangle
290 if (v.x > cr.x1) {
291 cr.x1 = v.x;
292 }
293 if (vx2 < cr.x2) {
294 cr.x2 = vx2;
295 }
296 if (v.y > cr.y1) {
297 cr.y1 = v.y;
298 }
299 if (vy2 < cr.y2) {
300 cr.y2 = vy2;
301 }
302
303 if (deltaX < 0) {
304 // Shift viewport left, redraw left section
305 x1 = 0;
306 w = - deltaX;
307 } else {
308 // Shift viewport right, redraw right section
309 x1 = v.w - deltaX;
310 w = deltaX;
311 }
312 if (deltaY < 0) {
313 // Shift viewport up, redraw top section
314 y1 = 0;
315 h = - deltaY;
316 } else {
317 // Shift viewport down, redraw bottom section
318 y1 = v.h - deltaY;
319 h = deltaY;
320 }
321
322 // Copy the valid part of the viewport to the shifted location
323 saveStyle = c_ctx.fillStyle;
324 c_ctx.fillStyle = "rgb(255,255,255)";
325 if (deltaX !== 0) {
326 //that.copyImage(0, 0, -deltaX, 0, v.w, v.h);
327 //that.fillRect(x1, 0, w, v.h, [255,255,255]);
328 c_ctx.drawImage(c, 0, 0, v.w, v.h, -deltaX, 0, v.w, v.h);
329 c_ctx.fillRect(x1, 0, w, v.h);
330 }
331 if (deltaY !== 0) {
332 //that.copyImage(0, 0, 0, -deltaY, v.w, v.h);
333 //that.fillRect(0, y1, v.w, h, [255,255,255]);
334 c_ctx.drawImage(c, 0, 0, v.w, v.h, 0, -deltaY, v.w, v.h);
335 c_ctx.fillRect(0, y1, v.w, h);
336 }
337 c_ctx.fillStyle = saveStyle;
338 };
339
340
341 // Return a map of clean and dirty areas of the viewport and reset the
342 // tracking of clean and dirty areas.
343 //
344 // Returns: {'cleanBox': {'x': x, 'y': y, 'w': w, 'h': h},
345 // 'dirtyBoxes': [{'x': x, 'y': y, 'w': w, 'h': h}, ...]}
346 that.getCleanDirtyReset = function() {
347 var v = viewport, c = cleanRect, cleanBox, dirtyBoxes = [],
348 vx2 = v.x + v.w - 1, vy2 = v.y + v.h - 1;
349
350
351 // Copy the cleanRect
352 cleanBox = {'x': c.x1, 'y': c.y1,
353 'w': c.x2 - c.x1 + 1, 'h': c.y2 - c.y1 + 1};
354
355 if ((c.x1 >= c.x2) || (c.y1 >= c.y2)) {
356 // Whole viewport is dirty
357 dirtyBoxes.push({'x': v.x, 'y': v.y, 'w': v.w, 'h': v.h});
358 } else {
359 // Redraw dirty regions
360 if (v.x < c.x1) {
361 // left side dirty region
362 dirtyBoxes.push({'x': v.x, 'y': v.y,
363 'w': c.x1 - v.x + 1, 'h': v.h});
364 }
365 if (vx2 > c.x2) {
366 // right side dirty region
367 dirtyBoxes.push({'x': c.x2 + 1, 'y': v.y,
368 'w': vx2 - c.x2, 'h': v.h});
369 }
370 if (v.y < c.y1) {
371 // top/middle dirty region
372 dirtyBoxes.push({'x': c.x1, 'y': v.y,
373 'w': c.x2 - c.x1 + 1, 'h': c.y1 - v.y});
374 }
375 if (vy2 > c.y2) {
376 // bottom/middle dirty region
377 dirtyBoxes.push({'x': c.x1, 'y': c.y2 + 1,
378 'w': c.x2 - c.x1 + 1, 'h': vy2 - c.y2});
379 }
380 }
381
382 // Reset the cleanRect to the whole viewport
383 cleanRect = {'x1': v.x, 'y1': v.y,
384 'x2': v.x + v.w - 1, 'y2': v.y + v.h - 1};
385
386 return {'cleanBox': cleanBox, 'dirtyBoxes': dirtyBoxes};
387 };
388
389 // Translate viewport coordinates to absolute coordinates
390 that.absX = function(x) {
391 return x + viewport.x;
392 };
393 that.absY = function(y) {
394 return y + viewport.y;
395 };
396
397
398 that.resize = function(width, height) {
399 c_prevStyle = "";
400
401 fb_width = width;
402 fb_height = height;
403
404 rescale(conf.scale);
405 that.viewportChange();
406 };
407
408 that.clear = function() {
409
410 if (conf.logo) {
411 that.resize(conf.logo.width, conf.logo.height);
412 that.blitStringImage(conf.logo.data, 0, 0);
413 } else {
414 that.resize(640, 20);
415 c_ctx.clearRect(0, 0, viewport.w, viewport.h);
416 }
417
418 renderQ = [];
419
420 // No benefit over default ("source-over") in Chrome and firefox
421 //c_ctx.globalCompositeOperation = "copy";
422 };
423
424 that.fillRect = function(x, y, width, height, color) {
425 setFillColor(color);
426 c_ctx.fillRect(x - viewport.x, y - viewport.y, width, height);
427 };
428
429 that.copyImage = function(old_x, old_y, new_x, new_y, w, h) {
430 var x1 = old_x - viewport.x, y1 = old_y - viewport.y,
431 x2 = new_x - viewport.x, y2 = new_y - viewport.y;
432 c_ctx.drawImage(conf.target, x1, y1, w, h, x2, y2, w, h);
433 };
434
435
436 // Start updating a tile
437 that.startTile = function(x, y, width, height, color) {
438 var data, bgr, red, green, blue, i;
439 tile_x = x;
440 tile_y = y;
441 if ((width === 16) && (height === 16)) {
442 tile = tile16x16;
443 } else {
444 tile = c_ctx.createImageData(width, height);
445 }
446 data = tile.data;
447 if (conf.prefer_js) {
448 if (conf.true_color) {
449 bgr = color;
450 } else {
451 bgr = conf.colourMap[color[0]];
452 }
453 red = bgr[2];
454 green = bgr[1];
455 blue = bgr[0];
456 for (i = 0; i < (width * height * 4); i+=4) {
457 data[i ] = red;
458 data[i + 1] = green;
459 data[i + 2] = blue;
460 data[i + 3] = 255;
461 }
462 } else {
463 that.fillRect(x, y, width, height, color);
464 }
465 };
466
467 // Update sub-rectangle of the current tile
468 that.subTile = function(x, y, w, h, color) {
469 var data, p, bgr, red, green, blue, width, j, i, xend, yend;
470 if (conf.prefer_js) {
471 data = tile.data;
472 width = tile.width;
473 if (conf.true_color) {
474 bgr = color;
475 } else {
476 bgr = conf.colourMap[color[0]];
477 }
478 red = bgr[2];
479 green = bgr[1];
480 blue = bgr[0];
481 xend = x + w;
482 yend = y + h;
483 for (j = y; j < yend; j += 1) {
484 for (i = x; i < xend; i += 1) {
485 p = (i + (j * width) ) * 4;
486 data[p ] = red;
487 data[p + 1] = green;
488 data[p + 2] = blue;
489 data[p + 3] = 255;
490 }
491 }
492 } else {
493 that.fillRect(tile_x + x, tile_y + y, w, h, color);
494 }
495 };
496
497 // Draw the current tile to the screen
498 that.finishTile = function() {
499 if (conf.prefer_js) {
500 c_ctx.putImageData(tile, tile_x - viewport.x, tile_y - viewport.y);
501 }
502 // else: No-op, if not prefer_js then already done by setSubTile
503 };
504
505 rgbImageData = function(x, y, vx, vy, width, height, arr, offset) {
506 var img, i, j, data;
507 /*
508 if ((x - v.x >= v.w) || (y - v.y >= v.h) ||
509 (x - v.x + width < 0) || (y - v.y + height < 0)) {
510 // Skipping because outside of viewport
511 return;
512 }
513 */
514 img = c_ctx.createImageData(width, height);
515 data = img.data;
516 for (i=0, j=offset; i < (width * height * 4); i=i+4, j=j+3) {
517 data[i ] = arr[j ];
518 data[i + 1] = arr[j + 1];
519 data[i + 2] = arr[j + 2];
520 data[i + 3] = 255; // Set Alpha
521 }
522 c_ctx.putImageData(img, x - vx, y - vy);
523 };
524
525 bgrxImageData = function(x, y, vx, vy, width, height, arr, offset) {
526 var img, i, j, data;
527 /*
528 if ((x - v.x >= v.w) || (y - v.y >= v.h) ||
529 (x - v.x + width < 0) || (y - v.y + height < 0)) {
530 // Skipping because outside of viewport
531 return;
532 }
533 */
534 img = c_ctx.createImageData(width, height);
535 data = img.data;
536 for (i=0, j=offset; i < (width * height * 4); i=i+4, j=j+4) {
537 data[i ] = arr[j + 2];
538 data[i + 1] = arr[j + 1];
539 data[i + 2] = arr[j ];
540 data[i + 3] = 255; // Set Alpha
541 }
542 c_ctx.putImageData(img, x - vx, y - vy);
543 };
544
545 cmapImageData = function(x, y, vx, vy, width, height, arr, offset) {
546 var img, i, j, data, bgr, cmap;
547 img = c_ctx.createImageData(width, height);
548 data = img.data;
549 cmap = conf.colourMap;
550 for (i=0, j=offset; i < (width * height * 4); i+=4, j+=1) {
551 bgr = cmap[arr[j]];
552 data[i ] = bgr[2];
553 data[i + 1] = bgr[1];
554 data[i + 2] = bgr[0];
555 data[i + 3] = 255; // Set Alpha
556 }
557 c_ctx.putImageData(img, x - vx, y - vy);
558 };
559
560 that.blitImage = function(x, y, width, height, arr, offset) {
561 if (conf.true_color) {
562 bgrxImageData(x, y, viewport.x, viewport.y, width, height, arr, offset);
563 } else {
564 cmapImageData(x, y, viewport.x, viewport.y, width, height, arr, offset);
565 }
566 };
567
568 that.blitRgbImage = function(x, y, width, height, arr, offset) {
569 if (conf.true_color) {
570 rgbImageData(x, y, viewport.x, viewport.y, width, height, arr, offset);
571 } else {
572 // prolly wrong...
573 cmapImageData(x, y, viewport.x, viewport.y, width, height, arr, offset);
574 }
575 };
576
577 that.blitStringImage = function(str, x, y) {
578 var img = new Image();
579 img.onload = function () {
580 c_ctx.drawImage(img, x - viewport.x, y - viewport.y);
581 };
582 img.src = str;
583 };
584
585 // Wrap ctx.drawImage but relative to viewport
586 that.drawImage = function(img, x, y) {
587 c_ctx.drawImage(img, x - viewport.x, y - viewport.y);
588 };
589
590 that.renderQ_push = function(action) {
591 renderQ.push(action);
592 if (renderQ.length === 1) {
593 // If this can be rendered immediately it will be, otherwise
594 // the scanner will start polling the queue (every
595 // requestAnimationFrame interval)
596 scan_renderQ();
597 }
598 };
599
600 scan_renderQ = function() {
601 var a, ready = true;
602 while (ready && renderQ.length > 0) {
603 a = renderQ[0];
604 switch (a.type) {
605 case 'copy':
606 that.copyImage(a.old_x, a.old_y, a.x, a.y, a.width, a.height);
607 break;
608 case 'fill':
609 that.fillRect(a.x, a.y, a.width, a.height, a.color);
610 break;
611 case 'blit':
612 that.blitImage(a.x, a.y, a.width, a.height, a.data, 0);
613 break;
614 case 'blitRgb':
615 that.blitRgbImage(a.x, a.y, a.width, a.height, a.data, 0);
616 break;
617 case 'img':
618 if (a.img.complete) {
619 that.drawImage(a.img, a.x, a.y);
620 } else {
621 // We need to wait for this image to 'load'
622 // to keep things in-order
623 ready = false;
624 }
625 break;
626 }
627 if (ready) {
628 a = renderQ.shift();
629 }
630 }
631 if (renderQ.length > 0) {
632 requestAnimFrame(scan_renderQ);
633 }
634 };
635
636
637 that.changeCursor = function(pixels, mask, hotx, hoty, w, h) {
638 if (conf.cursor_uri === false) {
639 Util.Warn("changeCursor called but no cursor data URI support");
640 return;
641 }
642
643 if (conf.true_color) {
644 changeCursor(conf.target, pixels, mask, hotx, hoty, w, h);
645 } else {
646 changeCursor(conf.target, pixels, mask, hotx, hoty, w, h, conf.colourMap);
647 }
648 };
649
650 that.defaultCursor = function() {
651 conf.target.style.cursor = "default";
652 };
653
654 return constructor(); // Return the public API interface
655
656 } // End of Display()
657
658
659 /* Set CSS cursor property using data URI encoded cursor file */
660 function changeCursor(target, pixels, mask, hotx, hoty, w0, h0, cmap) {
661 "use strict";
662 var cur = [], rgb, IHDRsz, RGBsz, ANDsz, XORsz, url, idx, alpha, x, y;
663 //Util.Debug(">> changeCursor, x: " + hotx + ", y: " + hoty + ", w0: " + w0 + ", h0: " + h0);
664
665 var w = w0;
666 var h = h0;
667 if (h < w)
668 h = w; // increase h to make it square
669 else
670 w = h; // increace w to make it square
671
672 // Push multi-byte little-endian values
673 cur.push16le = function (num) {
674 this.push((num ) & 0xFF,
675 (num >> 8) & 0xFF );
676 };
677 cur.push32le = function (num) {
678 this.push((num ) & 0xFF,
679 (num >> 8) & 0xFF,
680 (num >> 16) & 0xFF,
681 (num >> 24) & 0xFF );
682 };
683
684 IHDRsz = 40;
685 RGBsz = w * h * 4;
686 XORsz = Math.ceil( (w * h) / 8.0 );
687 ANDsz = Math.ceil( (w * h) / 8.0 );
688
689 // Main header
690 cur.push16le(0); // 0: Reserved
691 cur.push16le(2); // 2: .CUR type
692 cur.push16le(1); // 4: Number of images, 1 for non-animated ico
693
694 // Cursor #1 header (ICONDIRENTRY)
695 cur.push(w); // 6: width
696 cur.push(h); // 7: height
697 cur.push(0); // 8: colors, 0 -> true-color
698 cur.push(0); // 9: reserved
699 cur.push16le(hotx); // 10: hotspot x coordinate
700 cur.push16le(hoty); // 12: hotspot y coordinate
701 cur.push32le(IHDRsz + RGBsz + XORsz + ANDsz);
702 // 14: cursor data byte size
703 cur.push32le(22); // 18: offset of cursor data in the file
704
705
706 // Cursor #1 InfoHeader (ICONIMAGE/BITMAPINFO)
707 cur.push32le(IHDRsz); // 22: Infoheader size
708 cur.push32le(w); // 26: Cursor width
709 cur.push32le(h*2); // 30: XOR+AND height
710 cur.push16le(1); // 34: number of planes
711 cur.push16le(32); // 36: bits per pixel
712 cur.push32le(0); // 38: Type of compression
713
714 cur.push32le(XORsz + ANDsz); // 43: Size of Image
715 // Gimp leaves this as 0
716
717 cur.push32le(0); // 46: reserved
718 cur.push32le(0); // 50: reserved
719 cur.push32le(0); // 54: reserved
720 cur.push32le(0); // 58: reserved
721
722 // 62: color data (RGBQUAD icColors[])
723 for (y = h-1; y >= 0; y -= 1) {
724 for (x = 0; x < w; x += 1) {
725 if (x >= w0 || y >= h0) {
726 cur.push(0); // blue
727 cur.push(0); // green
728 cur.push(0); // red
729 cur.push(0); // alpha
730 } else {
731 idx = y * Math.ceil(w0 / 8) + Math.floor(x/8);
732 alpha = (mask[idx] << (x % 8)) & 0x80 ? 255 : 0;
733 if (cmap) {
734 idx = (w0 * y) + x;
735 rgb = cmap[pixels[idx]];
736 cur.push(rgb[2]); // blue
737 cur.push(rgb[1]); // green
738 cur.push(rgb[0]); // red
739 cur.push(alpha); // alpha
740 } else {
741 idx = ((w0 * y) + x) * 4;
742 cur.push(pixels[idx + 2]); // blue
743 cur.push(pixels[idx + 1]); // green
744 cur.push(pixels[idx ]); // red
745 cur.push(alpha); // alpha
746 }
747 }
748 }
749 }
750
751 // XOR/bitmask data (BYTE icXOR[])
752 // (ignored, just needs to be right size)
753 for (y = 0; y < h; y += 1) {
754 for (x = 0; x < Math.ceil(w / 8); x += 1) {
755 cur.push(0x00);
756 }
757 }
758
759 // AND/bitmask data (BYTE icAND[])
760 // (ignored, just needs to be right size)
761 for (y = 0; y < h; y += 1) {
762 for (x = 0; x < Math.ceil(w / 8); x += 1) {
763 cur.push(0x00);
764 }
765 }
766
767 url = "data:image/x-icon;base64," + Base64.encode(cur);
768 target.style.cursor = "url(" + url + ") " + hotx + " " + hoty + ", default";
769 //Util.Debug("<< changeCursor, cur.length: " + cur.length);
770 }