Fix text rendering for IE and Opera

This commit is contained in:
Niklas von Hertzen 2014-03-02 16:00:59 +02:00
parent 18d95d669b
commit 15ca3381eb
10 changed files with 225 additions and 125 deletions

View File

@ -13,6 +13,6 @@
"globals": {
"jQuery": true
},
"predef": ["NodeParser", "NodeContainer", "StackingContext", "TextContainer", "ImageLoader", "CanvasRenderer", "Renderer", "Support", "bind", "Promise",
"predef": ["NodeParser", "NodeContainer", "StackingContext", "TextContainer", "ImageLoader", "CanvasRenderer", "Renderer", "Support", "bind", "Promise", "getBounds", "offsetBounds",
"ImageContainer", "ProxyImageContainer", "DummyImageContainer", "Font", "FontMetrics", "GradientContainer", "LinearGradientContainer", "WebkitGradientContainer", "log", "smallImage", "parseBackgrounds"]
}

View File

@ -37,9 +37,9 @@ function renderDocument(document, options, windowWidth, windowHeight) {
document.querySelector(selector).removeAttribute(html2canvasNodeAttribute);
var clonedWindow = container.contentWindow;
var node = clonedWindow.document.querySelector(selector);
var support = new Support();
var support = new Support(clonedWindow.document);
var imageLoader = new ImageLoader(options, support);
var bounds = NodeParser.prototype.getBounds(node);
var bounds = getBounds(node);
var width = options.type === "view" ? Math.min(bounds.width, windowWidth) : documentWidth();
var height = options.type === "view" ? Math.min(bounds.height, windowHeight) : documentHeight();
var renderer = new CanvasRenderer(width, height, imageLoader);
@ -343,6 +343,7 @@ function NodeContainer(node, parent) {
this.parent = parent;
this.stack = null;
this.bounds = null;
this.offsetBounds = null;
this.visible = null;
this.computedStyles = null;
this.styles = {};
@ -500,19 +501,28 @@ NodeContainer.prototype.parseTextShadows = function() {
NodeContainer.prototype.parseTransform = function() {
var transformRegExp = /(matrix)\((.+)\)/;
var transform = this.prefixedCss("transform");
if (transform !== null && transform !== "none") {
var matrix = parseMatrix(transform.match(transformRegExp));
if (matrix) {
return {
origin: this.prefixedCss("transformOrigin"),
matrix: matrix
};
}
var matrix = parseMatrix(transform.match(transformRegExp));
var offset = this.parseBounds();
if (matrix) {
var origin = this.prefixedCss("transformOrigin").split(" ").map(removePx).map(asFloat);
origin[0] += offset.left;
origin[1] += offset.top;
return {
origin: origin,
matrix: matrix
};
}
return {
origin: [0, 0],
matrix: [1, 0, 0, 1, 0, 0]
};
};
NodeContainer.prototype.parseBounds = function() {
return this.bounds || (this.bounds = this.hasTransform() ? offsetBounds(this.node) : getBounds(this.node));
};
NodeContainer.prototype.hasTransform = function() {
var transform = this.prefixedCss("transform");
return (transform !== null && transform !== "none" && transform !== "matrix(1, 0, 0, 1, 0, 0)");
};
NodeContainer.prototype.TEXT_SHADOW_PROPERTY = /((rgba|rgb)\([^\)]+\)(\s-?\d+px){0,})/g;
@ -627,6 +637,44 @@ function parseBackgrounds(backgroundImage) {
return results;
}
function removePx(str) {
return str.replace("px", "");
}
function asFloat(str) {
return parseFloat(str);
}
function getBounds(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: width,
height: isBody ? node.scrollHeight : node.offsetHeight
};
}
return {};
}
function offsetBounds(node) {
var parent = node.offsetParent ? offsetBounds(node.offsetParent) : {top: 0, left: 0};
return {
top: node.offsetTop + parent.top,
bottom: node.offsetTop + node.offsetHeight + parent.top,
right: node.offsetLeft + parent.left + node.offsetWidth,
left: node.offsetLeft + parent.left,
width: node.offsetWidth,
height: node.offsetHeight
};
}
function NodeParser(element, renderer, support, imageLoader, options) {
log("Starting NodeParser");
this.renderer = renderer;
@ -763,7 +811,7 @@ NodeParser.prototype.newStackingContext = function(container, hasOwnStacking) {
NodeParser.prototype.createStackingContexts = function() {
this.nodes.forEach(function(container) {
if (isElement(container) && (this.isRootElement(container) || hasOpacity(container) || isPositionedForStacking(container) || this.isBodyWithTransparentRoot(container) || hasTransform(container))) {
if (isElement(container) && (this.isRootElement(container) || hasOpacity(container) || isPositionedForStacking(container) || this.isBodyWithTransparentRoot(container) || container.hasTransform())) {
this.newStackingContext(container, true);
} else if (isElement(container) && ((isPositioned(container) && zIndex0(container)) || isInlineBlock(container) || isFloating(container))) {
this.newStackingContext(container, false);
@ -786,55 +834,33 @@ NodeParser.prototype.sortStackingContexts = function(stack) {
stack.contexts.forEach(this.sortStackingContexts, this);
};
NodeParser.prototype.parseBounds = function(nodeContainer) {
return nodeContainer.bounds = this.getBounds(nodeContainer.node);
};
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: width,
height: isBody ? node.scrollHeight : node.offsetHeight
};
}
return {};
};
NodeParser.prototype.parseTextBounds = function(container) {
return function(text, index, textList) {
if (container.parent.css("textDecoration") !== "none" || text.trim().length !== 0) {
if (this.support.rangeBounds) {
if (container.parent.css("textDecoration").substr(0, 4) !== "none" || text.trim().length !== 0) {
if (this.support.rangeBounds && !container.parent.hasTransform()) {
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);
var bounds = this.getWrapperBounds(container.node);
var bounds = this.getWrapperBounds(container.node, container.parent.hasTransform());
container.node = replacementNode;
return bounds;
}
} else if (!this.support.rangeBounds) {
} else if(!this.support.rangeBounds || container.parent.hasTransform()){
container.node = container.node.splitText(text.length);
}
return {};
};
};
NodeParser.prototype.getWrapperBounds = function(node) {
var wrapper = node.ownerDocument.createElement('wrapper');
NodeParser.prototype.getWrapperBounds = function(node, transform) {
var wrapper = node.ownerDocument.createElement('html2canvaswrapper');
var parent = node.parentNode,
backupText = node.cloneNode(true);
wrapper.appendChild(node.cloneNode(true));
parent.replaceChild(wrapper, node);
var bounds = this.getBounds(wrapper);
var bounds = transform ? offsetBounds(wrapper) : getBounds(wrapper);
parent.replaceChild(backupText, wrapper);
return bounds;
};
@ -846,6 +872,8 @@ NodeParser.prototype.getRangeBounds = function(node, offset, length) {
return range.getBoundingClientRect();
};
function ClearTransform() {}
NodeParser.prototype.parse = function(stack) {
// http://www.w3.org/TR/CSS21/visuren.html#z-index
var negativeZindex = stack.contexts.filter(negativeZIndex); // 2. the child stacking contexts with negative stack levels (most negative first).
@ -862,13 +890,16 @@ NodeParser.prototype.parse = function(stack) {
this.renderQueue.push(container);
if (isStackingContext(container)) {
this.parse(container);
this.renderQueue.push(new ClearTransform());
}
}, this);
};
NodeParser.prototype.paint = function(container) {
try {
if (isTextNode(container)) {
if (container instanceof ClearTransform) {
this.renderer.ctx.restore();
} else if (isTextNode(container)) {
this.paintText(container);
} else {
this.paintNode(container);
@ -881,13 +912,12 @@ NodeParser.prototype.paint = function(container) {
NodeParser.prototype.paintNode = function(container) {
if (isStackingContext(container)) {
this.renderer.setOpacity(container.opacity);
var transform = container.parseTransform();
if (transform) {
this.renderer.setTransform(transform);
this.renderer.ctx.save();
if (container.hasTransform()) {
this.renderer.setTransform(container.parseTransform());
}
}
var bounds = this.parseBounds(container);
var bounds = container.parseBounds();
var borderData = this.parseBorders(container);
this.renderer.clip(borderData.clip, function() {
this.renderer.renderBackground(container, bounds, borderData.borders.map(getWidth));
@ -1254,11 +1284,6 @@ function hasOpacity(container) {
return container.css("opacity") < 1;
}
function hasTransform(container) {
var transform = container.prefixedCss("transform");
return transform !== null && transform !== "none";
}
function bind(callback, context) {
return function() {
return callback.apply(context, arguments);
@ -1516,8 +1541,8 @@ CanvasRenderer.prototype.setOpacity = function(opacity) {
CanvasRenderer.prototype.setTransform = function(transform) {
this.ctx.translate(transform.origin[0], transform.origin[1]);
this.ctx.setTransform.apply(this.ctx, transform.matrix);
this.ctx.translate(transform.origin[0], transform.origin[1]);
this.ctx.transform.apply(this.ctx, transform.matrix);
this.ctx.translate(-transform.origin[0], -transform.origin[1]);
};
CanvasRenderer.prototype.setVariable = function(property, value) {
@ -1588,12 +1613,12 @@ StackingContext.prototype.getParentStack = function(context) {
return parentStack ? (parentStack.ownStacking ? parentStack : parentStack.getParentStack(context)) : context.stack;
};
function Support() {
this.rangeBounds = this.testRangeBounds();
function Support(document) {
this.rangeBounds = this.testRangeBounds(document);
this.cors = this.testCORS();
}
Support.prototype.testRangeBounds = function() {
Support.prototype.testRangeBounds = function(document) {
var range, testElement, rangeBounds, rangeHeight, support = false;
if (document.createRange) {

File diff suppressed because one or more lines are too long

View File

@ -28,9 +28,9 @@ function renderDocument(document, options, windowWidth, windowHeight) {
document.querySelector(selector).removeAttribute(html2canvasNodeAttribute);
var clonedWindow = container.contentWindow;
var node = clonedWindow.document.querySelector(selector);
var support = new Support();
var support = new Support(clonedWindow.document);
var imageLoader = new ImageLoader(options, support);
var bounds = NodeParser.prototype.getBounds(node);
var bounds = getBounds(node);
var width = options.type === "view" ? Math.min(bounds.width, windowWidth) : documentWidth();
var height = options.type === "view" ? Math.min(bounds.height, windowHeight) : documentHeight();
var renderer = new CanvasRenderer(width, height, imageLoader);

View File

@ -3,6 +3,7 @@ function NodeContainer(node, parent) {
this.parent = parent;
this.stack = null;
this.bounds = null;
this.offsetBounds = null;
this.visible = null;
this.computedStyles = null;
this.styles = {};
@ -160,19 +161,28 @@ NodeContainer.prototype.parseTextShadows = function() {
NodeContainer.prototype.parseTransform = function() {
var transformRegExp = /(matrix)\((.+)\)/;
var transform = this.prefixedCss("transform");
if (transform !== null && transform !== "none") {
var matrix = parseMatrix(transform.match(transformRegExp));
if (matrix) {
return {
origin: this.prefixedCss("transformOrigin"),
matrix: matrix
};
}
var matrix = parseMatrix(transform.match(transformRegExp));
var offset = this.parseBounds();
if (matrix) {
var origin = this.prefixedCss("transformOrigin").split(" ").map(removePx).map(asFloat);
origin[0] += offset.left;
origin[1] += offset.top;
return {
origin: origin,
matrix: matrix
};
}
return {
origin: [0, 0],
matrix: [1, 0, 0, 1, 0, 0]
};
};
NodeContainer.prototype.parseBounds = function() {
return this.bounds || (this.bounds = this.hasTransform() ? offsetBounds(this.node) : getBounds(this.node));
};
NodeContainer.prototype.hasTransform = function() {
var transform = this.prefixedCss("transform");
return (transform !== null && transform !== "none" && transform !== "matrix(1, 0, 0, 1, 0, 0)");
};
NodeContainer.prototype.TEXT_SHADOW_PROPERTY = /((rgba|rgb)\([^\)]+\)(\s-?\d+px){0,})/g;
@ -286,3 +296,41 @@ function parseBackgrounds(backgroundImage) {
appendResult();
return results;
}
function removePx(str) {
return str.replace("px", "");
}
function asFloat(str) {
return parseFloat(str);
}
function getBounds(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: width,
height: isBody ? node.scrollHeight : node.offsetHeight
};
}
return {};
}
function offsetBounds(node) {
var parent = node.offsetParent ? offsetBounds(node.offsetParent) : {top: 0, left: 0};
return {
top: node.offsetTop + parent.top,
bottom: node.offsetTop + node.offsetHeight + parent.top,
right: node.offsetLeft + parent.left + node.offsetWidth,
left: node.offsetLeft + parent.left,
width: node.offsetWidth,
height: node.offsetHeight
};
}

View File

@ -134,7 +134,7 @@ NodeParser.prototype.newStackingContext = function(container, hasOwnStacking) {
NodeParser.prototype.createStackingContexts = function() {
this.nodes.forEach(function(container) {
if (isElement(container) && (this.isRootElement(container) || hasOpacity(container) || isPositionedForStacking(container) || this.isBodyWithTransparentRoot(container) || hasTransform(container))) {
if (isElement(container) && (this.isRootElement(container) || hasOpacity(container) || isPositionedForStacking(container) || this.isBodyWithTransparentRoot(container) || container.hasTransform())) {
this.newStackingContext(container, true);
} else if (isElement(container) && ((isPositioned(container) && zIndex0(container)) || isInlineBlock(container) || isFloating(container))) {
this.newStackingContext(container, false);
@ -157,55 +157,33 @@ NodeParser.prototype.sortStackingContexts = function(stack) {
stack.contexts.forEach(this.sortStackingContexts, this);
};
NodeParser.prototype.parseBounds = function(nodeContainer) {
return nodeContainer.bounds = this.getBounds(nodeContainer.node);
};
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: width,
height: isBody ? node.scrollHeight : node.offsetHeight
};
}
return {};
};
NodeParser.prototype.parseTextBounds = function(container) {
return function(text, index, textList) {
if (container.parent.css("textDecoration") !== "none" || text.trim().length !== 0) {
if (this.support.rangeBounds) {
if (container.parent.css("textDecoration").substr(0, 4) !== "none" || text.trim().length !== 0) {
if (this.support.rangeBounds && !container.parent.hasTransform()) {
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);
var bounds = this.getWrapperBounds(container.node);
var bounds = this.getWrapperBounds(container.node, container.parent.hasTransform());
container.node = replacementNode;
return bounds;
}
} else if (!this.support.rangeBounds) {
} else if(!this.support.rangeBounds || container.parent.hasTransform()){
container.node = container.node.splitText(text.length);
}
return {};
};
};
NodeParser.prototype.getWrapperBounds = function(node) {
var wrapper = node.ownerDocument.createElement('wrapper');
NodeParser.prototype.getWrapperBounds = function(node, transform) {
var wrapper = node.ownerDocument.createElement('html2canvaswrapper');
var parent = node.parentNode,
backupText = node.cloneNode(true);
wrapper.appendChild(node.cloneNode(true));
parent.replaceChild(wrapper, node);
var bounds = this.getBounds(wrapper);
var bounds = transform ? offsetBounds(wrapper) : getBounds(wrapper);
parent.replaceChild(backupText, wrapper);
return bounds;
};
@ -217,6 +195,8 @@ NodeParser.prototype.getRangeBounds = function(node, offset, length) {
return range.getBoundingClientRect();
};
function ClearTransform() {}
NodeParser.prototype.parse = function(stack) {
// http://www.w3.org/TR/CSS21/visuren.html#z-index
var negativeZindex = stack.contexts.filter(negativeZIndex); // 2. the child stacking contexts with negative stack levels (most negative first).
@ -233,13 +213,16 @@ NodeParser.prototype.parse = function(stack) {
this.renderQueue.push(container);
if (isStackingContext(container)) {
this.parse(container);
this.renderQueue.push(new ClearTransform());
}
}, this);
};
NodeParser.prototype.paint = function(container) {
try {
if (isTextNode(container)) {
if (container instanceof ClearTransform) {
this.renderer.ctx.restore();
} else if (isTextNode(container)) {
this.paintText(container);
} else {
this.paintNode(container);
@ -252,13 +235,12 @@ NodeParser.prototype.paint = function(container) {
NodeParser.prototype.paintNode = function(container) {
if (isStackingContext(container)) {
this.renderer.setOpacity(container.opacity);
var transform = container.parseTransform();
if (transform) {
this.renderer.setTransform(transform);
this.renderer.ctx.save();
if (container.hasTransform()) {
this.renderer.setTransform(container.parseTransform());
}
}
var bounds = this.parseBounds(container);
var bounds = container.parseBounds();
var borderData = this.parseBorders(container);
this.renderer.clip(borderData.clip, function() {
this.renderer.renderBackground(container, bounds, borderData.borders.map(getWidth));
@ -625,11 +607,6 @@ function hasOpacity(container) {
return container.css("opacity") < 1;
}
function hasTransform(container) {
var transform = container.prefixedCss("transform");
return transform !== null && transform !== "none";
}
function bind(callback, context) {
return function() {
return callback.apply(context, arguments);

View File

@ -66,8 +66,8 @@ CanvasRenderer.prototype.setOpacity = function(opacity) {
CanvasRenderer.prototype.setTransform = function(transform) {
this.ctx.translate(transform.origin[0], transform.origin[1]);
this.ctx.setTransform.apply(this.ctx, transform.matrix);
this.ctx.translate(transform.origin[0], transform.origin[1]);
this.ctx.transform.apply(this.ctx, transform.matrix);
this.ctx.translate(-transform.origin[0], -transform.origin[1]);
};
CanvasRenderer.prototype.setVariable = function(property, value) {

View File

@ -1,9 +1,9 @@
function Support() {
this.rangeBounds = this.testRangeBounds();
function Support(document) {
this.rangeBounds = this.testRangeBounds(document);
this.cors = this.testCORS();
}
Support.prototype.testRangeBounds = function() {
Support.prototype.testRangeBounds = function(document) {
var range, testElement, rangeBounds, rangeHeight, support = false;
if (document.createRange) {

View File

@ -10,7 +10,7 @@
margin-top: 100px;
}
#second {
border: 10px solid red;
border: 15px solid red;
background: darkseagreen;
-webkit-transform: rotate(7.5deg); /* Chrome, Safari 3.1+ */
-moz-transform: rotate(7.5deg); /* Firefox 3.5-15 */
@ -27,14 +27,19 @@
transform: rotate(-70.5deg); /* Firefox 16+, IE 10+, Opera 12.10+ */
}
#fourth {
background: #bc8f8f;
}
div {
display: inline-block;
padding: 10px;
}
</style>
</head>
<body>
<div id="first">First level content <div id="second">with second level content <div id="third">and third level content</div>, ending second</div>, ending first</div>
<div id="fourth">something else</div>
</body>
</html>

View File

@ -0,0 +1,45 @@
<!DOCTYPE html>
<html>
<head>
<title>Nested transform tests</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<script type="text/javascript" src="../../test.js"></script>
<style>
#first {
background: indianred;
margin-top: 100px;
}
#second {
border: 10px solid red;
background: darkseagreen;
-webkit-transform: translate(125px); /* Chrome, Safari 3.1+ */
-moz-transform: translate(125px); /* Firefox 3.5-15 */
-ms-transform: translate(125px); /* IE 9 */
-o-transform: translate(125px); /* Opera 10.50-12.00 */
transform: translate(125px);
}
#third {
background: cadetblue;
-webkit-transform: translate(-100px, -25px); /* Chrome, Safari 3.1+ */
-moz-transform: translate(100px, -25px); /* Firefox 3.5-15 */
-ms-transform: translate(100px, -25px); /* IE 9 */
-o-transform: translate(100px, -25px); /* Opera 10.50-12.00 */
transform: translate(100px, -25px);
-webkit-transform-origin: 100px 50px;
-moz-transform-origin: 100px 50px;
-ms-transform-origin: 100px 50px;
-o-transform-origin: 100px 50px;
transform-origin: 100px 50px;
}
div {
display: inline-block;
padding: 10px;
}
</style>
</head>
<body>
<div id="first">First level content <div id="second">with second level content <div id="third">and third level content</div>, ending second</div>, ending first</div>
</body>
</html>