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