mirror of
https://github.com/novnc/noVNC.git
synced 2026-05-27 15:39:41 +00:00
Conflicts: include/display.js include/rfb.js This merges in the fix for https://github.com/kanaka/noVNC/issues/70 This changes noVNC to use the preferred color ordering that most VNC server prefer and that VMWare VNC requires. It's possible this may break some VNC servers out there in which case we might have to do something a bit more subtle such as having alternate render functions for little and big endian color ordering.
672 lines
19 KiB
JavaScript
672 lines
19 KiB
JavaScript
/*
|
|
* noVNC: HTML5 VNC client
|
|
* Copyright (C) 2011 Joel Martin
|
|
* Licensed under LGPL-3 (see LICENSE.txt)
|
|
*
|
|
* See README.md for usage and integration instructions.
|
|
*/
|
|
|
|
/*jslint browser: true, white: false, bitwise: false */
|
|
/*global Util, Base64, changeCursor */
|
|
|
|
function Display(defaults) {
|
|
"use strict";
|
|
|
|
var that = {}, // Public API methods
|
|
conf = {}, // Configuration attributes
|
|
|
|
// Private Display namespace variables
|
|
c_ctx = null,
|
|
c_forceCanvas = false,
|
|
|
|
// Predefine function variables (jslint)
|
|
imageDataGet, bgrxImageData, cmapImageData,
|
|
setFillColor, rescale,
|
|
|
|
// The full frame buffer (logical canvas) size
|
|
fb_width = 0,
|
|
fb_height = 0,
|
|
// The visible "physical canvas" viewport
|
|
viewport = {'x': 0, 'y': 0, 'w' : 0, 'h' : 0 },
|
|
cleanRect = {'x1': 0, 'y1': 0, 'x2': -1, 'y2': -1},
|
|
|
|
c_prevStyle = "",
|
|
tile = null,
|
|
tile16x16 = null,
|
|
tile_x = 0,
|
|
tile_y = 0;
|
|
|
|
|
|
// Configuration attributes
|
|
Util.conf_defaults(conf, that, defaults, [
|
|
['target', 'wo', 'dom', null, 'Canvas element for rendering'],
|
|
['context', 'ro', 'raw', null, 'Canvas 2D context for rendering (read-only)'],
|
|
['logo', 'rw', 'raw', null, 'Logo to display when cleared: {"width": width, "height": height, "data": data}'],
|
|
['true_color', 'rw', 'bool', true, 'Use true-color pixel data'],
|
|
['colourMap', 'rw', 'arr', [], 'Colour map array (when not true-color)'],
|
|
['scale', 'rw', 'float', 1.0, 'Display area scale factor 0.0 - 1.0'],
|
|
['viewport', 'rw', 'bool', false, 'Use a viewport set with viewportChange()'],
|
|
['width', 'rw', 'int', null, 'Display area width'],
|
|
['height', 'rw', 'int', null, 'Display area height'],
|
|
|
|
['render_mode', 'ro', 'str', '', 'Canvas rendering mode (read-only)'],
|
|
|
|
['prefer_js', 'rw', 'str', null, 'Prefer Javascript over canvas methods'],
|
|
['cursor_uri', 'rw', 'raw', null, 'Can we render cursor using data URI']
|
|
]);
|
|
|
|
// Override some specific getters/setters
|
|
that.get_context = function () { return c_ctx; };
|
|
|
|
that.set_scale = function(scale) { rescale(scale); };
|
|
|
|
that.set_width = function (val) { that.resize(val, fb_height); };
|
|
that.get_width = function() { return fb_width; };
|
|
|
|
that.set_height = function (val) { that.resize(fb_width, val); };
|
|
that.get_height = function() { return fb_height; };
|
|
|
|
|
|
|
|
//
|
|
// Private functions
|
|
//
|
|
|
|
// Create the public API interface
|
|
function constructor() {
|
|
Util.Debug(">> Display.constructor");
|
|
|
|
var c, func, i, curDat, curSave,
|
|
has_imageData = false, UE = Util.Engine;
|
|
|
|
if (! conf.target) { throw("target must be set"); }
|
|
|
|
if (typeof conf.target === 'string') {
|
|
throw("target must be a DOM element");
|
|
}
|
|
|
|
c = conf.target;
|
|
|
|
if (! c.getContext) { throw("no getContext method"); }
|
|
|
|
if (! c_ctx) { c_ctx = c.getContext('2d'); }
|
|
|
|
Util.Debug("User Agent: " + navigator.userAgent);
|
|
if (UE.gecko) { Util.Debug("Browser: gecko " + UE.gecko); }
|
|
if (UE.webkit) { Util.Debug("Browser: webkit " + UE.webkit); }
|
|
if (UE.trident) { Util.Debug("Browser: trident " + UE.trident); }
|
|
if (UE.presto) { Util.Debug("Browser: presto " + UE.presto); }
|
|
|
|
that.clear();
|
|
|
|
// Check canvas features
|
|
if ('createImageData' in c_ctx) {
|
|
conf.render_mode = "canvas rendering";
|
|
} else {
|
|
throw("Canvas does not support createImageData");
|
|
}
|
|
if (conf.prefer_js === null) {
|
|
Util.Info("Prefering javascript operations");
|
|
conf.prefer_js = true;
|
|
}
|
|
|
|
// Initialize cached tile imageData
|
|
tile16x16 = c_ctx.createImageData(16, 16);
|
|
|
|
/*
|
|
* Determine browser support for setting the cursor via data URI
|
|
* scheme
|
|
*/
|
|
curDat = [];
|
|
for (i=0; i < 8 * 8 * 4; i += 1) {
|
|
curDat.push(255);
|
|
}
|
|
try {
|
|
curSave = c.style.cursor;
|
|
changeCursor(conf.target, curDat, curDat, 2, 2, 8, 8);
|
|
if (c.style.cursor) {
|
|
if (conf.cursor_uri === null) {
|
|
conf.cursor_uri = true;
|
|
}
|
|
Util.Info("Data URI scheme cursor supported");
|
|
} else {
|
|
if (conf.cursor_uri === null) {
|
|
conf.cursor_uri = false;
|
|
}
|
|
Util.Warn("Data URI scheme cursor not supported");
|
|
}
|
|
c.style.cursor = curSave;
|
|
} catch (exc2) {
|
|
Util.Error("Data URI scheme cursor test exception: " + exc2);
|
|
conf.cursor_uri = false;
|
|
}
|
|
|
|
Util.Debug("<< Display.constructor");
|
|
return that ;
|
|
}
|
|
|
|
rescale = function(factor) {
|
|
var c, tp, x, y,
|
|
properties = ['transform', 'WebkitTransform', 'MozTransform', null];
|
|
c = conf.target;
|
|
tp = properties.shift();
|
|
while (tp) {
|
|
if (typeof c.style[tp] !== 'undefined') {
|
|
break;
|
|
}
|
|
tp = properties.shift();
|
|
}
|
|
|
|
if (tp === null) {
|
|
Util.Debug("No scaling support");
|
|
return;
|
|
}
|
|
|
|
|
|
if (typeof(factor) === "undefined") {
|
|
factor = conf.scale;
|
|
} else if (factor > 1.0) {
|
|
factor = 1.0;
|
|
} else if (factor < 0.1) {
|
|
factor = 0.1;
|
|
}
|
|
|
|
if (conf.scale === factor) {
|
|
//Util.Debug("Display already scaled to '" + factor + "'");
|
|
return;
|
|
}
|
|
|
|
conf.scale = factor;
|
|
x = c.width - c.width * factor;
|
|
y = c.height - c.height * factor;
|
|
c.style[tp] = "scale(" + conf.scale + ") translate(-" + x + "px, -" + y + "px)";
|
|
};
|
|
|
|
setFillColor = function(color) {
|
|
var bgr, newStyle;
|
|
if (conf.true_color) {
|
|
bgr = color;
|
|
} else {
|
|
bgr = conf.colourMap[color[0]];
|
|
}
|
|
newStyle = "rgb(" + bgr[2] + "," + bgr[1] + "," + bgr[0] + ")";
|
|
if (newStyle !== c_prevStyle) {
|
|
c_ctx.fillStyle = newStyle;
|
|
c_prevStyle = newStyle;
|
|
}
|
|
};
|
|
|
|
|
|
//
|
|
// Public API interface functions
|
|
//
|
|
|
|
// Shift and/or resize the visible viewport
|
|
that.viewportChange = function(deltaX, deltaY, width, height) {
|
|
var c = conf.target, v = viewport, cr = cleanRect,
|
|
saveImg = null, saveStyle, x1, y1, vx2, vy2, w, h;
|
|
|
|
if (!conf.viewport) {
|
|
Util.Debug("Setting viewport to full display region");
|
|
deltaX = -v.w; // Clamped later if out of bounds
|
|
deltaY = -v.h; // Clamped later if out of bounds
|
|
width = fb_width;
|
|
height = fb_height;
|
|
}
|
|
|
|
if (typeof(deltaX) === "undefined") { deltaX = 0; }
|
|
if (typeof(deltaY) === "undefined") { deltaY = 0; }
|
|
if (typeof(width) === "undefined") { width = v.w; }
|
|
if (typeof(height) === "undefined") { height = v.h; }
|
|
|
|
// Size change
|
|
|
|
if (width > fb_width) { width = fb_width; }
|
|
if (height > fb_height) { height = fb_height; }
|
|
|
|
if ((v.w !== width) || (v.h !== height)) {
|
|
// Change width
|
|
if ((width < v.w) && (cr.x2 > v.x + width -1)) {
|
|
cr.x2 = v.x + width - 1;
|
|
}
|
|
v.w = width;
|
|
|
|
// Change height
|
|
if ((height < v.h) && (cr.y2 > v.y + height -1)) {
|
|
cr.y2 = v.y + height - 1;
|
|
}
|
|
v.h = height;
|
|
|
|
|
|
if (v.w > 0 && v.h > 0 && c.width > 0 && c.height > 0) {
|
|
saveImg = c_ctx.getImageData(0, 0,
|
|
(c.width < v.w) ? c.width : v.w,
|
|
(c.height < v.h) ? c.height : v.h);
|
|
}
|
|
|
|
c.width = v.w;
|
|
c.height = v.h;
|
|
|
|
if (saveImg) {
|
|
c_ctx.putImageData(saveImg, 0, 0);
|
|
}
|
|
}
|
|
|
|
vx2 = v.x + v.w - 1;
|
|
vy2 = v.y + v.h - 1;
|
|
|
|
|
|
// Position change
|
|
|
|
if ((deltaX < 0) && ((v.x + deltaX) < 0)) {
|
|
deltaX = - v.x;
|
|
}
|
|
if ((vx2 + deltaX) >= fb_width) {
|
|
deltaX -= ((vx2 + deltaX) - fb_width + 1);
|
|
}
|
|
|
|
if ((v.y + deltaY) < 0) {
|
|
deltaY = - v.y;
|
|
}
|
|
if ((vy2 + deltaY) >= fb_height) {
|
|
deltaY -= ((vy2 + deltaY) - fb_height + 1);
|
|
}
|
|
|
|
if ((deltaX === 0) && (deltaY === 0)) {
|
|
//Util.Debug("skipping viewport change");
|
|
return;
|
|
}
|
|
Util.Debug("viewportChange deltaX: " + deltaX + ", deltaY: " + deltaY);
|
|
|
|
v.x += deltaX;
|
|
vx2 += deltaX;
|
|
v.y += deltaY;
|
|
vy2 += deltaY;
|
|
|
|
// Update the clean rectangle
|
|
if (v.x > cr.x1) {
|
|
cr.x1 = v.x;
|
|
}
|
|
if (vx2 < cr.x2) {
|
|
cr.x2 = vx2;
|
|
}
|
|
if (v.y > cr.y1) {
|
|
cr.y1 = v.y;
|
|
}
|
|
if (vy2 < cr.y2) {
|
|
cr.y2 = vy2;
|
|
}
|
|
|
|
if (deltaX < 0) {
|
|
// Shift viewport left, redraw left section
|
|
x1 = 0;
|
|
w = - deltaX;
|
|
} else {
|
|
// Shift viewport right, redraw right section
|
|
x1 = v.w - deltaX;
|
|
w = deltaX;
|
|
}
|
|
if (deltaY < 0) {
|
|
// Shift viewport up, redraw top section
|
|
y1 = 0;
|
|
h = - deltaY;
|
|
} else {
|
|
// Shift viewport down, redraw bottom section
|
|
y1 = v.h - deltaY;
|
|
h = deltaY;
|
|
}
|
|
|
|
// Copy the valid part of the viewport to the shifted location
|
|
saveStyle = c_ctx.fillStyle;
|
|
c_ctx.fillStyle = "rgb(255,255,255)";
|
|
if (deltaX !== 0) {
|
|
//that.copyImage(0, 0, -deltaX, 0, v.w, v.h);
|
|
//that.fillRect(x1, 0, w, v.h, [255,255,255]);
|
|
c_ctx.drawImage(c, 0, 0, v.w, v.h, -deltaX, 0, v.w, v.h);
|
|
c_ctx.fillRect(x1, 0, w, v.h);
|
|
}
|
|
if (deltaY !== 0) {
|
|
//that.copyImage(0, 0, 0, -deltaY, v.w, v.h);
|
|
//that.fillRect(0, y1, v.w, h, [255,255,255]);
|
|
c_ctx.drawImage(c, 0, 0, v.w, v.h, 0, -deltaY, v.w, v.h);
|
|
c_ctx.fillRect(0, y1, v.w, h);
|
|
}
|
|
c_ctx.fillStyle = saveStyle;
|
|
};
|
|
|
|
|
|
// Return a map of clean and dirty areas of the viewport and reset the
|
|
// tracking of clean and dirty areas.
|
|
//
|
|
// Returns: {'cleanBox': {'x': x, 'y': y, 'w': w, 'h': h},
|
|
// 'dirtyBoxes': [{'x': x, 'y': y, 'w': w, 'h': h}, ...]}
|
|
that.getCleanDirtyReset = function() {
|
|
var v = viewport, c = cleanRect, cleanBox, dirtyBoxes = [],
|
|
vx2 = v.x + v.w - 1, vy2 = v.y + v.h - 1;
|
|
|
|
|
|
// Copy the cleanRect
|
|
cleanBox = {'x': c.x1, 'y': c.y1,
|
|
'w': c.x2 - c.x1 + 1, 'h': c.y2 - c.y1 + 1};
|
|
|
|
if ((c.x1 >= c.x2) || (c.y1 >= c.y2)) {
|
|
// Whole viewport is dirty
|
|
dirtyBoxes.push({'x': v.x, 'y': v.y, 'w': v.w, 'h': v.h});
|
|
} else {
|
|
// Redraw dirty regions
|
|
if (v.x < c.x1) {
|
|
// left side dirty region
|
|
dirtyBoxes.push({'x': v.x, 'y': v.y,
|
|
'w': c.x1 - v.x + 1, 'h': v.h});
|
|
}
|
|
if (vx2 > c.x2) {
|
|
// right side dirty region
|
|
dirtyBoxes.push({'x': c.x2 + 1, 'y': v.y,
|
|
'w': vx2 - c.x2, 'h': v.h});
|
|
}
|
|
if (v.y < c.y1) {
|
|
// top/middle dirty region
|
|
dirtyBoxes.push({'x': c.x1, 'y': v.y,
|
|
'w': c.x2 - c.x1 + 1, 'h': c.y1 - v.y});
|
|
}
|
|
if (vy2 > c.y2) {
|
|
// bottom/middle dirty region
|
|
dirtyBoxes.push({'x': c.x1, 'y': c.y2 + 1,
|
|
'w': c.x2 - c.x1 + 1, 'h': vy2 - c.y2});
|
|
}
|
|
}
|
|
|
|
// Reset the cleanRect to the whole viewport
|
|
cleanRect = {'x1': v.x, 'y1': v.y,
|
|
'x2': v.x + v.w - 1, 'y2': v.y + v.h - 1};
|
|
|
|
return {'cleanBox': cleanBox, 'dirtyBoxes': dirtyBoxes};
|
|
};
|
|
|
|
// Translate viewport coordinates to absolute coordinates
|
|
that.absX = function(x) {
|
|
return x + viewport.x;
|
|
};
|
|
that.absY = function(y) {
|
|
return y + viewport.y;
|
|
};
|
|
|
|
|
|
that.resize = function(width, height) {
|
|
c_prevStyle = "";
|
|
|
|
fb_width = width;
|
|
fb_height = height;
|
|
|
|
rescale(conf.scale);
|
|
that.viewportChange();
|
|
};
|
|
|
|
that.clear = function() {
|
|
|
|
if (conf.logo) {
|
|
that.resize(conf.logo.width, conf.logo.height);
|
|
that.blitStringImage(conf.logo.data, 0, 0);
|
|
} else {
|
|
that.resize(640, 20);
|
|
c_ctx.clearRect(0, 0, viewport.w, viewport.h);
|
|
}
|
|
|
|
// No benefit over default ("source-over") in Chrome and firefox
|
|
//c_ctx.globalCompositeOperation = "copy";
|
|
};
|
|
|
|
that.fillRect = function(x, y, width, height, color) {
|
|
setFillColor(color);
|
|
c_ctx.fillRect(x - viewport.x, y - viewport.y, width, height);
|
|
};
|
|
|
|
that.copyImage = function(old_x, old_y, new_x, new_y, w, h) {
|
|
var x1 = old_x - viewport.x, y1 = old_y - viewport.y,
|
|
x2 = new_x - viewport.x, y2 = new_y - viewport.y;
|
|
c_ctx.drawImage(conf.target, x1, y1, w, h, x2, y2, w, h);
|
|
};
|
|
|
|
|
|
// Start updating a tile
|
|
that.startTile = function(x, y, width, height, color) {
|
|
var data, bgr, red, green, blue, i;
|
|
tile_x = x;
|
|
tile_y = y;
|
|
if ((width === 16) && (height === 16)) {
|
|
tile = tile16x16;
|
|
} else {
|
|
tile = c_ctx.createImageData(width, height);
|
|
}
|
|
data = tile.data;
|
|
if (conf.prefer_js) {
|
|
if (conf.true_color) {
|
|
bgr = color;
|
|
} else {
|
|
bgr = conf.colourMap[color[0]];
|
|
}
|
|
red = bgr[2];
|
|
green = bgr[1];
|
|
blue = bgr[0];
|
|
for (i = 0; i < (width * height * 4); i+=4) {
|
|
data[i ] = red;
|
|
data[i + 1] = green;
|
|
data[i + 2] = blue;
|
|
data[i + 3] = 255;
|
|
}
|
|
} else {
|
|
that.fillRect(x, y, width, height, color);
|
|
}
|
|
};
|
|
|
|
// Update sub-rectangle of the current tile
|
|
that.subTile = function(x, y, w, h, color) {
|
|
var data, p, bgr, red, green, blue, width, j, i, xend, yend;
|
|
if (conf.prefer_js) {
|
|
data = tile.data;
|
|
width = tile.width;
|
|
if (conf.true_color) {
|
|
bgr = color;
|
|
} else {
|
|
bgr = conf.colourMap[color[0]];
|
|
}
|
|
red = bgr[2];
|
|
green = bgr[1];
|
|
blue = bgr[0];
|
|
xend = x + w;
|
|
yend = y + h;
|
|
for (j = y; j < yend; j += 1) {
|
|
for (i = x; i < xend; i += 1) {
|
|
p = (i + (j * width) ) * 4;
|
|
data[p ] = red;
|
|
data[p + 1] = green;
|
|
data[p + 2] = blue;
|
|
data[p + 3] = 255;
|
|
}
|
|
}
|
|
} else {
|
|
that.fillRect(tile_x + x, tile_y + y, w, h, color);
|
|
}
|
|
};
|
|
|
|
// Draw the current tile to the screen
|
|
that.finishTile = function() {
|
|
if (conf.prefer_js) {
|
|
c_ctx.putImageData(tile, tile_x - viewport.x, tile_y - viewport.y);
|
|
}
|
|
// else: No-op, if not prefer_js then already done by setSubTile
|
|
};
|
|
|
|
bgrxImageData = function(x, y, width, height, arr, offset) {
|
|
var img, i, j, data, v = viewport;
|
|
/*
|
|
if ((x - v.x >= v.w) || (y - v.y >= v.h) ||
|
|
(x - v.x + width < 0) || (y - v.y + height < 0)) {
|
|
// Skipping because outside of viewport
|
|
return;
|
|
}
|
|
*/
|
|
img = c_ctx.createImageData(width, height);
|
|
data = img.data;
|
|
for (i=0, j=offset; i < (width * height * 4); i=i+4, j=j+4) {
|
|
data[i ] = arr[j + 2];
|
|
data[i + 1] = arr[j + 1];
|
|
data[i + 2] = arr[j ];
|
|
data[i + 3] = 255; // Set Alpha
|
|
}
|
|
c_ctx.putImageData(img, x - v.x, y - v.y);
|
|
};
|
|
|
|
cmapImageData = function(x, y, width, height, arr, offset) {
|
|
var img, i, j, data, bgr, cmap;
|
|
img = c_ctx.createImageData(width, height);
|
|
data = img.data;
|
|
cmap = conf.colourMap;
|
|
for (i=0, j=offset; i < (width * height * 4); i+=4, j+=1) {
|
|
bgr = cmap[arr[j]];
|
|
data[i ] = bgr[2];
|
|
data[i + 1] = bgr[1];
|
|
data[i + 2] = bgr[0];
|
|
data[i + 3] = 255; // Set Alpha
|
|
}
|
|
c_ctx.putImageData(img, x - viewport.x, y - viewport.y);
|
|
};
|
|
|
|
that.blitImage = function(x, y, width, height, arr, offset) {
|
|
if (conf.true_color) {
|
|
bgrxImageData(x, y, width, height, arr, offset);
|
|
} else {
|
|
cmapImageData(x, y, width, height, arr, offset);
|
|
}
|
|
};
|
|
|
|
that.blitStringImage = function(str, x, y) {
|
|
var img = new Image();
|
|
img.onload = function () {
|
|
c_ctx.drawImage(img, x - viewport.x, y - viewport.y);
|
|
};
|
|
img.src = str;
|
|
};
|
|
|
|
that.changeCursor = function(pixels, mask, hotx, hoty, w, h) {
|
|
if (conf.cursor_uri === false) {
|
|
Util.Warn("changeCursor called but no cursor data URI support");
|
|
return;
|
|
}
|
|
|
|
if (conf.true_color) {
|
|
changeCursor(conf.target, pixels, mask, hotx, hoty, w, h);
|
|
} else {
|
|
changeCursor(conf.target, pixels, mask, hotx, hoty, w, h, conf.colourMap);
|
|
}
|
|
};
|
|
|
|
that.defaultCursor = function() {
|
|
conf.target.style.cursor = "default";
|
|
};
|
|
|
|
return constructor(); // Return the public API interface
|
|
|
|
} // End of Display()
|
|
|
|
|
|
/* Set CSS cursor property using data URI encoded cursor file */
|
|
function changeCursor(target, pixels, mask, hotx, hoty, w, h, cmap) {
|
|
"use strict";
|
|
var cur = [], rgb, IHDRsz, RGBsz, ANDsz, XORsz, url, idx, alpha, x, y;
|
|
//Util.Debug(">> changeCursor, x: " + hotx + ", y: " + hoty + ", w: " + w + ", h: " + h);
|
|
|
|
// Push multi-byte little-endian values
|
|
cur.push16le = function (num) {
|
|
this.push((num ) & 0xFF,
|
|
(num >> 8) & 0xFF );
|
|
};
|
|
cur.push32le = function (num) {
|
|
this.push((num ) & 0xFF,
|
|
(num >> 8) & 0xFF,
|
|
(num >> 16) & 0xFF,
|
|
(num >> 24) & 0xFF );
|
|
};
|
|
|
|
IHDRsz = 40;
|
|
RGBsz = w * h * 4;
|
|
XORsz = Math.ceil( (w * h) / 8.0 );
|
|
ANDsz = Math.ceil( (w * h) / 8.0 );
|
|
|
|
// Main header
|
|
cur.push16le(0); // 0: Reserved
|
|
cur.push16le(2); // 2: .CUR type
|
|
cur.push16le(1); // 4: Number of images, 1 for non-animated ico
|
|
|
|
// Cursor #1 header (ICONDIRENTRY)
|
|
cur.push(w); // 6: width
|
|
cur.push(h); // 7: height
|
|
cur.push(0); // 8: colors, 0 -> true-color
|
|
cur.push(0); // 9: reserved
|
|
cur.push16le(hotx); // 10: hotspot x coordinate
|
|
cur.push16le(hoty); // 12: hotspot y coordinate
|
|
cur.push32le(IHDRsz + RGBsz + XORsz + ANDsz);
|
|
// 14: cursor data byte size
|
|
cur.push32le(22); // 18: offset of cursor data in the file
|
|
|
|
|
|
// Cursor #1 InfoHeader (ICONIMAGE/BITMAPINFO)
|
|
cur.push32le(IHDRsz); // 22: Infoheader size
|
|
cur.push32le(w); // 26: Cursor width
|
|
cur.push32le(h*2); // 30: XOR+AND height
|
|
cur.push16le(1); // 34: number of planes
|
|
cur.push16le(32); // 36: bits per pixel
|
|
cur.push32le(0); // 38: Type of compression
|
|
|
|
cur.push32le(XORsz + ANDsz); // 43: Size of Image
|
|
// Gimp leaves this as 0
|
|
|
|
cur.push32le(0); // 46: reserved
|
|
cur.push32le(0); // 50: reserved
|
|
cur.push32le(0); // 54: reserved
|
|
cur.push32le(0); // 58: reserved
|
|
|
|
// 62: color data (RGBQUAD icColors[])
|
|
for (y = h-1; y >= 0; y -= 1) {
|
|
for (x = 0; x < w; x += 1) {
|
|
idx = y * Math.ceil(w / 8) + Math.floor(x/8);
|
|
alpha = (mask[idx] << (x % 8)) & 0x80 ? 255 : 0;
|
|
|
|
if (cmap) {
|
|
idx = (w * y) + x;
|
|
rgb = cmap[pixels[idx]];
|
|
cur.push(rgb[2]); // blue
|
|
cur.push(rgb[1]); // green
|
|
cur.push(rgb[0]); // red
|
|
cur.push(alpha); // alpha
|
|
} else {
|
|
idx = ((w * y) + x) * 4;
|
|
cur.push(pixels[idx + 2]); // blue
|
|
cur.push(pixels[idx + 1]); // green
|
|
cur.push(pixels[idx ]); // red
|
|
cur.push(alpha); // alpha
|
|
}
|
|
}
|
|
}
|
|
|
|
// XOR/bitmask data (BYTE icXOR[])
|
|
// (ignored, just needs to be right size)
|
|
for (y = 0; y < h; y += 1) {
|
|
for (x = 0; x < Math.ceil(w / 8); x += 1) {
|
|
cur.push(0x00);
|
|
}
|
|
}
|
|
|
|
// AND/bitmask data (BYTE icAND[])
|
|
// (ignored, just needs to be right size)
|
|
for (y = 0; y < h; y += 1) {
|
|
for (x = 0; x < Math.ceil(w / 8); x += 1) {
|
|
cur.push(0x00);
|
|
}
|
|
}
|
|
|
|
url = "data:image/x-icon;base64," + Base64.encode(cur);
|
|
target.style.cursor = "url(" + url + ") " + hotx + " " + hoty + ", default";
|
|
//Util.Debug("<< changeCursor, cur.length: " + cur.length);
|
|
}
|