├── tests ├── results │ └── .gitignore ├── mocha │ ├── clone.js │ ├── iframe3.htm │ ├── iframe1.htm │ ├── iframe2.htm │ ├── .jshintrc │ ├── ie9-clonenode-bug.html │ ├── selenium.js │ ├── proxy.htm │ ├── multiple-renders.html │ ├── options.onclone.html │ ├── scrolling.html │ ├── background.html │ ├── gradients.js │ └── lib │ │ └── mocha.css ├── assets │ ├── image.jpg │ ├── image2.jpg │ ├── image2_1.jpg │ ├── image_1.jpg │ └── iframe │ │ └── frame1.html ├── cases │ ├── iframe.html │ ├── crossorigin-iframe.html │ ├── images │ │ ├── svg │ │ │ ├── external.html │ │ │ ├── inline.html │ │ │ ├── node.html │ │ │ ├── base64.html │ │ │ └── native_only.html │ │ ├── cross-origin.html │ │ ├── base.html │ │ ├── empty.html │ │ ├── canvas.html │ │ └── images.html │ ├── zindex │ │ ├── z-index17.html │ │ ├── z-index14.html │ │ ├── z-index15.html │ │ ├── z-index13.html │ │ ├── z-index6.html │ │ ├── z-index5.html │ │ ├── z-index4.html │ │ ├── z-index16.html │ │ ├── z-index12.html │ │ ├── z-index11.html │ │ ├── z-index1.html │ │ ├── z-index8.html │ │ ├── z-index7.html │ │ ├── z-index18.html │ │ ├── z-index9.html │ │ ├── z-index2.html │ │ ├── z-index3.html │ │ └── z-index10.html │ ├── child-textnodes.html │ ├── visibility.html │ ├── border │ │ ├── dashed.html │ │ ├── dotted.html │ │ ├── double.html │ │ ├── solid.html │ │ ├── inset.html │ │ └── radius.html │ ├── google-maps.html │ ├── text │ │ ├── shadow.html │ │ ├── underline-lineheight.html │ │ ├── underline.html │ │ ├── linethrough.html │ │ ├── fontawesome.html │ │ └── chinese.html │ ├── list │ │ ├── decimal-leading-zero.html │ │ ├── decimal.html │ │ ├── lower-alpha.html │ │ └── upper-roman.html │ ├── background │ │ ├── multi.html │ │ ├── repeat.html │ │ ├── size.html │ │ ├── position.html │ │ ├── clip.html │ │ └── encoded.html │ ├── pseudoelements.html │ ├── transform │ │ ├── nested.html │ │ ├── translate.html │ │ └── rotate.html │ ├── clip.html │ ├── overflow.html │ └── forms.html ├── sauceconnect.js ├── node │ ├── package.js │ └── color.js ├── rangetest.html ├── utils.js ├── selenium.js └── test.js ├── .gitmodules ├── .npmignore ├── .gitignore ├── bower.json ├── src ├── log.js ├── fontmetrics.js ├── webkitgradientcontainer.js ├── imagecontainer.js ├── xhr.js ├── proxyimagecontainer.js ├── stackingcontext.js ├── gradientcontainer.js ├── dummyimagecontainer.js ├── svgnodecontainer.js ├── textcontainer.js ├── pseudoelementcontainer.js ├── framecontainer.js ├── font.js ├── support.js ├── svgcontainer.js ├── proxy.js ├── lineargradientcontainer.js ├── clone.js └── renderer.js ├── .jshintrc ├── .editorconfig ├── dist ├── html2canvas.svg.min.js └── html2canvas.svg.js ├── LICENSE ├── .travis.yml ├── package.json ├── examples ├── existing_canvas.html ├── demo2.html └── demo.html └── CHANGELOG.md /tests/results/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore -------------------------------------------------------------------------------- /tests/mocha/clone.js: -------------------------------------------------------------------------------- 1 | document.querySelector("#block").className += "class"; 2 | -------------------------------------------------------------------------------- /tests/assets/image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eKoopmans/html2canvas/HEAD/tests/assets/image.jpg -------------------------------------------------------------------------------- /tests/assets/image2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eKoopmans/html2canvas/HEAD/tests/assets/image2.jpg -------------------------------------------------------------------------------- /tests/assets/image2_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eKoopmans/html2canvas/HEAD/tests/assets/image2_1.jpg -------------------------------------------------------------------------------- /tests/assets/image_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eKoopmans/html2canvas/HEAD/tests/assets/image_1.jpg -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "src/fabric"] 2 | path = src/fabric 3 | url = https://github.com/kangax/fabric.js.git 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | tests/ 2 | examples/ 3 | Gruntfile.js 4 | bower.json 5 | src/ 6 | *.iml 7 | .idea/ 8 | .npmignore 9 | .jshintrc 10 | .travis.yml 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /nbproject/ 2 | image.jpg 3 | /.project 4 | /.settings/ 5 | node_modules/ 6 | .envrc 7 | *.sublime-workspace 8 | *.baseline 9 | *.iml 10 | .idea/ 11 | .DS_Store 12 | npm-debug.log 13 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "html2canvas", 3 | "description": "Screenshots with JavaScript", 4 | "main": "dist/html2canvas.js", 5 | "ignore": [ 6 | "tests", 7 | ".travis.yml" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /tests/cases/iframe.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | iframe test 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /tests/assets/iframe/frame1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | frame 1 6 | 12 | 13 | 14 | this is the content of frame1 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/log.js: -------------------------------------------------------------------------------- 1 | var logger = function() { 2 | if (logger.options.logging && window.console && window.console.log) { 3 | Function.prototype.bind.call(window.console.log, (window.console)).apply(window.console, [(Date.now() - logger.options.start) + "ms", "html2canvas:"].concat([].slice.call(arguments, 0))); 4 | } 5 | }; 6 | 7 | logger.options = {logging: false}; 8 | module.exports = logger; 9 | -------------------------------------------------------------------------------- /tests/sauceconnect.js: -------------------------------------------------------------------------------- 1 | const sauceConnectLauncher = require('sauce-connect-launcher'); 2 | 3 | sauceConnectLauncher({ 4 | username: process.env.SAUCE_USERNAME, 5 | accessKey: process.env.SAUCE_ACCESS_KEY, 6 | logger: console.log 7 | }, err => { 8 | if (err) { 9 | console.error(err.message); 10 | return; 11 | } 12 | console.log('Sauce Connect ready'); 13 | }); 14 | -------------------------------------------------------------------------------- /tests/cases/crossorigin-iframe.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | cross-origin iframe test 5 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/fontmetrics.js: -------------------------------------------------------------------------------- 1 | var Font = require('./font'); 2 | 3 | function FontMetrics() { 4 | this.data = {}; 5 | } 6 | 7 | FontMetrics.prototype.getMetrics = function(family, size) { 8 | if (this.data[family + "-" + size] === undefined) { 9 | this.data[family + "-" + size] = new Font(family, size); 10 | } 11 | return this.data[family + "-" + size]; 12 | }; 13 | 14 | module.exports = FontMetrics; 15 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "curly": true, 3 | "eqeqeq": true, 4 | "immed": true, 5 | "latedef": false, 6 | "newcap": true, 7 | "noarg": true, 8 | "sub": true, 9 | "undef": true, 10 | "boss": true, 11 | "eqnull": true, 12 | "browser": true, 13 | "node": true, 14 | "indent": 4, 15 | "globals": { 16 | "jQuery": true 17 | }, 18 | "predef": ["Promise", "define"] 19 | } 20 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | indent_style = space 12 | indent_size = 4 13 | 14 | [{.travis.yml,package.json}] 15 | # The indent size used in the `package.json` file cannot be changed 16 | # https://github.com/npm/npm/pull/3180#issuecomment-16336516 17 | indent_size = 2 18 | -------------------------------------------------------------------------------- /tests/cases/images/svg/external.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Image tests 5 | 6 | 7 | 8 | 9 | SVG taints image:
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/webkitgradientcontainer.js: -------------------------------------------------------------------------------- 1 | var GradientContainer = require('./gradientcontainer'); 2 | 3 | function WebkitGradientContainer(imageData) { 4 | GradientContainer.apply(this, arguments); 5 | this.type = imageData.args[0] === "linear" ? GradientContainer.TYPES.LINEAR : GradientContainer.TYPES.RADIAL; 6 | } 7 | 8 | WebkitGradientContainer.prototype = Object.create(GradientContainer.prototype); 9 | 10 | module.exports = WebkitGradientContainer; 11 | -------------------------------------------------------------------------------- /tests/mocha/iframe3.htm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 19 | 20 |
 
21 | 22 | -------------------------------------------------------------------------------- /tests/cases/images/cross-origin.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | External content tests 5 | 6 | 9 | 10 | 11 | 12 |

External image (CORS)

13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /tests/mocha/iframe1.htm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 19 | 20 | 21 |  
 
22 | 23 | 24 | -------------------------------------------------------------------------------- /tests/mocha/iframe2.htm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 21 | 22 |
 
23 | 24 | -------------------------------------------------------------------------------- /tests/cases/images/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | External content tests 5 | 6 | 7 | 8 | 9 | 10 | 11 |

External image

12 | 13 | 14 |

External image (using <base> href)

