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