/* html2canvas 0.5.0-rc1 Copyright (c) 2014 Niklas von Hertzen Released under MIT License */ (function(window, document, undefined){ window.html2canvas = function(nodeList, options) { var container = createWindowClone(document, window.innerWidth, window.innerHeight); var clonedWindow = container.contentWindow; var element = (nodeList === undefined) ? document.body : nodeList[0]; var node = clonedWindow.document.documentElement; var support = new Support(); var imageLoader = new ImageLoader(options, support); options = options || {}; if (options.logging) { window.html2canvas.logging = true; window.html2canvas.start = Date.now(); } var renderer = new CanvasRenderer(documentWidth(), documentHeight(), imageLoader); var parser = new NodeParser(node, renderer, support, imageLoader, options); window.console.log(parser); }; function documentWidth () { return Math.max( Math.max(document.body.scrollWidth, document.documentElement.scrollWidth), Math.max(document.body.offsetWidth, document.documentElement.offsetWidth), Math.max(document.body.clientWidth, document.documentElement.clientWidth) ); } function documentHeight () { return Math.max( Math.max(document.body.scrollHeight, document.documentElement.scrollHeight), Math.max(document.body.offsetHeight, document.documentElement.offsetHeight), Math.max(document.body.clientHeight, document.documentElement.clientHeight) ); } function createWindowClone(ownerDocument, width, height) { var documentElement = ownerDocument.documentElement.cloneNode(true), container = ownerDocument.createElement("iframe"); container.style.display = "hidden"; container.style.position = "absolute"; container.style.width = width + "px"; container.style.height = height + "px"; ownerDocument.body.appendChild(container); var documentClone = container.contentWindow.document; documentClone.replaceChild(documentClone.adoptNode(documentElement), documentClone.documentElement); return container; } function NodeParser(element, renderer, support, imageLoader, options) { this.renderer = renderer; this.options = options; this.range = null; this.support = support; this.stack = new StackingContext(true, 1, element.ownerDocument, null); var parent = new NodeContainer(element, null); parent.visibile = parent.isElementVisible(); this.nodes = [parent].concat(this.getChildren(parent)).filter(function(container) { return container.visible = container.isElementVisible(); }); this.images = imageLoader.fetch(this.nodes.filter(isElement)); this.createStackingContexts(); this.sortStackingContexts(this.stack); this.images.ready.then(bind(function() { log("Images loaded, starting parsing"); this.parse(this.stack); options.onrendered(renderer.canvas); }, this)); } NodeParser.prototype.getChildren = function(parentContainer) { return flatten([].filter.call(parentContainer.node.childNodes, renderableNode).map(function(node) { var container = [node.nodeType === Node.TEXT_NODE ? new TextContainer(node, parentContainer) : new NodeContainer(node, parentContainer)].filter(nonIgnoredElement); return node.nodeType === Node.ELEMENT_NODE && container.length ? (container[0].isElementVisible() ? container.concat(this.getChildren(container[0])) : []) : container; }, this)); }; NodeParser.prototype.newStackingContext = function(container, hasOwnStacking) { var stack = new StackingContext(hasOwnStacking, container.cssFloat('opacity'), container.node, container.parent); stack.visible = container.visible; var parentStack = stack.getParentStack(this); parentStack.contexts.push(stack); container.stack = stack; }; NodeParser.prototype.createStackingContexts = function() { this.nodes.forEach(function(container) { if (isElement(container) && (this.isRootElement(container) || hasOpacity(container) || isPositionedForStacking(container) || this.isBodyWithTransparentRoot(container))) { this.newStackingContext(container, true); } else if (isElement(container) && (isPositioned(container))) { this.newStackingContext(container, false); } else { container.assignStack(container.parent.stack); } }, this); }; NodeParser.prototype.isBodyWithTransparentRoot = function(container) { return container.node.nodeName === "BODY" && this.renderer.isTransparent(container.parent.css('backgroundColor')); }; NodeParser.prototype.isRootElement = function(container) { return container.node.nodeName === "HTML"; }; NodeParser.prototype.sortStackingContexts = function(stack) { stack.contexts.sort(zIndexSort); 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(); return { top: clientRect.top, bottom: clientRect.bottom || (clientRect.top + clientRect.height), left: clientRect.left, width: node.offsetWidth, height: node.offsetHeight }; } return {}; }; 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) { 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); container.node = replacementNode; return bounds; } } }; }; NodeParser.prototype.getWrapperBounds = function(node) { var wrapper = node.ownerDocument.createElement('wrapper'); var parent = node.parentNode, backupText = node.cloneNode(true); wrapper.appendChild(node.cloneNode(true)); parent.replaceChild(wrapper, node); var bounds = this.getBounds(wrapper); parent.replaceChild(backupText, wrapper); return bounds; }; NodeParser.prototype.getRangeBounds = function(node, offset, length) { var range = this.range || (this.range = node.ownerDocument.createRange()); range.setStart(node, offset); range.setEnd(node, offset + length); return range.getBoundingClientRect(); }; function negativeZIndex(container) { return container.cssInt("zIndex") < 0; } function positiveZIndex(container) { return container.cssInt("zIndex") > 0; } function zIndex0(container) { return container.cssInt("zIndex") === 0; } function inlineLevel(container) { return ["inline", "inline-block", "inline-table"].indexOf(container.css("display")) !== -1; } function isStackingContext(container) { return (container instanceof StackingContext); } function hasText(container) { return container.node.data.trim().length > 0; } function noLetterSpacing(container) { return (/^(normal|none|0px)$/.test(container.parent.css("letterSpacing"))); } 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). var descendantElements = stack.children.filter(isElement); var descendantNonFloats = descendantElements.filter(not(isFloating)); var nonInlineNonPositionedDescendants = descendantNonFloats.filter(not(isPositioned)).filter(not(inlineLevel)); // 3 the in-flow, non-inline-level, non-positioned descendants. var nonPositionedFloats = descendantElements.filter(not(isPositioned)).filter(isFloating); // 4. the non-positioned floats. var inFlow = descendantNonFloats.filter(not(isPositioned)).filter(inlineLevel); // 5. the in-flow, inline-level, non-positioned descendants, including inline tables and inline blocks. var stackLevel0 = stack.contexts.concat(descendantNonFloats.filter(isPositioned)).filter(zIndex0); // 6. the child stacking contexts with stack level 0 and the positioned descendants with stack level 0. var text = stack.children.filter(isTextNode).filter(hasText); var positiveZindex = stack.contexts.filter(positiveZIndex); // 7. the child stacking contexts with positive stack levels (least positive first). var rendered = []; negativeZindex.concat(nonInlineNonPositionedDescendants).concat(nonPositionedFloats) .concat(inFlow).concat(stackLevel0).concat(text).concat(positiveZindex).forEach(function(container) { this.paint(container); if (rendered.indexOf(container.node) !== -1) { window.console.log(container, container.node); throw new Error("rendering twice"); } rendered.push(container.node); if (isStackingContext(container)) { this.parse(container); } }, this); }; NodeParser.prototype.paint = function(container) { if (isTextNode(container)) { this.paintText(container); } else { this.paintNode(container); } }; NodeParser.prototype.paintNode = function(container) { if (isStackingContext(container)) { this.renderer.setOpacity(container.opacity); } var bounds = this.parseBounds(container); var borderData = this.parseBorders(container); this.renderer.clip(borderData.clip, function() { this.renderer.renderBackground(container, bounds); }, this); this.renderer.renderBorders(borderData.borders); }; NodeParser.prototype.paintText = function(container) { container.applyTextTransform(); var textList = container.node.data.split(!this.options.letterRendering || noLetterSpacing(container) ? /(\b| )/ : ""); var weight = container.parent.fontWeight(); var size = container.parent.css('fontSize'); var family = container.parent.css('fontFamily'); this.renderer.font(container.parent.css('color'), container.parent.css('fontStyle'), container.parent.css('fontVariant'), weight, size, family); 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); } /* 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.parseBorders = function(container) { var nodeBounds = container.bounds; var radius = getBorderRadiusData(container); var borders = ["Top", "Right", "Bottom", "Left"].map(function(side) { return { width: container.cssInt('border' + side + 'Width'), color: container.css('border' + side + 'Color'), args: null }; }); var borderPoints = calculateCurvePoints(nodeBounds, radius, borders); return { clip: this.parseBackgroundClip(container, borderPoints, borders, radius, nodeBounds), borders: borders.map(function(border, borderSide) { if (border.width > 0) { var bx = nodeBounds.left; var by = nodeBounds.top; var bw = nodeBounds.width; var bh = nodeBounds.height - (borders[2].width); switch(borderSide) { case 0: // top border bh = borders[0].width; border.args = drawSide({ c1: [bx, by], c2: [bx + bw, by], c3: [bx + bw - borders[1].width, by + bh], c4: [bx + borders[3].width, by + bh] }, radius[0], radius[1], borderPoints.topLeftOuter, borderPoints.topLeftInner, borderPoints.topRightOuter, borderPoints.topRightInner); break; case 1: // right border bx = nodeBounds.left + nodeBounds.width - (borders[1].width); bw = borders[1].width; border.args = drawSide({ c1: [bx + bw, by], c2: [bx + bw, by + bh + borders[2].width], c3: [bx, by + bh], c4: [bx, by + borders[0].width] }, radius[1], radius[2], borderPoints.topRightOuter, borderPoints.topRightInner, borderPoints.bottomRightOuter, borderPoints.bottomRightInner); break; case 2: // bottom border by = (by + nodeBounds.height) - (borders[2].width); bh = borders[2].width; border.args = drawSide({ c1: [bx + bw, by + bh], c2: [bx, by + bh], c3: [bx + borders[3].width, by], c4: [bx + bw - borders[3].width, by] }, radius[2], radius[3], borderPoints.bottomRightOuter, borderPoints.bottomRightInner, borderPoints.bottomLeftOuter, borderPoints.bottomLeftInner); break; case 3: // left border bw = borders[3].width; border.args = drawSide({ c1: [bx, by + bh + borders[2].width], c2: [bx, by], c3: [bx + bw, by + borders[0].width], c4: [bx + bw, by + bh] }, radius[3], radius[0], borderPoints.bottomLeftOuter, borderPoints.bottomLeftInner, borderPoints.topLeftOuter, borderPoints.topLeftInner); break; } } return border; }) }; }; NodeParser.prototype.parseBackgroundClip = function(container, borderPoints, borders, radius, bounds) { var backgroundClip = container.css('backgroundClip'), borderArgs = []; switch(backgroundClip) { case "content-box": case "padding-box": parseCorner(borderArgs, radius[0], radius[1], borderPoints.topLeftInner, borderPoints.topRightInner, bounds.left + borders[3].width, bounds.top + borders[0].width); parseCorner(borderArgs, radius[1], radius[2], borderPoints.topRightInner, borderPoints.bottomRightInner, bounds.left + bounds.width - borders[1].width, bounds.top + borders[0].width); parseCorner(borderArgs, radius[2], radius[3], borderPoints.bottomRightInner, borderPoints.bottomLeftInner, bounds.left + bounds.width - borders[1].width, bounds.top + bounds.height - borders[2].width); parseCorner(borderArgs, radius[3], radius[0], borderPoints.bottomLeftInner, borderPoints.topLeftInner, bounds.left + borders[3].width, bounds.top + bounds.height - borders[2].width); break; default: parseCorner(borderArgs, radius[0], radius[1], borderPoints.topLeftOuter, borderPoints.topRightOuter, bounds.left, bounds.top); parseCorner(borderArgs, radius[1], radius[2], borderPoints.topRightOuter, borderPoints.bottomRightOuter, bounds.left + bounds.width, bounds.top); parseCorner(borderArgs, radius[2], radius[3], borderPoints.bottomRightOuter, borderPoints.bottomLeftOuter, bounds.left + bounds.width, bounds.top + bounds.height); parseCorner(borderArgs, radius[3], radius[0], borderPoints.bottomLeftOuter, borderPoints.topLeftOuter, bounds.left, bounds.top + bounds.height); break; } return borderArgs; }; function parseCorner(borderArgs, radius1, radius2, corner1, corner2, x, y) { if (radius1[0] > 0 || radius1[1] > 0) { borderArgs.push(["line", corner1[0].start.x, corner1[0].start.y]); corner1[0].curveTo(borderArgs); corner1[1].curveTo(borderArgs); } else { borderArgs.push(["line", x, y]); } if (radius2[0] > 0 || radius2[1] > 0) { borderArgs.push(["line", corner2[0].start.x, corner2[0].start.y]); } } function getBorderRadiusData(container) { return ["TopLeft", "TopRight", "BottomRight", "BottomLeft"].map(function(side) { var value = container.css('border' + side + 'Radius'); var arr = value.split(" "); if (arr.length <= 1) { arr[1] = arr[0]; } return arr.map(asInt); }); } function asInt(value) { return parseInt(value, 10); } function getCurvePoints(x, y, r1, r2) { var kappa = 4 * ((Math.sqrt(2) - 1) / 3); var ox = (r1) * kappa, // control point offset horizontal oy = (r2) * kappa, // control point offset vertical xm = x + r1, // x-middle ym = y + r2; // y-middle return { topLeft: bezierCurve({x: x, y: ym}, {x: x, y: ym - oy}, {x: xm - ox, y: y}, {x: xm, y: y}), topRight: bezierCurve({x: x, y: y}, {x: x + ox,y: y}, {x: xm, y: ym - oy}, {x: xm, y: ym}), bottomRight: bezierCurve({x: xm, y: y}, {x: xm, y: y + oy}, {x: x + ox, y: ym}, {x: x, y: ym}), bottomLeft: bezierCurve({x: xm, y: ym}, {x: xm - ox, y: ym}, {x: x, y: y + oy}, {x: x, y:y}) }; } function calculateCurvePoints(bounds, borderRadius, borders) { var x = bounds.left, y = bounds.top, width = bounds.width, height = bounds.height, tlh = borderRadius[0][0], tlv = borderRadius[0][1], trh = borderRadius[1][0], trv = borderRadius[1][1], brh = borderRadius[2][0], brv = borderRadius[2][1], blh = borderRadius[3][0], blv = borderRadius[3][1]; var topWidth = width - trh, rightHeight = height - brv, bottomWidth = width - brh, leftHeight = height - blv; return { topLeftOuter: getCurvePoints(x, y, tlh, tlv).topLeft.subdivide(0.5), topLeftInner: getCurvePoints(x + borders[3].width, y + borders[0].width, Math.max(0, tlh - borders[3].width), Math.max(0, tlv - borders[0].width)).topLeft.subdivide(0.5), topRightOuter: getCurvePoints(x + topWidth, y, trh, trv).topRight.subdivide(0.5), topRightInner: getCurvePoints(x + Math.min(topWidth, width + borders[3].width), y + borders[0].width, (topWidth > width + borders[3].width) ? 0 :trh - borders[3].width, trv - borders[0].width).topRight.subdivide(0.5), bottomRightOuter: getCurvePoints(x + bottomWidth, y + rightHeight, brh, brv).bottomRight.subdivide(0.5), bottomRightInner: getCurvePoints(x + Math.min(bottomWidth, width + borders[3].width), y + Math.min(rightHeight, height + borders[0].width), Math.max(0, brh - borders[1].width), Math.max(0, brv - borders[2].width)).bottomRight.subdivide(0.5), bottomLeftOuter: getCurvePoints(x, y + leftHeight, blh, blv).bottomLeft.subdivide(0.5), bottomLeftInner: getCurvePoints(x + borders[3].width, y + leftHeight, Math.max(0, blh - borders[3].width), Math.max(0, blv - borders[2].width)).bottomLeft.subdivide(0.5) }; } function bezierCurve(start, startControl, endControl, end) { var lerp = function (a, b, t) { return { x: a.x + (b.x - a.x) * t, y: a.y + (b.y - a.y) * t }; }; return { start: start, startControl: startControl, endControl: endControl, end: end, subdivide: function(t) { var ab = lerp(start, startControl, t), bc = lerp(startControl, endControl, t), cd = lerp(endControl, end, t), abbc = lerp(ab, bc, t), bccd = lerp(bc, cd, t), dest = lerp(abbc, bccd, t); return [bezierCurve(start, ab, abbc, dest), bezierCurve(dest, bccd, cd, end)]; }, curveTo: function(borderArgs) { borderArgs.push(["bezierCurve", startControl.x, startControl.y, endControl.x, endControl.y, end.x, end.y]); }, curveToReversed: function(borderArgs) { borderArgs.push(["bezierCurve", endControl.x, endControl.y, startControl.x, startControl.y, start.x, start.y]); } }; } function drawSide(borderData, radius1, radius2, outer1, inner1, outer2, inner2) { var borderArgs = []; if (radius1[0] > 0 || radius1[1] > 0) { borderArgs.push(["line", outer1[1].start.x, outer1[1].start.y]); outer1[1].curveTo(borderArgs); } else { borderArgs.push([ "line", borderData.c1[0], borderData.c1[1]]); } if (radius2[0] > 0 || radius2[1] > 0) { borderArgs.push(["line", outer2[0].start.x, outer2[0].start.y]); outer2[0].curveTo(borderArgs); borderArgs.push(["line", inner2[0].end.x, inner2[0].end.y]); inner2[0].curveToReversed(borderArgs); } else { borderArgs.push(["line", borderData.c2[0], borderData.c2[1]]); borderArgs.push(["line", borderData.c3[0], borderData.c3[1]]); } if (radius1[0] > 0 || radius1[1] > 0) { borderArgs.push(["line", inner1[1].end.x, inner1[1].end.y]); inner1[1].curveToReversed(borderArgs); } else { borderArgs.push(["line", borderData.c4[0], borderData.c4[1]]); } return borderArgs; } function nonIgnoredElement(nodeContainer) { return (nodeContainer.node.nodeType !== Node.ELEMENT_NODE || ["SCRIPT", "HEAD", "TITLE", "OBJECT", "BR"].indexOf(nodeContainer.node.nodeName) === -1); } function flatten(arrays) { return [].concat.apply([], arrays); } function renderableNode(node) { return (node.nodeType === Node.TEXT_NODE || node.nodeType === Node.ELEMENT_NODE); } function isPositionedForStacking(container) { var position = container.css("position"); var zIndex = (position === "absolute" || position === "relative") ? container.css("zIndex") : "auto"; return zIndex !== "auto"; } function isPositioned(container) { return container.css("position") !== "static"; } function isFloating(container) { return container.css("float") !== "none"; } function not(callback) { var context = this; return function() { return !callback.apply(context, arguments); }; } function isElement(container) { return container.node.nodeType === Node.ELEMENT_NODE; } function isTextNode(container) { return container.node.nodeType === Node.TEXT_NODE; } function zIndexSort(a, b) { return a.cssInt("zIndex") - b.cssInt("zIndex"); } function hasOpacity(container) { return container.css("opacity") < 1; } function bind(callback, context) { return function() { return callback.apply(context, arguments); }; } function ImageContainer(src, cors) { this.src = src; this.image = new Image(); var image = this.image; this.promise = new Promise(function(resolve, reject) { image.onload = resolve; image.onerror = reject; if (cors) { image.crossOrigin = "anonymous"; } image.src = src; }); } function ImageLoader(options, support) { this.link = null; this.options = options; this.support = support; this.origin = window.location.protocol + window.location.host; } ImageLoader.prototype.findImages = function(images, container) { var backgrounds = container.parseBackgroundImages(); var backgroundImages = backgrounds.filter(this.isImageBackground).map(this.getBackgroundUrl).filter(this.imageExists(images)).map(this.loadImage, this); return images.concat(backgroundImages); }; ImageLoader.prototype.getBackgroundUrl = function(imageData) { return imageData.args[0]; }; ImageLoader.prototype.isImageBackground = function(imageData) { return imageData.method === "url"; }; ImageLoader.prototype.loadImage = function(src) { 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) { return new ImageContainer(src, false); } else if (this.support.cors && !this.options.allowTaint && this.options.useCORS) { return new ImageContainer(src, true); } else if (this.options.proxy) { return new ProxyImageContainer(src); } else { return new DummyImageContainer(src); } }; ImageLoader.prototype.imageExists = function(images) { return function(newImage) { return !images.some(function(image) { return image.src !== newImage.src; }); }; }; ImageLoader.prototype.isSameOrigin = function(url) { var link = this.link || (this.link = document.createElement("a")); link.href = url; link.href = link.href; // IE9, LOL! - http://jsfiddle.net/niklasvh/2e48b/ var origin = link.protocol + link.host; return (origin === this.origin); }; ImageLoader.prototype.getPromise = function(container) { return container.promise; }; ImageLoader.prototype.get = function(src) { var found = null; return this.images.some(function(img) { return (found = img).src === src; }) ? found : null; }; ImageLoader.prototype.fetch = function(nodes) { this.images = nodes.reduce(bind(this.findImages, this), []); this.ready = Promise.all(this.images.map(this.getPromise)); return this; }; 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))); } } function NodeContainer(node, parent) { this.node = node; this.parent = parent; this.stack = null; this.bounds = null; this.visible = null; this.computedStyles = null; this.styles = {}; this.backgroundImages = null; } NodeContainer.prototype.assignStack = function(stack) { this.stack = stack; stack.children.push(this); }; NodeContainer.prototype.isElementVisible = function() { return this.node.nodeType === Node.TEXT_NODE ? this.parent.visible : (this.css('display') !== "none" && this.css('visibility') !== "hidden" && !this.node.hasAttribute("data-html2canvas-ignore")); }; NodeContainer.prototype.css = function(attribute) { if (!this.computedStyles) { this.computedStyles = this.node.ownerDocument.defaultView.getComputedStyle(this.node, null); } return this.styles[attribute] || (this.styles[attribute] = this.computedStyles[attribute]); }; NodeContainer.prototype.cssInt = function(attribute) { var value = parseInt(this.css(attribute), 10); return (Number.isNaN(value)) ? 0 : value; // borders in old IE are throwing 'medium' for demo.html }; NodeContainer.prototype.cssFloat = function(attribute) { var value = parseFloat(this.css(attribute)); return (Number.isNaN(value)) ? 0 : value; }; NodeContainer.prototype.fontWeight = function() { var weight = this.css("fontWeight"); switch(parseInt(weight, 10)){ case 401: weight = "bold"; break; case 400: weight = "normal"; break; } return weight; }; NodeContainer.prototype.parseBackgroundImages = function() { var whitespace = ' \r\n\t', method, definition, prefix, prefix_i, block, results = [], mode = 0, numParen = 0, quote, args; var appendResult = function() { if(method) { if (definition.substr(0, 1) === '"') { definition = definition.substr(1, definition.length - 2); } if (definition) { args.push(definition); } if (method.substr(0, 1) === '-' && (prefix_i = method.indexOf('-', 1 ) + 1) > 0) { prefix = method.substr(0, prefix_i); method = method.substr(prefix_i); } results.push({ prefix: prefix, method: method.toLowerCase(), value: block, args: args, image: null }); } args = []; method = prefix = definition = block = ''; }; args = []; method = prefix = definition = block = ''; this.css("backgroundImage").split("").forEach(function(c) { if (mode === 0 && whitespace.indexOf(c) > -1) { return; } switch(c) { case '"': if(!quote) { quote = c; } else if(quote === c) { quote = null; } break; case '(': if(quote) { break; } else if(mode === 0) { mode = 1; block += c; return; } else { numParen++; } break; case ')': if (quote) { break; } else if(mode === 1) { if(numParen === 0) { mode = 0; block += c; appendResult(); return; } else { numParen--; } } break; case ',': if (quote) { break; } else if(mode === 0) { appendResult(); return; } else if (mode === 1) { if (numParen === 0 && !method.match(/^url$/i)) { args.push(definition); definition = ''; block += c; return; } } break; } block += c; if (mode === 0) { method += c; } else { definition += c; } }); appendResult(); return this.backgroundImages || (this.backgroundImages = results); }; NodeContainer.prototype.cssList = function(property, index) { var value = (this.css(property) || '').split(','); value = value[index || 0] || value[0] || 'auto'; value = value.trim().split(' '); if (value.length === 1) { value = [value[0], value[0]]; } return value; }; NodeContainer.prototype.parseBackgroundSize = function(bounds, image, index) { var size = this.cssList("backgroundSize", index); var width, height; if (isPercentage(size[0])) { width = bounds.width * parseFloat(size[0]) / 100; } else if (/contain|cover/.test(size[0])) { var targetRatio = bounds.width / bounds.height, currentRatio = image.width / image.height; return (targetRatio < currentRatio ^ size[0] === 'contain') ? {width: bounds.height * currentRatio, height: bounds.height} : {width: bounds.width, height: bounds.width / currentRatio}; } else { width = parseInt(size[0], 10); } if (size[0] === 'auto' && size[1] === 'auto') { height = image.height; } else if (size[1] === 'auto') { height = width / image.width * image.height; } else if (isPercentage(size[1])) { height = bounds.height * parseFloat(size[1]) / 100; } else { height = parseInt(size[1], 10); } if (size[0] === 'auto') { width = height / image.height * image.width; } return {width: width, height: height}; }; NodeContainer.prototype.parseBackgroundPosition = function(bounds, image, index, backgroundSize) { var position = this.cssList('backgroundPosition', index); var left, top; if (isPercentage(position[0])){ left = (bounds.width - (backgroundSize || image).width) * (parseFloat(position[0]) / 100); } else { left = parseInt(position[0], 10); } if (position[1] === 'auto') { top = left / image.width * image.height; } else if (isPercentage(position[1])){ top = (bounds.height - (backgroundSize || image).height) * parseFloat(position[1]) / 100; } else { top = parseInt(position[1], 10); } if (position[0] === 'auto') { left = top / image.height * image.width; } return {left: left, top: top}; }; NodeContainer.prototype.parseBackgroundRepeat = function(index) { return this.cssList("backgroundRepeat", index)[0]; }; function isPercentage(value) { return value.toString().indexOf("%") !== -1; } function Renderer(width, height, images) { this.width = width; this.height = height; this.images = images; } Renderer.prototype.renderBackground = function(container, bounds) { if (bounds.height > 0 && bounds.width > 0) { this.renderBackgroundColor(container, bounds); this.renderBackgroundImage(container, bounds); } }; Renderer.prototype.renderBackgroundColor = function(container, bounds) { var color = container.css("backgroundColor"); if (!this.isTransparent(color)) { this.rectangle(bounds.left, bounds.top, bounds.width, bounds.height, container.css("backgroundColor")); } }; Renderer.prototype.renderBorders = function(borders) { borders.forEach(this.renderBorder, this); }; Renderer.prototype.renderBorder = function(data) { if (!this.isTransparent(data.color) && data.args !== null) { this.drawShape(data.args, data.color); } }; Renderer.prototype.renderBackgroundImage = function(container, bounds) { var backgroundImages = container.parseBackgroundImages(); backgroundImages.reverse().forEach(function(backgroundImage, index) { if (backgroundImage.method === "url") { var image = this.images.get(backgroundImage.args[0]); if (image) { this.renderBackgroundRepeating(container, bounds, image, index); } else { log("Error loading background-image", backgroundImage.args[0]); } } }, this); }; Renderer.prototype.renderBackgroundRepeating = function(container, bounds, imageContainer, index) { var size = container.parseBackgroundSize(bounds, imageContainer.image, index); var position = container.parseBackgroundPosition(bounds, imageContainer.image, index, size); var repeat = container.parseBackgroundRepeat(index); // image = resizeImage(image, backgroundSize); switch (repeat) { case "repeat-x": case "repeat no-repeat": this.backgroundRepeatShape(imageContainer, position, bounds, bounds.left, bounds.top + position.top, 99999, imageContainer.image.height); break; case "repeat-y": case "no-repeat repeat": this.backgroundRepeatShape(imageContainer, position, bounds, bounds.left + position.left, bounds.top, imageContainer.image.width, 99999); break; case "no-repeat": this.backgroundRepeatShape(imageContainer, position, bounds, bounds.left + position.left, bounds.top + position.top, imageContainer.image.width, imageContainer.image.height); break; default: this.renderBackgroundRepeat(imageContainer, position, {top: bounds.top, left: bounds.left}); break; } }; Renderer.prototype.isTransparent = function(color) { return (!color || color === "transparent" || color === "rgba(0, 0, 0, 0)"); }; function CanvasRenderer(width, height) { Renderer.apply(this, arguments); this.canvas = document.createElement("canvas"); this.canvas.width = width; this.canvas.height = height; this.ctx = this.canvas.getContext("2d"); this.ctx.textBaseline = "bottom"; } CanvasRenderer.prototype = Object.create(Renderer.prototype); CanvasRenderer.prototype.setFillStyle = function(color) { this.ctx.fillStyle = color; return this.ctx; }; CanvasRenderer.prototype.rectangle = function(left, top, width, height, color) { this.setFillStyle(color).fillRect(left, top, width, height); }; CanvasRenderer.prototype.drawShape = function(shape, color) { this.shape(shape); this.setFillStyle(color).fill(); }; CanvasRenderer.prototype.clip = function(shape, callback, context) { this.ctx.save(); this.shape(shape).clip(); callback.call(context); this.ctx.restore(); }; CanvasRenderer.prototype.shape = function(shape) { this.ctx.beginPath(); shape.forEach(function(point, index) { this.ctx[(index === 0) ? "moveTo" : point[0] + "To" ].apply(this.ctx, point.slice(1)); }, this); this.ctx.closePath(); return this.ctx; }; CanvasRenderer.prototype.font = function(color, style, variant, weight, size, family) { this.setFillStyle(color).font = [style, variant, weight, size, family].join(" "); }; CanvasRenderer.prototype.setOpacity = function(opacity) { this.ctx.globalAlpha = opacity; }; CanvasRenderer.prototype.text = function(text, left, bottom) { this.ctx.fillText(text, left, bottom); }; CanvasRenderer.prototype.backgroundRepeatShape = function(imageContainer, backgroundPosition, bounds, left, top, width, height) { var shape = [ ["line", Math.round(left), Math.round(top)], ["line", Math.round(left + width), Math.round(top)], ["line", Math.round(left + width), Math.round(height + top)], ["line", Math.round(left), Math.round(height + top)] ]; console.log(shape); this.clip(shape, function() { this.renderBackgroundRepeat(imageContainer, backgroundPosition, bounds); }, this); }; CanvasRenderer.prototype.renderBackgroundRepeat = function(imageContainer, backgroundPosition, bounds) { var offsetX = Math.round(bounds.left + backgroundPosition.left), offsetY = Math.round(bounds.top + backgroundPosition.top); this.setFillStyle(this.ctx.createPattern(imageContainer.image, "repeat")); this.ctx.translate(offsetX, offsetY); this.ctx.fill(); this.ctx.translate(-offsetX, -offsetY); }; function StackingContext(hasOwnStacking, opacity, element, parent) { NodeContainer.call(this, element, parent); this.ownStacking = hasOwnStacking; this.contexts = []; this.children = []; this.opacity = (this.parent ? this.parent.stack.opacity : 1) * opacity; } StackingContext.prototype = Object.create(NodeContainer.prototype); StackingContext.prototype.getParentStack = function(context) { var parentStack = (this.parent) ? this.parent.stack : null; return parentStack ? (parentStack.ownStacking ? parentStack : parentStack.getParentStack(context)) : context.stack; }; function Support() { this.rangeBounds = this.testRangeBounds(); this.cors = this.testCORS(); } Support.prototype.testRangeBounds = function() { var range, testElement, rangeBounds, rangeHeight, support = false; if (document.createRange) { range = document.createRange(); if (range.getBoundingClientRect) { testElement = document.createElement('boundtest'); testElement.style.height = "123px"; testElement.style.display = "block"; document.body.appendChild(testElement); range.selectNode(testElement); rangeBounds = range.getBoundingClientRect(); rangeHeight = rangeBounds.height; if (rangeHeight === 123) { support = true; } document.body.removeChild(testElement); } } return support; }; Support.prototype.testCORS = function() { return typeof((new Image()).crossOrigin) !== "undefined"; }; function TextContainer(node, parent) { NodeContainer.call(this, node, parent); } TextContainer.prototype = Object.create(NodeContainer.prototype); TextContainer.prototype.applyTextTransform = function() { this.node.data = this.transform(this.parent.css("textTransform")); }; TextContainer.prototype.transform = function(transform) { var text = this.node.data; switch(transform){ case "lowercase": return text.toLowerCase(); case "capitalize": return text.replace(/(^|\s|:|-|\(|\))([a-z])/g, capitalize); case "uppercase": return text.toUpperCase(); default: return text; } }; function capitalize(m, p1, p2) { if (m.length > 0) { return p1 + p2.toUpperCase(); } } })(window,document);