function NodeContainer(node, parent) { this.node = node; this.parent = parent; this.stack = null; this.bounds = null; this.borders = null; this.clip = []; this.backgroundClip = []; this.offsetBounds = null; this.visible = null; this.computedStyles = null; this.styles = {}; this.backgroundImages = null; this.transformData = null; this.transformMatrix = null; this.isPseudoElement = false; this.opacity = null; } NodeContainer.prototype.cloneTo = function(stack) { stack.visible = this.visible; stack.borders = this.borders; stack.bounds = this.bounds; stack.clip = this.clip; stack.backgroundClip = this.backgroundClip; stack.computedStyles = this.computedStyles; stack.styles = this.styles; stack.backgroundImages = this.backgroundImages; stack.opacity = this.opacity; }; NodeContainer.prototype.getOpacity = function() { return this.opacity === null ? (this.opacity = this.cssFloat('opacity')) : this.opacity; }; 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") && (this.node.nodeName !== "INPUT" || this.node.getAttribute("type") !== "hidden") ); }; NodeContainer.prototype.css = function(attribute) { if (!this.computedStyles) { this.computedStyles = this.isPseudoElement ? this.parent.computedStyle(this.before ? ":before" : ":after") : 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.parseClip = function() { var matches = this.css('clip').match(this.CLIP); if (matches) { return { top: parseInt(matches[1], 10), right: parseInt(matches[2], 10), bottom: parseInt(matches[3], 10), left: parseInt(matches[4], 10) }; } return null; }; 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] ? parseFloat(s[1].replace('px', '')) : 0, offsetY: s[2] ? parseFloat(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; NodeContainer.prototype.CLIP = /^rect\((\d+)px,? (\d+)px,? (\d+)px,? (\d+)px\)$/; 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 width = node.offsetWidth == null ? clientRect.width : node.offsetWidth; return { top: clientRect.top, bottom: clientRect.bottom || (clientRect.top + clientRect.height), right: clientRect.left + width, left: clientRect.left, width: width, height: node.offsetHeight == null ? clientRect.height : 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 }; }