15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/imagecontainer.js: -------------------------------------------------------------------------------- 1 | function ImageContainer(src, cors) { 2 | this.src = src; 3 | this.image = new Image(); 4 | var self = this; 5 | this.tainted = null; 6 | this.promise = new Promise(function(resolve, reject) { 7 | self.image.onload = resolve; 8 | self.image.onerror = reject; 9 | if (cors) { 10 | self.image.crossOrigin = "anonymous"; 11 | } 12 | self.image.src = src; 13 | if (self.image.complete === true) { 14 | resolve(self.image); 15 | } 16 | }); 17 | } 18 | 19 | module.exports = ImageContainer; 20 | -------------------------------------------------------------------------------- /src/xhr.js: -------------------------------------------------------------------------------- 1 | function XHR(url) { 2 | return new Promise(function(resolve, reject) { 3 | var xhr = new XMLHttpRequest(); 4 | xhr.open('GET', url); 5 | 6 | xhr.onload = function() { 7 | if (xhr.status === 200) { 8 | resolve(xhr.responseText); 9 | } else { 10 | reject(new Error(xhr.statusText)); 11 | } 12 | }; 13 | 14 | xhr.onerror = function() { 15 | reject(new Error("Network Error")); 16 | }; 17 | 18 | xhr.send(); 19 | }); 20 | } 21 | 22 | module.exports = XHR; 23 | -------------------------------------------------------------------------------- /tests/cases/zindex/z-index17.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | z-index17 6 | 20 | 21 | 22 | 23 |
fixed z-index 10
24 | 25 | 26 | -------------------------------------------------------------------------------- /tests/cases/images/empty.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Image tests 5 | 6 | 7 | 8 | 9 | Image without src attribute, should not crash: 10 | 11 | Image with broken src attribute, should not crash: 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /dist/html2canvas.svg.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | html2canvas 0.5.0-beta4 3 | Copyright (c) 2017 Niklas von Hertzen 4 | 2017-06-14 Custom build by Erik Koopmans, featuring latest bugfixes and features 5 | 6 | Released under MIT License 7 | */ 8 | !function a(b,c,d){function e(g,h){if(!c[g]){if(!b[g]){var i="function"==typeof require&&require;if(!h&&i)return i(g,!0);if(f)return f(g,!0);var j=new Error("Cannot find module '"+g+"'");throw j.code="MODULE_NOT_FOUND",j}var k=c[g]={exports:{}};b[g][0].call(k.exports,function(a){var c=b[g][1][a];return e(c?c:a)},k,k.exports,a,b,c,d)}return c[g].exports}for(var f="function"==typeof require&&require,g=0;g 2 | 3 | 4 | Inline text in the top element 5 | 6 | 7 | 15 | 16 | 17 | 18 | Some inline text followed by text in span followed by more inline text. 19 |

Then a block level element.

20 | Then more inline text. 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/proxyimagecontainer.js: -------------------------------------------------------------------------------- 1 | var ProxyURL = require('./proxy').ProxyURL; 2 | 3 | function ProxyImageContainer(src, proxy) { 4 | var link = document.createElement("a"); 5 | link.href = src; 6 | src = link.href; 7 | this.src = src; 8 | this.image = new Image(); 9 | var self = this; 10 | this.promise = new Promise(function(resolve, reject) { 11 | self.image.crossOrigin = "Anonymous"; 12 | self.image.onload = resolve; 13 | self.image.onerror = reject; 14 | 15 | new ProxyURL(src, proxy, document).then(function(url) { 16 | self.image.src = url; 17 | })['catch'](reject); 18 | }); 19 | } 20 | 21 | module.exports = ProxyImageContainer; 22 | -------------------------------------------------------------------------------- /dist/html2canvas.svg.js: -------------------------------------------------------------------------------- 1 | /* 2 | html2canvas 0.5.0-beta4 3 | Copyright (c) 2017 Niklas von Hertzen 4 | 2017-06-14 Custom build by Erik Koopmans, featuring latest bugfixes and features 5 | 6 | Released under MIT License 7 | */ 8 | 9 | (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o 2 | 3 | 4 | body text above children with negative index but body bgcolor below 5 | 6 | 7 | 21 | 22 | body 23 |
24 | 25 | 26 | -------------------------------------------------------------------------------- /tests/node/package.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var path = require('path'); 3 | var html2canvas = require('../../'); 4 | 5 | describe("Package", function() { 6 | it("should have html2canvas defined", function() { 7 | assert.equal(typeof(html2canvas), "function"); 8 | }); 9 | }); 10 | 11 | describe.only("requirejs", function() { 12 | var requirejs = require('requirejs'); 13 | 14 | requirejs.config({ 15 | baseUrl: path.resolve(__dirname, '../../dist') 16 | }); 17 | 18 | it("should have html2canvas defined", function(done) { 19 | requirejs(['html2canvas'], function(h2c) { 20 | assert.equal(typeof(h2c), "function"); 21 | done(); 22 | }); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /tests/cases/zindex/z-index15.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | body text and bgcolor above children with negative z-index 5 | 6 | 7 | 22 | 23 | body 24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /tests/mocha/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "curly": true, 3 | "eqeqeq": true, 4 | "immed": true, 5 | "latedef": false, 6 | "newcap": true, 7 | "noarg": true, 8 | "sub": true, 9 | "undef": true, 10 | "boss": true, 11 | "eqnull": true, 12 | "browser": true, 13 | "globals": { 14 | "jQuery": true 15 | }, 16 | "predef": ["deepEqual", "module", "test", "$", "QUnit", "NodeParser", "NodeContainer", "StackingContext", "TextContainer", "ImageLoader", "CanvasRenderer", "Renderer", "Support", "bind", "Promise", 17 | "ImageContainer", "ProxyImageContainer", "DummyImageContainer", "Font", "FontMetrics", "GradientContainer", "LinearGradientContainer", "WebkitGradientContainer", "log", "smallImage", "parseBackgrounds"] 18 | } 19 | -------------------------------------------------------------------------------- /src/stackingcontext.js: -------------------------------------------------------------------------------- 1 | var NodeContainer = require('./nodecontainer'); 2 | 3 | function StackingContext(hasOwnStacking, opacity, element, parent) { 4 | NodeContainer.call(this, element, parent); 5 | this.ownStacking = hasOwnStacking; 6 | this.contexts = []; 7 | this.children = []; 8 | this.opacity = (this.parent ? this.parent.stack.opacity : 1) * opacity; 9 | } 10 | 11 | StackingContext.prototype = Object.create(NodeContainer.prototype); 12 | 13 | StackingContext.prototype.getParentStack = function(context) { 14 | var parentStack = (this.parent) ? this.parent.stack : null; 15 | return parentStack ? (parentStack.ownStacking ? parentStack : parentStack.getParentStack(context)) : context.stack; 16 | }; 17 | 18 | module.exports = StackingContext; 19 | -------------------------------------------------------------------------------- /src/gradientcontainer.js: -------------------------------------------------------------------------------- 1 | function GradientContainer(imageData) { 2 | this.src = imageData.value; 3 | this.colorStops = []; 4 | this.type = null; 5 | this.x0 = 0.5; 6 | this.y0 = 0.5; 7 | this.x1 = 0.5; 8 | this.y1 = 0.5; 9 | this.promise = Promise.resolve(true); 10 | } 11 | 12 | GradientContainer.TYPES = { 13 | LINEAR: 1, 14 | RADIAL: 2 15 | }; 16 | 17 | // TODO: support hsl[a], negative %/length values 18 | // TODO: support (e.g. -?\d{1,3}(?:\.\d+)deg, etc. : https://developer.mozilla.org/docs/Web/CSS/angle ) 19 | GradientContainer.REGEXP_COLORSTOP = /^\s*(rgba?\(\s*\d{1,3},\s*\d{1,3},\s*\d{1,3}(?:,\s*[0-9\.]+)?\s*\)|[a-z]{3,20}|#[a-f0-9]{3,6})(?:\s+(\d{1,3}(?:\.\d+)?)(%|px)?)?(?:\s|$)/i; 20 | 21 | module.exports = GradientContainer; 22 | -------------------------------------------------------------------------------- /src/dummyimagecontainer.js: -------------------------------------------------------------------------------- 1 | var log = require('./log'); 2 | var smallImage = require('./utils').smallImage; 3 | 4 | function DummyImageContainer(src) { 5 | this.src = src; 6 | log("DummyImageContainer for", src); 7 | if (!this.promise || !this.image) { 8 | log("Initiating DummyImageContainer"); 9 | DummyImageContainer.prototype.image = new Image(); 10 | var image = this.image; 11 | DummyImageContainer.prototype.promise = new Promise(function(resolve, reject) { 12 | image.onload = resolve; 13 | image.onerror = reject; 14 | image.src = smallImage(); 15 | if (image.complete === true) { 16 | resolve(image); 17 | } 18 | }); 19 | } 20 | } 21 | 22 | module.exports = DummyImageContainer; 23 | -------------------------------------------------------------------------------- /tests/cases/zindex/z-index13.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | text above children with negative z-index; z-index tests #13 5 | 6 | 7 | 25 | 26 | 27 |
outer 28 |
29 |
30 | 31 | 32 | -------------------------------------------------------------------------------- /tests/cases/images/canvas.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Image tests 5 | 6 | 7 | 8 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /tests/cases/zindex/z-index6.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | z-index tests #6 5 | 6 | 7 | 27 | 28 | 29 |
z-index:0
30 |
default z-index
31 |
z-index:1
32 | 33 | 34 | -------------------------------------------------------------------------------- /tests/cases/zindex/z-index5.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | z-index tests #5 5 | 6 | 7 | 24 | 25 | 26 |
27 | LEVEL #1 28 |
29 |
30 |
31 | LEVEL #1 32 |
33 | 34 | 35 | -------------------------------------------------------------------------------- /src/svgnodecontainer.js: -------------------------------------------------------------------------------- 1 | var SVGContainer = require('./svgcontainer'); 2 | 3 | function SVGNodeContainer(node, _native) { 4 | this.src = node; 5 | this.image = null; 6 | var self = this; 7 | 8 | this.promise = _native ? new Promise(function(resolve, reject) { 9 | self.image = new Image(); 10 | self.image.onload = resolve; 11 | self.image.onerror = reject; 12 | self.image.src = "data:image/svg+xml," + (new XMLSerializer()).serializeToString(node); 13 | if (self.image.complete === true) { 14 | resolve(self.image); 15 | } 16 | }) : this.hasFabric().then(function() { 17 | return new Promise(function(resolve) { 18 | window.html2canvas.svg.fabric.parseSVGDocument(node, self.createCanvas.call(self, resolve)); 19 | }); 20 | }); 21 | } 22 | 23 | SVGNodeContainer.prototype = Object.create(SVGContainer.prototype); 24 | 25 | module.exports = SVGNodeContainer; 26 | -------------------------------------------------------------------------------- /tests/cases/visibility.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Visible elements tests 5 | 6 | 7 | 12 | 13 | 14 | 15 |

Display:none and visible:hidden tests

16 |
This should be visible
17 |
display:none, This should be hidden
18 |
visibility:hidden, This should be hidden
19 |
20 |
display:none, This should be hidden
21 |
visibility:hidden, This should be hidden
22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /tests/cases/zindex/z-index4.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | z-index tests #4 5 | 6 | 7 | 25 | 26 | 27 |
28 | LEVEL #1 29 |
30 |
31 |
32 | LEVEL #1 33 |
34 | 35 | 36 | -------------------------------------------------------------------------------- /tests/cases/zindex/z-index16.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Z-order positioning 5 | 14 | 15 | 16 | 17 |

18 | A butterfly image 21 | 22 |

24 | This text will overlay the butterfly image. 25 |
26 | 27 |
28 | This text will be beneath everything. 29 |
30 | 31 |
33 | This text will underlay text1, but overlay the butterfly image 34 |
35 | 36 | 37 | -------------------------------------------------------------------------------- /src/textcontainer.js: -------------------------------------------------------------------------------- 1 | var NodeContainer = require('./nodecontainer'); 2 | 3 | function TextContainer(node, parent) { 4 | NodeContainer.call(this, node, parent); 5 | } 6 | 7 | TextContainer.prototype = Object.create(NodeContainer.prototype); 8 | 9 | TextContainer.prototype.applyTextTransform = function() { 10 | this.node.data = this.transform(this.parent.css("textTransform")); 11 | }; 12 | 13 | TextContainer.prototype.transform = function(transform) { 14 | var text = this.node.data; 15 | switch(transform){ 16 | case "lowercase": 17 | return text.toLowerCase(); 18 | case "capitalize": 19 | return text.replace(/(^|\s|:|-|\(|\))([a-z])/g, capitalize); 20 | case "uppercase": 21 | return text.toUpperCase(); 22 | default: 23 | return text; 24 | } 25 | }; 26 | 27 | function capitalize(m, p1, p2) { 28 | if (m.length > 0) { 29 | return p1 + p2.toUpperCase(); 30 | } 31 | } 32 | 33 | module.exports = TextContainer; 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Niklas von Hertzen 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /tests/rangetest.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Range tests 5 | 6 | 11 | 12 | 13 |
14 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /tests/cases/border/dashed.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Borders tests 5 | 6 | 7 | 40 | 41 | 42 |
 
43 |
 
44 |
 
45 |
 
46 | 47 | 48 | -------------------------------------------------------------------------------- /tests/cases/border/dotted.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Borders tests 5 | 6 | 7 | 40 | 41 | 42 |
 
43 |
 
44 |
 
45 |
 
46 | 47 | 48 | -------------------------------------------------------------------------------- /tests/cases/border/double.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Borders tests 5 | 6 | 7 | 40 | 41 | 42 |
 
43 |
 
44 |
 
45 |
 
46 | 47 | 48 | -------------------------------------------------------------------------------- /tests/cases/border/solid.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Borders tests 5 | 6 | 7 | 40 | 41 | 42 |
 
43 |
 
44 |
 
45 |
 
46 | 47 | 48 | -------------------------------------------------------------------------------- /tests/cases/zindex/z-index12.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | Negative z-indexes 7 | 8 | 9 | 35 | 36 | 37 |
38 |
39 |

Div Element #2

40 |
41 |
42 |

Div Element #3

43 |
44 |
45 |
46 |
47 | 48 | -------------------------------------------------------------------------------- /tests/cases/google-maps.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | google maps 6 | 9 | 10 | 11 | 23 | 24 | 25 |
26 |

Google maps

27 |
28 | 39 |
40 | 41 | 42 | -------------------------------------------------------------------------------- /tests/cases/zindex/z-index11.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | static position inside position relative 7 | 8 | 9 | 30 | 31 | 32 | 33 |
34 |

Div Element #1

35 | position: relative; 36 |
37 |

Div Element #2

38 | position: static; 39 |
40 |
41 |

Div Element #3

42 | float: left; 43 |
44 |
45 |
46 |
47 | 48 | -------------------------------------------------------------------------------- /tests/cases/images/svg/inline.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Inline svg 5 | 6 | 7 | 8 | 9 |
10 | Inline svg image:
11 | 12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /tests/cases/images/images.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Image tests 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /tests/cases/images/svg/node.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | SVG node 5 | 6 | 7 | 8 |
9 | SVG node image:
10 | 11 | 12 | 13 | 16 | 17 | 18 |
19 | 20 | 21 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '4.0' 4 | env: 5 | global: 6 | - secure: eW41gIqOizwO4pTgWnAAbW75AP7F+CK9qfSed/fSh4sJ9HWMIY1YRIaY8gjr+6jV/f7XVHcXuym6ZxgINYSkVKbF1JKxBJNLOXtSgNbVHSic58pYFvUjwxIBI9aPig9uux1+DbnpWqXFDTcACJSevQZE0xwmjdrSkDLgB0G34v8= 7 | - secure: Y2Av+Gd3z9uQEB36GwdOOuGka0hx0/HeitASEo59z934O8RxnmN9eNTXS7dDT3XtKtwxIyLTOEpS7qlRdWahH28hr/dS4xJj6ao58C+1xMcDs6NAPGmDxUlcJWpcGEsnjmXjQCc3fBioSTdpIBrK/gdvgpNh77UKG74Sk7Z+YGk= 8 | - secure: YI+YbTOGf2x4fPMKW+KhJiZWswoXT6xOKGwLfsQsVwmFX1LerJouil5D5iYOQuL4FE3pNaoJSNakIsokJQuGKJMmnPc8rdhMZuBJBk6MRghurE2Xe9qBHfuUBPlfD61nARESm4WDcyMwM0QVYaOKeY6aIpZ91qbUbyc60EEx3C4= 9 | addons: 10 | sauce_connect: true 11 | before_script: 12 | - npm install -g grunt-cli 13 | - npm install -g uglify-js 14 | notifications: 15 | webhooks: 16 | urls: 17 | - https://webhooks.gitter.im/e/2b007d4f86de89588804 18 | on_success: always 19 | on_failure: always 20 | on_start: false 21 | deploy: 22 | - provider: npm 23 | email: niklasvh@gmail.com 24 | api_key: 25 | secure: G/Szpr8q4/D6hp+H/Z9yyluUXtHAwf7LLa1Y07X59/Enlj1h7V5fQ7AW4/iAVM3XbIsrCPWR3dJU9g/ZxpxFg4OovIHVpS2Jr/mahtPYWdHR3pWuSmMW8QD+Twnq2VAFwSgg5Oumq3QxhX3YbCOnZox6+6Uviqk8FO7Z5B0RwW4= 26 | on: 27 | tags: true 28 | branch: master 29 | repo: niklasvh/html2canvas 30 | -------------------------------------------------------------------------------- /tests/cases/text/shadow.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Text shadow tests 5 | 6 | 7 | 31 | 32 | 33 | 34 |
35 | Some text followed by text with shadow followed by more text without shadow. 36 | Multi text shadow and some more text without shadow 37 |
38 |
39 | testing with transparent 40 | testing with low opacity 41 |
42 | 43 | 44 | -------------------------------------------------------------------------------- /tests/cases/list/decimal-leading-zero.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | List tests 5 | 6 | 7 | 8 | 16 | 17 | 52 | 53 | 54 | 55 |
    56 | 57 | 58 | -------------------------------------------------------------------------------- /tests/cases/list/decimal.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | List tests 5 | 6 | 7 | 8 | 16 | 17 | 52 | 53 | 54 | 55 | 56 |
      57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /tests/cases/list/lower-alpha.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | List tests 5 | 6 | 7 | 8 | 16 | 17 | 52 | 53 | 54 | 55 | 56 |
        57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /tests/cases/list/upper-roman.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | List tests 5 | 6 | 7 | 8 | 16 | 17 | 52 | 53 | 54 | 55 | 56 |
          57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /tests/cases/border/inset.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Borders tests 5 | 6 | 7 | 49 | 50 | 51 |
           
          52 |
           
          53 |
           
          54 |
           
          55 |
           
          56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /tests/cases/text/underline-lineheight.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Text-decoration:underline tests 5 | 6 | 7 | 19 | 20 | 43 | 44 | 45 | 46 | Creating content through JavaScript 47 | 48 | 49 | -------------------------------------------------------------------------------- /tests/cases/text/underline.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Text-decoration:underline tests 5 | 6 | 7 | 19 | 20 | 44 | 45 | 46 | 47 | Creating content through JavaScript 48 | 49 | 50 | -------------------------------------------------------------------------------- /tests/cases/text/linethrough.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Text-decoration:line-through tests 5 | 6 | 7 | 18 | 19 | 44 | 45 | 46 | 47 | Creating content through JavaScript 48 | 49 | 50 | -------------------------------------------------------------------------------- /tests/cases/background/multi.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Background attribute tests 5 | 6 | 7 | 39 | 40 | 41 | 42 | 43 |
          44 |
          45 |
          46 |
          47 |
          48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /src/pseudoelementcontainer.js: -------------------------------------------------------------------------------- 1 | var NodeContainer = require('./nodecontainer'); 2 | 3 | function PseudoElementContainer(node, parent, type) { 4 | NodeContainer.call(this, node, parent); 5 | this.isPseudoElement = true; 6 | this.before = type === ":before"; 7 | } 8 | 9 | PseudoElementContainer.prototype.cloneTo = function(stack) { 10 | PseudoElementContainer.prototype.cloneTo.call(this, stack); 11 | stack.isPseudoElement = true; 12 | stack.before = this.before; 13 | }; 14 | 15 | PseudoElementContainer.prototype = Object.create(NodeContainer.prototype); 16 | 17 | PseudoElementContainer.prototype.appendToDOM = function() { 18 | if (this.before) { 19 | this.parent.node.insertBefore(this.node, this.parent.node.firstChild); 20 | } else { 21 | this.parent.node.appendChild(this.node); 22 | } 23 | this.parent.node.className += " " + this.getHideClass(); 24 | }; 25 | 26 | PseudoElementContainer.prototype.cleanDOM = function() { 27 | this.node.parentNode.removeChild(this.node); 28 | this.parent.node.className = this.parent.node.className.replace(this.getHideClass(), ""); 29 | }; 30 | 31 | PseudoElementContainer.prototype.getHideClass = function() { 32 | return this["PSEUDO_HIDE_ELEMENT_CLASS_" + (this.before ? "BEFORE" : "AFTER")]; 33 | }; 34 | 35 | PseudoElementContainer.prototype.PSEUDO_HIDE_ELEMENT_CLASS_BEFORE = "___html2canvas___pseudoelement_before"; 36 | PseudoElementContainer.prototype.PSEUDO_HIDE_ELEMENT_CLASS_AFTER = "___html2canvas___pseudoelement_after"; 37 | 38 | module.exports = PseudoElementContainer; 39 | -------------------------------------------------------------------------------- /src/framecontainer.js: -------------------------------------------------------------------------------- 1 | var utils = require('./utils'); 2 | var getBounds = utils.getBounds; 3 | var loadUrlDocument = require('./proxy').loadUrlDocument; 4 | 5 | function FrameContainer(container, sameOrigin, options) { 6 | this.image = null; 7 | this.src = container; 8 | var self = this; 9 | var bounds = getBounds(container); 10 | this.promise = (!sameOrigin ? this.proxyLoad(options.proxy, bounds, options) : new Promise(function(resolve) { 11 | if (container.contentWindow.document.URL === "about:blank" || container.contentWindow.document.documentElement == null) { 12 | container.contentWindow.onload = container.onload = function() { 13 | resolve(container); 14 | }; 15 | } else { 16 | resolve(container); 17 | } 18 | })).then(function(container) { 19 | var html2canvas = require('./core'); 20 | return html2canvas(container.contentWindow.document.documentElement, {type: 'view', width: container.width, height: container.height, proxy: options.proxy, javascriptEnabled: options.javascriptEnabled, removeContainer: options.removeContainer, allowTaint: options.allowTaint, imageTimeout: options.imageTimeout / 2}); 21 | }).then(function(canvas) { 22 | return self.image = canvas; 23 | }); 24 | } 25 | 26 | FrameContainer.prototype.proxyLoad = function(proxy, bounds, options) { 27 | var container = this.src; 28 | return loadUrlDocument(container.src, proxy, container.ownerDocument, bounds.width, bounds.height, options); 29 | }; 30 | 31 | module.exports = FrameContainer; 32 | -------------------------------------------------------------------------------- /tests/cases/images/svg/base64.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Base64 svg 5 | 6 | 7 | 8 | 9 |
          10 | Inline svg image:
          11 |
          12 | 13 | 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "html2canvas", 3 | "name": "html2canvas", 4 | "description": "Screenshots with JavaScript", 5 | "main": "dist/html2canvas.js", 6 | "version": "0.5.0-beta4", 7 | "author": { 8 | "name": "Niklas von Hertzen", 9 | "email": "niklasvh@gmail.com", 10 | "url": "http://hertzen.com" 11 | }, 12 | "engines": { 13 | "node": ">=4.0.0" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git@github.com:niklasvh/html2canvas.git" 18 | }, 19 | "bugs": { 20 | "url": "https://github.com/niklasvh/html2canvas/issues" 21 | }, 22 | "devDependencies": { 23 | "base64-arraybuffer": "^0.1.5", 24 | "bluebird": "^3.0.6", 25 | "browserify-derequire": "^0.9.4", 26 | "grunt": "^0.4.5", 27 | "grunt-browserify": "^4.0.1", 28 | "grunt-cli": "^0.1.13", 29 | "grunt-contrib-connect": "^0.11.2", 30 | "grunt-contrib-jshint": "^0.11.3", 31 | "grunt-contrib-uglify": "^0.11.0", 32 | "grunt-contrib-watch": "^0.6.1", 33 | "grunt-execute": "^0.2.2", 34 | "grunt-mocha-cli": "^1.12.0", 35 | "grunt-mocha-phantomjs": "^2.0.0", 36 | "html2canvas-proxy": "0.0.5", 37 | "humanize-duration": "^2.0.1", 38 | "lodash": "^3.10.1", 39 | "pngjs": "^2.2.0", 40 | "requirejs": "^2.1.20", 41 | "sauce-connect-launcher": "^0.13.0", 42 | "wd": "^0.4.0" 43 | }, 44 | "scripts": { 45 | "test": "grunt travis --verbose", 46 | "start": "grunt server", 47 | "sauceconnect": "tests/sauceconnect.js" 48 | }, 49 | "homepage": "http://html2canvas.hertzen.com", 50 | "license": "MIT" 51 | } 52 | -------------------------------------------------------------------------------- /src/font.js: -------------------------------------------------------------------------------- 1 | var smallImage = require('./utils').smallImage; 2 | 3 | function Font(family, size) { 4 | var container = document.createElement('div'), 5 | img = document.createElement('img'), 6 | span = document.createElement('span'), 7 | sampleText = 'Hidden Text', 8 | baseline, 9 | middle; 10 | 11 | container.style.visibility = "hidden"; 12 | container.style.fontFamily = family; 13 | container.style.fontSize = size; 14 | container.style.margin = 0; 15 | container.style.padding = 0; 16 | 17 | document.body.appendChild(container); 18 | 19 | img.src = smallImage(); 20 | img.width = 1; 21 | img.height = 1; 22 | 23 | img.style.margin = 0; 24 | img.style.padding = 0; 25 | img.style.verticalAlign = "baseline"; 26 | 27 | span.style.fontFamily = family; 28 | span.style.fontSize = size; 29 | span.style.margin = 0; 30 | span.style.padding = 0; 31 | 32 | span.appendChild(document.createTextNode(sampleText)); 33 | container.appendChild(span); 34 | container.appendChild(img); 35 | baseline = (img.offsetTop - span.offsetTop) + 1; 36 | 37 | container.removeChild(span); 38 | container.appendChild(document.createTextNode(sampleText)); 39 | 40 | container.style.lineHeight = "normal"; 41 | img.style.verticalAlign = "super"; 42 | 43 | middle = (img.offsetTop-container.offsetTop) + 1; 44 | 45 | document.body.removeChild(container); 46 | 47 | this.baseline = baseline; 48 | this.lineWidth = 1; 49 | this.middle = middle; 50 | } 51 | 52 | module.exports = Font; 53 | -------------------------------------------------------------------------------- /tests/cases/pseudoelements.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Pseudoelement tests 5 | 6 | 7 | 46 | 47 | 48 | 49 |
          50 | Content 1 51 | Content 2 52 |
          53 | 54 |
          55 | Content 1 56 | Content 2 57 |
          58 | 59 |
          60 | Content 1 61 | Content 2 62 |
          63 | 64 |
          65 | Content 1 66 | Content 2 67 |
          68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /src/support.js: -------------------------------------------------------------------------------- 1 | function Support(document) { 2 | this.rangeBounds = this.testRangeBounds(document); 3 | this.cors = this.testCORS(); 4 | this.svg = this.testSVG(); 5 | } 6 | 7 | Support.prototype.testRangeBounds = function(document) { 8 | var range, testElement, rangeBounds, rangeHeight, support = false; 9 | 10 | if (document.createRange) { 11 | range = document.createRange(); 12 | if (range.getBoundingClientRect) { 13 | testElement = document.createElement('boundtest'); 14 | testElement.style.height = "123px"; 15 | testElement.style.display = "block"; 16 | document.body.appendChild(testElement); 17 | 18 | range.selectNode(testElement); 19 | rangeBounds = range.getBoundingClientRect(); 20 | rangeHeight = rangeBounds.height; 21 | 22 | if (rangeHeight === 123) { 23 | support = true; 24 | } 25 | document.body.removeChild(testElement); 26 | } 27 | } 28 | 29 | return support; 30 | }; 31 | 32 | Support.prototype.testCORS = function() { 33 | return typeof((new Image()).crossOrigin) !== "undefined"; 34 | }; 35 | 36 | Support.prototype.testSVG = function() { 37 | var img = new Image(); 38 | var canvas = document.createElement("canvas"); 39 | var ctx = canvas.getContext("2d"); 40 | img.src = "data:image/svg+xml,"; 41 | 42 | try { 43 | ctx.drawImage(img, 0, 0); 44 | canvas.toDataURL(); 45 | } catch(e) { 46 | return false; 47 | } 48 | return true; 49 | }; 50 | 51 | module.exports = Support; 52 | -------------------------------------------------------------------------------- /tests/cases/transform/nested.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Nested transform tests 5 | 6 | 7 | 39 | 40 | 41 | 42 |
          First level content
          with second level content
          and third level content
          , ending second
          , ending first
          43 |
          something else
          44 | 45 | 46 | -------------------------------------------------------------------------------- /tests/cases/text/fontawesome.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | fontawesome icons 6 | 7 | 8 | 9 | 10 |
          11 | Fontawesome icons 12 |
          13 | fa-5x 14 | 15 |
            16 |
          • List icons
          • 17 |
          • can be used
          • 18 |
          • as bullets
          • 19 |
          • in lists
          • 20 |
          21 | 22 |
          23 | 24 | 25 | 26 | 27 | fa-twitter on fa-square-o
          28 | 29 | 30 | 31 | 32 | fa-flag on fa-circle
          33 | 34 | 35 | 36 | 37 | fa-terminal on fa-square
          38 | 39 | 40 | 41 | 42 | fa-ban on fa-camera 43 |
          44 |
          45 | 46 | 47 | -------------------------------------------------------------------------------- /examples/existing_canvas.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Using an existing canvas to draw on 6 | 19 | 20 | 21 |

          HTML content to render:

          22 |
          Render the content in this element only onto the existing canvas element
          23 |
          24 |

          Existing canvas:

          25 | 26 | 27 | 28 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /tests/cases/clip.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Inline text in the top element 5 | 6 | 7 | 19 | 20 | 21 | 22 |
          Some inline text followed by text in span followed by more inline text. 23 |

          Then a block level element.

          24 | Then more inline text.
          25 | 26 |
          Some inline text followed by text in span followed by more inline text. 27 |

          Then a block level element.

          28 | Then more inline text.
          29 | 30 |
          Some inline text followed by text in span followed by more inline text. 31 |

          Then a block level element.

          32 | Then more inline text.
          33 | 34 |
          Some inline text followed by text in span followed by more inline text. 35 |

          Then a block level element.

          36 | Then more inline text.
          37 | 38 | 39 |
          Some inline text followed by text in span followed by more inline text. 40 |

          Then a block level element.

          41 | Then more inline text.
          42 | 43 | 44 | -------------------------------------------------------------------------------- /tests/cases/transform/translate.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Nested transform tests 5 | 6 | 7 | 40 | 41 | 42 | 43 |
          First level content
          with second level content
          and third level content
          , ending second
          , ending first
          44 | 45 | 46 | -------------------------------------------------------------------------------- /examples/demo2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 19 | 20 | 38 | 39 | 40 |
          41 |
          42 |
          43 |

          Heading

          44 | Text that isn't wrapped in anything. 45 |

          Followed by some text wrapped in a <p> paragraph.

          46 | Maybe add a link or a different style of link with a highlight. 47 |
          48 |

          More content

          49 |
          a
          50 |
          51 | 52 |
          53 | 54 |
          55 |
          56 | 57 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /tests/cases/background/repeat.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Background attribute tests 5 | 6 | 7 | 39 | 40 | 41 | 42 | 43 |
          44 |
          45 |
          46 |
          47 |
          48 |
          49 |
          50 |
          51 |
          52 |
          53 |
          54 |
          55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /tests/cases/background/size.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Background size tests 5 | 6 | 7 | 40 | 41 | 42 |
          43 |
          44 |
          45 |
          46 |
          47 |
          48 |
          49 |
          50 |
          51 |
          52 | 53 |
          54 |
          55 |
          56 |
          57 |
          58 |
          59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /src/svgcontainer.js: -------------------------------------------------------------------------------- 1 | var XHR = require('./xhr'); 2 | var decode64 = require('./utils').decode64; 3 | 4 | function SVGContainer(src) { 5 | this.src = src; 6 | this.image = null; 7 | var self = this; 8 | 9 | this.promise = this.hasFabric().then(function() { 10 | return (self.isInline(src) ? Promise.resolve(self.inlineFormatting(src)) : XHR(src)); 11 | }).then(function(svg) { 12 | return new Promise(function(resolve) { 13 | window.html2canvas.svg.fabric.loadSVGFromString(svg, self.createCanvas.call(self, resolve)); 14 | }); 15 | }); 16 | } 17 | 18 | SVGContainer.prototype.hasFabric = function() { 19 | return !window.html2canvas.svg || !window.html2canvas.svg.fabric ? Promise.reject(new Error("html2canvas.svg.js is not loaded, cannot render svg")) : Promise.resolve(); 20 | }; 21 | 22 | SVGContainer.prototype.inlineFormatting = function(src) { 23 | return (/^data:image\/svg\+xml;base64,/.test(src)) ? this.decode64(this.removeContentType(src)) : this.removeContentType(src); 24 | }; 25 | 26 | SVGContainer.prototype.removeContentType = function(src) { 27 | return src.replace(/^data:image\/svg\+xml(;base64)?,/,''); 28 | }; 29 | 30 | SVGContainer.prototype.isInline = function(src) { 31 | return (/^data:image\/svg\+xml/i.test(src)); 32 | }; 33 | 34 | SVGContainer.prototype.createCanvas = function(resolve) { 35 | var self = this; 36 | return function (objects, options) { 37 | var canvas = new window.html2canvas.svg.fabric.StaticCanvas('c'); 38 | self.image = canvas.lowerCanvasEl; 39 | canvas 40 | .setWidth(options.width) 41 | .setHeight(options.height) 42 | .add(window.html2canvas.svg.fabric.util.groupSVGElements(objects, options)) 43 | .renderAll(); 44 | resolve(canvas.lowerCanvasEl); 45 | }; 46 | }; 47 | 48 | SVGContainer.prototype.decode64 = function(str) { 49 | return (typeof(window.atob) === "function") ? window.atob(str) : decode64(str); 50 | }; 51 | 52 | module.exports = SVGContainer; 53 | -------------------------------------------------------------------------------- /tests/utils.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var wd = require('wd'); 3 | var path = require('path'); 4 | var Promise = require('bluebird'); 5 | var _ = require('lodash'); 6 | 7 | Promise.promisifyAll(fs); 8 | 9 | var colors = { 10 | red: "\x1b[1;31m", 11 | blue: "\x1b[1;36m", 12 | violet: "\x1b[0;35m", 13 | green: "\x1b[0;32m", 14 | clear: "\x1b[0m" 15 | }; 16 | 17 | function isHtmlFile(filename) { 18 | return path.extname(filename) === '.html'; 19 | } 20 | 21 | function getTests(path) { 22 | return fs.readdirAsync(path).map(function(name) { 23 | var filename = path + "/" + name; 24 | return fs.statAsync(filename).then(function(stat) { 25 | return stat.isDirectory() ? getTests(filename) : filename; 26 | }); 27 | }).then(function(t) { 28 | return _.flatten(t).filter(isHtmlFile); 29 | }); 30 | } 31 | 32 | function initBrowser(settings) { 33 | var browser = wd.remote({ 34 | hostname: 'localhost', 35 | port: 4445, 36 | user: process.env.SAUCE_USERNAME, 37 | pwd: process.env.SAUCE_ACCESS_KEY 38 | }, 'promiseChain'); 39 | 40 | if (process.env.TRAVIS_JOB_NUMBER) { 41 | settings["tunnel-identifier"] = process.env.TRAVIS_JOB_NUMBER; 42 | settings["name"] = process.env.TRAVIS_COMMIT.substring(0, 10); 43 | settings["build"] = process.env.TRAVIS_BUILD_NUMBER; 44 | } else { 45 | settings["name"] = "Manual run"; 46 | } 47 | 48 | return browser.resolve(Promise).init(settings).then(function(b) { 49 | return Promise.resolve(b).disposer(function() { 50 | return browser.quit(); 51 | }); 52 | }); 53 | } 54 | 55 | function loadTestPage(browser, test, port) { 56 | return function(settings) { 57 | return browser.get("http://localhost:" + port + "/" + test + "?selenium").then(function() { 58 | return settings; 59 | }); 60 | }; 61 | } 62 | 63 | module.exports.colors = colors; 64 | module.exports.getTests = getTests; 65 | module.exports.initBrowser = initBrowser; 66 | module.exports.loadTestPage = loadTestPage; 67 | -------------------------------------------------------------------------------- /tests/cases/zindex/z-index1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | z-index tests #1 5 | 6 | 7 | 49 | 50 | 51 | 52 | 53 |
          54 |
          55 | 56 | No z-indexed content 57 |
          58 |
          DIV #1 59 |
          position: relative; 60 |
          61 |
          DIV #2 62 |
          position: absolute; 63 |
          z-index: 1; 64 |
          65 |
          66 | 67 |
          68 | 69 |
          70 |
          DIV #3 71 |
          position: relative; 72 |
          73 |
          DIV #4 74 |
          position: absolute; 75 |
          z-index: 2; 76 |
          77 |
          78 | Some more non-zindexed content 79 |
          80 | 81 | 82 | -------------------------------------------------------------------------------- /tests/cases/transform/rotate.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Rotation transform tests 5 | 6 | 7 | 41 | 42 |
          43 |
          44 | 45 |
          46 |
          47 | 48 |
          49 |
          50 | 51 |
          52 |
          53 | 54 | 55 | -------------------------------------------------------------------------------- /tests/cases/background/position.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Background attribute tests 5 | 6 | 7 | 39 | 40 | 41 | 42 | 43 |
          44 |
          45 |
          46 |
          47 |
          48 |
          49 | 50 | 51 |
          52 |
          53 |
          54 |
          55 |
          56 |
          57 | 58 |
          59 |
          60 |
          61 |
          62 |
          63 |
          64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /tests/cases/border/radius.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Borders tests 5 | 6 | 7 | 70 | 71 | 72 | 73 |
           
          74 |
           
          75 |
           
          76 |
           
          77 |
           
          78 |
           
          79 | 80 | 81 | -------------------------------------------------------------------------------- /tests/mocha/ie9-clonenode-bug.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Proxy tests 6 | 7 | 8 | 9 | 10 | 11 | 20 | 21 | 22 |
          23 | 24 | 25 |
          26 | 27 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /tests/cases/zindex/z-index8.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | Stacking and float 6 | 7 | 8 | 72 | 73 | 74 | 75 |

          76 | 77 |
          78 |
          DIV #1 79 |
          position: absolute; 80 |
          81 | 82 |
          83 |
          DIV #2 84 |
          float: left; 85 |
          86 | 87 |
          88 |
          DIV #3 89 |
          float: right; 90 |
          91 | 92 |
          93 | 94 |
          95 |
          DIV #4 96 |
          no positioning 97 |
          98 | 99 |
          100 |
          DIV #5 101 |
          position: absolute; 102 |
          103 | 104 | -------------------------------------------------------------------------------- /tests/cases/background/clip.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Background attribute tests 5 | 6 | 7 | 41 | 42 | 43 | 44 |
          45 |
          46 |
          47 |
          48 |
          49 |
          50 | 51 |
          52 |
          53 |
          54 |
          55 |
          56 |
          57 | 58 |
          59 |
          60 |
          61 |
          62 |
          63 |
          64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /tests/cases/zindex/z-index7.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | Stacking without z-index 6 | 7 | 8 | 72 | 73 | 74 | 75 |

          76 | 77 |
          78 |
          DIV #1 79 |
          position: absolute; 80 |
          81 | 82 |
          83 |
          DIV #2 84 |
          position: relative; 85 |
          86 | 87 |
          88 |
          DIV #3 89 |
          position: relative; 90 |
          91 | 92 |
          93 |
          DIV #4 94 |
          position: absolute; 95 |
          96 | 97 |
          98 |
          DIV #5 99 |
          no positioning 100 |
          101 | 102 | -------------------------------------------------------------------------------- /tests/cases/zindex/z-index18.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | z-index18 6 | 61 | 62 | 63 | 64 |
          65 |
          a
          66 |
          b
          67 |
          c
          68 |
          d
          69 |
          e
          70 |
          f
          71 |
          g
          72 |
          h
          73 |
          i
          74 |
          j
          75 |
          k
          76 |
          l
          77 |
          78 | 79 | 80 | -------------------------------------------------------------------------------- /tests/cases/text/chinese.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Chinese text 5 | 6 | 7 | 15 | 16 | 17 | 18 |
          19 |

              注    释 20 |

              〔1〕 见本书第一卷《实践论》注〔6〕。 21 |

              〔2〕 见列宁《党的组织和党的出版物》。列宁在这篇论文中说:“这将是自由的写作,因为把一批又一批新生力量吸引到写作队伍中来的,不是私利贪欲,也不是名誉地位,而是社会主义思想和对劳动人民的同情。这将是自由的写作,因为它不是为饱食终日的贵妇人服务,不是为百无聊赖、胖得发愁的‘一万个上层分子’服务,而是为千千万万劳动人民,为这些国家的精华、国家的力量、国家的未来服务。这将是自由的写作,它要用社会主义无产阶级的经验和生气勃勃的工作去丰富人类革命思想的最新成就,它要使过去的经验(从原始空想的社会主义发展而成的科学社会主义)和现在的经验(工人同志们当前的斗争)之间经常发生相互作用。”(《列宁全集》第12卷,人民出版社1987年版,第96—97页) 22 |

              〔3〕 梁实秋(一九○三——一九八七),北京人。新月社主要成员。先后在复旦大学、北京大学等校任教。曾写过一些文艺评论,长时期致力于文学翻译工作和散文的写作。鲁迅对梁实秋的批评,见《三闲集·新月社批评家的任务》、《二心集·“硬译”与“文学的阶级性”》等文。(《鲁迅全集》第4卷,人民文学出版社1981年版,第159、195—212页) 23 |

              〔4〕 周作人(一八八五——一九六七),浙江绍兴人。曾在北京大学、燕京大学等校任教。五四运动时从事新文学写作。他的著述很多,有大量的散文集、文学专著和翻译作品。张资平(一八九三——一九五九),广东梅县人。他写过很多小说,曾在暨南大学、大夏大学兼任教职。周作人、张资平于一九三八年和一九三九年先后在北平、上海依附侵略中国的日本占领者。 24 |

              〔5〕 见鲁迅《二心集·对于左翼作家联盟的意见》(《鲁迅全集》第4卷,人民文学出版社1981年版,第237—238页)。 25 |

              〔6〕 参见鲁迅《且介亭杂文末编·附集·死》(《鲁迅全集》第6卷,人民文学出版社1981年版,第612页)。 26 |

              〔7〕 “小放牛”是中国一出传统的小歌舞剧。全剧只有两个角色,男角是牧童,女角是乡村小姑娘,以互相对唱的方式表现剧的内容。抗日战争初期,革命的文艺工作者利用这个歌舞剧的形式,变动其原来的词句,宣传抗日,一时颇为流行。 27 |

              〔8〕 “人、手、口、刀、牛、羊”是笔画比较简单的汉字,旧时一些小学国语读本把这几个字编在第一册的最初几课里。 28 |

              〔9〕 “阳春白雪”和“下里巴人”,都是公元前三世纪楚国的歌曲。“阳春白雪”是供少数人欣赏的较高级的歌曲;“下里巴人”是流传很广的民间歌曲。《文选·宋玉对楚王问》记载一个故事,说有人在楚都唱歌,唱“阳春白雪”时,“国中属而和者(跟着唱的),不过数十人”;但唱“下里巴人”时,“国中属而和者数千人”。 29 |

              〔10〕 见列宁《党的组织和党的出版物》。列宁在这篇论文中说:“写作事业应当成为整个无产阶级事业的一部分,成为由整个工人阶级的整个觉悟的先锋队所开动的一部巨大的社会民主主义机器的‘齿轮和螺丝钉’。”(《列宁全集》第12卷,人民出版社1987年版,第93页) 30 |

              〔11〕 亭子间是上海里弄房子中的一种小房间,位置在房子后部的楼梯中侧,狭小黑暗,因此租金比较低廉。解放以前,贫苦的作家、艺术家、知识分子和机关小职员,多半租这种房间居住。 31 |

              〔12〕 见本书第二卷《和中央社、扫荡报、新民报三记者的谈话》注〔3〕。 32 |

              〔13〕 法捷耶夫(一九○一——一九五六),苏联名作家。他所作的小说《毁灭》于一九二七年出版,内容是描写苏联国内战争时期由苏联远东滨海边区工人、农民和革命知识分子所组成的一支游击队同国内反革命白卫军以及日本武装干涉军进行斗争的故事。这部小说曾由鲁迅译为汉文。 33 |

              〔14〕 见鲁迅《集外集·自嘲》(《鲁迅全集》第7卷,人民文学出版社1981年版,第147页)。

          34 |
          35 | 36 | 37 | -------------------------------------------------------------------------------- /tests/cases/zindex/z-index9.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | Adding z-index 5 | 6 | 7 | 73 | 74 | 75 | 76 |

          77 | 78 |
          79 |
          DIV #1 80 |
          position: absolute; 81 |
          z-index: 5; 82 |
          83 | 84 |
          85 |
          DIV #2 86 |
          position: relative; 87 |
          z-index: 3; 88 |
          89 | 90 |
          91 |
          DIV #3 92 |
          position: relative; 93 |
          z-index: 2; 94 |
          95 | 96 |
          97 |
          DIV #4 98 |
          position: absolute; 99 |
          z-index: 1; 100 |
          101 | 102 |
          103 |
          DIV #5 104 |
          no positioning 105 |
          z-index: 8; 106 |
          107 | 108 | -------------------------------------------------------------------------------- /tests/cases/zindex/z-index2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | z-index tests #2 5 | 6 | 7 | 70 | 71 | 72 | 73 |
          74 | 75 |
          76 |
          DIV #1 77 |
          position: relative; 78 |
          79 |
          DIV #2 80 |
          position: absolute; 81 |
          z-index: 2; 82 |
          83 |
          84 | 85 |
          86 | 87 |
          88 |
          DIV #3 89 |
          position: relative; 90 |
          z-index: 1; 91 |
          92 |
          DIV #4 93 |
          position: absolute; 94 |
          z-index: 10; 95 |
          96 |
          97 | 98 |

          DIV #5
          position:relative;
          99 | 100 |

          DIV #6
          position:static;
          101 | 102 | 103 | 104 | -------------------------------------------------------------------------------- /tests/cases/overflow.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Overflow tests 5 | 6 | 7 | 29 | 30 | 31 | 32 |

          Overflow: visible

          33 |
          Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum. 34 |
          35 | 36 |

          Overflow: hidden

          37 |
          38 | Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s 39 | 40 | with the release of
          a
          Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum. 41 | 42 | 43 |
          position:relative within a overflow:hidden element

          44 | 45 | Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum. 46 | 47 |
          48 |
          49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /tests/mocha/selenium.js: -------------------------------------------------------------------------------- 1 | var wd = require('wd'); 2 | var http = require("http"); 3 | var https = require("https"); 4 | var url = require("url"); 5 | var path = require("path"); 6 | var Promise = require('bluebird'); 7 | var _ = require('lodash'); 8 | var humanizeDuration = require("humanize-duration"); 9 | var utils = require('../utils'); 10 | var colors = utils.colors; 11 | var port = 8080; 12 | 13 | function runTestWithRetries(browser, test, retries) { 14 | retries = retries || 0; 15 | return runTest(browser, test) 16 | .timeout(30000) 17 | .catch(Promise.TimeoutError, function() { 18 | if (retries < 3) { 19 | console.log(colors.violet, "Retry", (retries + 1), test); 20 | return runTestWithRetries(browser, test, retries + 1); 21 | } else { 22 | throw new Error("Couldn't run test after 3 retries"); 23 | } 24 | }); 25 | } 26 | 27 | function getResults(browser) { 28 | return function() { 29 | return Promise.props({ 30 | dataUrl: browser.waitForElementByCss("body[data-complete='true']", 90000).then(function() { 31 | return browser.elementsByCssSelector('.test.fail'); 32 | }).then(function(nodes) { 33 | return Array.isArray(nodes) ? Promise.map(nodes, function(node) { 34 | return browser.text(node).then(function(error) { 35 | return Promise.reject(error); 36 | }); 37 | }) : Promise.resolve([]); 38 | }) 39 | }); 40 | }; 41 | } 42 | 43 | function runTest(browser, test) { 44 | return Promise.resolve(browser 45 | .then(utils.loadTestPage(browser, test, port)) 46 | .then(getResults(browser)) 47 | ).cancellable(); 48 | } 49 | 50 | exports.tests = function(browsers, singleTest) { 51 | var path = "tests/mocha"; 52 | return (singleTest ? Promise.resolve([singleTest]) : utils.getTests(path)).then(function(tests) { 53 | return Promise.map(browsers, function(settings) { 54 | var name = [settings.browserName, settings.version, settings.platform].join("-"); 55 | var count = 0; 56 | var browser = utils.initBrowser(settings); 57 | return Promise.using(browser, function() { 58 | return Promise.map(tests, function(test, index, total) { 59 | console.log(colors.green, "STARTING", "(" + (++count) + "/" + total + ")", name, test, colors.clear); 60 | var start = Date.now(); 61 | return runTestWithRetries(browser, test).then(function() { 62 | console.log(colors.green, "COMPLETE", humanizeDuration(Date.now() - start), "(" + count + "/" + total + ")", name, colors.clear); 63 | }); 64 | }, {concurrency: 1}) 65 | .settle() 66 | .catch(function(error) { 67 | console.error(colors.red, "ERROR", name, error); 68 | throw error; 69 | }); 70 | }); 71 | }, {concurrency: 3}); 72 | }); 73 | }; 74 | -------------------------------------------------------------------------------- /tests/cases/zindex/z-index3.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | z-index tests #3 5 | 6 | 7 | 57 | 58 | 59 | 60 |
          61 | 62 |
          63 | LEVEL #1 64 | 65 |
          66 | 67 |
          68 |
          LEVEL #2 69 |
          z-index: 1; 70 | 71 |
          72 | 73 |
          LEVEL #3
          74 |
          LEVEL #3
          75 |
          LEVEL #3
          76 |
          LEVEL #3
          77 |
          LEVEL #3
          78 |
          LEVEL #3
          79 |
          LEVEL #3
          80 |
          LEVEL #3
          81 |
          LEVEL #3
          82 |
          LEVEL #3
          83 |
          LEVEL #3
          84 | 85 |
          86 | 87 |
          88 | 89 |
          90 |
          LEVEL #2 91 |
          z-index: 1; 92 |
          93 | 94 |
          95 |
          96 | 97 |
          98 | LEVEL #1 99 |
          100 | 101 |
          102 | LEVEL #1 103 |
          104 | 105 |
          106 | LEVEL #1 107 |
          108 | 109 | 110 | -------------------------------------------------------------------------------- /tests/cases/zindex/z-index10.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 8 | Understanding CSS z-index: The Stacking Context: Example Source 9 | 10 | 11 | 77 | 78 | 79 | 80 | 81 |
          82 |

          Division Element #1

          83 | position: relative;
          84 | z-index: 5;
          85 |
          86 | 87 |
          88 |

          Division Element #2

          89 | position: relative;
          90 | z-index: 2;
          91 |
          92 | 93 |
          94 | 95 |
          96 |

          Division Element #4

          97 | position: relative;
          98 | z-index: 6;
          99 |
          100 | 101 |

          Division Element #3

          102 | position: absolute;
          103 | z-index: 4;
          104 | 105 |
          106 |

          Division Element #5

          107 | position: relative;
          108 | z-index: 1;
          109 |
          110 | 111 |
          112 |

          Division Element #6

          113 | position: absolute;
          114 | z-index: 3;
          115 |
          116 | 117 |
          118 | 119 | 120 | -------------------------------------------------------------------------------- /tests/mocha/proxy.htm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Proxy tests 6 | 7 | 8 | 9 | 10 | 11 | 17 | 18 | 19 |
          20 | 21 |
          22 | 23 |
          24 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /tests/cases/images/svg/native_only.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Native svg only 5 | 6 | 9 | 10 | 11 | 12 |
          13 | 14 | 15 | 16 | 17 | 18 | 21 | 22 | 23 |
          24 | 25 | 26 | -------------------------------------------------------------------------------- /src/proxy.js: -------------------------------------------------------------------------------- 1 | var XHR = require('./xhr'); 2 | var utils = require('./utils'); 3 | var log = require('./log'); 4 | var createWindowClone = require('./clone'); 5 | var decode64 = utils.decode64; 6 | 7 | function Proxy(src, proxyUrl, document) { 8 | var supportsCORS = ('withCredentials' in new XMLHttpRequest()); 9 | if (!proxyUrl) { 10 | return Promise.reject("No proxy configured"); 11 | } 12 | var callback = createCallback(supportsCORS); 13 | var url = createProxyUrl(proxyUrl, src, callback); 14 | 15 | return supportsCORS ? XHR(url) : (jsonp(document, url, callback).then(function(response) { 16 | return decode64(response.content); 17 | })); 18 | } 19 | var proxyCount = 0; 20 | 21 | function ProxyURL(src, proxyUrl, document) { 22 | var supportsCORSImage = ('crossOrigin' in new Image()); 23 | var callback = createCallback(supportsCORSImage); 24 | var url = createProxyUrl(proxyUrl, src, callback); 25 | return (supportsCORSImage ? Promise.resolve(url) : jsonp(document, url, callback).then(function(response) { 26 | return "data:" + response.type + ";base64," + response.content; 27 | })); 28 | } 29 | 30 | function jsonp(document, url, callback) { 31 | return new Promise(function(resolve, reject) { 32 | var s = document.createElement("script"); 33 | var cleanup = function() { 34 | delete window.html2canvas.proxy[callback]; 35 | document.body.removeChild(s); 36 | }; 37 | window.html2canvas.proxy[callback] = function(response) { 38 | cleanup(); 39 | resolve(response); 40 | }; 41 | s.src = url; 42 | s.onerror = function(e) { 43 | cleanup(); 44 | reject(e); 45 | }; 46 | document.body.appendChild(s); 47 | }); 48 | } 49 | 50 | function createCallback(useCORS) { 51 | return !useCORS ? "html2canvas_" + Date.now() + "_" + (++proxyCount) + "_" + Math.round(Math.random() * 100000) : ""; 52 | } 53 | 54 | function createProxyUrl(proxyUrl, src, callback) { 55 | return proxyUrl + "?url=" + encodeURIComponent(src) + (callback.length ? "&callback=html2canvas.proxy." + callback : ""); 56 | } 57 | 58 | function documentFromHTML(src) { 59 | return function(html) { 60 | var parser = new DOMParser(), doc; 61 | try { 62 | doc = parser.parseFromString(html, "text/html"); 63 | } catch(e) { 64 | log("DOMParser not supported, falling back to createHTMLDocument"); 65 | doc = document.implementation.createHTMLDocument(""); 66 | try { 67 | doc.open(); 68 | doc.write(html); 69 | doc.close(); 70 | } catch(ee) { 71 | log("createHTMLDocument write not supported, falling back to document.body.innerHTML"); 72 | doc.body.innerHTML = html; // ie9 doesnt support writing to documentElement 73 | } 74 | } 75 | 76 | var b = doc.querySelector("base"); 77 | if (!b || !b.href.host) { 78 | var base = doc.createElement("base"); 79 | base.href = src; 80 | doc.head.insertBefore(base, doc.head.firstChild); 81 | } 82 | 83 | return doc; 84 | }; 85 | } 86 | 87 | function loadUrlDocument(src, proxy, document, width, height, options) { 88 | return new Proxy(src, proxy, window.document).then(documentFromHTML(src)).then(function(doc) { 89 | return createWindowClone(doc, document, width, height, options, 0, 0); 90 | }); 91 | } 92 | 93 | exports.Proxy = Proxy; 94 | exports.ProxyURL = ProxyURL; 95 | exports.loadUrlDocument = loadUrlDocument; 96 | -------------------------------------------------------------------------------- /tests/mocha/multiple-renders.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Mocha Tests 5 | 6 | 7 | 8 | 9 | 10 | 11 | 22 | 23 | 24 |
          25 |
          26 |
          27 | 28 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /tests/cases/forms.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Form tests 5 | 6 | 7 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
          27 | 32 | 35 | 38 | 39 | 40 | 45 |
          46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 |
          56 | 57 | 58 | 59 | 60 | 61 | 62 |
          63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 |
          80 | 81 | 82 | -------------------------------------------------------------------------------- /tests/mocha/options.onclone.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Mocha Tests 5 | 6 | 7 | 8 | 9 | 10 | 11 | 20 | 21 | 22 |
          23 | 24 |
          25 |
          26 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /tests/node/color.js: -------------------------------------------------------------------------------- 1 | var Color = require('../../src/color'); 2 | var assert = require('assert'); 3 | 4 | describe("Colors", function() { 5 | describe("named colors", function() { 6 | it("bisque", function () { 7 | var c = new Color("bisque"); 8 | assertColor(c, 255, 228, 196, null); 9 | assert.equal(c.isTransparent(), false); 10 | }); 11 | 12 | it("BLUE", function () { 13 | var c = new Color("BLUE"); 14 | assertColor(c, 0, 0, 255, null); 15 | assert.equal(c.isTransparent(), false); 16 | }); 17 | }); 18 | 19 | describe("rgb()", function() { 20 | it("rgb(1,3,5)", function () { 21 | var c = new Color("rgb(1,3,5)"); 22 | assertColor(c, 1, 3, 5, null); 23 | assert.equal(c.isTransparent(), false); 24 | }); 25 | 26 | it("rgb(222, 111, 50)", function () { 27 | var c = new Color("rgb(222, 111, 50)"); 28 | assertColor(c, 222, 111, 50, null); 29 | assert.equal(c.isTransparent(), false); 30 | }); 31 | 32 | it("rgb( 222, 111 , 50)", function () { 33 | var c = new Color("rgb(222 , 111 , 50)"); 34 | assertColor(c, 222, 111, 50, null); 35 | assert.equal(c.isTransparent(), false); 36 | }); 37 | }); 38 | 39 | describe("rgba()", function() { 40 | it("rgba(200,3,5,1)", function () { 41 | var c = new Color("rgba(200,3,5,1)"); 42 | assertColor(c, 200, 3, 5, 1); 43 | assert.equal(c.isTransparent(), false); 44 | }); 45 | 46 | it("rgba(222, 111, 50, 0.22)", function () { 47 | var c = new Color("rgba(222, 111, 50, 0.22)"); 48 | assertColor(c, 222, 111, 50, 0.22); 49 | assert.equal(c.isTransparent(), false); 50 | }); 51 | 52 | it("rgba( 222, 111 , 50, 0.123 )", function () { 53 | var c = new Color("rgba(222 , 111 , 50, 0.123)"); 54 | assertColor(c, 222, 111, 50, 0.123); 55 | assert.equal(c.isTransparent(), false); 56 | }); 57 | }); 58 | 59 | describe("hex", function() { 60 | it("#7FFFD4", function () { 61 | var c = new Color("#7FFFD4"); 62 | assertColor(c, 127, 255, 212, null); 63 | assert.equal(c.isTransparent(), false); 64 | }); 65 | 66 | it("#f0ffff", function () { 67 | var c = new Color("#f0ffff"); 68 | assertColor(c, 240, 255, 255, null); 69 | assert.equal(c.isTransparent(), false); 70 | }); 71 | 72 | it("#fff", function () { 73 | var c = new Color("#fff"); 74 | assertColor(c, 255, 255, 255, null); 75 | assert.equal(c.isTransparent(), false); 76 | }); 77 | }); 78 | 79 | describe("from array", function() { 80 | it("[1,2,3]", function () { 81 | var c = new Color([1,2,3]); 82 | assertColor(c, 1, 2, 3, null); 83 | assert.equal(c.isTransparent(), false); 84 | }); 85 | 86 | it("[5,6,7,1]", function () { 87 | var c = new Color([5,6,7, 1]); 88 | assertColor(c, 5, 6, 7, 1); 89 | assert.equal(c.isTransparent(), false); 90 | }); 91 | 92 | it("[5,6,7,0]", function () { 93 | var c = new Color([5,6,7, 0]); 94 | assertColor(c, 5, 6, 7, 0); 95 | assert.equal(c.isTransparent(), true); 96 | }); 97 | }); 98 | 99 | describe("transparency", function() { 100 | it("transparent", function () { 101 | var c = new Color("transparent"); 102 | assertColor(c, 0, 0, 0, 0); 103 | assert.equal(c.isTransparent(), true); 104 | }); 105 | 106 | it("rgba(255,255,255,0)", function () { 107 | var c = new Color("rgba(255,255,255,0)"); 108 | assertColor(c, 255, 255, 255, 0); 109 | assert.equal(c.isTransparent(), true); 110 | }); 111 | }); 112 | }); 113 | 114 | function assertColor(c, r, g, b, a) { 115 | assert.equal(c.r, r); 116 | assert.equal(c.g, g); 117 | assert.equal(c.b, b); 118 | assert.equal(c.a, a); 119 | } 120 | -------------------------------------------------------------------------------- /src/lineargradientcontainer.js: -------------------------------------------------------------------------------- 1 | var GradientContainer = require('./gradientcontainer'); 2 | var Color = require('./color'); 3 | 4 | function LinearGradientContainer(imageData) { 5 | GradientContainer.apply(this, arguments); 6 | this.type = GradientContainer.TYPES.LINEAR; 7 | 8 | var hasDirection = LinearGradientContainer.REGEXP_DIRECTION.test( imageData.args[0] ) || 9 | !GradientContainer.REGEXP_COLORSTOP.test( imageData.args[0] ); 10 | 11 | if (hasDirection) { 12 | imageData.args[0].split(/\s+/).reverse().forEach(function(position, index) { 13 | switch(position) { 14 | case "left": 15 | this.x0 = 0; 16 | this.x1 = 1; 17 | break; 18 | case "top": 19 | this.y0 = 0; 20 | this.y1 = 1; 21 | break; 22 | case "right": 23 | this.x0 = 1; 24 | this.x1 = 0; 25 | break; 26 | case "bottom": 27 | this.y0 = 1; 28 | this.y1 = 0; 29 | break; 30 | case "to": 31 | var y0 = this.y0; 32 | var x0 = this.x0; 33 | this.y0 = this.y1; 34 | this.x0 = this.x1; 35 | this.x1 = x0; 36 | this.y1 = y0; 37 | break; 38 | case "center": 39 | break; // centered by default 40 | // Firefox internally converts position keywords to percentages: 41 | // http://www.w3.org/TR/2010/WD-CSS2-20101207/colors.html#propdef-background-position 42 | default: // percentage or absolute length 43 | // TODO: support absolute start point positions (e.g., use bounds to convert px to a ratio) 44 | var ratio = parseFloat(position, 10) * 1e-2; 45 | if (isNaN(ratio)) { // invalid or unhandled value 46 | break; 47 | } 48 | if (index === 0) { 49 | this.y0 = ratio; 50 | this.y1 = 1 - this.y0; 51 | } else { 52 | this.x0 = ratio; 53 | this.x1 = 1 - this.x0; 54 | } 55 | break; 56 | } 57 | }, this); 58 | } else { 59 | this.y0 = 0; 60 | this.y1 = 1; 61 | } 62 | 63 | this.colorStops = imageData.args.slice(hasDirection ? 1 : 0).map(function(colorStop) { 64 | var colorStopMatch = colorStop.match(GradientContainer.REGEXP_COLORSTOP); 65 | var value = +colorStopMatch[2]; 66 | var unit = value === 0 ? "%" : colorStopMatch[3]; // treat "0" as "0%" 67 | return { 68 | color: new Color(colorStopMatch[1]), 69 | // TODO: support absolute stop positions (e.g., compute gradient line length & convert px to ratio) 70 | stop: unit === "%" ? value / 100 : null 71 | }; 72 | }); 73 | 74 | if (this.colorStops[0].stop === null) { 75 | this.colorStops[0].stop = 0; 76 | } 77 | 78 | if (this.colorStops[this.colorStops.length - 1].stop === null) { 79 | this.colorStops[this.colorStops.length - 1].stop = 1; 80 | } 81 | 82 | // calculates and fills-in explicit stop positions when omitted from rule 83 | this.colorStops.forEach(function(colorStop, index) { 84 | if (colorStop.stop === null) { 85 | this.colorStops.slice(index).some(function(find, count) { 86 | if (find.stop !== null) { 87 | colorStop.stop = ((find.stop - this.colorStops[index - 1].stop) / (count + 1)) + this.colorStops[index - 1].stop; 88 | return true; 89 | } else { 90 | return false; 91 | } 92 | }, this); 93 | } 94 | }, this); 95 | } 96 | 97 | LinearGradientContainer.prototype = Object.create(GradientContainer.prototype); 98 | 99 | // TODO: support (e.g. -?\d{1,3}(?:\.\d+)deg, etc. : https://developer.mozilla.org/docs/Web/CSS/angle ) 100 | LinearGradientContainer.REGEXP_DIRECTION = /^\s*(?:to|left|right|top|bottom|center|\d{1,3}(?:\.\d+)?%?)(?:\s|$)/i; 101 | 102 | module.exports = LinearGradientContainer; 103 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### Changelog ### 2 | 3 | v0.5.0-beta4 - 23.1.2016 4 | * Fix logger requiring access to window object 5 | * Derequire browserify build 6 | * Fix rendering of specific elements when window is scrolled and `type` isn't set to `view` 7 | 8 | v0.5.0-beta3 - 6.12.2015 9 | * Handle color names in linear gradients 10 | 11 | v0.5.0-beta2 - 20.10.2015 12 | * Remove Promise polyfill (use native or provide it yourself) 13 | 14 | v0.5.0-beta1 - 19.10.2015 15 | * Fix bug with unmatched color stops in gradients 16 | * Fix scrolling issues with iOS 17 | * Correctly handle named colors in gradients 18 | * Accept matrix3d transforms 19 | * Fix transparent colors breaking gradients 20 | * Preserve scrolling positions on render 21 | 22 | v0.5.0-alpha2 - 3.2.2015 23 | * Switch to using browserify for building 24 | * Fix (#517) Chrome stretches background images with 'auto' or single attributes 25 | 26 | v0.5.0-alpha - 19.1.2015 27 | * Complete rewrite of library 28 | * Switched interface to return Promise 29 | * Uses hidden iframe window to perform rendering, allowing async rendering and doesn't force the viewport to be scrolled to the top anymore. 30 | * Better support for unicode 31 | * Checkbox/radio button rendering 32 | * SVG rendering 33 | * iframe rendering 34 | * Changed format for proxy requests, permitting binary responses with CORS headers as well 35 | * Fixed many layering issues (see z-index tests) 36 | 37 | v0.4.1 - 7.9.2013 38 | * Added support for bower 39 | * Improved z-index ordering 40 | * Basic implementation for CSS transformations 41 | * Fixed inline text in top element 42 | * Basic implementation for text-shadow 43 | 44 | v0.4.0 - 30.1.2013 45 | * Added rendering tests with webdriver 46 | * Switched to using grunt for building 47 | * Removed support for IE<9, including any FlashCanvas bits 48 | * Support for border-radius 49 | * Support for multiple background images, size, and clipping 50 | * Support for :before and :after pseudo elements 51 | * Support for placeholder rendering 52 | * Reformatted all tests to small units to test specific features 53 | 54 | v0.3.4 - 26.6.2012 55 | 56 | * Removed (last?) jQuery dependencies (niklasvh) 57 | * SVG-powered rendering (niklasvh) 58 | * Radial gradients (SunboX) 59 | * Split renderers to their own objects (niklasvh) 60 | * Simplified API, cleaned up code (niklasvh) 61 | 62 | v0.3.3 - 2.3.2012 63 | 64 | * SVG taint fix, and additional taint testing options for rendering (niklasvh) 65 | * Added support for CORS images and option to create canvas as tainted (niklasvh) 66 | * Improved minification saved ~1K! (cobexer) 67 | * Added integrated support for Flashcanvas (niklasvh) 68 | * Fixed a variety of legacy IE bugs (niklasvh) 69 | 70 | v0.3.2 - 20.2.2012 71 | 72 | * Added changelog! 73 | * Added bookmarklet (cobexer) 74 | * Option to select single element to render (niklasvh) 75 | * Fixed closure compiler warnings (cobexer) 76 | * Enable profiling in FF (cobexer) 77 | -------------------------------------------------------------------------------- /src/clone.js: -------------------------------------------------------------------------------- 1 | var log = require('./log'); 2 | 3 | function restoreOwnerScroll(ownerDocument, x, y) { 4 | if (ownerDocument.defaultView && (x !== ownerDocument.defaultView.pageXOffset || y !== ownerDocument.defaultView.pageYOffset)) { 5 | ownerDocument.defaultView.scrollTo(x, y); 6 | } 7 | } 8 | 9 | function cloneCanvasContents(canvas, clonedCanvas) { 10 | try { 11 | if (clonedCanvas) { 12 | clonedCanvas.width = canvas.width; 13 | clonedCanvas.height = canvas.height; 14 | clonedCanvas.getContext("2d").putImageData(canvas.getContext("2d").getImageData(0, 0, canvas.width, canvas.height), 0, 0); 15 | } 16 | } catch(e) { 17 | log("Unable to copy canvas content from", canvas, e); 18 | } 19 | } 20 | 21 | function cloneNode(node, javascriptEnabled) { 22 | var clone = node.nodeType === 3 ? document.createTextNode(node.nodeValue) : node.cloneNode(false); 23 | 24 | var child = node.firstChild; 25 | while(child) { 26 | if (javascriptEnabled === true || child.nodeType !== 1 || child.nodeName !== 'SCRIPT') { 27 | clone.appendChild(cloneNode(child, javascriptEnabled)); 28 | } 29 | child = child.nextSibling; 30 | } 31 | 32 | if (node.nodeType === 1) { 33 | clone._scrollTop = node.scrollTop; 34 | clone._scrollLeft = node.scrollLeft; 35 | if (node.nodeName === "CANVAS") { 36 | cloneCanvasContents(node, clone); 37 | } else if (node.nodeName === "TEXTAREA" || node.nodeName === "SELECT") { 38 | clone.value = node.value; 39 | } 40 | } 41 | 42 | return clone; 43 | } 44 | 45 | function initNode(node) { 46 | if (node.nodeType === 1) { 47 | node.scrollTop = node._scrollTop; 48 | node.scrollLeft = node._scrollLeft; 49 | 50 | var child = node.firstChild; 51 | while(child) { 52 | initNode(child); 53 | child = child.nextSibling; 54 | } 55 | } 56 | } 57 | 58 | module.exports = function(ownerDocument, containerDocument, width, height, options, x ,y) { 59 | var documentElement = cloneNode(ownerDocument.documentElement, options.javascriptEnabled); 60 | var container = containerDocument.createElement("iframe"); 61 | 62 | container.className = "html2canvas-container"; 63 | container.style.visibility = "hidden"; 64 | container.style.position = "fixed"; 65 | container.style.left = "-10000px"; 66 | container.style.top = "0px"; 67 | container.style.border = "0"; 68 | container.width = width; 69 | container.height = height; 70 | container.scrolling = "no"; // ios won't scroll without it 71 | containerDocument.body.appendChild(container); 72 | 73 | return new Promise(function(resolve) { 74 | var documentClone = container.contentWindow.document; 75 | 76 | /* Chrome doesn't detect relative background-images assigned in inline 128 | 129 | 130 |
          131 |
          132 | toggle 133 |
          134 |
          135 |
            136 |
          • 137 | the way 138 |
          • 139 |
          • 140 |

            141 | the world ends 142 |

            143 |
            144 |

            145 | bang 146 | 147 |

            148 |

            149 | whimper 150 | 151 |

            152 |
            153 |
          • 154 |
          • 155 | i grow old 156 |
          • 157 |
          • 158 | pluot? 159 |
          • 160 |
          161 |
          162 |
          163 | bar maids, 164 |
          165 |
          166 |

          167 | sing to me, erbarme dich 168 |

          169 |
          170 |
          171 |

          172 | This is a nonsensical document, but syntactically valid HTML 4.0. All 100% conformant CSS1 agents should be able to render the document elements above this paragraph indistinguishably (to the pixel) from this reference rendering, (except font rasterization and form widgets). All discrepancies should be traceable to CSS1 implementation shortcomings. Once you have finished evaluating this test, you can return to the parent page. 173 |

          174 | 175 | 180 | 181 | 182 | -------------------------------------------------------------------------------- /tests/cases/background/encoded.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Background attribute tests 5 | 6 | 7 | 43 | 44 | 45 | 46 | 47 |
          48 |
          49 |
          50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /tests/mocha/background.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Mocha Tests 5 | 6 | 7 | 8 | 9 | 10 | 11 | 34 | 35 | 36 |
          37 | 38 |
          39 |
          40 |
          41 |
          42 | 135 | 136 | 137 | -------------------------------------------------------------------------------- /src/renderer.js: -------------------------------------------------------------------------------- 1 | var log = require('./log'); 2 | 3 | function Renderer(width, height, images, options, document) { 4 | this.width = width; 5 | this.height = height; 6 | this.images = images; 7 | this.options = options; 8 | this.document = document; 9 | } 10 | 11 | Renderer.prototype.renderImage = function(container, bounds, borderData, imageContainer) { 12 | var paddingLeft = container.cssInt('paddingLeft'), 13 | paddingTop = container.cssInt('paddingTop'), 14 | paddingRight = container.cssInt('paddingRight'), 15 | paddingBottom = container.cssInt('paddingBottom'), 16 | borders = borderData.borders; 17 | 18 | var width = bounds.width - (borders[1].width + borders[3].width + paddingLeft + paddingRight); 19 | var height = bounds.height - (borders[0].width + borders[2].width + paddingTop + paddingBottom); 20 | this.drawImage( 21 | imageContainer, 22 | 0, 23 | 0, 24 | imageContainer.image.width || width, 25 | imageContainer.image.height || height, 26 | bounds.left + paddingLeft + borders[3].width, 27 | bounds.top + paddingTop + borders[0].width, 28 | width, 29 | height 30 | ); 31 | }; 32 | 33 | Renderer.prototype.renderBackground = function(container, bounds, borderData) { 34 | if (bounds.height > 0 && bounds.width > 0) { 35 | this.renderBackgroundColor(container, bounds); 36 | this.renderBackgroundImage(container, bounds, borderData); 37 | } 38 | }; 39 | 40 | Renderer.prototype.renderBackgroundColor = function(container, bounds) { 41 | var color = container.color("backgroundColor"); 42 | if (!color.isTransparent()) { 43 | this.rectangle(bounds.left, bounds.top, bounds.width, bounds.height, color); 44 | } 45 | }; 46 | 47 | Renderer.prototype.renderShadows = function(container, shape) { 48 | var boxShadow = container.css('boxShadow'); 49 | if (boxShadow !== 'none') { 50 | var shadows = boxShadow.split(/,(?![^(]*\))/); 51 | this.shadow(shape, shadows); 52 | } 53 | }; 54 | 55 | Renderer.prototype.renderBorders = function(borders) { 56 | borders.forEach(this.renderBorder, this); 57 | }; 58 | 59 | Renderer.prototype.renderBorder = function(data) { 60 | if (!data.color.isTransparent() && data.args !== null) { 61 | if (data.style === 'dashed' || data.style === 'dotted') { 62 | var dash = (data.style === 'dashed') ? 3 : data.width; 63 | this.ctx.setLineDash([dash]); 64 | this.path(data.pathArgs); 65 | this.ctx.strokeStyle = data.color; 66 | this.ctx.lineWidth = data.width; 67 | this.ctx.stroke(); 68 | } else { 69 | this.drawShape(data.args, data.color); 70 | } 71 | } 72 | }; 73 | 74 | Renderer.prototype.renderBackgroundImage = function(container, bounds, borderData) { 75 | var backgroundImages = container.parseBackgroundImages(); 76 | backgroundImages.reverse().forEach(function(backgroundImage, index, arr) { 77 | switch(backgroundImage.method) { 78 | case "url": 79 | var image = this.images.get(backgroundImage.args[0]); 80 | if (image) { 81 | this.renderBackgroundRepeating(container, bounds, image, arr.length - (index+1), borderData); 82 | } else { 83 | log("Error loading background-image", backgroundImage.args[0]); 84 | } 85 | break; 86 | case "linear-gradient": 87 | case "gradient": 88 | var gradientImage = this.images.get(backgroundImage.value); 89 | if (gradientImage) { 90 | this.renderBackgroundGradient(gradientImage, bounds, borderData); 91 | } else { 92 | log("Error loading background-image", backgroundImage.args[0]); 93 | } 94 | break; 95 | case "none": 96 | break; 97 | default: 98 | log("Unknown background-image type", backgroundImage.args[0]); 99 | } 100 | }, this); 101 | }; 102 | 103 | Renderer.prototype.renderBackgroundRepeating = function(container, bounds, imageContainer, index, borderData) { 104 | var size = container.parseBackgroundSize(bounds, imageContainer.image, index); 105 | var position = container.parseBackgroundPosition(bounds, imageContainer.image, index, size); 106 | var repeat = container.parseBackgroundRepeat(index); 107 | switch (repeat) { 108 | case "repeat-x": 109 | case "repeat no-repeat": 110 | this.backgroundRepeatShape(imageContainer, position, size, bounds, bounds.left + borderData[3], bounds.top + position.top + borderData[0], 99999, size.height, borderData); 111 | break; 112 | case "repeat-y": 113 | case "no-repeat repeat": 114 | this.backgroundRepeatShape(imageContainer, position, size, bounds, bounds.left + position.left + borderData[3], bounds.top + borderData[0], size.width, 99999, borderData); 115 | break; 116 | case "no-repeat": 117 | this.backgroundRepeatShape(imageContainer, position, size, bounds, bounds.left + position.left + borderData[3], bounds.top + position.top + borderData[0], size.width, size.height, borderData); 118 | break; 119 | default: 120 | this.renderBackgroundRepeat(imageContainer, position, size, {top: bounds.top, left: bounds.left}, borderData[3], borderData[0]); 121 | break; 122 | } 123 | }; 124 | 125 | module.exports = Renderer; 126 | -------------------------------------------------------------------------------- /tests/mocha/gradients.js: -------------------------------------------------------------------------------- 1 | describe("Gradients", function() { 2 | var expected = [ 3 | { 4 | method: "linear-gradient", 5 | args: [ 6 | "left", 7 | " rgb(255, 0, 0)", 8 | " rgb(255, 255, 0)", 9 | " rgb(0, 255, 0)" 10 | ] 11 | }, 12 | { 13 | method: "linear-gradient", 14 | args: [ 15 | "left", 16 | " red", 17 | " rgb(255, 255, 0)", 18 | " rgb(0, 255, 0)" 19 | ] 20 | }, 21 | { 22 | method: 'linear-gradient', 23 | args: [ 24 | "left", 25 | " rgb(206, 219, 233) 0%", 26 | " rgb(170, 197, 222) 17%", 27 | " rgb(97, 153, 199) 50%", 28 | " rgb(58, 132, 195) 51%", 29 | " rgb(65, 154, 214) 59%", 30 | " rgb(75, 184, 240) 71%", 31 | " rgb(58, 139, 194) 84%", 32 | " rgb(38, 85, 139) 100%" 33 | ] 34 | }, 35 | { 36 | method: 'linear-gradient', 37 | args: [ 38 | "left", 39 | " rgb(206, 219, 233) 0%", 40 | " rgb(170, 197, 222) 17px", 41 | " rgb(97, 153, 199) 50%", 42 | " rgb(58, 132, 195) 51px", 43 | " rgb(65, 154, 214) 59%", 44 | " rgb(75, 184, 240) 71px", 45 | " rgb(58, 139, 194) 84%", 46 | " rgb(38, 85, 139) 100px" 47 | ] 48 | }, 49 | { 50 | method: "gradient", 51 | args: [ 52 | "linear", 53 | " 50% 0%", 54 | " 50% 100%", 55 | " from(rgb(240, 183, 161))", 56 | " color-stop(0.5, rgb(140, 51, 16))", 57 | " color-stop(0.51, rgb(117, 34, 1))", 58 | " to(rgb(191, 110, 78))" 59 | ] 60 | }, 61 | { 62 | method: "gradient", 63 | args: [ 64 | "linear", 65 | " 50% 0%", 66 | " 50% 100%", 67 | " from(rgb(255, 0, 0))", 68 | " color-stop(0.314159, green)", 69 | " color-stop(0.51, rgb(0, 0, 255))", 70 | // temporary workaround for Blink/WebKit bug: crbug.com/453414 71 | //" to(rgba(0, 0, 0, 0.5))" 72 | " to(rgba(0, 0, 0, 0))" 73 | ] 74 | }, 75 | { 76 | method: 'linear-gradient', 77 | args: [ 78 | "0deg", 79 | " rgb(221, 221, 221)", 80 | " rgb(221, 221, 221) 50%", 81 | " transparent 50%" 82 | ] 83 | }, 84 | { 85 | method: "radial-gradient", 86 | args: [ 87 | "75% 19%", 88 | " ellipse closest-side", 89 | " rgb(171, 171, 171)", 90 | " rgb(0, 0, 255) 33%", 91 | " rgb(153, 31, 31) 100%" 92 | ] 93 | }, 94 | { 95 | method: "radial-gradient", 96 | args: [ 97 | "75% 19%", 98 | " ellipse closest-corner", 99 | " rgb(171, 171, 171)", 100 | " rgb(0, 0, 255) 33%", 101 | " rgb(153, 31, 31) 100%" 102 | ] 103 | }, 104 | { 105 | method: "radial-gradient", 106 | args: [ 107 | "75% 19%", 108 | " ellipse farthest-side", 109 | " rgb(171, 171, 171)", 110 | " rgb(0, 0, 255) 33%", 111 | " rgb(153, 31, 31) 100%" 112 | ] 113 | }, 114 | { 115 | method: "radial-gradient", 116 | args: [ 117 | "75% 19%", 118 | " ellipse farthest-corner", 119 | " rgb(171, 171, 171)", 120 | " rgb(0, 0, 255) 33%", 121 | " rgb(153, 31, 31) 100%" 122 | ] 123 | }, 124 | { 125 | method: "radial-gradient", 126 | args: [ 127 | "75% 19%", 128 | " ellipse contain", 129 | " rgb(171, 171, 171)", 130 | " rgb(0, 0, 255) 33%", 131 | " rgb(153, 31, 31) 100%" 132 | ] 133 | }, 134 | { 135 | method: "radial-gradient", 136 | args: [ 137 | "75% 19%", 138 | " ellipse cover", 139 | " rgb(171, 171, 171)", 140 | " rgb(0, 0, 255) 33%", 141 | " rgb(153, 31, 31) 100%" 142 | ] 143 | } 144 | ]; 145 | 146 | [].slice.call(document.querySelectorAll('#backgroundGradients div'), 0).forEach(function(node, i) { 147 | var container = new html2canvas.NodeContainer(node, null); 148 | var value = container.css("backgroundImage"); 149 | it(value, function() { 150 | var parsedBackground = html2canvas.utils.parseBackgrounds(value); 151 | if (parsedBackground[0].args[0] === "0% 50%") { 152 | parsedBackground[0].args[0] = 'left'; 153 | } 154 | expect(parsedBackground[0].args).to.eql(expected[i].args); 155 | expect(parsedBackground[0].method).to.eql(expected[i].method); 156 | }); 157 | }); 158 | }); 159 | -------------------------------------------------------------------------------- /tests/test.js: -------------------------------------------------------------------------------- 1 | var h2cSelector, h2cOptions; 2 | (function(document, window) { 3 | function appendScript(src) { 4 | document.write(''); 5 | } 6 | 7 | ['/node_modules/bluebird/js/browser/bluebird', '/tests/assets/jquery-1.6.2', '/dist/html2canvas'].forEach(appendScript); 8 | 9 | if (typeof(noFabric) === "undefined") { 10 | appendScript('/dist/html2canvas.svg'); 11 | } 12 | 13 | window.onload = function() { 14 | (function( $ ){ 15 | $.fn.html2canvas = function(options) { 16 | if (options && options.profile && window.console && window.console.profile && window.location.search !== "?selenium2") { 17 | window.console.profile(); 18 | } 19 | var date = new Date(), 20 | $message = null, 21 | timeoutTimer = false, 22 | timer = date.getTime(); 23 | options = options || {}; 24 | 25 | var promise = html2canvas(this, options); 26 | promise['catch'](function(err) { 27 | console.log("html2canvas threw an error", err); 28 | }); 29 | 30 | promise.then(function(canvas) { 31 | var $canvas = $(canvas), 32 | finishTime = new Date(); 33 | 34 | if (options && options.profile && window.console && window.console.profileEnd) { 35 | window.console.profileEnd(); 36 | } 37 | $canvas.addClass("html2canvas") 38 | .css({ 39 | position: 'absolute', 40 | left: 0, 41 | top: 0 42 | }).appendTo(document.body); 43 | 44 | if (window.location.search !== "?selenium") { 45 | $canvas.siblings().toggle(); 46 | $(window).click(function(){ 47 | $canvas.toggle().siblings().toggle(); 48 | $(document.documentElement).css('background', $canvas.is(':visible') ? "none" : ""); 49 | $(document.body).css('background', $canvas.is(':visible') ? "none" : ""); 50 | throwMessage("Canvas Render " + ($canvas.is(':visible') ? "visible" : "hidden")); 51 | }); 52 | $(document.documentElement).css('background', $canvas.is(':visible') ? "none" : ""); 53 | $(document.body).css('background', $canvas.is(':visible') ? "none" : ""); 54 | throwMessage('Screenshot created in '+ ((finishTime.getTime()-timer)) + " ms
          ",4000); 55 | } else { 56 | $canvas.css('display', 'none'); 57 | } 58 | // test if canvas is read-able 59 | try { 60 | $canvas[0].toDataURL(); 61 | } catch(e) { 62 | if ($canvas[0].nodeName.toLowerCase() === "canvas") { 63 | // TODO, maybe add a bit less offensive way to present this, but still something that can easily be noticed 64 | window.alert("Canvas is tainted, unable to read data"); 65 | } 66 | } 67 | }); 68 | 69 | function throwMessage(msg,duration){ 70 | window.clearTimeout(timeoutTimer); 71 | timeoutTimer = window.setTimeout(function(){ 72 | $message.fadeOut(function(){ 73 | $message.remove(); 74 | $message = null; 75 | }); 76 | },duration || 2000); 77 | if ($message) 78 | $message.remove(); 79 | $message = $('
          ').html(msg).css({ 80 | margin:0, 81 | padding:10, 82 | background: "#000", 83 | opacity:0.7, 84 | position:"fixed", 85 | top:10, 86 | right:10, 87 | fontFamily: 'Tahoma', 88 | color:'#fff', 89 | fontSize:12, 90 | borderRadius:12, 91 | width:'auto', 92 | height:'auto', 93 | textAlign:'center', 94 | textDecoration:'none', 95 | display:'none' 96 | }).appendTo(document.body).fadeIn(); 97 | console.log(msg); 98 | } 99 | }; 100 | })(jQuery); 101 | 102 | h2cSelector = [document.documentElement]; 103 | 104 | if (window.setUp) { 105 | window.setUp(); 106 | } 107 | 108 | window.run = function() { 109 | $(h2cSelector).html2canvas($.extend({ 110 | logging: true, 111 | profile: true, 112 | proxy: "http://localhost:8082", 113 | useCORS: false, 114 | removeContainer: false 115 | }, h2cOptions)); 116 | }; 117 | 118 | if (typeof(dontRun) === "undefined") { 119 | setTimeout(window.run, 100); 120 | } 121 | }; 122 | }(document, window)); 123 | -------------------------------------------------------------------------------- /tests/mocha/lib/mocha.css: -------------------------------------------------------------------------------- 1 | @charset "utf-8"; 2 | 3 | body { 4 | margin:0; 5 | } 6 | 7 | #mocha { 8 | font: 20px/1.5 "Helvetica Neue", Helvetica, Arial, sans-serif; 9 | margin: 60px 50px; 10 | } 11 | 12 | #mocha ul, 13 | #mocha li { 14 | margin: 0; 15 | padding: 0; 16 | } 17 | 18 | #mocha ul { 19 | list-style: none; 20 | } 21 | 22 | #mocha h1, 23 | #mocha h2 { 24 | margin: 0; 25 | } 26 | 27 | #mocha h1 { 28 | margin-top: 15px; 29 | font-size: 1em; 30 | font-weight: 200; 31 | } 32 | 33 | #mocha h1 a { 34 | text-decoration: none; 35 | color: inherit; 36 | } 37 | 38 | #mocha h1 a:hover { 39 | text-decoration: underline; 40 | } 41 | 42 | #mocha .suite .suite h1 { 43 | margin-top: 0; 44 | font-size: .8em; 45 | } 46 | 47 | #mocha .hidden { 48 | display: none; 49 | } 50 | 51 | #mocha h2 { 52 | font-size: 12px; 53 | font-weight: normal; 54 | cursor: pointer; 55 | } 56 | 57 | #mocha .suite { 58 | margin-left: 15px; 59 | } 60 | 61 | #mocha .test { 62 | margin-left: 15px; 63 | overflow: hidden; 64 | } 65 | 66 | #mocha .test.pending:hover h2::after { 67 | content: '(pending)'; 68 | font-family: arial, sans-serif; 69 | } 70 | 71 | #mocha .test.pass.medium .duration { 72 | background: #c09853; 73 | } 74 | 75 | #mocha .test.pass.slow .duration { 76 | background: #b94a48; 77 | } 78 | 79 | #mocha .test.pass::before { 80 | content: '✓'; 81 | font-size: 12px; 82 | display: block; 83 | float: left; 84 | margin-right: 5px; 85 | color: #00d6b2; 86 | } 87 | 88 | #mocha .test.pass .duration { 89 | font-size: 9px; 90 | margin-left: 5px; 91 | padding: 2px 5px; 92 | color: #fff; 93 | -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.2); 94 | -moz-box-shadow: inset 0 1px 1px rgba(0,0,0,.2); 95 | box-shadow: inset 0 1px 1px rgba(0,0,0,.2); 96 | -webkit-border-radius: 5px; 97 | -moz-border-radius: 5px; 98 | -ms-border-radius: 5px; 99 | -o-border-radius: 5px; 100 | border-radius: 5px; 101 | } 102 | 103 | #mocha .test.pass.fast .duration { 104 | display: none; 105 | } 106 | 107 | #mocha .test.pending { 108 | color: #0b97c4; 109 | } 110 | 111 | #mocha .test.pending::before { 112 | content: '◦'; 113 | color: #0b97c4; 114 | } 115 | 116 | #mocha .test.fail { 117 | color: #c00; 118 | } 119 | 120 | #mocha .test.fail pre { 121 | color: black; 122 | } 123 | 124 | #mocha .test.fail::before { 125 | content: '✖'; 126 | font-size: 12px; 127 | display: block; 128 | float: left; 129 | margin-right: 5px; 130 | color: #c00; 131 | } 132 | 133 | #mocha .test pre.error { 134 | color: #c00; 135 | max-height: 300px; 136 | overflow: auto; 137 | } 138 | 139 | /** 140 | * (1): approximate for browsers not supporting calc 141 | * (2): 42 = 2*15 + 2*10 + 2*1 (padding + margin + border) 142 | * ^^ seriously 143 | */ 144 | #mocha .test pre { 145 | display: block; 146 | float: left; 147 | clear: left; 148 | font: 12px/1.5 monaco, monospace; 149 | margin: 5px; 150 | padding: 15px; 151 | border: 1px solid #eee; 152 | max-width: 85%; /*(1)*/ 153 | max-width: calc(100% - 42px); /*(2)*/ 154 | word-wrap: break-word; 155 | border-bottom-color: #ddd; 156 | -webkit-border-radius: 3px; 157 | -webkit-box-shadow: 0 1px 3px #eee; 158 | -moz-border-radius: 3px; 159 | -moz-box-shadow: 0 1px 3px #eee; 160 | border-radius: 3px; 161 | } 162 | 163 | #mocha .test h2 { 164 | position: relative; 165 | } 166 | 167 | #mocha .test a.replay { 168 | position: absolute; 169 | top: 3px; 170 | right: 0; 171 | text-decoration: none; 172 | vertical-align: middle; 173 | display: block; 174 | width: 15px; 175 | height: 15px; 176 | line-height: 15px; 177 | text-align: center; 178 | background: #eee; 179 | font-size: 15px; 180 | -moz-border-radius: 15px; 181 | border-radius: 15px; 182 | -webkit-transition: opacity 200ms; 183 | -moz-transition: opacity 200ms; 184 | transition: opacity 200ms; 185 | opacity: 0.3; 186 | color: #888; 187 | } 188 | 189 | #mocha .test:hover a.replay { 190 | opacity: 1; 191 | } 192 | 193 | #mocha-report.pass .test.fail { 194 | display: none; 195 | } 196 | 197 | #mocha-report.fail .test.pass { 198 | display: none; 199 | } 200 | 201 | #mocha-report.pending .test.pass, 202 | #mocha-report.pending .test.fail { 203 | display: none; 204 | } 205 | #mocha-report.pending .test.pass.pending { 206 | display: block; 207 | } 208 | 209 | #mocha-error { 210 | color: #c00; 211 | font-size: 1.5em; 212 | font-weight: 100; 213 | letter-spacing: 1px; 214 | } 215 | 216 | #mocha-stats { 217 | position: fixed; 218 | top: 15px; 219 | right: 10px; 220 | font-size: 12px; 221 | margin: 0; 222 | color: #888; 223 | z-index: 1; 224 | } 225 | 226 | #mocha-stats .progress { 227 | float: right; 228 | padding-top: 0; 229 | } 230 | 231 | #mocha-stats em { 232 | color: black; 233 | } 234 | 235 | #mocha-stats a { 236 | text-decoration: none; 237 | color: inherit; 238 | } 239 | 240 | #mocha-stats a:hover { 241 | border-bottom: 1px solid #eee; 242 | } 243 | 244 | #mocha-stats li { 245 | display: inline-block; 246 | margin: 0 5px; 247 | list-style: none; 248 | padding-top: 11px; 249 | } 250 | 251 | #mocha-stats canvas { 252 | width: 40px; 253 | height: 40px; 254 | } 255 | 256 | #mocha code .comment { color: #ddd; } 257 | #mocha code .init { color: #2f6fad; } 258 | #mocha code .string { color: #5890ad; } 259 | #mocha code .keyword { color: #8a6343; } 260 | #mocha code .number { color: #2f6fad; } 261 | 262 | @media screen and (max-device-width: 480px) { 263 | #mocha { 264 | margin: 60px 0px; 265 | } 266 | 267 | #mocha #stats { 268 | position: absolute; 269 | } 270 | } 271 | --------------------------------------------------------------------------------