Switch to using Promises

This commit is contained in:
Niklas von Hertzen 2014-02-03 19:42:42 +02:00
parent 9beae48cf0
commit 467ff87482
6 changed files with 299 additions and 73 deletions

View File

@ -13,7 +13,17 @@ window.html2canvas = function(nodeList, options) {
window.html2canvas.logging = true;
window.html2canvas.start = Date.now();
}
createWindowClone(document, window.innerWidth, window.innerHeight).then(function(container) {
return renderDocument(document, options, window.innerWidth, window.innerHeight).then(function(canvas) {
if (typeof(options.onrendered) === "function") {
log("options.onrendered is deprecated, html2canvas returns a Promise containing the canvas");
options.onrendered(canvas);
}
return canvas;
});
};
function renderDocument(document, options, width, height) {
return createWindowClone(document, width, height).then(function(container) {
log("Document cloned");
var clonedWindow = container.contentWindow;
//var element = (nodeList === undefined) ? document.body : nodeList[0];
@ -21,14 +31,16 @@ window.html2canvas = function(nodeList, options) {
var support = new Support();
var imageLoader = new ImageLoader(options, support);
var bounds = NodeParser.prototype.getBounds(node);
var width = options.type === "view" ? Math.min(bounds.width, window.innerWidth) : documentWidth();
var height = options.type === "view" ? Math.min(bounds.height, window.innerHeight) : documentHeight();
var width = options.type === "view" ? Math.min(bounds.width, width) : documentWidth();
var height = options.type === "view" ? Math.min(bounds.height, height) : documentHeight();
var renderer = new CanvasRenderer(width, height, imageLoader);
var parser = new NodeParser(node, renderer, support, imageLoader, options);
window.console.log(parser);
return parser.ready.then(function() {
container.parentNode.removeChild(container);
return renderer.canvas;
});
};
});
}
function documentWidth () {
return Math.max(
@ -46,11 +58,15 @@ function documentHeight () {
);
}
function smallImage() {
return "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
}
function createWindowClone(ownerDocument, width, height) {
var documentElement = ownerDocument.documentElement.cloneNode(true),
container = ownerDocument.createElement("iframe");
container.style.display = "hidden";
container.style.visibility = "hidden";
container.style.position = "absolute";
container.width = width;
container.height = height;
@ -84,12 +100,88 @@ function createWindowClone(ownerDocument, width, height) {
div.className = "html2canvas-ready-test";
documentClone.body.appendChild(div);
var style = documentClone.createElement("style");
style.innerHTML = "body div.html2canvas-ready-test { background-image:url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7); }";
style.innerHTML = "body div.html2canvas-ready-test { background-image:url(" + smallImage() + "); }";
documentClone.body.appendChild(style);
window.setTimeout(loadedTimer, 1000);
});
}
function Font(family, size) {
var container = document.createElement('div'),
img = document.createElement('img'),
span = document.createElement('span'),
sampleText = 'Hidden Text',
baseline,
middle;
container.style.visibility = "hidden";
container.style.fontFamily = family;
container.style.fontSize = size;
container.style.margin = 0;
container.style.padding = 0;
document.body.appendChild(container);
img.src = smallImage();
img.width = 1;
img.height = 1;
img.style.margin = 0;
img.style.padding = 0;
img.style.verticalAlign = "baseline";
span.style.fontFamily = family;
span.style.fontSize = size;
span.style.margin = 0;
span.style.padding = 0;
span.appendChild(document.createTextNode(sampleText));
container.appendChild(span);
container.appendChild(img);
baseline = (img.offsetTop - span.offsetTop) + 1;
container.removeChild(span);
container.appendChild(document.createTextNode(sampleText));
container.style.lineHeight = "normal";
img.style.verticalAlign = "super";
middle = (img.offsetTop-container.offsetTop) + 1;
document.body.removeChild(container);
this.baseline = baseline;
this.lineWidth = 1;
this.middle = middle;
}
function FontMetrics() {
this.data = {};
}
FontMetrics.prototype.getMetrics = function(family, size) {
if (this.data[family + "-" + size] === undefined) {
this.data[family + "-" + size] = new Font(family, size);
}
return this.data[family + "-" + size];
};
function GradientContainer(imageData) {
this.src = imageData.value;
this.colorStops = [];
this.type = null;
this.x0 = 0.5;
this.y0 = 0.5;
this.x1 = 0.5;
this.y1 = 0.5;
this.promise = Promise.resolve(true);
}
GradientContainer.prototype.TYPES = {
LINEAR: 1,
RADIAL: 2
};
function ImageContainer(src, cors) {
this.src = src;
this.image = new Image();
@ -121,7 +213,7 @@ ImageLoader.prototype.findImages = function(nodes) {
};
ImageLoader.prototype.findBackgroundImage = function(images, container) {
container.parseBackgroundImages().filter(this.isImageBackground).map(this.getBackgroundUrl).forEach(this.addImage(images, this.loadImage), this);
container.parseBackgroundImages().filter(this.hasImageBackground).forEach(this.addImage(images, this.loadImage), this);
return images;
};
@ -129,20 +221,18 @@ ImageLoader.prototype.addImage = function(images, callback) {
return function(newImage) {
if (!this.imageExists(images, newImage)) {
images.splice(0, 0, callback.apply(this, arguments));
log('Added image #' + (images.length), newImage.substring(0, 100));
log('Added image #' + (images.length), newImage);
}
};
};
ImageLoader.prototype.getBackgroundUrl = function(imageData) {
return imageData.args[0];
ImageLoader.prototype.hasImageBackground = function(imageData) {
return imageData.method !== "none";
};
ImageLoader.prototype.isImageBackground = function(imageData) {
return imageData.method === "url";
};
ImageLoader.prototype.loadImage = function(src) {
ImageLoader.prototype.loadImage = function(imageData) {
if (imageData.method === "url") {
var src = imageData.args[0];
if (src.match(/data:image\/.*;base64,/i)) {
return new ImageContainer(src.replace(/url\(['"]{0,}|['"]{0,}\)$/ig, ''), false);
} else if (this.isSameOrigin(src) || this.options.allowTaint === true) {
@ -154,6 +244,11 @@ ImageLoader.prototype.loadImage = function(src) {
} else {
return new DummyImageContainer(src);
}
} else if (imageData.method === "linear-gradient") {
return new LinearGradientContainer(imageData);
} else if (imageData.method === "gradient") {
return new WebkitGradientContainer(imageData);
}
};
ImageLoader.prototype.imageExists = function(images, src) {
@ -203,6 +298,40 @@ function src(container) {
return container.node.src;
}
function LinearGradientContainer(imageData) {
GradientContainer.apply(this, arguments);
this.type = this.TYPES.LINEAR;
imageData.args[0].split(" ").forEach(function(position) {
switch(position) {
case "left":
this.x0 = 0;
this.x1 = 1;
break;
case "top":
this.y0 = 0;
this.y1 = 1;
break;
case "right":
this.x0 = 1;
this.x1 = 0;
break;
case "bottom":
this.y0 = 1;
this.y1 = 0;
break;
}
}, this);
imageData.args.slice(1).forEach(function(colorStop) {
// console.log(colorStop, colorStop.match(this.stepRegExp));
}, this);
}
LinearGradientContainer.prototype = Object.create(GradientContainer.prototype);
LinearGradientContainer.prototype.stepRegExp = /((?:rgb|rgba)\(\d{1,3},\s\d{1,3},\s\d{1,3}(?:,\s[0-9\.]+)?\))\s*(\d{1,3})?(%|px)?/;
function log() {
if (window.html2canvas.logging && window.console && window.console.log) {
window.console.log.apply(window.console, [(Date.now() - window.html2canvas.start) + "ms", "html2canvas:"].concat([].slice.call(arguments, 0)));
@ -427,6 +556,28 @@ NodeContainer.prototype.parseBackgroundRepeat = function(index) {
return this.cssList("backgroundRepeat", index)[0];
};
NodeContainer.prototype.parseTextShadows = function() {
var textShadow = this.css("textShadow");
var results = [];
if (textShadow && textShadow !== 'none') {
var shadows = textShadow.match(this.TEXT_SHADOW_PROPERTY);
for (var i = 0; shadows && (i < shadows.length); i++) {
var s = shadows[i].match(this.TEXT_SHADOW_VALUES);
results.push({
color: s[0],
offsetX: s[1] ? s[1].replace('px', '') : 0,
offsetY: s[2] ? s[2].replace('px', '') : 0,
blur: s[3] ? s[3].replace('px', '') : 0
});
}
}
return results;
};
NodeContainer.prototype.TEXT_SHADOW_PROPERTY = /((rgba|rgb)\([^\)]+\)(\s-?\d+px){0,})/g;
NodeContainer.prototype.TEXT_SHADOW_VALUES = /(-?\d+px)|(#.+)|(rgb\(.+\))|(rgba\(.+\))/g;
function isPercentage(value) {
return value.toString().indexOf("%") !== -1;
}
@ -443,17 +594,17 @@ function NodeParser(element, renderer, support, imageLoader, options) {
this.nodes = [parent].concat(this.getChildren(parent)).filter(function(container) {
return container.visible = container.isElementVisible();
});
this.fontMetrics = new FontMetrics();
log("Fetched nodes");
this.images = imageLoader.fetch(this.nodes.filter(isElement));
log("Creating stacking contexts");
this.createStackingContexts();
log("Sorting stacking contexts");
this.sortStackingContexts(this.stack);
this.images.ready.then(bind(function() {
this.ready = this.images.ready.then(bind(function() {
log("Images loaded, starting parsing");
this.parse(this.stack);
log("Finished rendering");
options.onrendered(renderer.canvas);
}, this));
}
@ -505,11 +656,13 @@ NodeParser.prototype.getBounds = function(node) {
if (node.getBoundingClientRect) {
var clientRect = node.getBoundingClientRect();
var isBody = node.nodeName === "BODY";
var width = isBody ? node.scrollWidth : node.offsetWidth;
return {
top: clientRect.top,
bottom: clientRect.bottom || (clientRect.top + clientRect.height),
right: clientRect.left + width,
left: clientRect.left,
width: isBody ? node.scrollWidth : node.offsetWidth,
width: width,
height: isBody ? node.scrollHeight : node.offsetHeight
};
}
@ -519,8 +672,8 @@ NodeParser.prototype.getBounds = function(node) {
NodeParser.prototype.parseTextBounds = function(container) {
return function(text, index, textList) {
if (container.parent.css("textDecoration") !== "none" || text.trim().length !== 0) {
var offset = textList.slice(0, index).join("").length;
if (this.support.rangeBounds) {
var offset = textList.slice(0, index).join("").length;
return this.getRangeBounds(container.node, offset, text.length);
} else if (container.node && typeof(container.node.data) === "string") {
var replacementNode = container.node.splitText(text.length);
@ -528,7 +681,10 @@ NodeParser.prototype.parseTextBounds = function(container) {
container.node = replacementNode;
return bounds;
}
} else if (!this.support.rangeBounds) {
container.node = container.node.splitText(text.length);
}
return {};
};
};
@ -621,21 +777,41 @@ NodeParser.prototype.paintText = function(container) {
var weight = container.parent.fontWeight();
var size = container.parent.css('fontSize');
var family = container.parent.css('fontFamily');
var shadows = container.parent.parseTextShadows();
this.renderer.font(container.parent.css('color'), container.parent.css('fontStyle'), container.parent.css('fontVariant'), weight, size, family);
if (shadows.length) {
// TODO: support multiple text shadows
this.renderer.fontShadow(shadows[0].color, shadows[0].offsetX, shadows[0].offsetY, shadows[0].blur);
} else {
this.renderer.clearShadow();
}
textList.map(this.parseTextBounds(container), this).forEach(function(bounds, index) {
if (bounds) {
this.renderer.text(textList[index], bounds.left, bounds.bottom);
// renderTextDecoration(ctx, textDecoration, bounds, metrics, color);
this.renderTextDecoration(container.parent, bounds, this.fontMetrics.getMetrics(family, size));
}
/* var bounds = getTextBounds(state, text, textDecoration, (index < textList.length - 1), stack.transform.matrix);
if (bounds) {
drawText(text, bounds.left, bounds.bottom, ctx);
renderTextDecoration(ctx, textDecoration, bounds, metrics, color);
} */
}, this);
};
NodeParser.prototype.renderTextDecoration = function(container, bounds, metrics) {
switch(container.css("textDecoration").split(" ")[0]) {
case "underline":
// Draws a line at the baseline of the font
// TODO As some browsers display the line as more than 1px if the font-size is big, need to take that into account both in position and size
this.renderer.rectangle(bounds.left, Math.round(bounds.top + metrics.baseline + metrics.lineWidth), bounds.width, 1, container.css("color"));
break;
case "overline":
this.renderer.rectangle(bounds.left, Math.round(bounds.top), bounds.width, 1, container.css("color"));
break;
case "line-through":
// TODO try and find exact position for line-through
this.renderer.rectangle(bounds.left, Math.ceil(bounds.top + metrics.middle + metrics.lineWidth), bounds.width, 1, container.css("color"));
break;
}
};
NodeParser.prototype.parseBorders = function(container) {
var nodeBounds = container.bounds;
var radius = getBorderRadiusData(container);
@ -1031,13 +1207,26 @@ Renderer.prototype.renderBorder = function(data) {
Renderer.prototype.renderBackgroundImage = function(container, bounds) {
var backgroundImages = container.parseBackgroundImages();
backgroundImages.reverse().forEach(function(backgroundImage, index, arr) {
if (backgroundImage.method === "url") {
switch(backgroundImage.method) {
case "url":
var image = this.images.get(backgroundImage.args[0]);
if (image) {
this.renderBackgroundRepeating(container, bounds, image, arr.length - (index+1));
} else {
log("Error loading background-image", backgroundImage.args[0]);
}
break;
case "linear-gradient":
case "gradient":
var gradientImage = this.images.get(backgroundImage.value);
if (gradientImage) {
this.renderBackgroundGradient(gradientImage, bounds);
} else {
log("Error loading background-image", backgroundImage.args[0]);
}
break;
default:
log("Unknown background-image type", backgroundImage.args[0]);
}
}, this);
};
@ -1076,6 +1265,7 @@ function CanvasRenderer(width, height) {
this.canvas.height = height;
this.ctx = this.canvas.getContext("2d");
this.ctx.textBaseline = "bottom";
this.variables = {};
log("Initialized CanvasRenderer");
}
@ -1119,10 +1309,29 @@ CanvasRenderer.prototype.font = function(color, style, variant, weight, size, fa
this.setFillStyle(color).font = [style, variant, weight, size, family].join(" ");
};
CanvasRenderer.prototype.fontShadow = function(color, offsetX, offsetY, blur) {
this.setVariable("shadowColor", color)
.setVariable("shadowOffsetY", offsetX)
.setVariable("shadowOffsetX", offsetY)
.setVariable("shadowBlur", blur);
};
CanvasRenderer.prototype.clearShadow = function() {
this.setVariable("shadowColor", "rgba(0,0,0,0)");
};
CanvasRenderer.prototype.setOpacity = function(opacity) {
this.ctx.globalAlpha = opacity;
};
CanvasRenderer.prototype.setVariable = function(property, value) {
if (this.variables[property] !== value) {
this.variables[property] = this.ctx[property] = value;
}
return this;
};
CanvasRenderer.prototype.text = function(text, left, bottom) {
this.ctx.fillText(text, left, bottom);
};
@ -1147,6 +1356,13 @@ CanvasRenderer.prototype.renderBackgroundRepeat = function(imageContainer, backg
this.ctx.translate(-offsetX, -offsetY);
};
CanvasRenderer.prototype.renderBackgroundGradient = function(gradientImage, bounds) {
if (gradientImage instanceof LinearGradientContainer) {
var gradient = this.ctx.createLinearGradient(bounds.left, bounds.top, bounds.right, bounds.bottom);
//console.log(gradientImage, bounds, gradient);
}
};
CanvasRenderer.prototype.resizeImage = function(imageContainer, size) {
var image = imageContainer.image;
if(image.width === size.width && image.height === size.height) {
@ -1240,4 +1456,11 @@ function capitalize(m, p1, p2) {
}
}
function WebkitGradientContainer(imageData) {
GradientContainer.apply(this, arguments);
this.type = (imageData.args[0] === "linear") ? this.TYPES.LINEAR : this.TYPES.RADIAL;
}
WebkitGradientContainer.prototype = Object.create(GradientContainer.prototype);
})(window,document);

File diff suppressed because one or more lines are too long

View File

@ -33,17 +33,12 @@ As each CSS property needs to be manually built to be supported, there are a num
### Usage ###
To render an `element` with html2canvas, simply call:
` html2canvas(element, options);`
` html2canvas(element[, options]);`
To access the created canvas, provide the `onrendered` event in the options which returns the canvas element as the first argument, as such:
The function returns a [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) containing the `<canvas>` element. Simply add a promise fullfillment handler to the promise using `then`:
html2canvas(document.body, {
onrendered: function(canvas) {
/* canvas is the actual canvas element,
to append it to the page call for example
html2canvas(document.body).then(function(canvas) {
document.body.appendChild(canvas);
*/
}
});
### Building ###

View File

@ -4,7 +4,17 @@ window.html2canvas = function(nodeList, options) {
window.html2canvas.logging = true;
window.html2canvas.start = Date.now();
}
createWindowClone(document, window.innerWidth, window.innerHeight).then(function(container) {
return renderDocument(document, options, window.innerWidth, window.innerHeight).then(function(canvas) {
if (typeof(options.onrendered) === "function") {
log("options.onrendered is deprecated, html2canvas returns a Promise containing the canvas");
options.onrendered(canvas);
}
return canvas;
});
};
function renderDocument(document, options, width, height) {
return createWindowClone(document, width, height).then(function(container) {
log("Document cloned");
var clonedWindow = container.contentWindow;
//var element = (nodeList === undefined) ? document.body : nodeList[0];
@ -12,14 +22,16 @@ window.html2canvas = function(nodeList, options) {
var support = new Support();
var imageLoader = new ImageLoader(options, support);
var bounds = NodeParser.prototype.getBounds(node);
var width = options.type === "view" ? Math.min(bounds.width, window.innerWidth) : documentWidth();
var height = options.type === "view" ? Math.min(bounds.height, window.innerHeight) : documentHeight();
var width = options.type === "view" ? Math.min(bounds.width, width) : documentWidth();
var height = options.type === "view" ? Math.min(bounds.height, height) : documentHeight();
var renderer = new CanvasRenderer(width, height, imageLoader);
var parser = new NodeParser(node, renderer, support, imageLoader, options);
window.console.log(parser);
return parser.ready.then(function() {
container.parentNode.removeChild(container);
return renderer.canvas;
});
};
});
}
function documentWidth () {
return Math.max(
@ -45,7 +57,7 @@ function createWindowClone(ownerDocument, width, height) {
var documentElement = ownerDocument.documentElement.cloneNode(true),
container = ownerDocument.createElement("iframe");
container.style.display = "hidden";
container.style.visibility = "hidden";
container.style.position = "absolute";
container.width = width;
container.height = height;
@ -81,6 +93,6 @@ function createWindowClone(ownerDocument, width, height) {
var style = documentClone.createElement("style");
style.innerHTML = "body div.html2canvas-ready-test { background-image:url(" + smallImage() + "); }";
documentClone.body.appendChild(style);
window.setTimeout(loadedTimer, 10);
window.setTimeout(loadedTimer, 1000);
});
}

View File

@ -17,11 +17,10 @@ function NodeParser(element, renderer, support, imageLoader, options) {
this.createStackingContexts();
log("Sorting stacking contexts");
this.sortStackingContexts(this.stack);
this.images.ready.then(bind(function() {
this.ready = this.images.ready.then(bind(function() {
log("Images loaded, starting parsing");
this.parse(this.stack);
log("Finished rendering");
options.onrendered(renderer.canvas);
}, this));
}

View File

@ -4,21 +4,20 @@
(function( $ ){
$.fn.html2canvas = function(options) {
if (options && options.profile && window.console && window.console.profile && window.location.search !== "?selenium2") {
console.profile();
window.console.profile();
}
var date = new Date(),
html2obj,
$message = null,
timeoutTimer = false,
timer = date.getTime();
options = options || {};
options.onrendered = options.onrendered || function( canvas ) {
html2canvas(this, options).then(function(canvas) {
var $canvas = $(canvas),
finishTime = new Date();
if (options && options.profile && window.console && window.console.profileEnd) {
console.profileEnd();
window.console.profileEnd();
}
$canvas.addClass("html2canvas")
.css({
@ -43,12 +42,10 @@
} catch(e) {
if ($canvas[0].nodeName.toLowerCase() === "canvas") {
// TODO, maybe add a bit less offensive way to present this, but still something that can easily be noticed
alert("Canvas is tainted, unable to read data");
window.alert("Canvas is tainted, unable to read data");
}
}
};
html2obj = html2canvas(this, options);
});
function throwMessage(msg,duration){
window.clearTimeout(timeoutTimer);