Correctly handle overflow content

This commit is contained in:
Niklas von Hertzen 2014-09-27 16:54:53 +03:00
parent b1f948bb60
commit 1d8a316f13
6 changed files with 361 additions and 287 deletions

65
dist/html2canvas.js vendored
View File

@ -1067,6 +1067,9 @@ function NodeContainer(node, parent) {
this.parent = parent; this.parent = parent;
this.stack = null; this.stack = null;
this.bounds = null; this.bounds = null;
this.borders = null;
this.clip = [];
this.backgroundClip = [];
this.offsetBounds = null; this.offsetBounds = null;
this.visible = null; this.visible = null;
this.computedStyles = null; this.computedStyles = null;
@ -1331,8 +1334,7 @@ function parseBackgrounds(backgroundImage) {
case '"': case '"':
if(!quote) { if(!quote) {
quote = c; quote = c;
} } else if(quote === c) {
else if(quote === c) {
quote = null; quote = null;
} }
break; break;
@ -1450,12 +1452,14 @@ function NodeParser(element, renderer, support, imageLoader, options) {
this.fontMetrics = new FontMetrics(); this.fontMetrics = new FontMetrics();
log("Fetched nodes"); log("Fetched nodes");
this.images = imageLoader.fetch(this.nodes.filter(isElement)); this.images = imageLoader.fetch(this.nodes.filter(isElement));
this.ready = this.images.ready.then(bind(function() {
log("Images loaded, starting parsing");
log("Calculate overflow clips");
this.calculateOverflowClips();
log("Creating stacking contexts"); log("Creating stacking contexts");
this.createStackingContexts(); this.createStackingContexts();
log("Sorting stacking contexts"); log("Sorting stacking contexts");
this.sortStackingContexts(this.stack); this.sortStackingContexts(this.stack);
this.ready = this.images.ready.then(bind(function() {
log("Images loaded, starting parsing");
this.parse(this.stack); this.parse(this.stack);
log("Render queue created with " + this.renderQueue.length + " items"); log("Render queue created with " + this.renderQueue.length + " items");
return new Promise(bind(function(resolve) { return new Promise(bind(function(resolve) {
@ -1472,6 +1476,24 @@ function NodeParser(element, renderer, support, imageLoader, options) {
}, this)); }, this));
} }
NodeParser.prototype.calculateOverflowClips = function() {
this.nodes.forEach(function(container) {
if (isElement(container)) {
container.borders = this.parseBorders(container);
var clip = (container.css('overflow') === "hidden") ? [container.borders.clip] : [];
container.clip = hasParentClip(container) ? container.parent.clip.concat(clip) : clip;
container.backgroundClip = (container.css('overflow') !== "hidden") ? container.clip.concat([container.borders.clip]) : container.clip;
} else if (isTextNode(container)) {
container.clip = hasParentClip(container) ? container.parent.clip : [];
}
container.bounds = null;
}, this);
};
function hasParentClip(container) {
return container.parent && container.parent.clip.length;
}
NodeParser.prototype.asyncRenderer = function(queue, resolve, asyncTimer) { NodeParser.prototype.asyncRenderer = function(queue, resolve, asyncTimer) {
asyncTimer = asyncTimer || Date.now(); asyncTimer = asyncTimer || Date.now();
this.paint(queue[this.renderIndex++]); this.paint(queue[this.renderIndex++]);
@ -1561,6 +1583,10 @@ NodeParser.prototype.getChildren = function(parentContainer) {
NodeParser.prototype.newStackingContext = function(container, hasOwnStacking) { NodeParser.prototype.newStackingContext = function(container, hasOwnStacking) {
var stack = new StackingContext(hasOwnStacking, container.cssFloat('opacity'), container.node, container.parent); var stack = new StackingContext(hasOwnStacking, container.cssFloat('opacity'), container.node, container.parent);
stack.visible = container.visible; stack.visible = container.visible;
stack.borders = container.borders;
stack.bounds = container.bounds;
stack.clip = container.clip;
stack.backgroundClip = container.backgroundClip;
var parentStack = hasOwnStacking ? stack.getParentStack(this) : stack.parent.stack; var parentStack = hasOwnStacking ? stack.getParentStack(this) : stack.parent.stack;
parentStack.contexts.push(stack); parentStack.contexts.push(stack);
container.stack = stack; container.stack = stack;
@ -1675,17 +1701,19 @@ NodeParser.prototype.paintNode = function(container) {
} }
} }
var bounds = container.parseBounds(); var bounds = container.parseBounds();
var borderData = this.parseBorders(container); this.renderer.clip(container.backgroundClip, function() {
this.renderer.clip(borderData.clip, function() { this.renderer.renderBackground(container, bounds, container.borders.borders.map(getWidth));
this.renderer.renderBackground(container, bounds, borderData.borders.map(getWidth));
}, this); }, this);
this.renderer.renderBorders(borderData.borders);
this.renderer.clip(container.clip, function() {
this.renderer.renderBorders(container.borders.borders);
switch (container.node.nodeName) { switch (container.node.nodeName) {
case "svg": case "svg":
case "IFRAME": case "IFRAME":
var imgContainer = this.images.get(container.node); var imgContainer = this.images.get(container.node);
if (imgContainer) { if (imgContainer) {
this.renderer.renderImage(container, bounds, borderData, imgContainer); this.renderer.renderImage(container, bounds, container.borders, imgContainer);
} else { } else {
log("Error loading <" + container.node.nodeName + ">", container.node); log("Error loading <" + container.node.nodeName + ">", container.node);
} }
@ -1693,7 +1721,7 @@ NodeParser.prototype.paintNode = function(container) {
case "IMG": case "IMG":
var imageContainer = this.images.get(container.node.src); var imageContainer = this.images.get(container.node.src);
if (imageContainer) { if (imageContainer) {
this.renderer.renderImage(container, bounds, borderData, imageContainer); this.renderer.renderImage(container, bounds, container.borders, imageContainer);
} else { } else {
log("Error loading <img>", container.node.src); log("Error loading <img>", container.node.src);
} }
@ -1704,6 +1732,7 @@ NodeParser.prototype.paintNode = function(container) {
this.paintFormValue(container); this.paintFormValue(container);
break; break;
} }
}, this);
}; };
NodeParser.prototype.paintFormValue = function(container) { NodeParser.prototype.paintFormValue = function(container) {
@ -1754,12 +1783,14 @@ NodeParser.prototype.paintText = function(container) {
this.renderer.clearShadow(); this.renderer.clearShadow();
} }
this.renderer.clip(container.parent.clip, function() {
textList.map(this.parseTextBounds(container), this).forEach(function(bounds, index) { textList.map(this.parseTextBounds(container), this).forEach(function(bounds, index) {
if (bounds) { if (bounds) {
this.renderer.text(textList[index], bounds.left, bounds.bottom); this.renderer.text(textList[index], bounds.left, bounds.bottom);
this.renderTextDecoration(container.parent, bounds, this.fontMetrics.getMetrics(family, size)); this.renderTextDecoration(container.parent, bounds, this.fontMetrics.getMetrics(family, size));
} }
}, this); }, this);
}, this);
}; };
NodeParser.prototype.renderTextDecoration = function(container, bounds, metrics) { NodeParser.prototype.renderTextDecoration = function(container, bounds, metrics) {
@ -1780,7 +1811,7 @@ NodeParser.prototype.renderTextDecoration = function(container, bounds, metrics)
}; };
NodeParser.prototype.parseBorders = function(container) { NodeParser.prototype.parseBorders = function(container) {
var nodeBounds = container.bounds; var nodeBounds = container.parseBounds();
var radius = getBorderRadiusData(container); var radius = getBorderRadiusData(container);
var borders = ["Top", "Right", "Bottom", "Left"].map(function(side) { var borders = ["Top", "Right", "Bottom", "Left"].map(function(side) {
return { return {
@ -2148,7 +2179,7 @@ function isWordBoundary(characterCode) {
} }
function hasUnicode(string) { function hasUnicode(string) {
return /[^\u0000-\u00ff]/.test(string); return (/[^\u0000-\u00ff]/).test(string);
} }
function Proxy(src, proxyUrl, document) { function Proxy(src, proxyUrl, document) {
@ -2605,9 +2636,11 @@ CanvasRenderer.prototype.drawImage = function(imageContainer, sx, sy, sw, sh, dx
} }
}; };
CanvasRenderer.prototype.clip = function(shape, callback, context) { CanvasRenderer.prototype.clip = function(shapes, callback, context) {
this.ctx.save(); this.ctx.save();
shapes.filter(hasEntries).forEach(function(shape) {
this.shape(shape).clip(); this.shape(shape).clip();
}, this);
callback.call(context); callback.call(context);
this.ctx.restore(); this.ctx.restore();
}; };
@ -2665,7 +2698,7 @@ CanvasRenderer.prototype.backgroundRepeatShape = function(imageContainer, backgr
["line", Math.round(left + width), Math.round(height + top)], ["line", Math.round(left + width), Math.round(height + top)],
["line", Math.round(left), Math.round(height + top)] ["line", Math.round(left), Math.round(height + top)]
]; ];
this.clip(shape, function() { this.clip([shape], function() {
this.renderBackgroundRepeat(imageContainer, backgroundPosition, size, bounds, borderData[3], borderData[0]); this.renderBackgroundRepeat(imageContainer, backgroundPosition, size, bounds, borderData[3], borderData[0]);
}, this); }, this);
}; };
@ -2706,4 +2739,8 @@ CanvasRenderer.prototype.resizeImage = function(imageContainer, size) {
return canvas; return canvas;
}; };
function hasEntries(array) {
return array.length > 0;
}
})(window, document); })(window, document);

File diff suppressed because one or more lines are too long

View File

@ -3,6 +3,9 @@ function NodeContainer(node, parent) {
this.parent = parent; this.parent = parent;
this.stack = null; this.stack = null;
this.bounds = null; this.bounds = null;
this.borders = null;
this.clip = [];
this.backgroundClip = [];
this.offsetBounds = null; this.offsetBounds = null;
this.visible = null; this.visible = null;
this.computedStyles = null; this.computedStyles = null;
@ -267,8 +270,7 @@ function parseBackgrounds(backgroundImage) {
case '"': case '"':
if(!quote) { if(!quote) {
quote = c; quote = c;
} } else if(quote === c) {
else if(quote === c) {
quote = null; quote = null;
} }
break; break;

View File

@ -20,12 +20,14 @@ function NodeParser(element, renderer, support, imageLoader, options) {
this.fontMetrics = new FontMetrics(); this.fontMetrics = new FontMetrics();
log("Fetched nodes"); log("Fetched nodes");
this.images = imageLoader.fetch(this.nodes.filter(isElement)); this.images = imageLoader.fetch(this.nodes.filter(isElement));
this.ready = this.images.ready.then(bind(function() {
log("Images loaded, starting parsing");
log("Calculate overflow clips");
this.calculateOverflowClips();
log("Creating stacking contexts"); log("Creating stacking contexts");
this.createStackingContexts(); this.createStackingContexts();
log("Sorting stacking contexts"); log("Sorting stacking contexts");
this.sortStackingContexts(this.stack); this.sortStackingContexts(this.stack);
this.ready = this.images.ready.then(bind(function() {
log("Images loaded, starting parsing");
this.parse(this.stack); this.parse(this.stack);
log("Render queue created with " + this.renderQueue.length + " items"); log("Render queue created with " + this.renderQueue.length + " items");
return new Promise(bind(function(resolve) { return new Promise(bind(function(resolve) {
@ -42,6 +44,24 @@ function NodeParser(element, renderer, support, imageLoader, options) {
}, this)); }, this));
} }
NodeParser.prototype.calculateOverflowClips = function() {
this.nodes.forEach(function(container) {
if (isElement(container)) {
container.borders = this.parseBorders(container);
var clip = (container.css('overflow') === "hidden") ? [container.borders.clip] : [];
container.clip = hasParentClip(container) ? container.parent.clip.concat(clip) : clip;
container.backgroundClip = (container.css('overflow') !== "hidden") ? container.clip.concat([container.borders.clip]) : container.clip;
} else if (isTextNode(container)) {
container.clip = hasParentClip(container) ? container.parent.clip : [];
}
container.bounds = null;
}, this);
};
function hasParentClip(container) {
return container.parent && container.parent.clip.length;
}
NodeParser.prototype.asyncRenderer = function(queue, resolve, asyncTimer) { NodeParser.prototype.asyncRenderer = function(queue, resolve, asyncTimer) {
asyncTimer = asyncTimer || Date.now(); asyncTimer = asyncTimer || Date.now();
this.paint(queue[this.renderIndex++]); this.paint(queue[this.renderIndex++]);
@ -131,6 +151,10 @@ NodeParser.prototype.getChildren = function(parentContainer) {
NodeParser.prototype.newStackingContext = function(container, hasOwnStacking) { NodeParser.prototype.newStackingContext = function(container, hasOwnStacking) {
var stack = new StackingContext(hasOwnStacking, container.cssFloat('opacity'), container.node, container.parent); var stack = new StackingContext(hasOwnStacking, container.cssFloat('opacity'), container.node, container.parent);
stack.visible = container.visible; stack.visible = container.visible;
stack.borders = container.borders;
stack.bounds = container.bounds;
stack.clip = container.clip;
stack.backgroundClip = container.backgroundClip;
var parentStack = hasOwnStacking ? stack.getParentStack(this) : stack.parent.stack; var parentStack = hasOwnStacking ? stack.getParentStack(this) : stack.parent.stack;
parentStack.contexts.push(stack); parentStack.contexts.push(stack);
container.stack = stack; container.stack = stack;
@ -245,17 +269,19 @@ NodeParser.prototype.paintNode = function(container) {
} }
} }
var bounds = container.parseBounds(); var bounds = container.parseBounds();
var borderData = this.parseBorders(container); this.renderer.clip(container.backgroundClip, function() {
this.renderer.clip(borderData.clip, function() { this.renderer.renderBackground(container, bounds, container.borders.borders.map(getWidth));
this.renderer.renderBackground(container, bounds, borderData.borders.map(getWidth));
}, this); }, this);
this.renderer.renderBorders(borderData.borders);
this.renderer.clip(container.clip, function() {
this.renderer.renderBorders(container.borders.borders);
switch (container.node.nodeName) { switch (container.node.nodeName) {
case "svg": case "svg":
case "IFRAME": case "IFRAME":
var imgContainer = this.images.get(container.node); var imgContainer = this.images.get(container.node);
if (imgContainer) { if (imgContainer) {
this.renderer.renderImage(container, bounds, borderData, imgContainer); this.renderer.renderImage(container, bounds, container.borders, imgContainer);
} else { } else {
log("Error loading <" + container.node.nodeName + ">", container.node); log("Error loading <" + container.node.nodeName + ">", container.node);
} }
@ -263,7 +289,7 @@ NodeParser.prototype.paintNode = function(container) {
case "IMG": case "IMG":
var imageContainer = this.images.get(container.node.src); var imageContainer = this.images.get(container.node.src);
if (imageContainer) { if (imageContainer) {
this.renderer.renderImage(container, bounds, borderData, imageContainer); this.renderer.renderImage(container, bounds, container.borders, imageContainer);
} else { } else {
log("Error loading <img>", container.node.src); log("Error loading <img>", container.node.src);
} }
@ -274,6 +300,7 @@ NodeParser.prototype.paintNode = function(container) {
this.paintFormValue(container); this.paintFormValue(container);
break; break;
} }
}, this);
}; };
NodeParser.prototype.paintFormValue = function(container) { NodeParser.prototype.paintFormValue = function(container) {
@ -324,12 +351,14 @@ NodeParser.prototype.paintText = function(container) {
this.renderer.clearShadow(); this.renderer.clearShadow();
} }
this.renderer.clip(container.parent.clip, function() {
textList.map(this.parseTextBounds(container), this).forEach(function(bounds, index) { textList.map(this.parseTextBounds(container), this).forEach(function(bounds, index) {
if (bounds) { if (bounds) {
this.renderer.text(textList[index], bounds.left, bounds.bottom); this.renderer.text(textList[index], bounds.left, bounds.bottom);
this.renderTextDecoration(container.parent, bounds, this.fontMetrics.getMetrics(family, size)); this.renderTextDecoration(container.parent, bounds, this.fontMetrics.getMetrics(family, size));
} }
}, this); }, this);
}, this);
}; };
NodeParser.prototype.renderTextDecoration = function(container, bounds, metrics) { NodeParser.prototype.renderTextDecoration = function(container, bounds, metrics) {
@ -350,7 +379,7 @@ NodeParser.prototype.renderTextDecoration = function(container, bounds, metrics)
}; };
NodeParser.prototype.parseBorders = function(container) { NodeParser.prototype.parseBorders = function(container) {
var nodeBounds = container.bounds; var nodeBounds = container.parseBounds();
var radius = getBorderRadiusData(container); var radius = getBorderRadiusData(container);
var borders = ["Top", "Right", "Bottom", "Left"].map(function(side) { var borders = ["Top", "Right", "Bottom", "Left"].map(function(side) {
return { return {
@ -718,5 +747,5 @@ function isWordBoundary(characterCode) {
} }
function hasUnicode(string) { function hasUnicode(string) {
return /[^\u0000-\u00ff]/.test(string); return (/[^\u0000-\u00ff]/).test(string);
} }

View File

@ -47,9 +47,11 @@ CanvasRenderer.prototype.drawImage = function(imageContainer, sx, sy, sw, sh, dx
} }
}; };
CanvasRenderer.prototype.clip = function(shape, callback, context) { CanvasRenderer.prototype.clip = function(shapes, callback, context) {
this.ctx.save(); this.ctx.save();
shapes.filter(hasEntries).forEach(function(shape) {
this.shape(shape).clip(); this.shape(shape).clip();
}, this);
callback.call(context); callback.call(context);
this.ctx.restore(); this.ctx.restore();
}; };
@ -107,7 +109,7 @@ CanvasRenderer.prototype.backgroundRepeatShape = function(imageContainer, backgr
["line", Math.round(left + width), Math.round(height + top)], ["line", Math.round(left + width), Math.round(height + top)],
["line", Math.round(left), Math.round(height + top)] ["line", Math.round(left), Math.round(height + top)]
]; ];
this.clip(shape, function() { this.clip([shape], function() {
this.renderBackgroundRepeat(imageContainer, backgroundPosition, size, bounds, borderData[3], borderData[0]); this.renderBackgroundRepeat(imageContainer, backgroundPosition, size, bounds, borderData[3], borderData[0]);
}, this); }, this);
}; };
@ -147,3 +149,7 @@ CanvasRenderer.prototype.resizeImage = function(imageContainer, size) {
ctx.drawImage(image, 0, 0, image.width, image.height, 0, 0, size.width, size.height ); ctx.drawImage(image, 0, 0, image.width, image.height, 0, 0, size.width, size.height );
return canvas; return canvas;
}; };
function hasEntries(array) {
return array.length > 0;
}

View File

@ -37,7 +37,7 @@
<div style="overflow:hidden;"> <div style="overflow:hidden;">
Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s
with the release of <div style="border-width:10px 0 5px 0;background:green;">a</div>Letraset sheets containing Lorem Ipsum passages, <img src="image.jpg" /> and more recently with desktop publishing software like <b>Aldus PageMaker</b> including versions of Lorem Ipsum. with the release of <div style="border-width:10px 0 5px 0;background:green;">a</div>Letraset sheets containing Lorem Ipsum passages, <img src="../assets/image.jpg" /> and more recently with desktop publishing software like <b>Aldus PageMaker</b> including versions of Lorem Ipsum.
<div style="overflow:visible;position:relative;"><u>position:relative within a overflow:hidden element</u><br /> <br /> <div style="overflow:visible;position:relative;"><u>position:relative within a overflow:hidden element</u><br /> <br />