/* html2canvas 0.5.0-rc1 Copyright (c) 2014 Niklas von Hertzen Released under MIT License */ (function(window, document, undefined){ /* Copyright (c) 2013 Yehuda Katz, Tom Dale, and contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ !function(){var a,b,c,d;!function(){var e={},f={};a=function(a,b,c){e[a]={deps:b,callback:c}},d=c=b=function(a){function c(b){if("."!==b.charAt(0))return b;for(var c=b.split("/"),d=a.split("/").slice(0,-1),e=0,f=c.length;f>e;e++){var g=c[e];if(".."===g)d.pop();else{if("."===g)continue;d.push(g)}}return d.join("/")}if(d._eak_seen=e,f[a])return f[a];if(f[a]={},!e[a])throw new Error("Could not find module "+a);for(var g,h=e[a],i=h.deps,j=h.callback,k=[],l=0,m=i.length;m>l;l++)"exports"===i[l]?k.push(g={}):k.push(b(c(i[l])));var n=j.apply(this,k);return f[a]=g||n}}(),a("promise/all",["./utils","exports"],function(a,b){"use strict";function c(a){var b=this;if(!d(a))throw new TypeError("You must pass an array to all.");return new b(function(b,c){function d(a){return function(b){f(a,b)}}function f(a,c){h[a]=c,0===--i&&b(h)}var g,h=[],i=a.length;0===i&&b([]);for(var j=0;j sheets when fetched through getComputedStyle if window url is about:blank, we can assign the url to current by writing onto the document */ container.contentWindow.onload = container.onload = function() { resolve(container); }; documentClone.open(); documentClone.write(""); documentClone.close(); documentClone.replaceChild(removeScriptNodes(documentClone.adoptNode(documentElement)), documentClone.documentElement); if (options.type === "view") { container.contentWindow.scrollTo(window.pageXOffset, window.pageYOffset); } }); } function removeScriptNodes(parent) { [].slice.call(parent.childNodes, 0).filter(isElementNode).forEach(function(node) { if (node.tagName === "SCRIPT") { parent.removeChild(node); } else { removeScriptNodes(node); } }); return parent; } function isElementNode(node) { return node.nodeType === Node.ELEMENT_NODE; } function DummyImageContainer(src) { this.src = src; log("DummyImageContainer for", src); if (!this.promise || !this.image) { log("Initiating DummyImageContainer"); DummyImageContainer.prototype.image = new Image(); var image = this.image; DummyImageContainer.prototype.promise = new Promise(function(resolve, reject) { image.onload = resolve; image.onerror = reject; image.src = smallImage(); if (image.complete === true) { resolve(image); } }); } } 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 }; GradientContainer.prototype.angleRegExp = /([+-]?\d*\.?\d+)(deg|grad|rad|turn)/; function ImageContainer(src, cors) { this.src = src; this.image = new Image(); var self = this; this.promise = new Promise(function(resolve, reject) { self.image.onload = resolve; self.image.onerror = reject; if (cors) { self.image.crossOrigin = "anonymous"; } self.image.src = src; if (self.image.complete === true) { resolve(self.image); } })['catch'](function() { var dummy = new DummyImageContainer(src); return dummy.promise.then(function(image) { self.image = image; }); }); } function ImageLoader(options, support) { this.link = null; this.options = options; this.support = support; this.origin = window.location.protocol + window.location.hostname + window.location.port; } ImageLoader.prototype.findImages = function(nodes) { var images = []; nodes.filter(isImage).map(urlImage).forEach(this.addImage(images, this.loadImage), this); return images; }; ImageLoader.prototype.findBackgroundImage = function(images, container) { container.parseBackgroundImages().filter(this.hasImageBackground).forEach(this.addImage(images, this.loadImage), this); return images; }; 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); } }; }; ImageLoader.prototype.hasImageBackground = function(imageData) { return imageData.method !== "none"; }; 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) { 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, this.options.proxy); } else { return new DummyImageContainer(src); } } else if (imageData.method === "linear-gradient") { return new LinearGradientContainer(imageData); } else if (imageData.method === "gradient") { return new WebkitGradientContainer(imageData); } else { return new DummyImageContainer(imageData); } }; ImageLoader.prototype.imageExists = function(images, src) { return images.some(function(image) { return image.src === 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.hostname + link.port; 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.findBackgroundImage, this), this.findImages(nodes)); this.images.forEach(function(image, index) { image.promise.then(function() { log("Succesfully loaded image #"+ (index+1)); }, function() { log("Failed loading image #"+ (index+1)); }); }); this.ready = Promise.all(this.images.map(this.getPromise)); log("Finished searching images"); return this; }; function isImage(container) { return container.node.nodeName === "IMG"; } function urlImage(container) { return { args: [container.node.src], method: "url" }; } function LinearGradientContainer(imageData) { GradientContainer.apply(this, arguments); this.type = this.TYPES.LINEAR; var hasDirection = imageData.args[0].match(this.stepRegExp) === null; if (hasDirection) { imageData.args[0].split(" ").reverse().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; case "to": var y0 = this.y0; var x0 = this.x0; this.y0 = this.y1; this.x0 = this.x1; this.x1 = x0; this.y1 = y0; break; } }, this); } else { this.y0 = 0; this.y1 = 1; } this.colorStops = imageData.args.slice(hasDirection ? 1 : 0).map(function(colorStop) { var colorStopMatch = colorStop.match(this.stepRegExp); return { color: colorStopMatch[1], stop: colorStopMatch[3] === "%" ? colorStopMatch[2] / 100 : null }; }, this); if (this.colorStops[0].stop === null) { this.colorStops[0].stop = 0; } if (this.colorStops[this.colorStops.length - 1].stop === null) { this.colorStops[this.colorStops.length - 1].stop = 1; } this.colorStops.forEach(function(colorStop, index) { if (colorStop.stop === null) { this.colorStops.slice(index).some(function(find, count) { if (find.stop !== null) { colorStop.stop = ((find.stop - this.colorStops[index - 1].stop) / (count + 1)) + this.colorStops[index - 1].stop; return true; } else { return false; } }, this); } }, 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) { Function.prototype.bind.call(window.console.log, (window.console)).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.offsetBounds = null; this.visible = null; this.computedStyles = null; this.styles = {}; this.backgroundImages = null; this.transformData = null; this.transformMatrix = 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.computedStyle(null); } return this.styles[attribute] || (this.styles[attribute] = this.computedStyles[attribute]); }; NodeContainer.prototype.prefixedCss = function(attribute) { var prefixes = ["webkit", "moz", "ms", "o"]; var value = this.css(attribute); if (value === undefined) { prefixes.some(function(prefix) { value = this.css(prefix + attribute.substr(0, 1).toUpperCase() + attribute.substr(1)); return value !== undefined; }, this); } return value === undefined ? null : value; }; NodeContainer.prototype.computedStyle = function(type) { return this.node.ownerDocument.defaultView.getComputedStyle(this.node, type); }; NodeContainer.prototype.cssInt = function(attribute) { var value = parseInt(this.css(attribute), 10); return (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 (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() { return this.backgroundImages || (this.backgroundImages = parseBackgrounds(this.css("backgroundImage"))); }; 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]; }; 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.parseTransform = function() { if (!this.transformData) { if (this.hasTransform()) { var offset = this.parseBounds(); var origin = this.prefixedCss("transformOrigin").split(" ").map(removePx).map(asFloat); origin[0] += offset.left; origin[1] += offset.top; this.transformData = { origin: origin, matrix: this.parseTransformMatrix() }; } else { this.transformData = { origin: [0, 0], matrix: [1, 0, 0, 1, 0, 0] }; } } return this.transformData; }; NodeContainer.prototype.parseTransformMatrix = function() { if (!this.transformMatrix) { var transform = this.prefixedCss("transform"); var matrix = transform ? parseMatrix(transform.match(this.MATRIX_PROPERTY)) : null; this.transformMatrix = matrix ? matrix : [1, 0, 0, 1, 0, 0]; } return this.transformMatrix; }; NodeContainer.prototype.parseBounds = function() { return this.bounds || (this.bounds = this.hasTransform() ? offsetBounds(this.node) : getBounds(this.node)); }; NodeContainer.prototype.hasTransform = function() { return this.parseTransformMatrix().join(",") !== "1,0,0,1,0,0" || (this.parent && this.parent.hasTransform()); }; NodeContainer.prototype.getValue = function() { var value = this.node.value || ""; value = (this.node.tagName === "SELECT") ? selectionValue(this.node) : value; return value.length === 0 ? (this.node.placeholder || "") : value; }; NodeContainer.prototype.MATRIX_PROPERTY = /(matrix)\((.+)\)/; NodeContainer.prototype.TEXT_SHADOW_PROPERTY = /((rgba|rgb)\([^\)]+\)(\s-?\d+px){0,})/g; NodeContainer.prototype.TEXT_SHADOW_VALUES = /(-?\d+px)|(#.+)|(rgb\(.+\))|(rgba\(.+\))/g; function selectionValue(node) { var option = node.options[node.selectedIndex || 0]; return option ? (option.text || "") : ""; } function parseMatrix(match) { if (match && match[1] === "matrix") { return match[2].split(",").map(function(s) { return parseFloat(s.trim()); }); } } function isPercentage(value) { return value.toString().indexOf("%") !== -1; } function parseBackgrounds(backgroundImage) { 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 = ''; 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 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; this.options = options; this.range = null; this.support = support; this.renderQueue = []; this.stack = new StackingContext(true, 1, element.ownerDocument, null); var parent = new NodeContainer(element, null); if (element !== element.ownerDocument.documentElement && this.renderer.isTransparent(parent.css('backgroundColor'))) { renderer.rectangle(0, 0, renderer.width, renderer.height, new NodeContainer(element.ownerDocument.documentElement, null).css('backgroundColor')); } parent.visibile = parent.isElementVisible(); this.createPseudoHideStyles(element.ownerDocument); this.nodes = flatten([parent].concat(this.getChildren(parent)).filter(function(container) { return container.visible = container.isElementVisible(); }).map(this.getPseudoElements, this)); 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.ready = this.images.ready.then(bind(function() { log("Images loaded, starting parsing"); this.parse(this.stack); log("Render queue created with " + this.renderQueue.length + " items"); return new Promise(bind(function(resolve) { if (!options.async) { this.renderQueue.forEach(this.paint, this); resolve(); } else if (typeof(options.async) === "function") { options.async.call(this, this.renderQueue, resolve); } else { this.renderIndex = 0; this.asyncRenderer(this.renderQueue, resolve); } }, this)); }, this)); } NodeParser.prototype.asyncRenderer = function(queue, resolve, asyncTimer) { asyncTimer = asyncTimer || Date.now(); this.paint(queue[this.renderIndex++]); if (queue.length === this.renderIndex) { resolve(); } else if (asyncTimer + 20 > Date.now()) { this.asyncRenderer(queue, resolve, asyncTimer); } else { setTimeout(bind(function() { this.asyncRenderer(queue, resolve); }, this), 0); } }; NodeParser.prototype.createPseudoHideStyles = function(document) { var hidePseudoElements = document.createElement('style'); hidePseudoElements.innerHTML = '.' + this.pseudoHideClass + ':before { content: "" !important; display: none !important; }' + '.' + this.pseudoHideClass + ':after { content: "" !important; display: none !important; }'; document.body.appendChild(hidePseudoElements); }; NodeParser.prototype.getPseudoElements = function(container) { var nodes = [[container]]; if (container.node.nodeType === Node.ELEMENT_NODE) { var before = this.getPseudoElement(container, ":before"); var after = this.getPseudoElement(container, ":after"); if (before) { container.node.insertBefore(before[0].node, container.node.firstChild); nodes.push(before); } if (after) { container.node.appendChild(after[0].node); nodes.push(after); } if (before || after) { container.node.className += " " + this.pseudoHideClass; } } return flatten(nodes); }; function toCamelCase(str) { return str.replace(/(\-[a-z])/g, function(match){ return match.toUpperCase().replace('-',''); }); } NodeParser.prototype.getPseudoElement = function(container, type) { var style = container.computedStyle(type); if(!style || !style.content || style.content === "none" || style.content === "-moz-alt-content" || style.display === "none") { return null; } var content = stripQuotes(style.content); var isImage = content.substr(0, 3) === 'url'; var pseudoNode = document.createElement(isImage ? 'img' : 'html2canvaspseudoelement'); var pseudoContainer = new NodeContainer(pseudoNode, container); for (var i = style.length-1; i >= 0; i--) { var property = toCamelCase(style.item(i)); pseudoNode.style[property] = style[property]; } pseudoNode.className = this.pseudoHideClass; if (isImage) { pseudoNode.src = parseBackgrounds(content)[0].args[0]; return [pseudoContainer]; } else { var text = document.createTextNode(content); pseudoNode.appendChild(text); return [pseudoContainer, new TextContainer(text, pseudoContainer)]; } }; 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 && node.tagName !== "TEXTAREA" ? (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 = hasOwnStacking ? stack.getParentStack(this) : stack.parent.stack; 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) || container.hasTransform())) { this.newStackingContext(container, true); } else if (isElement(container) && ((isPositioned(container) && zIndex0(container)) || isInlineBlock(container) || isFloating(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.parent === null; }; NodeParser.prototype.sortStackingContexts = function(stack) { stack.contexts.sort(zIndexSort); stack.contexts.forEach(this.sortStackingContexts, this); }; NodeParser.prototype.parseTextBounds = function(container) { return function(text, index, textList) { 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, container.parent.hasTransform()); container.node = replacementNode; return bounds; } } else if(!this.support.rangeBounds || container.parent.hasTransform()){ container.node = container.node.splitText(text.length); } return {}; }; }; 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 = transform ? offsetBounds(wrapper) : 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 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). 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). negativeZindex.concat(nonInlineNonPositionedDescendants).concat(nonPositionedFloats) .concat(inFlow).concat(stackLevel0).concat(text).concat(positiveZindex).forEach(function(container) { this.renderQueue.push(container); if (isStackingContext(container)) { this.parse(container); this.renderQueue.push(new ClearTransform()); } }, this); }; NodeParser.prototype.paint = function(container) { try { if (container instanceof ClearTransform) { this.renderer.ctx.restore(); } else if (isTextNode(container)) { this.paintText(container); } else { this.paintNode(container); } } catch(e) { log(e); } }; NodeParser.prototype.paintNode = function(container) { if (isStackingContext(container)) { this.renderer.setOpacity(container.opacity); this.renderer.ctx.save(); if (container.hasTransform()) { this.renderer.setTransform(container.parseTransform()); } } var bounds = container.parseBounds(); var borderData = this.parseBorders(container); this.renderer.clip(borderData.clip, function() { this.renderer.renderBackground(container, bounds, borderData.borders.map(getWidth)); }, this); this.renderer.renderBorders(borderData.borders); switch(container.node.nodeName) { case "IMG": var imageContainer = this.images.get(container.node.src); if (imageContainer) { this.renderer.renderImage(container, bounds, borderData, imageContainer.image); } else { log("Error loading ", container.node.src); } break; case "SELECT": case "INPUT": case "TEXTAREA": this.paintFormValue(container); break; } }; NodeParser.prototype.paintFormValue = function(container) { if (container.getValue().length > 0) { var document = container.node.ownerDocument; var wrapper = document.createElement('html2canvaswrapper'); var properties = ['lineHeight', 'textAlign', 'fontFamily', 'fontWeight', 'fontSize', 'color', 'paddingLeft', 'paddingTop', 'paddingRight', 'paddingBottom', 'width', 'height', 'borderLeftStyle', 'borderTopStyle', 'borderLeftWidth', 'borderTopWidth', 'boxSizing', 'whiteSpace', 'wordWrap']; properties.forEach(function(property) { try { wrapper.style[property] = container.css(property); } catch(e) { // Older IE has issues with "border" log("html2canvas: Parse: Exception caught in renderFormValue: " + e.message); } }); var bounds = container.parseBounds(); wrapper.style.position = "absolute"; wrapper.style.left = bounds.left + "px"; wrapper.style.top = bounds.top + "px"; wrapper.textContent = container.getValue(); document.body.appendChild(wrapper); this.paintText(new TextContainer(wrapper.firstChild, container)); document.body.removeChild(wrapper); } }; 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'); 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); this.renderTextDecoration(container.parent, bounds, this.fontMetrics.getMetrics(family, size)); } }, 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); 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; }; NodeParser.prototype.pseudoHideClass = "___html2canvas___pseudoelement"; 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 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 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"))); } 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 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 isInlineBlock(container) { return ["inline-block", "inline-table"].indexOf(container.css("display")) !== -1; } 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 asInt(value) { return parseInt(value, 10); } function getWidth(border) { return border.width; } function nonIgnoredElement(nodeContainer) { return (nodeContainer.node.nodeType !== Node.ELEMENT_NODE || ["SCRIPT", "HEAD", "TITLE", "OBJECT", "BR", "OPTION"].indexOf(nodeContainer.node.nodeName) === -1); } function flatten(arrays) { return [].concat.apply([], arrays); } function stripQuotes(content) { var first = content.substr(0, 1); return (first === content.substr(content.length - 1) && first.match(/'|"/)) ? content.substr(1, content.length - 2) : content; } function ProxyImageContainer(src, proxy) { var callbackName = "html2canvas_" + proxyImageCount++; var script = document.createElement("script"); var link = document.createElement("a"); link.href = src; src = link.href; var requestUrl = proxy + ((proxy.indexOf("?") > -1) ? "&" : "?" ) + 'url=' + encodeURIComponent(src) + '&callback=' + callbackName; this.src = src; this.image = new Image(); var self = this; this.promise = new Promise(function(resolve, reject) { self.image.onload = resolve; self.image.onerror = reject; window[callbackName] = function(a){ if (a.substring(0,6) === "error:"){ reject(); } else { self.image.src = a; } window[callbackName] = undefined; // to work with IE<9 // NOTE: that the undefined callback property-name still exists on the window object (for IE<9) try { delete window[callbackName]; // for all browser that support this } catch(ex) {} script.parentNode.removeChild(script); }; script.setAttribute("type", "text/javascript"); script.setAttribute("src", requestUrl); document.body.appendChild(script); })['catch'](function() { var dummy = new DummyImageContainer(src); return dummy.promise.then(function(image) { self.image = image; }); }); } var proxyImageCount = 0; function Renderer(width, height, images) { this.width = width; this.height = height; this.images = images; } Renderer.prototype.renderImage = function(container, bounds, borderData, image) { var paddingLeft = container.cssInt('paddingLeft'), paddingTop = container.cssInt('paddingTop'), paddingRight = container.cssInt('paddingRight'), paddingBottom = container.cssInt('paddingBottom'), borders = borderData.borders; this.drawImage( image, 0, 0, image.width, image.height, bounds.left + paddingLeft + borders[3].width, bounds.top + paddingTop + borders[0].width, bounds.width - (borders[1].width + borders[3].width + paddingLeft + paddingRight), bounds.height - (borders[0].width + borders[2].width + paddingTop + paddingBottom) ); }; Renderer.prototype.renderBackground = function(container, bounds, borderData) { if (bounds.height > 0 && bounds.width > 0) { this.renderBackgroundColor(container, bounds); this.renderBackgroundImage(container, bounds, borderData); } }; 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, borderData) { var backgroundImages = container.parseBackgroundImages(); backgroundImages.reverse().forEach(function(backgroundImage, index, arr) { switch(backgroundImage.method) { case "url": var image = this.images.get(backgroundImage.args[0]); if (image) { this.renderBackgroundRepeating(container, bounds, image, arr.length - (index+1), borderData); } 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, borderData); } else { log("Error loading background-image", backgroundImage.args[0]); } break; case "none": break; default: log("Unknown background-image type", backgroundImage.args[0]); } }, this); }; Renderer.prototype.renderBackgroundRepeating = function(container, bounds, imageContainer, index, borderData) { var size = container.parseBackgroundSize(bounds, imageContainer.image, index); var position = container.parseBackgroundPosition(bounds, imageContainer.image, index, size); var repeat = container.parseBackgroundRepeat(index); switch (repeat) { case "repeat-x": case "repeat no-repeat": this.backgroundRepeatShape(imageContainer, position, size, bounds, bounds.left + borderData[3], bounds.top + position.top + borderData[0], 99999, imageContainer.image.height, borderData); break; case "repeat-y": case "no-repeat repeat": this.backgroundRepeatShape(imageContainer, position, size, bounds, bounds.left + position.left + borderData[3], bounds.top + borderData[0], imageContainer.image.width, 99999, borderData); break; case "no-repeat": this.backgroundRepeatShape(imageContainer, position, size, bounds, bounds.left + position.left + borderData[3], bounds.top + position.top + borderData[0], imageContainer.image.width, imageContainer.image.height, borderData); break; default: this.renderBackgroundRepeat(imageContainer, position, size, {top: bounds.top, left: bounds.left}, borderData[3], borderData[0]); 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"; this.variables = {}; log("Initialized CanvasRenderer"); } 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.drawImage = function(image, sx, sy, sw, sh, dx, dy, dw, dh) { this.ctx.drawImage(image, sx, sy, sw, sh, dx, dy, dw, dh); }; 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.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.setTransform = function(transform) { 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) { 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); }; CanvasRenderer.prototype.backgroundRepeatShape = function(imageContainer, backgroundPosition, size, bounds, left, top, width, height, borderData) { 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)] ]; this.clip(shape, function() { this.renderBackgroundRepeat(imageContainer, backgroundPosition, size, bounds, borderData[3], borderData[0]); }, this); }; CanvasRenderer.prototype.renderBackgroundRepeat = function(imageContainer, backgroundPosition, size, bounds, borderLeft, borderTop) { var offsetX = Math.round(bounds.left + backgroundPosition.left + borderLeft), offsetY = Math.round(bounds.top + backgroundPosition.top + borderTop); this.setFillStyle(this.ctx.createPattern(this.resizeImage(imageContainer, size), "repeat")); this.ctx.translate(offsetX, offsetY); this.ctx.fill(); this.ctx.translate(-offsetX, -offsetY); }; CanvasRenderer.prototype.renderBackgroundGradient = function(gradientImage, bounds) { if (gradientImage instanceof LinearGradientContainer) { var gradient = this.ctx.createLinearGradient( bounds.left + bounds.width * gradientImage.x0, bounds.top + bounds.height * gradientImage.y0, bounds.left + bounds.width * gradientImage.x1, bounds.top + bounds.height * gradientImage.y1); gradientImage.colorStops.forEach(function(colorStop) { gradient.addColorStop(colorStop.stop, colorStop.color); }); this.rectangle(bounds.left, bounds.top, bounds.width, bounds.height, gradient); } }; CanvasRenderer.prototype.resizeImage = function(imageContainer, size) { var image = imageContainer.image; if(image.width === size.width && image.height === size.height) { return image; } var ctx, canvas = document.createElement('canvas'); canvas.width = size.width; canvas.height = size.height; ctx = canvas.getContext("2d"); ctx.drawImage(image, 0, 0, image.width, image.height, 0, 0, size.width, size.height ); return canvas; }; 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(document) { this.rangeBounds = this.testRangeBounds(document); this.cors = this.testCORS(); } Support.prototype.testRangeBounds = function(document) { 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(); } } 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);