Compare commits

...

89 Commits
0.4.0 ... 0.4.1

Author SHA1 Message Date
0515765788 Release version 0.4.1 2013-09-07 21:29:31 +03:00
0fd25f048d Add bower.json 2013-09-07 21:24:41 +03:00
14ff672c6e Disable screenshot storing 2013-09-07 21:10:06 +03:00
38fad5ac17 Update travis key 2013-09-07 00:10:53 +03:00
a31de83368 update token 2013-09-07 00:05:47 +03:00
1fb3b53fc0 update certificate 2013-09-06 23:57:47 +03:00
4d465116da update travis.yml key decrypt argument 2013-09-06 23:48:18 +03:00
74ce2c5062 update travis.yml secret 2013-09-06 23:26:35 +03:00
fbeb6e72f2 Update test certificate 2013-09-06 23:11:04 +03:00
c097f11ce3 Store webdriver screenshots to google drive 2013-09-06 22:30:24 +03:00
b6ebf2acf6 Remove unnecessary custom complete event checker 2013-09-04 20:17:38 +03:00
e9c3d9d332 Fix background color rendering layering with no documentElement color 2013-09-04 20:03:16 +03:00
c232da2595 Fix grunt watch build order 2013-09-04 20:02:20 +03:00
c759600c06 Refactoring 2013-09-04 19:29:12 +03:00
5f45968154 Merge pull request #260 from arrix/develop
negative z-index
2013-08-15 12:15:04 -07:00
feb2fd0a63 test case for body bgcolor and negative z-index #256 2013-08-16 00:26:40 +08:00
fb944d9381 test case for negative z-index below text 2013-08-15 23:31:48 +08:00
564634ba97 stacking context creator should be at the bottom
passes z-index12. issue #256
2013-08-15 23:24:01 +08:00
dd7468c446 use solid border in z-index tests 2013-08-15 22:31:32 +08:00
eb00650b02 fix text 2013-08-14 21:56:43 +03:00
1d03a5f9a4 z-index test case for negative z-indexes 2013-08-14 21:56:32 +03:00
ea7d6b485d preserve stacking nesting with special ancestor. issue #256 2013-08-15 02:12:38 +08:00
fd4fd95429 Test case for static position inside position relative 2013-08-13 19:59:54 +03:00
10b40821e5 first implementation for matrix transforms 2013-08-06 21:11:08 +03:00
518dd702a2 list all tests in index.html 2013-08-06 21:10:46 +03:00
056953f2c1 Fix cropping of canvas for float sized elements 2013-08-06 19:15:46 +03:00
9a57a08c72 Refactoring logging and gradients 2013-08-06 18:55:04 +03:00
26a81da2f0 Ignore transforms if non found 2013-08-06 18:20:20 +03:00
57028ab423 initial commit for transforms 2013-08-06 18:17:33 +03:00
c9e2fc27c8 Refactoring 2013-08-06 17:55:13 +03:00
2777a3e079 Refactoring 2013-08-06 17:46:47 +03:00
02ab96dc5f passes z-index8 non-positioned element with opactiy < 1
The MDN article Understanding_z_index/Stacking_and_float is wrong about this
2013-08-06 10:34:13 +08:00
65746bd2e3 coding optimizations 2013-08-06 10:01:20 +08:00
16d3bef255 z-index overhaul. relative above static; negative z-index
1. when stacking without z-index, positioned > floated > normal flow
2. supports negative z-index
3. new stacking context formed when opacity < 1 (standard)
4. new stacking context formed for position:fixed (no standard yet, done in mobile webkit and chrome 22+)
2013-08-06 03:36:12 +08:00
0277c34310 first transform test 2013-08-04 19:21:00 +03:00
f35ef0fe6f Refactor 2013-08-04 18:33:18 +03:00
2c8dd18d55 Fix build order 2013-08-04 18:33:02 +03:00
6b5f31eef0 Add tests for text shadow 2013-08-04 17:00:41 +03:00
832b9ee934 add grunt watch 2013-08-04 16:42:12 +03:00
73698e8ceb Update readme 2013-08-04 16:41:41 +03:00
37fbd3f90e Fix border rendering bug 2013-08-04 16:41:36 +03:00
5300f20b78 use element instead of element-array in examples 2013-08-04 16:03:47 +03:00
f0e234a1d8 Remove jQuery from examples 2013-08-04 15:58:42 +03:00
30163ab16f Update readme 2013-08-04 15:51:16 +03:00
407145da94 Update package.json 2013-08-04 15:50:54 +03:00
c5e6eaa849 Change iframe test url 2013-08-04 15:41:46 +03:00
2d39cd0719 fix shadow parsing for IE9 2013-08-04 15:41:00 +03:00
ebd7828dc8 Update sauce-connect logging 2013-08-04 15:14:34 +03:00
877367d499 Merge branch 'arrix-child_textnodes' into develop 2013-08-04 14:36:55 +03:00
fd888bde8d fixes #251. inline text in top element 2013-08-02 14:36:42 +08:00
7d2e12c3dd added test case for #251 inline text in top element 2013-08-02 10:11:07 +08:00
a7d3e9c2a2 Merge branch 'master' of git://github.com/fdn/html2canvas into fdn-master 2013-06-23 19:27:53 +03:00
f49e147b2f Added qunit tests for text-shadow 2013-06-18 23:47:08 -07:00
a902f92a14 remove svg rendering 2013-06-15 11:53:28 +03:00
e1573f8aed Parse out multiple text-shadow values and only honor the first one. 2013-06-12 16:48:23 -07:00
655779743b Better text-shadow parsing 2013-06-12 15:48:00 -07:00
1a30167f6a Basic implementation of text-shadow 2013-06-12 14:54:46 -07:00
2c58c56fbe add iframe test 2013-05-31 18:07:57 +03:00
288b851d05 revert image smooth disabling 2013-05-29 22:50:01 +03:00
0afb0fae0e disable image smoothing 2013-05-29 22:31:00 +03:00
a4702423cc adjust logging for results 2013-05-29 22:28:42 +03:00
f4aef61e5a update readme 2013-05-29 22:28:32 +03:00
cb210b2e61 delete .DS_Store 2013-05-29 22:14:50 +03:00
a80ef26c42 http->https 2013-05-29 22:13:22 +03:00
2580b48d16 compare results from db 2013-05-29 22:01:07 +03:00
87d7894d71 fix test result report posting 2013-05-29 21:16:28 +03:00
7842768707 encrypt api key only 2013-05-29 20:52:27 +03:00
403908c7da add logging for test reporting 2013-05-29 20:32:58 +03:00
8c8128b80a add test result storing 2013-05-29 20:12:37 +03:00
0d4b6ba665 setup travis config 2013-05-29 18:41:15 +03:00
b91fd9bc87 use sync-webdriver for selenium tests 2013-05-29 18:01:28 +03:00
62d27c20c3 fix typo 2013-05-21 23:23:56 +03:00
e811effe2a update download link 2013-05-21 23:23:31 +03:00
6156e28721 Merge pull request #214 from felfert/master
Fixed #207
2013-05-21 12:32:26 -07:00
2b000f0061 Fixed #207 2013-05-19 22:34:57 +02:00
843db27f72 Merge pull request #190 from cobexer/zIndex-1
added test for zIndex -1 element used as background
2013-04-15 07:44:47 -07:00
bbdfe8a035 added test for zIndex -1 element used as background 2013-04-15 13:08:54 +02:00
9da3bd7769 Merge pull request #189 from felfert/master
Fixed parsing of generic INPUT elements on IE9
2013-04-09 12:24:32 -07:00
85fa81ad95 - Fix parsing of input fields on IE9 2013-04-07 21:56:49 +02:00
a5d74bcfd9 Merge pull request #188 from felfert/master
Fetch images of current element only
2013-04-07 10:52:26 -07:00
cf735a9fa1 - Fixed rendering of ExtJS 4.2 windows. 2013-04-06 00:30:16 +02:00
9b051b8749 - Fetch images of current element only 2013-04-05 18:06:37 +02:00
822311ed0c Merge branch 'develop' of github.com:niklasvh/html2canvas into develop 2013-04-02 18:34:49 +03:00
c39225ceac add idea files to gitignore 2013-04-02 18:31:18 +03:00
763125ce6d Merge pull request #180 from djfarrelly/master
Upgraded to Grunt 0.4.0 and added Safari for Mac bug fix.
2013-03-24 07:52:45 -07:00
5ac4b42e33 Added reference to build shortcut on readme.md 2013-03-23 20:06:31 -04:00
cacb9a468f Modified markdown function. Added grunt build task.
Markdown format output currently commented out.
2013-03-23 20:00:01 -04:00
8623e4014b Upgrade to Grunt 0.4.0 complete. Safari on OSX bug fix. 2013-03-22 14:57:38 -04:00
2d95a761b0 Upgraded to Grunt 0.4.0. 2013-03-21 22:40:09 -04:00
42 changed files with 4904 additions and 880 deletions

7
.gitignore vendored
View File

@ -5,13 +5,16 @@
/tests/flashcanvas.html
/lib/
/bin/
/build/
image.jpg
/.project
/.settings/
/tests/certificate.pem
node_modules/
.envrc
server.js
*.sublime-workspace
chromedriver.log
*.baseline
*.baseline
*.iml
.idea/
.DS_Store

12
.travis.yml Normal file
View File

@ -0,0 +1,12 @@
---
language: node_js
node_js:
- '0.10'
env:
global:
- secure: "eW41gIqOizwO4pTgWnAAbW75AP7F+CK9qfSed/fSh4sJ9HWMIY1YRIaY8gjr+6jV/f7XVHcXuym6ZxgINYSkVKbF1JKxBJNLOXtSgNbVHSic58pYFvUjwxIBI9aPig9uux1+DbnpWqXFDTcACJSevQZE0xwmjdrSkDLgB0G34v8="
- secure: "Y2Av+Gd3z9uQEB36GwdOOuGka0hx0/HeitASEo59z934O8RxnmN9eNTXS7dDT3XtKtwxIyLTOEpS7qlRdWahH28hr/dS4xJj6ao58C+1xMcDs6NAPGmDxUlcJWpcGEsnjmXjQCc3fBioSTdpIBrK/gdvgpNh77UKG74Sk7Z+YGk="
- secure: "YI+YbTOGf2x4fPMKW+KhJiZWswoXT6xOKGwLfsQsVwmFX1LerJouil5D5iYOQuL4FE3pNaoJSNakIsokJQuGKJMmnPc8rdhMZuBJBk6MRghurE2Xe9qBHfuUBPlfD61nARESm4WDcyMwM0QVYaOKeY6aIpZ91qbUbyc60EEx3C4="
before_script:
- npm install -g grunt-cli
- curl https://gist.github.com/niklasvh/6150144/raw/sauce_connect_setup.sh | bash

99
Gruntfile.js Normal file
View File

@ -0,0 +1,99 @@
/*global module:false*/
module.exports = function(grunt) {
var meta = {
banner: '/*\n <%= pkg.title || pkg.name %> <%= pkg.version %>' +
'<%= pkg.homepage ? " <" + pkg.homepage + ">" : "" %>' + '\n' +
' Copyright (c) <%= grunt.template.today("yyyy") %> <%= pkg.author.name %>' +
'\n\n Released under <%= _.pluck(pkg.licenses, "type").join(", ") %> License\n*/\n',
pre: '\n(function(window, document, undefined){\n\n',
post: '\n})(window,document);'
};
// Project configuration.
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
qunit: {
files: ['tests/qunit/index.html']
},
concat: {
dist: {
src: [
'src/Core.js',
'src/Font.js',
'src/Generate.js',
'src/Queue.js',
'src/Parse.js',
'src/Preload.js',
'src/Renderer.js',
'src/Support.js',
'src/Util.js',
'src/renderers/Canvas.js'
],
dest: 'build/<%= pkg.name %>.js'
},
options:{
banner: meta.banner + meta.pre,
footer: meta.post
}
},
uglify: {
dist: {
src: ['<%= concat.dist.dest %>'],
dest: 'build/<%= pkg.name %>.min.js'
},
options: {
banner: meta.banner
}
},
watch: {
files: 'src/*',
tasks: ['build', 'jshint']
},
jshint: {
all: ['<%= concat.dist.dest %>'],
options: {
curly: true,
eqeqeq: true,
immed: true,
latedef: true,
newcap: true,
noarg: true,
sub: true,
undef: true,
boss: true,
eqnull: true,
browser: true,
globals: {
jQuery: true
}
}
}
});
grunt.registerTask('webdriver', 'Browser render tests', function(arg1) {
var selenium = require("./tests/selenium.js");
var done = this.async();
if (arguments.length) {
selenium[arg1].apply(null, arguments);
} else {
selenium.tests();
}
});
// Load tasks
grunt.loadNpmTasks('grunt-contrib-watch');
grunt.loadNpmTasks('grunt-contrib-concat');
grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.loadNpmTasks('grunt-contrib-jshint');
grunt.loadNpmTasks('grunt-contrib-qunit');
// Default task.
grunt.registerTask('build', ['concat', 'uglify']);
grunt.registerTask('default', ['concat', 'jshint', 'qunit', 'uglify']);
grunt.registerTask('travis', ['concat', 'jshint', 'qunit', 'uglify', 'webdriver']);
};

10
bower.json Normal file
View File

@ -0,0 +1,10 @@
{
"name": "html2canvas",
"version": "0.4.1",
"description": "Screenshots with JavaScript",
"main": "build/html2canvas.js",
"ignore": [
"tests",
".travis.yml"
]
}

2868
build/html2canvas.js Normal file

File diff suppressed because it is too large Load Diff

8
build/html2canvas.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -1,17 +1,6 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">
<html>
<head>
<script type="text/javascript" src="../tests/assets/jquery-1.6.2.js"></script>
<script type="text/javascript" src="../build/html2canvas.js"></script>
<script type="text/javascript">
$(document).ready(function() {
html2canvas( [ document.body ], {
onrendered: function(canvas) {
document.body.appendChild(canvas);
}
});
});
</script>
<title>
display/box/float/clear test
</title>
@ -182,5 +171,13 @@
<p style="color: black; font-size: 1em; line-height: 1.3em; clear: both">
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 <b>indistinguishably</b> (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 <A HREF="sec5526c.htm" style="text-decoration:none">parent page</A>.
</p>
<script type="text/javascript" src="../build/html2canvas.js"></script>
<script type="text/javascript">
html2canvas(document.body, {
onrendered: function(canvas) {
document.body.appendChild(canvas);
}
});
</script>
</body>
</html>

View File

@ -3,17 +3,6 @@
<head>
<title></title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<script type="text/javascript" src="../tests/assets/jquery-1.6.2.js"></script>
<script type="text/javascript" src="../build/html2canvas.js"></script>
<script type="text/javascript">
$(document).ready(function() {
html2canvas( [ document.body ], {
onrendered: function(canvas) {
document.body.appendChild(canvas);
}
});
});
</script>
<style>
.feedback-overlay-black{
background-color:#000;
@ -64,5 +53,13 @@
</div>
</div>
<script type="text/javascript" src="../build/html2canvas.js"></script>
<script type="text/javascript">
html2canvas(document.body, {
onrendered: function(canvas) {
document.body.appendChild(canvas);
}
});
</script>
</body>
</html>

View File

@ -1,73 +0,0 @@
/*global module:false*/
module.exports = function(grunt) {
// Project configuration.
grunt.initConfig({
pkg: '<json:package.json>',
meta: {
banner: '/*\n <%= pkg.title || pkg.name %> <%= pkg.version %>' +
'<%= pkg.homepage ? " <" + pkg.homepage + ">\n" : "" %>' +
' Copyright (c) <%= grunt.template.today("yyyy") %> <%= pkg.author.name %>' +
'\n\n Released under <%= _.pluck(pkg.licenses, "type").join(", ") %> License\n*/',
pre: '(function(window, document, undefined){',
post: '})(window,document);'
},
lint: {
files: ['build/<%= pkg.name %>.js']
},
qunit: {
files: ['tests/qunit/index.html']
},
concat: {
dist: {
src: ['<banner:meta.banner>', '<banner:meta.pre>','src/*.js', 'src/renderers/Canvas.js', '<banner:meta.post>'],
dest: 'build/<%= pkg.name %>.js'
}
},
min: {
dist: {
src: ['<banner:meta.banner>', '<config:concat.dist.dest>'],
dest: 'build/<%= pkg.name %>.min.js'
}
},
watch: {
files: '<config:lint.files>',
tasks: 'lint qunit'
},
jshint: {
options: {
curly: true,
eqeqeq: true,
immed: true,
latedef: true,
newcap: true,
noarg: true,
sub: true,
undef: true,
boss: true,
eqnull: true,
browser: true
},
globals: {
jQuery: true
}
},
uglify: {}
});
var selenium = require("./tests/selenium.js");
grunt.registerTask('webdriver', 'Browser render tests', function(arg1) {
var done = this.async();
if (arguments.length === 0) {
selenium.tests();
} else {
selenium[arg1].apply(null, arguments);
}
});
// Default task.
grunt.registerTask('default', 'concat lint qunit min webdriver');
};

View File

@ -2,18 +2,45 @@
"title": "html2canvas",
"name": "html2canvas",
"description": "Screenshots with JavaScript",
"version": "0.4.0",
"version": "0.4.1",
"author": {
"name":"Niklas von Hertzen (@niklasvh)"
"name": "Niklas von Hertzen",
"email": "niklasvh@gmail.com",
"url": "http://hertzen.com"
},
"dependencies": {
"engines": {
"node": ">=0.8.0"
},
"dependencies": {},
"repository": {
"type": "git",
"url": "git@github.com:niklasvh/html2canvas.git"
},
"bugs": {
"url": "https://github.com/niklasvh/html2canvas/issues"
},
"devDependencies": {
"grunt": ">=0.4.0",
"grunt-contrib-concat": "*",
"grunt-contrib-uglify": "*",
"grunt-contrib-jshint": "*",
"grunt-contrib-qunit": "*",
"grunt-contrib-watch": "~0.5.1",
"googleapis": "~0.4.3",
"jwt-sign": "~0.1.0",
"base64-arraybuffer": ">= 0.1.0",
"png-js": ">= 0.1.1",
"webdriver.js": ">= 0.1.0"
"sync-webdriver": ">=0.1.1",
"express": "~3.2.3",
"baconjs": "~0.3.15"
},
"scripts": {
"test": "grunt travis --verbose"
},
"homepage": "http://html2canvas.hertzen.com",
"licenses": [{
"type": "MIT"
}]
}
"licenses": [
{
"type": "MIT"
}
]
}

View File

@ -1,16 +1,19 @@
html2canvas
===========
### Current build status ###
[![Build Status](https://travis-ci.org/niklasvh/html2canvas.png)](https://travis-ci.org/niklasvh/html2canvas)
#### JavaScript HTML renderer ####
This script allows you to take "screenshots" of webpages or parts of it, directly on the users browser. The screenshot is based on the DOM and as such may not be 100% accurate to the real representation as it does not make an actual screenshot, but builds the screenshot based on the information available on the page.
The script allows you to take "screenshots" of webpages or parts of it, directly on the users browser. The screenshot is based on the DOM and as such may not be 100% accurate to the real representation as it does not make an actual screenshot, but builds the screenshot based on the information available on the page.
###How does it work?###
The script renders the current page as a canvas image, by reading the DOM and the different styles applied to the elements.
It does <b>not require any rendering from the server</b>, as the whole image is created on the <b>clients browser</b>. However, as it is heavily dependent on the browser, this library is *not suitable* to be used on for example on node.js.
It doesn't magically circumvent and browser content policy restrictions either, so rendering cross origin content will require a <a href="https://github.com/niklasvh/html2canvas/wiki/Proxies">proxy</a> to get the content to the <a href="http://en.wikipedia.org/wiki/Same_origin_policy">same origin</a>.
It does **not require any rendering from the server**, as the whole image is created on the **clients browser**. However, as it is heavily dependent on the browser, this library is *not suitable* to be used in nodejs.
It doesn't magically circumvent any browser content policy restrictions either, so rendering cross-origin content will require a [proxy](https://github.com/niklasvh/html2canvas/wiki/Proxies) to get the content to the [same origin](http://en.wikipedia.org/wiki/Same_origin_policy).
The script is still in a **very experimental state**, so I don't recommend using it in a production environment nor start building applications with it yet, as there will be still major changes made.
@ -22,27 +25,28 @@ The script should work fine on the following browsers:
* Google Chrome
* Opera 12+
* IE9+
* Safari 6+
As each CSS property needs to be manually built to be supported, there are a number of properties that are not yet supported.
### Usage ###
To render an `element` with html2canvas, simply call:
` html2canvas( [ element ], options);`
` html2canvas(element, options);`
To access the created canvas, provide the `onrendered` event in the options which returns the canvas element as the first argument, as such:
html2canvas( [ document.body ], {
html2canvas(document.body, {
onrendered: function(canvas) {
/* canvas is the actual canvas element,
to append it to the page call for example
document.body.appendChild( canvas );
*/
}
}
});
### Building ###
The library uses <a href="http://gruntjs.com/">grunt</a> for building. Alternatively, you can download ready builds from the <a href="https://github.com/niklasvh/html2canvas/downloads">downloads page</a>.
The library uses [grunt](http://gruntjs.com/) for building. Alternatively, you can download the latest build from [here](http://html2canvas.hertzen.com/build/html2canvas.js).
Run the full build process (including lint, qunit and webdriver tests):
@ -50,14 +54,13 @@ Run the full build process (including lint, qunit and webdriver tests):
Skip lint and tests and simply build from source:
$ grunt concat
$ grunt min
$ grunt build
### Running tests ###
The library has two sets of tests. The first set is a number of qunit tests that check that different values parsed by browsers are correctly converted in html2canvas. To run these tests with grunt you'll need <a href="http://phantomjs.org/">phantomjs</a>.
The library has two sets of tests. The first set is a number of qunit tests that check that different values parsed by browsers are correctly converted in html2canvas. To run these tests with grunt you'll need [phantomjs](http://phantomjs.org/).
The other set of tests run Firefox, Chrome and Internet Explorer with <a href="https://github.com/niklasvh/webdriver.js">webdriver</a>. The selenium standalone server (runs on Java) is required for these tests and can be downloaded from <a href="http://code.google.com/p/selenium/downloads/list">here</a>. They capture an actual screenshot from the test pages and compare the image to the screenshot created by html2canvas and calculate the percentage differences. These tests generally aren't expected to provide 100% matches, but while commiting changes, these should generally not go decrease from the baseline values.
The other set of tests run Firefox, Chrome and Internet Explorer with [webdriver](https://github.com/niklasvh/webdriver.js). The selenium standalone server (runs on Java) is required for these tests and can be downloaded from [here](http://code.google.com/p/selenium/downloads/list). They capture an actual screenshot from the test pages and compare the image to the screenshot created by html2canvas and calculate the percentage differences. These tests generally aren't expected to provide 100% matches, but while commiting changes, these should generally not go decrease from the baseline values.
Start by downloading the dependencies:
@ -67,26 +70,24 @@ Run qunit tests:
$ grunt test
Run webdriver tests:
$ java -jar /path/to/selenium-server-standalone-2.xx.x.jar
$ grunt webdriver
Commiting improvements in baseline values:
$ grunt webdriver:baseline
### Examples ###
For more information and examples, please visit the <a href="http://html2canvas.hertzen.com">homepage</a> or try the <a href="http://html2canvas.hertzen.com/screenshots.html">test console</a>.
For more information and examples, please visit the [homepage](http://html2canvas.hertzen.com) or try the [test console](http://html2canvas.hertzen.com/screenshots.html).
### Contributing ###
If you wish to contribute to the project, please send the pull requests to the develop branch. Before making any changes, make sure to run the webdriver tests to create the baseline results. If some CSS property isn't supported or is incomplete, please create appropriate tests for it as well before submitting any code changes.
If you wish to contribute to the project, please send the pull requests to the develop branch. Before submitting any changes, try and test that the changes work with all the support browsers. If some CSS property isn't supported or is incomplete, please create appropriate tests for it as well before submitting any code changes.
### Changelog ###
v0.40 -
v0.4.1 - 7.9.2013
* Added support for bower
* Improved z-index ordering
* Basic implementation for CSS transformations
* Fixed inline text in top element
* Basic implementation for text-shadow
v0.4.0 - 30.1.2013
* Added rendering tests with <a href="https://github.com/niklasvh/webdriver.js">webdriver</a>
* Switched to using grunt for building
* Removed support for IE<9, including any FlashCanvas bits
@ -96,7 +97,7 @@ v0.40 -
* Support for placeholder rendering
* Reformatted all tests to small units to test specific features
v0.34 - 26.6.2012
v0.3.4 - 26.6.2012
* Removed (last?) jQuery dependencies (<a href="https://github.com/niklasvh/html2canvas/commit/343b86705fe163766fcf735eb0217130e4bd5b17">niklasvh</a>)
* SVG-powered rendering (<a href="https://github.com/niklasvh/html2canvas/commit/67d3e0d0f59a5a654caf71a2e3be6494ff146c75">niklasvh</a>)
@ -104,7 +105,7 @@ v0.34 - 26.6.2012
* Split renderers to their own objects (<a href="https://github.com/niklasvh/html2canvas/commit/94f2f799a457cd29a21cc56ef8c06f1697866739">niklasvh</a>)
* Simplified API, cleaned up code (<a href="https://github.com/niklasvh/html2canvas/commit/c7d526c9eaa6a4abf4754d205fe1dee360c7660e">niklasvh</a>)
v0.33 - 2.3.2012
v0.3.3 - 2.3.2012
* SVG taint fix, and additional taint testing options for rendering (<a href="https://github.com/niklasvh/html2canvas/commit/2dc8b9385e656696cb019d615bdfa1d98b17d5d4">niklasvh</a>)
* Added support for CORS images and option to create canvas as tainted (<a href="https://github.com/niklasvh/html2canvas/commit/3ad49efa0032cde25c6ed32a39e35d1505d3b2ef">niklasvh</a>)
@ -112,7 +113,7 @@ v0.33 - 2.3.2012
* Added integrated support for Flashcanvas (<a href="https://github.com/niklasvh/html2canvas/commit/e9257191519f67d74fd5e364d8dee3c0963ba5fc">niklasvh</a>)
* Fixed a variety of legacy IE bugs (<a href="https://github.com/niklasvh/html2canvas/commit/b65357c55d0701017bafcd357bc654b54d458f8f">niklasvh</a>)
v0.32 - 20.2.2012
v0.3.2 - 20.2.2012
* Added changelog!
* Added bookmarklet (<a href="https://github.com/niklasvh/html2canvas/commit/b320dd306e1a2d32a3bc5a71b6ebf6d8c060cde5">cobexer</a>)

View File

@ -5,20 +5,49 @@ previousElement,
computedCSS,
html2canvas;
function h2clog(a) {
_html2canvas.Util = {};
_html2canvas.Util.log = function(a) {
if (_html2canvas.logging && window.console && window.console.log) {
window.console.log(a);
}
}
_html2canvas.Util = {};
};
_html2canvas.Util.trimText = (function(isNative){
return function(input){
if(isNative) { return isNative.apply( input ); }
else { return ((input || '') + '').replace( /^\s+|\s+$/g , '' ); }
return function(input) {
return isNative ? isNative.apply(input) : ((input || '') + '').replace( /^\s+|\s+$/g , '' );
};
})( String.prototype.trim );
})(String.prototype.trim);
_html2canvas.Util.asFloat = function(v) {
return parseFloat(v);
};
(function() {
// TODO: support all possible length values
var TEXT_SHADOW_PROPERTY = /((rgba|rgb)\([^\)]+\)(\s-?\d+px){0,})/g;
var TEXT_SHADOW_VALUES = /(-?\d+px)|(#.+)|(rgb\(.+\))|(rgba\(.+\))/g;
_html2canvas.Util.parseTextShadows = function (value) {
if (!value || value === 'none') {
return [];
}
// find multiple shadow declarations
var shadows = value.match(TEXT_SHADOW_PROPERTY),
results = [];
for (var i = 0; shadows && (i < shadows.length); i++) {
var s = shadows[i].match(TEXT_SHADOW_VALUES);
results.push({
color: s[0],
offsetX: s[1] ? s[1].replace('px', '') : 0,
offsetY: s[2] ? s[2].replace('px', '') : 0,
blur: s[3] ? s[3].replace('px', '') : 0
});
}
return results;
};
})();
_html2canvas.Util.parseBackgroundImage = function (value) {
var whitespace = ' \r\n\t',
@ -119,38 +148,42 @@ _html2canvas.Util.parseBackgroundImage = function (value) {
return results;
};
_html2canvas.Util.Bounds = function getBounds (el) {
var clientRect,
bounds = {};
if (el.getBoundingClientRect){
clientRect = el.getBoundingClientRect();
_html2canvas.Util.Bounds = function (element) {
var clientRect, bounds = {};
if (element.getBoundingClientRect){
clientRect = element.getBoundingClientRect();
// TODO add scroll position to bounds, so no scrolling of window necessary
bounds.top = clientRect.top;
bounds.bottom = clientRect.bottom || (clientRect.top + clientRect.height);
bounds.left = clientRect.left;
// older IE doesn't have width/height, but top/bottom instead
bounds.width = clientRect.width || (clientRect.right - clientRect.left);
bounds.height = clientRect.height || (clientRect.bottom - clientRect.top);
return bounds;
bounds.width = element.offsetWidth;
bounds.height = element.offsetHeight;
}
return bounds;
};
_html2canvas.Util.getCSS = function (el, attribute, index) {
// return $(el).css(attribute);
// TODO ideally, we'd want everything to go through this function instead of Util.Bounds,
// but would require further work to calculate the correct positions for elements with offsetParents
_html2canvas.Util.OffsetBounds = function (element) {
var parent = element.offsetParent ? _html2canvas.Util.OffsetBounds(element.offsetParent) : {top: 0, left: 0};
var val,
isBackgroundSizePosition = attribute.match( /^background(Size|Position)$/ );
return {
top: element.offsetTop + parent.top,
bottom: element.offsetTop + element.offsetHeight + parent.top,
left: element.offsetLeft + parent.left,
width: element.offsetWidth,
height: element.offsetHeight
};
};
function toPX( attribute, val ) {
var rsLeft = el.runtimeStyle && el.runtimeStyle[ attribute ],
left,
style = el.style;
function toPX(element, attribute, value ) {
var rsLeft = element.runtimeStyle && element.runtimeStyle[attribute],
left,
style = element.style;
// Check if we are not dealing with pixels, (Opera has issues with this)
// Ported from jQuery css.js
@ -160,71 +193,76 @@ _html2canvas.Util.getCSS = function (el, attribute, index) {
// If we're not dealing with a regular pixel number
// but a number that has a weird ending, we need to convert it to pixels
if ( !/^-?[0-9]+\.?[0-9]*(?:px)?$/i.test( val ) && /^-?\d/.test( val ) ) {
if ( !/^-?[0-9]+\.?[0-9]*(?:px)?$/i.test( value ) && /^-?\d/.test(value) ) {
// Remember the original values
left = style.left;
// Remember the original values
left = style.left;
// Put in the new values to get a computed value out
if ( rsLeft ) {
el.runtimeStyle.left = el.currentStyle.left;
}
style.left = attribute === "fontSize" ? "1em" : (val || 0);
val = style.pixelLeft + "px";
// Revert the changed values
style.left = left;
if ( rsLeft ) {
el.runtimeStyle.left = rsLeft;
}
}
if (!/^(thin|medium|thick)$/i.test( val )) {
return Math.round(parseFloat( val )) + "px";
}
return val;
}
if (previousElement !== el) {
computedCSS = document.defaultView.getComputedStyle(el, null);
}
val = computedCSS[attribute];
if (isBackgroundSizePosition) {
val = (val || '').split( ',' );
val = val[index || 0] || val[0] || 'auto';
val = _html2canvas.Util.trimText(val).split(' ');
if(attribute === 'backgroundSize' && (!val[ 0 ] || val[ 0 ].match( /cover|contain|auto/ ))) {
//these values will be handled in the parent function
} else {
val[ 0 ] = ( val[ 0 ].indexOf( "%" ) === -1 ) ? toPX( attribute + "X", val[ 0 ] ) : val[ 0 ];
if(val[ 1 ] === undefined) {
if(attribute === 'backgroundSize') {
val[ 1 ] = 'auto';
return val;
}
else {
// IE 9 doesn't return double digit always
val[ 1 ] = val[ 0 ];
}
// Put in the new values to get a computed value out
if (rsLeft) {
element.runtimeStyle.left = element.currentStyle.left;
}
style.left = attribute === "fontSize" ? "1em" : (value || 0);
value = style.pixelLeft + "px";
// Revert the changed values
style.left = left;
if (rsLeft) {
element.runtimeStyle.left = rsLeft;
}
val[ 1 ] = ( val[ 1 ].indexOf( "%" ) === -1 ) ? toPX( attribute + "Y", val[ 1 ] ) : val[ 1 ];
}
} else if ( /border(Top|Bottom)(Left|Right)Radius/.test( attribute) ) {
var arr = val.split(" ");
if ( arr.length <= 1 ) {
arr[ 1 ] = arr[ 0 ];
}
arr[ 0 ] = parseInt( arr[ 0 ], 10 );
arr[ 1 ] = parseInt( arr[ 1 ], 10 );
val = arr;
}
return val;
if (!/^(thin|medium|thick)$/i.test(value)) {
return Math.round(parseFloat(value)) + "px";
}
return value;
}
function asInt(val) {
return parseInt(val, 10);
}
function parseBackgroundSizePosition(value, element, attribute, index) {
value = (value || '').split(',');
value = value[index || 0] || value[0] || 'auto';
value = _html2canvas.Util.trimText(value).split(' ');
if(attribute === 'backgroundSize' && (!value[0] || value[0].match(/cover|contain|auto/))) {
//these values will be handled in the parent function
} else {
value[0] = (value[0].indexOf( "%" ) === -1) ? toPX(element, attribute + "X", value[0]) : value[0];
if(value[1] === undefined) {
if(attribute === 'backgroundSize') {
value[1] = 'auto';
return value;
} else {
// IE 9 doesn't return double digit always
value[1] = value[0];
}
}
value[1] = (value[1].indexOf("%") === -1) ? toPX(element, attribute + "Y", value[1]) : value[1];
}
return value;
}
_html2canvas.Util.getCSS = function (element, attribute, index) {
if (previousElement !== element) {
computedCSS = document.defaultView.getComputedStyle(element, null);
}
var value = computedCSS[attribute];
if (/^background(Size|Position)$/.test(attribute)) {
return parseBackgroundSizePosition(value, element, attribute, index);
} else if (/border(Top|Bottom)(Left|Right)Radius/.test(attribute)) {
var arr = value.split(" ");
if (arr.length <= 1) {
arr[1] = arr[0];
}
return arr.map(asInt);
}
return value;
};
_html2canvas.Util.resizeBounds = function( current_width, current_height, target_width, target_height, stretch_mode ){
@ -235,18 +273,18 @@ _html2canvas.Util.resizeBounds = function( current_width, current_height, target
if(!stretch_mode || stretch_mode === 'auto') {
output_width = target_width;
output_height = target_height;
} else if(target_ratio < current_ratio ^ stretch_mode === 'contain') {
output_height = target_height;
output_width = target_height * current_ratio;
} else {
if(target_ratio < current_ratio ^ stretch_mode === 'contain') {
output_height = target_height;
output_width = target_height * current_ratio;
} else {
output_width = target_width;
output_height = target_width / current_ratio;
}
output_width = target_width;
output_height = target_width / current_ratio;
}
return { width: output_width, height: output_height };
return {
width: output_width,
height: output_height
};
};
function backgroundBoundsFactory( prop, el, bounds, image, imageIndex, backgroundSize ) {
@ -271,24 +309,21 @@ function backgroundBoundsFactory( prop, el, bounds, image, imageIndex, backgroun
if(prop !== 'backgroundSize') {
left -= (backgroundSize || image).width*percentage;
}
} else {
if(prop === 'backgroundSize') {
if(bgposition[0] === 'auto') {
left = image.width;
} else {
if(bgposition[0].match(/contain|cover/)) {
var resized = _html2canvas.Util.resizeBounds( image.width, image.height, bounds.width, bounds.height, bgposition[0] );
if (/contain|cover/.test(bgposition[0])) {
var resized = _html2canvas.Util.resizeBounds(image.width, image.height, bounds.width, bounds.height, bgposition[0]);
left = resized.width;
topPos = resized.height;
} else {
left = parseInt (bgposition[0], 10 );
left = parseInt(bgposition[0], 10);
}
}
} else {
left = parseInt( bgposition[0], 10 );
left = parseInt( bgposition[0], 10);
}
}
@ -313,6 +348,7 @@ _html2canvas.Util.BackgroundPosition = function( el, bounds, image, imageIndex,
var result = backgroundBoundsFactory( 'backgroundPosition', el, bounds, image, imageIndex, backgroundSize );
return { left: result[0], top: result[1] };
};
_html2canvas.Util.BackgroundSize = function( el, bounds, image, imageIndex ) {
var result = backgroundBoundsFactory( 'backgroundSize', el, bounds, image, imageIndex );
return { width: result[0], height: result[1] };
@ -335,45 +371,40 @@ _html2canvas.Util.Extend = function (options, defaults) {
* http://jquery.org/license
*/
_html2canvas.Util.Children = function( elem ) {
var children;
try {
children = (elem.nodeName && elem.nodeName.toUpperCase() === "IFRAME") ?
elem.contentDocument || elem.contentWindow.document : (function( array ){
children = (elem.nodeName && elem.nodeName.toUpperCase() === "IFRAME") ? elem.contentDocument || elem.contentWindow.document : (function(array) {
var ret = [];
if ( array !== null ) {
(function( first, second ) {
if (array !== null) {
(function(first, second ) {
var i = first.length,
j = 0;
if ( typeof second.length === "number" ) {
for ( var l = second.length; j < l; j++ ) {
first[ i++ ] = second[ j ];
if (typeof second.length === "number") {
for (var l = second.length; j < l; j++) {
first[i++] = second[j];
}
} else {
while ( second[j] !== undefined ) {
first[ i++ ] = second[ j++ ];
while (second[j] !== undefined) {
first[i++] = second[j++];
}
}
first.length = i;
return first;
})( ret, array );
})(ret, array);
}
return ret;
})( elem.childNodes );
})(elem.childNodes);
} catch (ex) {
h2clog("html2canvas.Util.Children failed with exception: " + ex.message);
_html2canvas.Util.log("html2canvas.Util.Children failed with exception: " + ex.message);
children = [];
}
return children;
};
_html2canvas.Util.isTransparent = function(backgroundColor) {
return (backgroundColor === "transparent" || backgroundColor === "rgba(0, 0, 0, 0)");
};

View File

@ -1,6 +1,8 @@
(function(){
var Util = _html2canvas.Util,
Generate = {};
_html2canvas.Generate = {};
_html2canvas.Generate = Generate;
var reGradients = [
/^(-webkit-linear-gradient)\(([a-z\s]+)([\w\d\.\s,%\(\)]+)\)$/,
@ -18,7 +20,7 @@
* TODO: Add old Webkit -webkit-gradient(radial, ...) support
* TODO: Maybe some RegExp optimizations are possible ;o)
*/
_html2canvas.Generate.parseGradient = function(css, bounds) {
Generate.parseGradient = function(css, bounds) {
var gradient, i, len = reGradients.length, m1, stop, m2, m2Len, step, m3, tl,tr,br,bl;
for(i = 0; i < len; i+=1){
@ -320,14 +322,25 @@
return gradient;
};
_html2canvas.Generate.Gradient = function(src, bounds) {
function addScrollStops(grad) {
return function(colorStop) {
try {
grad.addColorStop(colorStop.stop, colorStop.color);
}
catch(e) {
Util.log(['failed to add color stop: ', e, '; tried to add: ', colorStop]);
}
};
}
Generate.Gradient = function(src, bounds) {
if(bounds.width === 0 || bounds.height === 0) {
return;
}
var canvas = document.createElement('canvas'),
ctx = canvas.getContext('2d'),
gradient, grad, i, len;
gradient, grad;
canvas.width = bounds.width;
canvas.height = bounds.height;
@ -336,72 +349,46 @@
gradient = _html2canvas.Generate.parseGradient(src, bounds);
if(gradient) {
if(gradient.type === 'linear') {
grad = ctx.createLinearGradient(gradient.x0, gradient.y0, gradient.x1, gradient.y1);
switch(gradient.type) {
case 'linear':
grad = ctx.createLinearGradient(gradient.x0, gradient.y0, gradient.x1, gradient.y1);
gradient.colorStops.forEach(addScrollStops(grad));
ctx.fillStyle = grad;
ctx.fillRect(0, 0, bounds.width, bounds.height);
break;
for (i = 0, len = gradient.colorStops.length; i < len; i+=1) {
try {
grad.addColorStop(gradient.colorStops[i].stop, gradient.colorStops[i].color);
}
catch(e) {
h2clog(['failed to add color stop: ', e, '; tried to add: ', gradient.colorStops[i], '; stop: ', i, '; in: ', src]);
}
}
case 'circle':
grad = ctx.createRadialGradient(gradient.cx, gradient.cy, 0, gradient.cx, gradient.cy, gradient.rx);
gradient.colorStops.forEach(addScrollStops(grad));
ctx.fillStyle = grad;
ctx.fillRect(0, 0, bounds.width, bounds.height);
break;
ctx.fillStyle = grad;
ctx.fillRect(0, 0, bounds.width, bounds.height);
case 'ellipse':
var canvasRadial = document.createElement('canvas'),
ctxRadial = canvasRadial.getContext('2d'),
ri = Math.max(gradient.rx, gradient.ry),
di = ri * 2;
} else if(gradient.type === 'circle') {
canvasRadial.width = canvasRadial.height = di;
grad = ctx.createRadialGradient(gradient.cx, gradient.cy, 0, gradient.cx, gradient.cy, gradient.rx);
grad = ctxRadial.createRadialGradient(gradient.rx, gradient.ry, 0, gradient.rx, gradient.ry, ri);
gradient.colorStops.forEach(addScrollStops(grad));
for (i = 0, len = gradient.colorStops.length; i < len; i+=1) {
try {
grad.addColorStop(gradient.colorStops[i].stop, gradient.colorStops[i].color);
}
catch(e) {
h2clog(['failed to add color stop: ', e, '; tried to add: ', gradient.colorStops[i], '; stop: ', i, '; in: ', src]);
}
}
ctx.fillStyle = grad;
ctx.fillRect(0, 0, bounds.width, bounds.height);
} else if(gradient.type === 'ellipse') {
// draw circle
var canvasRadial = document.createElement('canvas'),
ctxRadial = canvasRadial.getContext('2d'),
ri = Math.max(gradient.rx, gradient.ry),
di = ri * 2, imgRadial;
canvasRadial.width = canvasRadial.height = di;
grad = ctxRadial.createRadialGradient(gradient.rx, gradient.ry, 0, gradient.rx, gradient.ry, ri);
for (i = 0, len = gradient.colorStops.length; i < len; i+=1) {
try {
grad.addColorStop(gradient.colorStops[i].stop, gradient.colorStops[i].color);
}
catch(e) {
h2clog(['failed to add color stop: ', e, '; tried to add: ', gradient.colorStops[i], '; stop: ', i, '; in: ', src]);
}
}
ctxRadial.fillStyle = grad;
ctxRadial.fillRect(0, 0, di, di);
ctx.fillStyle = gradient.colorStops[i - 1].color;
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(canvasRadial, gradient.cx - gradient.rx, gradient.cy - gradient.ry, 2 * gradient.rx, 2 * gradient.ry);
ctxRadial.fillStyle = grad;
ctxRadial.fillRect(0, 0, di, di);
ctx.fillStyle = gradient.colorStops[gradient.colorStops.length - 1].color;
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(canvasRadial, gradient.cx - gradient.rx, gradient.cy - gradient.ry, 2 * gradient.rx, 2 * gradient.ry);
break;
}
}
return canvas;
};
_html2canvas.Generate.ListAlpha = function(number) {
Generate.ListAlpha = function(number) {
var tmp = "",
modulus;
@ -414,7 +401,7 @@
return tmp;
};
_html2canvas.Generate.ListRoman = function(number) {
Generate.ListRoman = function(number) {
var romanArray = ["M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I"],
decimal = [1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1],
roman = "",
@ -433,7 +420,5 @@
}
return roman;
};
})();

View File

@ -4,10 +4,11 @@ _html2canvas.Parse = function (images, options) {
var element = (( options.elements === undefined ) ? document.body : options.elements[0]), // select body by default
numDraws = 0,
doc = element.ownerDocument,
support = _html2canvas.Util.Support(options, doc),
Util = _html2canvas.Util,
support = Util.Support(options, doc),
ignoreElementsRegExp = new RegExp("(" + options.ignoreElements + ")"),
body = doc.body,
getCSS = _html2canvas.Util.getCSS,
getCSS = Util.getCSS,
pseudoHide = "___html2canvas___pseudoelement",
hidePseudoElements = doc.createElement('style');
@ -47,16 +48,18 @@ _html2canvas.Parse = function (images, options) {
}
}
function capitalize(m, p1, p2) {
if (m.length > 0) {
return p1 + p2.toUpperCase();
}
}
function textTransform (text, transform) {
switch(transform){
case "lowercase":
return text.toLowerCase();
case "capitalize":
return text.replace( /(^|\s|:|-|\(|\))([a-z])/g , function (m, p1, p2) {
if (m.length > 0) {
return p1 + p2.toUpperCase();
}
} );
return text.replace( /(^|\s|:|-|\(|\))([a-z])/g, capitalize);
case "uppercase":
return text.toUpperCase();
default:
@ -69,7 +72,7 @@ _html2canvas.Parse = function (images, options) {
}
function drawText(currentText, x, y, ctx){
if (currentText !== null && _html2canvas.Util.trimText(currentText).length > 0) {
if (currentText !== null && Util.trimText(currentText).length > 0) {
ctx.fillText(currentText, x, y);
numDraws+=1;
}
@ -79,7 +82,8 @@ _html2canvas.Parse = function (images, options) {
var align = false,
bold = getCSS(el, "fontWeight"),
family = getCSS(el, "fontFamily"),
size = getCSS(el, "fontSize");
size = getCSS(el, "fontSize"),
shadows = Util.parseTextShadows(getCSS(el, "textShadow"));
switch(parseInt(bold, 10)){
case 401:
@ -94,8 +98,17 @@ _html2canvas.Parse = function (images, options) {
ctx.setVariable("font", [getCSS(el, "fontStyle"), getCSS(el, "fontVariant"), bold, size, family].join(" "));
ctx.setVariable("textAlign", (align) ? "right" : "left");
if (shadows.length) {
// TODO: support multiple text shadows
// apply the first text shadow
ctx.setVariable("shadowColor", shadows[0].color);
ctx.setVariable("shadowOffsetX", shadows[0].offsetX);
ctx.setVariable("shadowOffsetY", shadows[0].offsetY);
ctx.setVariable("shadowBlur", shadows[0].blur);
}
if (text_decoration !== "none"){
return _html2canvas.Util.Font(family, size, doc);
return Util.Font(family, size, doc);
}
}
@ -116,16 +129,16 @@ _html2canvas.Parse = function (images, options) {
}
}
function getTextBounds(state, text, textDecoration, isLast) {
function getTextBounds(state, text, textDecoration, isLast, transform) {
var bounds;
if (support.rangeBounds) {
if (textDecoration !== "none" || _html2canvas.Util.trimText(text).length !== 0) {
if (support.rangeBounds && !transform) {
if (textDecoration !== "none" || Util.trimText(text).length !== 0) {
bounds = textRangeBounds(text, state.node, state.textOffset);
}
state.textOffset += text.length;
} else if (state.node && typeof state.node.nodeValue === "string" ){
var newTextNode = (isLast) ? state.node.splitText(text.length) : null;
bounds = textWrapperBounds(state.node);
bounds = textWrapperBounds(state.node, transform);
state.node = newTextNode;
}
return bounds;
@ -138,7 +151,7 @@ _html2canvas.Parse = function (images, options) {
return range.getBoundingClientRect();
}
function textWrapperBounds(oldTextNode) {
function textWrapperBounds(oldTextNode, transform) {
var parent = oldTextNode.parentNode,
wrapElement = doc.createElement('wrapper'),
backupText = oldTextNode.cloneNode(true);
@ -146,7 +159,7 @@ _html2canvas.Parse = function (images, options) {
wrapElement.appendChild(oldTextNode.cloneNode(true));
parent.replaceChild(wrapElement, oldTextNode);
var bounds = _html2canvas.Util.Bounds(wrapElement);
var bounds = transform ? Util.OffsetBounds(wrapElement) : Util.Bounds(wrapElement);
parent.replaceChild(backupText, wrapElement);
return bounds;
}
@ -163,7 +176,7 @@ _html2canvas.Parse = function (images, options) {
textOffset: 0
};
if (_html2canvas.Util.trimText(textNode.nodeValue).length > 0) {
if (Util.trimText(textNode.nodeValue).length > 0) {
textNode.nodeValue = textTransform(textNode.nodeValue, getCSS(el, "textTransform"));
textAlign = textAlign.replace(["-webkit-auto"],["auto"]);
@ -184,7 +197,7 @@ _html2canvas.Parse = function (images, options) {
}
textList.forEach(function(text, index) {
var bounds = getTextBounds(state, text, textDecoration, (index < textList.length - 1));
var bounds = getTextBounds(state, text, textDecoration, (index < textList.length - 1), stack.transform.matrix);
if (bounds) {
drawText(text, bounds.left, bounds.bottom, ctx);
renderTextDecoration(ctx, textDecoration, bounds, metrics, color);
@ -207,20 +220,20 @@ _html2canvas.Parse = function (images, options) {
element.insertBefore(boundElement, element.firstChild);
bounds = _html2canvas.Util.Bounds(boundElement);
bounds = Util.Bounds(boundElement);
element.removeChild(boundElement);
element.style.listStyleType = originalType;
return bounds;
}
function elementIndex( el ) {
function elementIndex(el) {
var i = -1,
count = 1,
childs = el.parentNode.childNodes;
if (el.parentNode) {
while( childs[ ++i ] !== el ) {
if ( childs[ i ].nodeType === 1 ) {
while(childs[++i] !== el) {
if (childs[i].nodeType === 1) {
count++;
}
}
@ -231,8 +244,7 @@ _html2canvas.Parse = function (images, options) {
}
function listItemText(element, type) {
var currentIndex = elementIndex(element),
text;
var currentIndex = elementIndex(element), text;
switch(type){
case "decimal":
text = currentIndex;
@ -254,8 +266,7 @@ _html2canvas.Parse = function (images, options) {
break;
}
text += ". ";
return text;
return text + ". ";
}
function renderListItem(element, stack, elBounds) {
@ -283,11 +294,7 @@ _html2canvas.Parse = function (images, options) {
function loadImage (src){
var img = images[src];
if (img && img.succeeded === true) {
return img.img;
} else {
return false;
}
return (img && img.succeeded === true) ? img.img : false;
}
function clipBounds(src, dst){
@ -304,22 +311,29 @@ _html2canvas.Parse = function (images, options) {
};
}
function setZ(zIndex, parentZ){
// TODO fix static elements overlapping relative/absolute elements under same stack, if they are defined after them
var newContext;
if (!parentZ){
newContext = h2czContext(0);
return newContext;
function setZ(element, stack, parentStack){
var newContext,
isPositioned = stack.cssPosition !== 'static',
zIndex = isPositioned ? getCSS(element, 'zIndex') : 'auto',
opacity = getCSS(element, 'opacity'),
isFloated = getCSS(element, 'cssFloat') !== 'none';
// https://developer.mozilla.org/en-US/docs/Web/Guide/CSS/Understanding_z_index/The_stacking_context
// When a new stacking context should be created:
// the root element (HTML),
// positioned (absolutely or relatively) with a z-index value other than "auto",
// elements with an opacity value less than 1. (See the specification for opacity),
// on mobile WebKit and Chrome 22+, position: fixed always creates a new stacking context, even when z-index is "auto" (See this post)
stack.zIndex = newContext = h2czContext(zIndex);
newContext.isPositioned = isPositioned;
newContext.isFloated = isFloated;
newContext.opacity = opacity;
newContext.ownStacking = (zIndex !== 'auto' || opacity < 1);
if (parentStack) {
parentStack.zIndex.children.push(stack);
}
if (zIndex !== "auto"){
newContext = h2czContext(zIndex);
parentZ.children.push(newContext);
return newContext;
}
return parentZ;
}
function renderImage(ctx, element, image, bounds, borders) {
@ -509,8 +523,8 @@ _html2canvas.Parse = function (images, options) {
tlv = borderRadius[0][1],
trh = borderRadius[1][0],
trv = borderRadius[1][1],
brv = borderRadius[2][0],
brh = borderRadius[2][1],
brh = borderRadius[2][0],
brv = borderRadius[2][1],
blh = borderRadius[3][0],
blv = borderRadius[3][1],
@ -664,7 +678,7 @@ _html2canvas.Parse = function (images, options) {
c1: [bx + bw, by + bh],
c2: [bx, by + bh],
c3: [bx + borders[3].width, by],
c4: [bx + bw - borders[2].width, by]
c4: [bx + bw - borders[3].width, by]
}, borderRadius[2], borderRadius[3],
borderPoints.bottomRightOuter, borderPoints.bottomRightInner, borderPoints.bottomLeftOuter, borderPoints.bottomLeftInner);
break;
@ -722,7 +736,7 @@ _html2canvas.Parse = function (images, options) {
valueWrap.style[property] = getCSS(el, property);
} catch(e) {
// Older IE has issues with "border"
h2clog("html2canvas: Parse: Exception caught in renderFormValue: " + e.message);
Util.log("html2canvas: Parse: Exception caught in renderFormValue: " + e.message);
}
});
@ -759,7 +773,7 @@ _html2canvas.Parse = function (images, options) {
function getPseudoElement(el, which) {
var elStyle = window.getComputedStyle(el, which);
if(!elStyle || !elStyle.content || elStyle.content === "none" || elStyle.content === "-moz-alt-content") {
if(!elStyle || !elStyle.content || elStyle.content === "none" || elStyle.content === "-moz-alt-content" || elStyle.display === "none") {
return;
}
var content = elStyle.content + '',
@ -775,11 +789,16 @@ _html2canvas.Parse = function (images, options) {
elps.className = pseudoHide + "-before " + pseudoHide + "-after";
Object.keys(elStyle).filter(indexedProperty).forEach(function(prop) {
elps.style[prop] = elStyle[prop];
// Prevent assigning of read only CSS Rules, ex. length, parentRule
try {
elps.style[prop] = elStyle[prop];
} catch (e) {
Util.log(['Tried to assign readonly property ', prop, 'Error:', e]);
}
});
if(isImage) {
elps.src = _html2canvas.Util.parseBackgroundImage(content)[0].args[0];
elps.src = Util.parseBackgroundImage(content)[0].args[0];
} else {
elps.innerHTML = content;
}
@ -850,11 +869,9 @@ _html2canvas.Parse = function (images, options) {
}
function renderBackgroundRepeating(el, bounds, ctx, image, imageIndex) {
var backgroundSize = _html2canvas.Util.BackgroundSize(el, bounds, image, imageIndex),
backgroundPosition = _html2canvas.Util.BackgroundPosition(el, bounds, image, imageIndex, backgroundSize),
backgroundRepeat = getCSS(el, "backgroundRepeat").split(",").map(function(value) {
return value.trim();
});
var backgroundSize = Util.BackgroundSize(el, bounds, image, imageIndex),
backgroundPosition = Util.BackgroundPosition(el, bounds, image, imageIndex, backgroundSize),
backgroundRepeat = getCSS(el, "backgroundRepeat").split(",").map(Util.trimText);
image = resizeImage(image, backgroundSize);
@ -889,7 +906,7 @@ _html2canvas.Parse = function (images, options) {
function renderBackgroundImage(element, bounds, ctx) {
var backgroundImage = getCSS(element, "backgroundImage"),
backgroundImages = _html2canvas.Util.parseBackgroundImage(backgroundImage),
backgroundImages = Util.parseBackgroundImage(backgroundImage),
image,
imageIndex = backgroundImages.length;
@ -910,7 +927,7 @@ _html2canvas.Parse = function (images, options) {
if (image) {
renderBackgroundRepeating(element, bounds, ctx, image, imageIndex);
} else {
h2clog("html2canvas: Error loading background:", backgroundImage);
Util.log("html2canvas: Error loading background:", backgroundImage);
}
}
}
@ -929,30 +946,57 @@ _html2canvas.Parse = function (images, options) {
}
function setOpacity(ctx, element, parentStack) {
var opacity = getCSS(element, "opacity") * ((parentStack) ? parentStack.opacity : 1);
ctx.setVariable("globalAlpha", opacity);
return opacity;
return ctx.setVariable("globalAlpha", getCSS(element, "opacity") * ((parentStack) ? parentStack.opacity : 1));
}
function createStack(element, parentStack, bounds) {
function removePx(str) {
return str.replace("px", "");
}
var transformRegExp = /(matrix)\((.+)\)/;
function getTransform(element, parentStack) {
var transform = getCSS(element, "transform") || getCSS(element, "-webkit-transform") || getCSS(element, "-moz-transform") || getCSS(element, "-ms-transform") || getCSS(element, "-o-transform");
var transformOrigin = getCSS(element, "transform-origin") || getCSS(element, "-webkit-transform-origin") || getCSS(element, "-moz-transform-origin") || getCSS(element, "-ms-transform-origin") || getCSS(element, "-o-transform-origin") || "0px 0px";
transformOrigin = transformOrigin.split(" ").map(removePx).map(Util.asFloat);
var matrix;
if (transform && transform !== "none") {
var match = transform.match(transformRegExp);
if (match) {
switch(match[1]) {
case "matrix":
matrix = match[2].split(",").map(Util.trimText).map(Util.asFloat);
break;
}
}
}
return {
origin: transformOrigin,
matrix: matrix
};
}
function createStack(element, parentStack, bounds, transform) {
var ctx = h2cRenderContext((!parentStack) ? documentWidth() : bounds.width , (!parentStack) ? documentHeight() : bounds.height),
stack = {
ctx: ctx,
zIndex: setZ(getCSS(element, "zIndex"), (parentStack) ? parentStack.zIndex : null),
opacity: setOpacity(ctx, element, parentStack),
cssPosition: getCSS(element, "position"),
borders: getBorderData(element),
clip: (parentStack && parentStack.clip) ? _html2canvas.Util.Extend( {}, parentStack.clip ) : null
transform: transform,
clip: (parentStack && parentStack.clip) ? Util.Extend( {}, parentStack.clip ) : null
};
setZ(element, stack, parentStack);
// TODO correct overflow for absolute content residing under a static position
if (options.useOverflow === true && /(hidden|scroll|auto)/.test(getCSS(element, "overflow")) === true && /(BODY)/i.test(element.nodeName) === false){
stack.clip = (stack.clip) ? clipBounds(stack.clip, bounds) : bounds;
}
stack.zIndex.children.push(stack);
return stack;
}
@ -971,24 +1015,35 @@ _html2canvas.Parse = function (images, options) {
return backgroundBounds;
}
function renderElement(element, parentStack, pseudoElement){
var bounds = _html2canvas.Util.Bounds(element),
function getBounds(element, transform) {
var bounds = (transform.matrix) ? Util.OffsetBounds(element) : Util.Bounds(element);
transform.origin[0] += bounds.left;
transform.origin[1] += bounds.top;
return bounds;
}
function renderElement(element, parentStack, pseudoElement, ignoreBackground) {
var transform = getTransform(element, parentStack),
bounds = getBounds(element, transform),
image,
bgcolor = (ignoreElementsRegExp.test(element.nodeName)) ? "#efefef" : getCSS(element, "backgroundColor"),
stack = createStack(element, parentStack, bounds),
stack = createStack(element, parentStack, bounds, transform),
borders = stack.borders,
ctx = stack.ctx,
backgroundBounds = getBackgroundBounds(borders, bounds, stack.clip),
borderData = parseBorders(element, bounds, borders);
borderData = parseBorders(element, bounds, borders),
backgroundColor = (ignoreElementsRegExp.test(element.nodeName)) ? "#efefef" : getCSS(element, "backgroundColor");
createShape(ctx, borderData.clip);
ctx.save();
ctx.clip();
if (backgroundBounds.height > 0 && backgroundBounds.width > 0){
renderBackgroundColor(ctx, bounds, bgcolor);
if (backgroundBounds.height > 0 && backgroundBounds.width > 0 && !ignoreBackground) {
renderBackgroundColor(ctx, bounds, backgroundColor);
renderBackgroundImage(element, backgroundBounds, ctx);
} else if (ignoreBackground) {
stack.backgroundColor = backgroundColor;
}
ctx.restore();
@ -1006,13 +1061,13 @@ _html2canvas.Parse = function (images, options) {
if ((image = loadImage(element.getAttribute('src')))) {
renderImage(ctx, element, image, bounds, borders);
} else {
h2clog("html2canvas: Error loading <img>:" + element.getAttribute('src'));
Util.log("html2canvas: Error loading <img>:" + element.getAttribute('src'));
}
break;
case "INPUT":
// TODO add all relevant type's, i.e. HTML5 new stuff
// todo add support for placeholder attribute for browsers which support it
if (/^(text|url|email|submit|button|reset)$/.test(element.type) && (element.value || element.placeholder).length > 0){
if (/^(text|url|email|submit|button|reset)$/.test(element.type) && (element.value || element.placeholder || "").length > 0){
renderFormValue(element, bounds, stack);
}
break;
@ -1041,102 +1096,40 @@ _html2canvas.Parse = function (images, options) {
return (getCSS(element, 'display') !== "none" && getCSS(element, 'visibility') !== "hidden" && !element.hasAttribute("data-html2canvas-ignore"));
}
function parseElement (el, stack, pseudoElement) {
if (isElementVisible(el)) {
stack = renderElement(el, stack, pseudoElement) || stack;
if (!ignoreElementsRegExp.test(el.nodeName)) {
_html2canvas.Util.Children(el).forEach(function(node) {
if (node.nodeType === 1) {
parseElement(node, stack, pseudoElement);
} else if (node.nodeType === 3) {
renderText(el, node, stack);
}
});
function parseElement (element, stack, pseudoElement) {
if (isElementVisible(element)) {
stack = renderElement(element, stack, pseudoElement, false) || stack;
if (!ignoreElementsRegExp.test(element.nodeName)) {
parseChildren(element, stack, pseudoElement);
}
}
}
function svgDOMRender(body, stack) {
var img = new Image(),
docWidth = documentWidth(),
docHeight = documentHeight(),
html = "";
function parseDOM(el) {
var children = _html2canvas.Util.Children( el ),
len = children.length,
attr,
a,
alen,
elm,
i;
for ( i = 0; i < len; i+=1 ) {
elm = children[ i ];
if ( elm.nodeType === 3 ) {
// Text node
html += elm.nodeValue.replace(/</g,"&lt;").replace(/>/g,"&gt;");
} else if ( elm.nodeType === 1 ) {
// Element
if ( !/^(script|meta|title)$/.test(elm.nodeName.toLowerCase()) ) {
html += "<" + elm.nodeName.toLowerCase();
// add attributes
if ( elm.hasAttributes() ) {
attr = elm.attributes;
alen = attr.length;
for ( a = 0; a < alen; a+=1 ) {
html += " " + attr[ a ].name + '="' + attr[ a ].value + '"';
}
}
html += '>';
parseDOM( elm );
html += "</" + elm.nodeName.toLowerCase() + ">";
}
}
function parseChildren(element, stack, pseudoElement) {
Util.Children(element).forEach(function(node) {
if (node.nodeType === node.ELEMENT_NODE) {
parseElement(node, stack, pseudoElement);
} else if (node.nodeType === node.TEXT_NODE) {
renderText(element, node, stack);
}
}
parseDOM(body);
img.src = [
"data:image/svg+xml,",
"<svg xmlns='http://www.w3.org/2000/svg' version='1.1' width='" + docWidth + "' height='" + docHeight + "'>",
"<foreignObject width='" + docWidth + "' height='" + docHeight + "'>",
"<html xmlns='http://www.w3.org/1999/xhtml' style='margin:0;'>",
html.replace(/\#/g,"%23"),
"</html>",
"</foreignObject>",
"</svg>"
].join("");
img.onload = function() {
stack.svgRender = img;
};
});
}
function init() {
var stack = renderElement(element, null);
var background = getCSS(document.documentElement, "backgroundColor"),
transparentBackground = (Util.isTransparent(background) && element === document.body),
stack = renderElement(element, null, false, transparentBackground);
parseChildren(element, stack);
if (support.svgRendering) {
svgDOMRender(document.documentElement, stack);
if (transparentBackground) {
background = stack.backgroundColor;
}
Array.prototype.slice.call(element.children, 0).forEach(function(childElement) {
parseElement(childElement, stack);
});
stack.backgroundColor = getCSS(document.documentElement, "backgroundColor");
body.removeChild(hidePseudoElements);
return stack;
return {
backgroundColor: background,
stack: stack
};
}
return init();
@ -1147,4 +1140,4 @@ function h2czContext(zindex) {
zindex: zindex,
children: []
};
}
}

View File

@ -7,12 +7,13 @@ _html2canvas.Preload = function( options ) {
cleanupDone: false
},
pageOrigin,
Util = _html2canvas.Util,
methods,
i,
count = 0,
element = options.elements[0] || document.body,
doc = element.ownerDocument,
domImages = doc.images, // TODO probably should limit it to images present in the element only
domImages = element.getElementsByTagName('img'), // Fetch images of the present element only
imgLen = domImages.length,
link = doc.createElement("a"),
supportCORS = (function( img ){
@ -31,9 +32,9 @@ _html2canvas.Preload = function( options ) {
}
function start(){
h2clog("html2canvas: start: images: " + images.numLoaded + " / " + images.numTotal + " (failed: " + images.numFailed + ")");
Util.log("html2canvas: start: images: " + images.numLoaded + " / " + images.numTotal + " (failed: " + images.numFailed + ")");
if (!images.firstRun && images.numLoaded >= images.numTotal){
h2clog("Finished loading images: # " + images.numTotal + " (failed: " + images.numFailed + ")");
Util.log("Finished loading images: # " + images.numTotal + " (failed: " + images.numFailed + ")");
if (typeof options.complete === "function"){
options.complete(images);
@ -141,9 +142,7 @@ _html2canvas.Preload = function( options ) {
// Firefox fails with permission denied on pages with iframes
try {
_html2canvas.Util.Children(el).forEach(function(img) {
getImages(img);
});
Util.Children(el).forEach(getImages);
}
catch( e ) {}
@ -151,15 +150,15 @@ _html2canvas.Preload = function( options ) {
elNodeType = el.nodeType;
} catch (ex) {
elNodeType = false;
h2clog("html2canvas: failed to access some element's nodeType - Exception: " + ex.message);
Util.log("html2canvas: failed to access some element's nodeType - Exception: " + ex.message);
}
if (elNodeType === 1 || elNodeType === undefined) {
loadPseudoElementImages(el);
try {
loadBackgroundImages(_html2canvas.Util.getCSS(el, 'backgroundImage'), el);
loadBackgroundImages(Util.getCSS(el, 'backgroundImage'), el);
} catch(e) {
h2clog("html2canvas: failed to get background-image - Exception: " + e.message);
Util.log("html2canvas: failed to get background-image - Exception: " + e.message);
}
loadBackgroundImages(el);
}
@ -231,17 +230,6 @@ _html2canvas.Preload = function( options ) {
images.numTotal++;
setImageLoadHandlers(img, imageObj);
img.src = src;
// work around for https://bugs.webkit.org/show_bug.cgi?id=80028
img.customComplete = function () {
if (!this.img.complete) {
this.timer = window.setTimeout(this.img.customComplete, 100);
} else {
this.img.onerror();
}
}.bind(imageObj);
img.customComplete();
} else if ( options.proxy ) {
imageObj = images[src] = {
img: img
@ -256,9 +244,9 @@ _html2canvas.Preload = function( options ) {
var img, src;
if (!images.cleanupDone) {
if (cause && typeof cause === "string") {
h2clog("html2canvas: Cleanup because: " + cause);
Util.log("html2canvas: Cleanup because: " + cause);
} else {
h2clog("html2canvas: Cleanup after timeout: " + options.timeout + " ms.");
Util.log("html2canvas: Cleanup after timeout: " + options.timeout + " ms.");
}
for (src in images) {
@ -276,7 +264,7 @@ _html2canvas.Preload = function( options ) {
}
images.numLoaded++;
images.numFailed++;
h2clog("html2canvas: Cleaned up failed img: '" + src + "' Steps: " + images.numLoaded + " / " + images.numTotal);
Util.log("html2canvas: Cleaned up failed img: '" + src + "' Steps: " + images.numLoaded + " / " + images.numTotal);
}
}
}
@ -308,23 +296,22 @@ _html2canvas.Preload = function( options ) {
timeoutTimer = window.setTimeout(methods.cleanupDOM, options.timeout);
}
h2clog('html2canvas: Preload starts: finding background-images');
Util.log('html2canvas: Preload starts: finding background-images');
images.firstRun = true;
getImages(element);
h2clog('html2canvas: Preload: Finding images');
Util.log('html2canvas: Preload: Finding images');
// load <img> images
for (i = 0; i < imgLen; i+=1){
methods.loadImage( domImages[i].getAttribute( "src" ) );
}
images.firstRun = false;
h2clog('html2canvas: Preload: Done.');
if ( images.numTotal === images.numLoaded ) {
Util.log('html2canvas: Preload: Done.');
if (images.numTotal === images.numLoaded) {
start();
}
return methods;
};
};

View File

@ -117,6 +117,7 @@ function h2cRenderContext(width, height) {
name: variable,
'arguments': value
});
return value;
}
};
}

View File

@ -1,38 +1,81 @@
_html2canvas.Renderer = function(parseQueue, options){
// http://www.w3.org/TR/CSS21/zindex.html
function createRenderQueue(parseQueue) {
var queue = [];
var queue = [],
rootContext;
var sortZ = function(zStack){
var subStacks = [],
stackValues = [];
rootContext = (function buildStackingContext(rootNode) {
var rootContext = {};
function insert(context, node, specialParent) {
var zi = (node.zIndex.zindex === 'auto') ? 0 : Number(node.zIndex.zindex),
contextForChildren = context, // the stacking context for children
isPositioned = node.zIndex.isPositioned,
isFloated = node.zIndex.isFloated,
stub = {node: node},
childrenDest = specialParent; // where children without z-index should be pushed into
zStack.children.forEach(function(stackChild) {
if (stackChild.children && stackChild.children.length > 0){
subStacks.push(stackChild);
stackValues.push(stackChild.zindex);
} else {
queue.push(stackChild);
if (node.zIndex.ownStacking) {
// '!' comes before numbers in sorted array
contextForChildren = stub.context = { '!': [{node:node, children: []}]};
childrenDest = undefined;
} else if (isPositioned || isFloated) {
childrenDest = stub.children = [];
}
});
stackValues.sort(function(a, b) {
return a - b;
});
if (zi === 0 && specialParent) {
specialParent.push(stub);
} else {
if (!context[zi]) { context[zi] = []; }
context[zi].push(stub);
}
stackValues.forEach(function(zValue) {
var index;
subStacks.some(function(stack, i){
index = i;
return (stack.zindex === zValue);
node.zIndex.children.forEach(function(childNode) {
insert(contextForChildren, childNode, childrenDest);
});
sortZ(subStacks.splice(index, 1)[0]);
}
insert(rootContext, rootNode);
return rootContext;
})(parseQueue);
function sortZ(context) {
Object.keys(context).sort().forEach(function(zi) {
var nonPositioned = [],
floated = [],
positioned = [],
list = [];
// positioned after static
context[zi].forEach(function(v) {
if (v.node.zIndex.isPositioned || v.node.zIndex.opacity < 1) {
// http://www.w3.org/TR/css3-color/#transparency
// non-positioned element with opactiy < 1 should be stacked as if it were a positioned element with z-index: 0 and opacity: 1.
positioned.push(v);
} else if (v.node.zIndex.isFloated) {
floated.push(v);
} else {
nonPositioned.push(v);
}
});
(function walk(arr) {
arr.forEach(function(v) {
list.push(v);
if (v.children) { walk(v.children); }
});
})(nonPositioned.concat(floated, positioned));
list.forEach(function(v) {
if (v.context) {
sortZ(v.context);
} else {
queue.push(v.node);
}
});
});
};
}
sortZ(parseQueue.zIndex);
sortZ(rootContext);
return queue;
}
@ -54,5 +97,5 @@ _html2canvas.Renderer = function(parseQueue, options){
return renderer;
}
return getRenderer(options.renderer)(parseQueue, options, document, createRenderQueue(parseQueue), _html2canvas);
return getRenderer(options.renderer)(parseQueue, options, document, createRenderQueue(parseQueue.stack), _html2canvas);
};

View File

@ -24,7 +24,7 @@ _html2canvas.Util.Support = function (options, doc) {
} catch(e) {
return false;
}
h2clog('html2canvas: Parse: SVG powered rendering available');
_html2canvas.Util.log('html2canvas: Parse: SVG powered rendering available');
return true;
}

View File

@ -71,11 +71,11 @@ window.html2canvas = function(elements, opts) {
preload: function( opts ) {
return _html2canvas.Preload( _html2canvas.Util.Extend(opts, options) );
},
log: h2clog
log: _html2canvas.Util.log
};
};
window.html2canvas.log = h2clog; // for renderers
window.html2canvas.log = _html2canvas.Util.log; // for renderers
window.html2canvas.Renderer = {
Canvas: undefined // We are assuming this will be used
};

View File

@ -1,14 +1,13 @@
_html2canvas.Renderer.Canvas = function(options) {
options = options || {};
var doc = document,
safeImages = [],
testCanvas = document.createElement("canvas"),
testctx = testCanvas.getContext("2d"),
Util = _html2canvas.Util,
canvas = options.canvas || doc.createElement('canvas');
function createShape(ctx, args) {
ctx.beginPath();
args.forEach(function(arg) {
@ -32,99 +31,90 @@ _html2canvas.Renderer.Canvas = function(options) {
return true;
}
function isTransparent(backgroundColor) {
return (backgroundColor === "transparent" || backgroundColor === "rgba(0, 0, 0, 0)");
}
function renderItem(ctx, item) {
switch(item.type){
case "variable":
ctx[item.name] = item['arguments'];
break;
case "function":
if (item.name === "createPattern") {
if (item['arguments'][0].width > 0 && item['arguments'][0].height > 0) {
try {
ctx.fillStyle = ctx.createPattern(item['arguments'][0], "repeat");
switch(item.name) {
case "createPattern":
if (item['arguments'][0].width > 0 && item['arguments'][0].height > 0) {
try {
ctx.fillStyle = ctx.createPattern(item['arguments'][0], "repeat");
}
catch(e) {
Util.log("html2canvas: Renderer: Error creating pattern", e.message);
}
}
catch(e) {
h2clog("html2canvas: Renderer: Error creating pattern", e.message);
break;
case "drawShape":
createShape(ctx, item['arguments']);
break;
case "drawImage":
if (item['arguments'][8] > 0 && item['arguments'][7] > 0) {
if (!options.taintTest || (options.taintTest && safeImage(item))) {
ctx.drawImage.apply( ctx, item['arguments'] );
}
}
}
} else if (item.name === "drawShape") {
createShape(ctx, item['arguments']);
} else if (item.name === "drawImage") {
if (item['arguments'][8] > 0 && item['arguments'][7] > 0) {
if (!options.taintTest || (options.taintTest && safeImage(item))) {
ctx.drawImage.apply( ctx, item['arguments'] );
}
}
} else {
ctx[item.name].apply(ctx, item['arguments']);
break;
default:
ctx[item.name].apply(ctx, item['arguments']);
}
break;
}
}
return function(zStack, options, doc, queue, _html2canvas) {
return function(parsedData, options, document, queue, _html2canvas) {
var ctx = canvas.getContext("2d"),
storageContext,
i,
queueLen,
newCanvas,
bounds,
fstyle;
fstyle,
zStack = parsedData.stack;
canvas.width = canvas.style.width = options.width || zStack.ctx.width;
canvas.height = canvas.style.height = options.height || zStack.ctx.height;
fstyle = ctx.fillStyle;
ctx.fillStyle = (isTransparent(zStack.backgroundColor) && options.background !== undefined) ? options.background : zStack.backgroundColor;
ctx.fillStyle = (Util.isTransparent(zStack.backgroundColor) && options.background !== undefined) ? options.background : parsedData.backgroundColor;
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = fstyle;
queue.forEach(function(storageContext) {
// set common settings for canvas
ctx.textBaseline = "bottom";
ctx.save();
if ( options.svgRendering && zStack.svgRender !== undefined ) {
// TODO: enable async rendering to support this
ctx.drawImage( zStack.svgRender, 0, 0 );
} else {
for ( i = 0, queueLen = queue.length; i < queueLen; i+=1 ) {
storageContext = queue.splice(0, 1)[0];
storageContext.canvasPosition = storageContext.canvasPosition || {};
// set common settings for canvas
ctx.textBaseline = "bottom";
if (storageContext.clip){
ctx.save();
ctx.beginPath();
// console.log(storageContext);
ctx.rect(storageContext.clip.left, storageContext.clip.top, storageContext.clip.width, storageContext.clip.height);
ctx.clip();
}
if (storageContext.ctx.storage) {
storageContext.ctx.storage.forEach(renderItem.bind(null, ctx));
}
if (storageContext.clip){
ctx.restore();
}
if (storageContext.transform.matrix) {
ctx.translate(storageContext.transform.origin[0], storageContext.transform.origin[1]);
ctx.transform.apply(ctx, storageContext.transform.matrix);
ctx.translate(-storageContext.transform.origin[0], -storageContext.transform.origin[1]);
}
}
h2clog("html2canvas: Renderer: Canvas renderer done - returning canvas obj");
if (storageContext.clip){
ctx.beginPath();
ctx.rect(storageContext.clip.left, storageContext.clip.top, storageContext.clip.width, storageContext.clip.height);
ctx.clip();
}
queueLen = options.elements.length;
if (storageContext.ctx.storage) {
storageContext.ctx.storage.forEach(function(item) {
renderItem(ctx, item);
});
}
if (queueLen === 1) {
ctx.restore();
});
Util.log("html2canvas: Renderer: Canvas renderer done - returning canvas obj");
if (options.elements.length === 1) {
if (typeof options.elements[0] === "object" && options.elements[0].nodeName !== "BODY") {
// crop image to the bounds of selected (single) element
bounds = _html2canvas.Util.Bounds(options.elements[0]);
newCanvas = doc.createElement('canvas');
newCanvas.width = bounds.width;
newCanvas.height = bounds.height;
newCanvas = document.createElement('canvas');
newCanvas.width = Math.ceil(bounds.width);
newCanvas.height = Math.ceil(bounds.height);
ctx = newCanvas.getContext("2d");
ctx.drawImage(canvas, bounds.left, bounds.top, bounds.width, bounds.height, 0, 0, bounds.width, bounds.height);

View File

@ -194,7 +194,7 @@ _html2canvas.Renderer.SVG = function( options ) {
h2clog("html2canvas: Renderer: SVG Renderer done - returning SVG DOM obj");
_html2canvas.Util.log("html2canvas: Renderer: SVG Renderer done - returning SVG DOM obj");
return svg;
}

View File

@ -0,0 +1,22 @@
<!DOCTYPE html>
<html>
<head>
<title>Inline text in the top element</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<script type="text/javascript" src="../test.js"></script>
<style>
span {
color:blue;
}
p {
background-color: green;
}
</style>
</head>
<body>
Some inline text <span> followed by text in span </span> followed by more inline text.
<p>Then a block level element.</p>
Then more inline text.
</body>
</html>

10
tests/cases/iframe.html Normal file
View File

@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<title>iframe test</title>
<script type="text/javascript" src="../test.js"></script>
</head>
<body>
<iframe src="http://experiments.hertzen.com/csstree/" width="500"></iframe>
</body>
</html>

View File

@ -0,0 +1,21 @@
<!DOCTYPE html>
<html>
<head>
<title>Text shadow tests</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<script type="text/javascript" src="../../test.js"></script>
<style>
span{
text-shadow: 1px 1px 3px #888;
}
strong {
text-shadow: 1px 1px 2px black, 0 0 1em blue, 0 0 0.2em blue;
}
</style>
</head>
<body>
Some text <span> followed by text with shadow </span> followed by more text without shadow.
<strong>Multi text shadow</strong> and some more text without shadow
</body>
</html>

View File

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

View File

@ -0,0 +1,120 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html>
<head>
<!-- https://developer.mozilla.org/en-US/docs/Web/Guide/CSS/Understanding_z_index/The_stacking_context?redirectlocale=en-US&redirectslug=CSS%2FUnderstanding_z-index%2FThe_stacking_context
-->
<title>Understanding CSS z-index: The Stacking Context: Example Source</title>
<script type="text/javascript" src="../../test.js"></script>
<style type="text/css">
* {
margin: 0;
}
html {
padding: 20px;
font: 12px/20px Arial, sans-serif;
}
div {
opacity: 0.7;
position: relative;
}
h1 {
font: inherit;
font-weight: bold;
}
#div1, #div2 {
border: 1px solid #696;
padding: 10px;
background-color: #cfc;
}
#div1 {
z-index: 5;
margin-bottom: 190px;
}
#div2 {
z-index: 2;
}
#div3 {
z-index: 4;
opacity: 1;
position: absolute;
top: 40px;
left: 180px;
width: 330px;
border: 1px solid #900;
background-color: #fdd;
padding: 40px 20px 20px;
}
#div4, #div5 {
border: 1px solid #996;
background-color: #ffc;
}
#div4 {
z-index: 6;
margin-bottom: 15px;
padding: 25px 10px 5px;
}
#div5 {
z-index: 1;
margin-top: 15px;
padding: 5px 10px;
}
#div6 {
z-index: 3;
position: absolute;
top: 20px;
left: 180px;
width: 150px;
height: 125px;
border: 1px solid #009;
padding-top: 125px;
background-color: #ddf;
text-align: center;
}
</style>
</head>
<body>
<div id="div1">
<h1>Division Element #1</h1>
<code>position: relative;<br/>
z-index: 5;</code>
</div>
<div id="div2">
<h1>Division Element #2</h1>
<code>position: relative;<br/>
z-index: 2;</code>
</div>
<div id="div3">
<div id="div4">
<h1>Division Element #4</h1>
<code>position: relative;<br/>
z-index: 6;</code>
</div>
<h1>Division Element #3</h1>
<code>position: absolute;<br/>
z-index: 4;</code>
<div id="div5">
<h1>Division Element #5</h1>
<code>position: relative;<br/>
z-index: 1;</code>
</div>
<div id="div6">
<h1>Division Element #6</h1>
<code>position: absolute;<br/>
z-index: 3;</code>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,48 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html>
<head>
<title>static position inside position relative</title>
<script type="text/javascript" src="../../test.js"></script>
<style type="text/css">
html {
padding: 20px;
font: 12px/20px Arial, sans-serif;
}
#div1 {
padding: 10px;
background: #9bfff8;
position: relative;
}
#div2 {
background: #7cb659;
display: table;
}
#div3 {
float:left;
}
</style>
</head>
<body>
<div id="div1">
<h1>Div Element #1</h1>
<code>position: relative;</code>
<div id="div2">
<h1>Div Element #2</h1>
<code>position: static;</code>
<div id="div3">
<div>
<h1>Div Element #3</h1>
<code>float: left;</code>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,48 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html>
<head>
<title>Negative z-indexes</title>
<script type="text/javascript" src="../../test.js"></script>
<style type="text/css">
html {
padding: 20px;
font: 12px/20px Arial, sans-serif;
}
#div1 {
padding: 10px;
position: relative;
}
#div2 {
background: #7cb659;
position: absolute;
z-index:-999998;
left: 0px;
top: 0px;
}
#div3 {
background: #b69f1a;
position: absolute;
z-index: -999999;
left: 0px;
top: 0px;
}
</style>
</head>
<body>
<div id="div1">
<div id="div2">
<h1>Div Element #2</h1>
<div id="div3">
<div>
<h1>Div Element #3</h1>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,31 @@
<!DOCTYPE html>
<html>
<head>
<title>text above children with negative z-index; z-index tests #13</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<script type="text/javascript" src="../../test.js"></script>
<style type="text/css">
.outer {
background-color:cyan;
width:200px;
height:200px;
z-index:0;
position:relative;
}
.inner {
background-color:green;
width:100px;
height:100px;
z-index:-1;
position:absolute;
top:0;left:0;
}
</style></head>
<body>
<div class="outer">outer
<div class="inner"></div>
</div>
</body>
</html>

View File

@ -0,0 +1,25 @@
<!DOCTYPE html>
<html>
<head>
<title>body text above children with negative index but body bgcolor below</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<script type="text/javascript" src="../../test.js"></script>
<style type="text/css">
* {margin:0;padding:0;}
body {
background-color: green;
}
#div1 {
background-color:cyan;
width:200px;
height:200px;
z-index:-1;
position:absolute;
top:0; left:0;
}
</style></head>
<body>body
<div id="div1"></div>
</body>
</html>

View File

@ -0,0 +1,26 @@
<!DOCTYPE html>
<html>
<head>
<title>body text and bgcolor above children with negative z-index</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<script type="text/javascript" src="../../test.js"></script>
<style type="text/css">
* {margin:0;padding:0;}
html {background-color: gray;}
body {
background-color: green;
}
#div1 {
background-color:cyan;
width:200px;
height:200px;
z-index:-1;
position:absolute;
top:0; left:0;
}
</style></head>
<body>body
<div id="div1"></div>
</body>
</html>

View File

@ -0,0 +1,35 @@
<!DOCTYPE html>
<html>
<head>
<title>z-index tests #4</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<script type="text/javascript" src="../../test.js"></script>
<style type="text/css">
div.lev1 {
width: 250px;
height: 70px;
position: relative;
border: 2px solid #669966;
background-color: #ccffcc;
padding-left: 5px;
}
div.background {
position: absolute;
top: 0;
bottom: 0;
width: 250px;
background-color: #ffdddd;
z-index: -1;
}
</style></head>
<body>
<div class="lev1">
<span>LEVEL #1</span>
</div>
<div class="background"></div>
<div class="lev1">
<span>LEVEL #1</span>
</div>
</body>
</html>

View File

@ -0,0 +1,34 @@
<!DOCTYPE html>
<html>
<head>
<title>z-index tests #5</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<script type="text/javascript" src="../../test.js"></script>
<style type="text/css">
div.lev1 {
width: 250px;
height: 70px;
position: relative;
border: 2px solid #669966;
background-color: #ccffcc;
padding-left: 5px;
}
div.background {
position: fixed;
top: 0;
bottom: 0;
width: 250px;
background-color: #ffdddd;
}
</style></head>
<body>
<div class="lev1">
LEVEL #1
</div>
<div class="background"></div>
<div class="lev1">
<span>LEVEL #1</span>
</div>
</body>
</html>

View File

@ -0,0 +1,33 @@
<!DOCTYPE html>
<html>
<head>
<title>z-index tests #6</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<script type="text/javascript" src="../../test.js"></script>
<style type="text/css">
div {
width: 250px;
height: 70px;
position: relative;
border: 2px solid #669966;
background-color: #ccffcc;
padding-left: 5px;
}
div.z0 {
z-index: 0;
top:105px;
left:20px;
background-color: #ffdddd;
}
div.z1 {
z-index: 1;
}
</style></head>
<body>
<div class="z0"><span>z-index:0</span></div>
<div>default z-index</div>
<div class="z1">z-index:1</div>
</body>
</html>

View File

@ -0,0 +1,102 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
<title>Stacking without z-index</title>
<!-- https://developer.mozilla.org/en-US/docs/Web/Guide/CSS/Understanding_z_index/Stacking_without_z-index?redirectlocale=en-US&redirectslug=CSS%2FUnderstanding_z-index%2FStacking_without_z-index -->
<script type="text/javascript" src="../../test.js"></script>
<style type="text/css">
div {
font: 12px Arial;
}
span.bold { font-weight: bold; }
#normdiv {
height: 70px;
border: 1px solid #999966;
background-color: #ffffcc;
margin: 0px 50px 0px 50px;
text-align: center;
}
#reldiv1 {
opacity: 0.7;
height: 100px;
position: relative;
top: 30px;
border: 1px solid #669966;
background-color: #ccffcc;
margin: 0px 50px 0px 50px;
text-align: center;
}
#reldiv2 {
opacity: 0.7;
height: 100px;
position: relative;
top: 15px;
left: 20px;
border: 1px solid #669966;
background-color: #ccffcc;
margin: 0px 50px 0px 50px;
text-align: center;
}
#absdiv1 {
opacity: 0.7;
position: absolute;
width: 150px;
height: 350px;
top: 10px;
left: 10px;
border: 1px solid #990000;
background-color: #ffdddd;
text-align: center;
}
#absdiv2 {
opacity: 0.7;
position: absolute;
width: 150px;
height: 350px;
top: 10px;
right: 10px;
border: 1px solid #990000;
background-color: #ffdddd;
text-align: center;
}
</style></head>
<body>
<br /><br />
<div id="absdiv1">
<br /><span class="bold">DIV #1</span>
<br />position: absolute;
</div>
<div id="reldiv1">
<br /><span class="bold">DIV #2</span>
<br />position: relative;
</div>
<div id="reldiv2">
<br /><span class="bold">DIV #3</span>
<br />position: relative;
</div>
<div id="absdiv2">
<br /><span class="bold">DIV #4</span>
<br />position: absolute;
</div>
<div id="normdiv">
<br /><span class="bold">DIV #5</span>
<br />no positioning
</div>
</body></html>

View File

@ -0,0 +1,104 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
<title>Stacking and float</title>
<!-- https://developer.mozilla.org/en-US/docs/Web/Guide/CSS/Understanding_z_index/Stacking_and_float?redirectlocale=en-US&redirectslug=CSS%2FUnderstanding_z-index%2FStacking_and_float -->
<script type="text/javascript" src="../../test.js"></script>
<style type="text/css">
div {
font: 12px Arial;
}
span.bold { font-weight: bold; }
#absdiv1 {
opacity: 0.7;
position: absolute;
width: 150px;
height: 200px;
top: 10px;
right: 140px;
border: 1px solid #990000;
background-color: #ffdddd;
text-align: center;
}
#normdiv {
/*opacity: 0.7;*/
height: 100px;
border: 1px solid #999966;
background-color: #ffffcc;
margin: 0px 10px 0px 10px;
text-align: left;
}
#flodiv1 {
opacity: 0.7;
margin: 0px 10px 0px 20px;
float: left;
width: 150px;
height: 200px;
border: 1px solid #009900;
background-color: #ccffcc;
text-align: center;
}
#flodiv2 {
opacity: 0.7;
margin: 0px 20px 0px 10px;
float: right;
width: 150px;
height: 200px;
border: 1px solid #009900;
background-color: #ccffcc;
text-align: center;
}
#absdiv2 {
opacity: 0.7;
position: absolute;
width: 150px;
height: 100px;
top: 130px;
left: 100px;
border: 1px solid #990000;
background-color: #ffdddd;
text-align: center;
}
</style></head>
<body>
<br /><br />
<div id="absdiv1">
<br /><span class="bold">DIV #1</span>
<br />position: absolute;
</div>
<div id="flodiv1">
<br /><span class="bold">DIV #2</span>
<br />float: left;
</div>
<div id="flodiv2">
<br /><span class="bold">DIV #3</span>
<br />float: right;
</div>
<br />
<div id="normdiv">
<br /><span class="bold">DIV #4</span>
<br />no positioning
</div>
<div id="absdiv2">
<br /><span class="bold">DIV #5</span>
<br />position: absolute;
</div>
</body></html>

View File

@ -0,0 +1,108 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head><title>Adding z-index</title>
<!-- https://developer.mozilla.org/en-US/docs/Web/Guide/CSS/Understanding_z_index/Adding_z-index?redirectlocale=en-US&redirectslug=CSS%2FUnderstanding_z-index%2FAdding_z-index -->
<script type="text/javascript" src="../../test.js"></script>
<style type="text/css">
div {
opacity: 0.7;
font: 12px Arial;
}
span.bold { font-weight: bold; }
#normdiv {
z-index: 8;
height: 70px;
border: 1px solid #999966;
background-color: #ffffcc;
margin: 0px 50px 0px 50px;
text-align: center;
}
#reldiv1 {
z-index: 3;
height: 100px;
position: relative;
top: 30px;
border: 1px solid #669966;
background-color: #ccffcc;
margin: 0px 50px 0px 50px;
text-align: center;
}
#reldiv2 {
z-index: 2;
height: 100px;
position: relative;
top: 15px;
left: 20px;
border: 1px solid #669966;
background-color: #ccffcc;
margin: 0px 50px 0px 50px;
text-align: center;
}
#absdiv1 {
z-index: 5;
position: absolute;
width: 150px;
height: 350px;
top: 10px;
left: 10px;
border: 1px solid #990000;
background-color: #ffdddd;
text-align: center;
}
#absdiv2 {
z-index: 1;
position: absolute;
width: 150px;
height: 350px;
top: 10px;
right: 10px;
border: 1px solid #990000;
background-color: #ffdddd;
text-align: center;
}
</style></head>
<body>
<br /><br />
<div id="absdiv1">
<br /><span class="bold">DIV #1</span>
<br />position: absolute;
<br />z-index: 5;
</div>
<div id="reldiv1">
<br /><span class="bold">DIV #2</span>
<br />position: relative;
<br />z-index: 3;
</div>
<div id="reldiv2">
<br /><span class="bold">DIV #3</span>
<br />position: relative;
<br />z-index: 2;
</div>
<div id="absdiv2">
<br /><span class="bold">DIV #4</span>
<br />position: absolute;
<br />z-index: 1;
</div>
<div id="normdiv">
<br /><span class="bold">DIV #5</span>
<br />no positioning
<br />z-index: 8;
</div>
</body></html>

43
tests/certificate.pem.enc Normal file
View File

@ -0,0 +1,43 @@
U2FsdGVkX1/1OhTi++Zu8+GzAWDR9xD8SFu4sB/1mvbeKt7eF3z/t217AJii/H52
FYMoPSnwFzEvXuiS/EIpVzRYugcfc4qSkOA2PE726V1b6UQoxbcJnHK4UW2464Yb
a8dw+saN7KFpzaY0+L09QzIGqVMvtYQG2dTQk3dfAZ4Y/hduoGuCDReyRP7DbIJw
WD933bja/p1HL91AnNo7lIGD529nXugBdlCAEeYGkeAfa7/Or+68sw7fhki3MQJw
xWbo/oND/Fr4WzlGDeTQMTVfEVGYmT8K+aNtcKrgAp3SDQf37zuMnxVaKREmlXOF
IRKaoR4jzDEgiwlsMZ7CvASwKDZDckFKjg3v/yKLjg+NKg2eET+VD+3CvTai5PHQ
xRzPqkmyh+gh3Op2ZpTCtqnlgx7CfobAXjL4BOtu8aT6LZsoS/mMmqfbX49SDBw3
wVXFuhPPGsXDJD2F39kG5vX6LeygOGPMOPbzqimUftWlxcUJhb4mGKV5qcLz+WzH
17E2fv6kEwMRTfjGjUR9zMR8JjBBu0GcKGyiW6MqQwjF+BXvWoIRuwv99x2cpOjN
fAHk9Y9IgJlzWmrTpg1i5sqTNKgmQaKqarJqXwvuq86kJuCMkYr0SxleUlAmbzpC
w25JAMcNqabQLDaTJfV6V5EWAh3sHh+lAcL+QKs2Ddx5K9RwwCnNXtIhatECsu0n
oNDeWcFtaLmGVF/zXpkCkKBg0EsEcFIXZM662TaGCqukIktq61/UsuR7hVQdPXyH
/4rJTITQAXbi+FaQXzrrxKrikU7Rdovn6az5YNXBXh0pMQ9FrahxhYhDrpzpCsNc
STL2JN50u3X28ZgfRs7rB5fY9Xr5pERIAFSrNzFPCHzlStoYNSphJP8lO/XjNkcn
CTOzZrzxVpzqqzXlmArCfx3QtVo+WUUUW9hdOBgKLl19Wt0x5ZvYDPm6ODht9BjD
wfbmkRvdP9CTICBR17l2tPiGcu+4paTKBF6W6aSxQU7Z48uZP19/4cuOiITbcYnt
4wHoNk+M5G9SjLSsOlfmYNMcl0D57AOcbyUUTMZ2Wu0zc3/StJ9aotzEcFSxA1Gs
wPZr/2iqeEynJmUQeFjs01JSwxwYlLF+HOStdn6MEbsW4ftxsXO9Mr2NGAOTaO1/
Y8pyMCRJKOH6kRvHPpzza/stIZQQZk+PMRzS3PgMXsnJoF7cuanrWXJTgiNT6B37
y/ARNr3KtZTGOjc5gE+k32hPuqPi5tVKPrFVUrG2l/87Gqjs4JqNDTjTHBlUIViT
y87uB2f8/8/V50lXf+RYGPKaJ+jzbyaUxp6QRZdqJk8CEZQnrGwpUEzBVMREd+Ow
jn99f/Isn5Vadsdmt/aMy7+TnLVyjPqucIo8dUdkWdfLKF1Q7M06rYgu9MCrsbgi
hGp1/YjqSmYxWZuSUZHvd4ps2x9YI6wuwdWXAH/7+L0IVUghpSNoARrsmi/D7Rg5
4zF6eCOqH13z1luMRg1ZMpRyVQcI/Yu3IbyepnuIeYIqPklDJ6jl3kIdubEtXZTV
1XMlTvZjFuRKLeUXsF/ecTmlob+rc06YvOu91vwpxSMkpYUIc/Yt4tTWey1AbmTG
t4pf8q0cVXrVRuXf8HFEEqQlXumJGWi/WCxAawj35znEIquQDu9ERLgSemdTFH2z
ncoJseApHD9Gf2UpM4Vva+XdI9wrlYURr83/gOxenCgMq7wCWH87yuCG1IsOryPx
1DbES6axy6P+XBQJTDHK560zKSNF+3VVBGEeTQRMAnQs/IHlwWu7Af0DTmUAEO2Z
S1R+Ex2HWMM3i8/Sr8P1JDkm0u7gn2wkfI0xEIf5B10md1iniKb0GovMlfMGJTTo
+mytGTp3XUZl8lXDpbLTR9nqfpB+hL5Qi30Dv08CAQdvX9OzOEQz5T//t13wAKlo
bsbzwIuLhyYbua+wz/3uMS6QAbNNVCgIm/J1rqlwLRmzvSTJyhraamjd2UuwwIau
fgdIA5bYqdagzbUuIk8msItfUrAbhxkyBElVb5RgLVJsUbBQTHW3anrbsTxxMrcR
9jnZZV2Is2ahbnmcqqIjr7fxSqTCtwpyBxRkW8Bh+Sfu2R8x+DLcJ+ID25fbxWto
QrDSGEifpW/5bjGx4lM0j8IxoWqOl0600hlzGsD5ynSu5WPpXQ0CMvuuLXOR4CbZ
kzE5kQXAeQzfwwPcWMMhwt5F4hXAXY/+jM0TfuSc7Z9PYYRWdoDg3awRNlQJS5rk
wDwbMHKUcdi4XSwIhEdbZ4zBo0o7xXKn2XSBNy+Ym39pKWMwOrffsPtWDV+qTHpD
P5rhXEW8aD01kMkxjO2pRZ7nEDQ7VGpi3MuW9FAjXZjcc1grnZSxbKz437K9obI7
zHXGcodAXjU4Oxhs/m8JHVaHsDG5LNxLSmArsmbwcu65mlBCXcefcwOUNoVQDmZG
ENSAMlVAUX7RJ9nxXI9AqKKuVAYSMznseY3V22BCDrRR0nbVkAWjuBTKU6ajfjQ6
W2uhAzl8kH+9Xi10jt5Fhn93b8z4VjrcpZmW/gJuEHm8FY2UFF0P1y3YMCDqCPaz
gXsFZVKPPIswg/xBHB9GhiCSHnvivu/K8Ni4FcBW7smBHuqkk0VoN7CdZvNF0CuX
EANCwDD53hSmfyxR/SY6y1PQCEsX5AiRS8rMGV2bqOjP6/oM9+2JmkRZ3I6Jht2f
sxJsRUTyjecCcwnGDgmLDCZDJqLJoOE6zlRuAOvY1U7m2cFrf9EouOlYUXSeBaEh

View File

@ -70,6 +70,17 @@
<div style="padding: 15% 0 3%;"></div>
</div>
<div id="textShadows">
<div style=""></div>
<div style="text-shadow: 1px 1px 1px"></div>
<div style="text-shadow: 2px 2px rgb(2, 2, 2)"></div>
<div style="text-shadow: 3px 3px rgba(2, 2, 2, .2)"></div>
<div style="text-shadow: rgb(2, 2, 2) 4px 4px"></div>
<div style="text-shadow: rgba(2, 2, 2, .2) 5px 5px"></div>
<div style="text-shadow: rgb(2, 2, 2) 6px 6px, #222222 2px 2px"></div>
<div style="text-shadow: 7px 7px rgba(2, 2, 2, .2), #222222 2px 2px"></div>
</div>
<div id="backgroundPosition">
<div style="background-position: 1px 0;"></div>
<div style="background-position: 1em 0;"></div>

View File

@ -143,7 +143,36 @@ $(function() {
});
});
});
});
test('text-shadow', function() {
$('#textShadows div').each(function(i, el) {
var index = i+1;
var value = _html2canvas.Util.getCSS(el, 'textShadow'),
shadows = _html2canvas.Util.parseTextShadows(value);
if (i == 0) {
QUnit.equal(shadows.length, 0, 'div #' + index);
} else {
QUnit.equal(shadows.length, (i >= 6 ? 2 : 1), 'div #' + index);
QUnit.equal(shadows[0].offsetX, i, 'div #' + index + ' offsetX');
QUnit.equal(shadows[0].offsetY, i, 'div #' + index + ' offsetY');
if (i < 2) {
QUnit.equal(shadows[0].color, 'rgba(0, 0, 0, 0)', 'div #' + index + ' color');
} else if (i % 2 == 0) {
QUnit.equal(shadows[0].color, 'rgb(2, 2, 2)', 'div #' + index + ' color');
} else {
var opacity = '0.199219';
QUnit.equal(shadows[0].color, 'rgba(2, 2, 2, '+opacity+')', 'div #' + index + ' color');
}
// only testing blur once
if (i == 1) {
QUnit.equal(shadows[0].blur, '1', 'div #' + index + ' blur');
}
}
});
});
test('background-image', function () {

View File

@ -1,35 +1,35 @@
<table><thead><tr><td></td><th>chrome<br />23.0.1271.97</th><th>firefox<br />12.0</th><th>iexplorer<br />9</th></tr></thead><tbody>
<tr><td>background/clip.html</td><td>100%</td><td>100%</td><td>99.89%</td></tr>
<tr><td>background/encoded.html</td><td>100%</td><td>100%</td><td>100%</td></tr>
<tr><td>background/linear-gradient.html</td><td>89.87%</td><td>90.73%</td><td>100%</td></tr>
<tr><td>background/multi.html</td><td>100%</td><td>100%</td><td>99.93%</td></tr>
<tr><td>background/position.html</td><td>100%</td><td>100%</td><td>99.87%</td></tr>
<tr><td>background/radial-gradient.html</td><td>73.23%</td><td>70.32%</td><td>94.02%</td></tr>
<tr><td>background/repeat.html</td><td>100%</td><td>100%</td><td>99.92%</td></tr>
<tr><td>border/dashed.html</td><td>96.45%</td><td>98.38%</td><td>97.7%</td></tr>
<tr><td>border/dotted.html</td><td>97.41%</td><td>96.46%</td><td>95.93%</td></tr>
<tr><td>border/double.html</td><td>97.96%</td><td>97.87%</td><td>97.95%</td></tr>
<tr><td>border/radius.html</td><td>99.74%</td><td>99.77%</td><td>99.75%</td></tr>
<tr><td>border/solid.html</td><td>99.97%</td><td>99.97%</td><td>99.98%</td></tr>
<tr><td>forms.html</td><td>95.96%</td><td>94.55%</td><td>95.01%</td></tr>
<tr><td>images/canvas.html</td><td>99.86%</td><td>100%</td><td>100%</td></tr>
<tr><td>images/cross-origin.html</td><td>97.99%</td><td>97.58%</td><td>99.35%</td></tr>
<tr><td>images/empty.html</td><td>99.86%</td><td>99.87%</td><td>99.85%</td></tr>
<tr><td>images/images.html</td><td>83.72%</td><td>96.93%</td><td>55.09%</td></tr>
<tr><td>images/svg.html</td><td>99.92%</td><td>96.79%</td><td>99.93%</td></tr>
<tr><td>list/decimal-leading-zero.html</td><td>99.63%</td><td>99.72%</td><td>35.88%</td></tr>
<tr><td>list/decimal.html</td><td>99.64%</td><td>99.73%</td><td>35.89%</td></tr>
<tr><td>list/lower-alpha.html</td><td>99.65%</td><td>99.73%</td><td>35.89%</td></tr>
<tr><td>list/upper-roman.html</td><td>99.45%</td><td>99.61%</td><td>35.94%</td></tr>
<tr><td>overflow.html</td><td>96.85%</td><td>97.49%</td><td>96.5%</td></tr>
<tr><td>pseudoelements.html</td><td>97.36%</td><td>97.94%</td><td>99.37%</td></tr>
<tr><td>text/chinese.html</td><td>99.75%</td><td>99.74%</td><td>65.76%</td></tr>
<tr><td>text/linethrough.html</td><td>97.14%</td><td>94.12%</td><td>47.08%</td></tr>
<tr><td>text/text.html</td><td>95.71%</td><td>94.67%</td><td>85.01%</td></tr>
<tr><td>text/underline-lineheight.html</td><td>97.06%</td><td>92.35%</td><td>53%</td></tr>
<tr><td>text/underline.html</td><td>97.65%</td><td>93.5%</td><td>47.02%</td></tr>
<tr><td>visibility.html</td><td>99.19%</td><td>98.92%</td><td>99.39%</td></tr>
<tr><td>zindex/z-index1.html</td><td>96.99%</td><td>99.27%</td><td>99.44%</td></tr>
<tr><td>zindex/z-index2.html</td><td>95.85%</td><td>98.06%</td><td>97.72%</td></tr>
<tr><td>zindex/z-index3.html</td><td>98.6%</td><td>98.29%</td><td>98.56%</td></tr>
</tbody></table>
<table><thead><tr><td></td><th>chrome<br />23.0.1271.97</th><th>firefox<br />12.0</th><th>iexplorer<br />9</th><th>chrome<br />25.0.1364.172<br />Mac 10.7.4</th><th>firefox<br />19.0.2<br />Mac 10.7.4</th><th>safari<br />6.0<br />Mac 10.7.4</th></tr></thead><tbody>
<tr><td>background/clip.html</td><td>100%</td><td>100%</td><td>99.89%</td><td>100%</td><td>100%</td><td>100%</td></tr>
<tr><td>background/encoded.html</td><td>100%</td><td>100%</td><td>100%</td><td>100%</td><td>100%</td><td>100%</td></tr>
<tr><td>background/linear-gradient.html</td><td>89.87%</td><td>90.73%</td><td>100%</td><td>95.35%</td><td>91.33%</td><td>91.69%</td></tr>
<tr><td>background/multi.html</td><td>100%</td><td>100%</td><td>99.93%</td><td>100%</td><td>100%</td><td>100%</td></tr>
<tr><td>background/position.html</td><td>100%</td><td>100%</td><td>99.87%</td><td>100%</td><td>100%</td><td>100%</td></tr>
<tr><td>background/radial-gradient.html</td><td>73.23%</td><td>70.32%</td><td>94.02%</td><td>87.65%</td><td>57.99%</td><td>79.95%</td></tr>
<tr><td>background/repeat.html</td><td>100%</td><td>100%</td><td>99.92%</td><td>100%</td><td>100%</td><td>100%</td></tr>
<tr><td>border/dashed.html</td><td>96.45%</td><td>98.38%</td><td>97.7%</td><td>98.36%</td><td>98.47%</td><td>97.43%</td></tr>
<tr><td>border/dotted.html</td><td>97.41%</td><td>96.46%</td><td>95.93%</td><td>98.81%</td><td>96.31%</td><td>98.13%</td></tr>
<tr><td>border/double.html</td><td>97.96%</td><td>97.87%</td><td>97.95%</td><td>99.06%</td><td>97.99%</td><td>98.53%</td></tr>
<tr><td>border/radius.html</td><td>99.74%</td><td>99.77%</td><td>99.75%</td><td>99.88%</td><td>99.81%</td><td>99.75%</td></tr>
<tr><td>border/solid.html</td><td>99.97%</td><td>99.97%</td><td>99.98%</td><td>99.99%</td><td>99.97%</td><td>99.98%</td></tr>
<tr><td>forms.html</td><td>95.96%</td><td>94.55%</td><td>95.01%</td><td>98.57%</td><td>94.2%</td><td>97.69%</td></tr>
<tr><td>images/canvas.html</td><td>99.86%</td><td>100%</td><td>100%</td><td>99.93%</td><td>100%</td><td>99.87%</td></tr>
<tr><td>images/cross-origin.html</td><td>97.99%</td><td>97.58%</td><td>99.35%</td><td>99.7%</td><td>98.41%</td><td>99.89%</td></tr>
<tr><td>images/empty.html</td><td>99.86%</td><td>99.87%</td><td>99.85%</td><td>99.91%</td><td>99.81%</td><td>99.86%</td></tr>
<tr><td>images/images.html</td><td>83.72%</td><td>96.93%</td><td>55.09%</td><td>92.45%</td><td>95.81%</td><td>87.06%</td></tr>
<tr><td>images/svg.html</td><td>99.92%</td><td>96.79%</td><td>99.93%</td><td>99.95%</td><td>96.51%</td><td>99.92%</td></tr>
<tr><td>list/decimal-leading-zero.html</td><td>99.63%</td><td>99.72%</td><td>35.88%</td><td>99.7%</td><td>99.99%</td><td>15.05%</td></tr>
<tr><td>list/decimal.html</td><td>99.64%</td><td>99.73%</td><td>35.89%</td><td>99.71%</td><td>99.99%</td><td>15.06%</td></tr>
<tr><td>list/lower-alpha.html</td><td>99.65%</td><td>99.73%</td><td>35.89%</td><td>99.72%</td><td>99.98%</td><td>15.06%</td></tr>
<tr><td>list/upper-roman.html</td><td>99.45%</td><td>99.61%</td><td>35.94%</td><td>99.59%</td><td>99.99%</td><td>13.97%</td></tr>
<tr><td>overflow.html</td><td>96.85%</td><td>97.49%</td><td>96.5%</td><td>98.15%</td><td>97.96%</td><td>99.42%</td></tr>
<tr><td>pseudoelements.html</td><td>97.36%</td><td>97.94%</td><td>99.37%</td><td>98.73%</td><td>97.81%</td><td>98.29%</td></tr>
<tr><td>text/chinese.html</td><td>99.75%</td><td>99.74%</td><td>65.76%</td><td>93.93%</td><td>96%</td><td>46.75%</td></tr>
<tr><td>text/linethrough.html</td><td>97.14%</td><td>94.12%</td><td>47.08%</td><td>98.99%</td><td>90.28%</td><td>31.02%</td></tr>
<tr><td>text/text.html</td><td>95.71%</td><td>94.67%</td><td>85.01%</td><td>96.83%</td><td>95.6%</td><td>94.63%</td></tr>b
<tr><td>text/underline-lineheight.html</td><td>97.06%</td><td>92.35%</td><td>53%</td><td>99.15%</td><td>93.69%</td><td>40.76%</td></tr>
<tr><td>text/underline.html</td><td>97.65%</td><td>93.5%</td><td>47.02%</td><td>99.35%</td><td>89.85%</td><td>31.07%</td></tr>
<tr><td>visibility.html</td><td>99.19%</td><td>98.92%</td><td>99.39%</td><td>99.69%</td><td>99.32%</td><td>99.74%</td></tr>
<tr><td>zindex/z-index1.html</td><td>96.99%</td><td>99.27%</td><td>99.44%</td><td>98.62%</td><td>98.48%</td><td>98%</td></tr>
<tr><td>zindex/z-index2.html</td><td>95.85%</td><td>98.06%</td><td>97.72%</td><td>97.79%</td><td>96.69%</td><td>96.72%</td></tr>
<tr><td>zindex/z-index3.html</td><td>98.6%</td><td>98.29%</td><td>98.56%</td><td>99.35%</td><td>96.49%</td><td>97.92%</td></tr>
</tbody></table>

View File

@ -1,272 +1,427 @@
(function(){
"use strict;"
var webdriver = require("webdriver.js").webdriver,
http = require("http"),
url = require("url"),
path = require("path"),
base64_arraybuffer = require('base64-arraybuffer'),
PNG = require('png-js'),
fs = require("fs");
"use strict;";
var WebDriver = require('sync-webdriver'),
Bacon = require('baconjs').Bacon,
express = require('express'),
http = require("http"),
https = require("https"),
url = require("url"),
path = require("path"),
base64_arraybuffer = require('base64-arraybuffer'),
PNG = require('png-js'),
fs = require("fs"),
googleapis = require('googleapis'),
jwt = require('jwt-sign');
function createServer(port) {
return http.createServer(function(request, response) {
var uri = url.parse(request.url).pathname,
filename = path.join(process.cwd(), uri);
var port = 8080,
app = express(),
colors = {
red: "\x1b[1;31m",
blue: "\x1b[1;36m",
violet: "\x1b[0;35m",
green: "\x1b[0;32m",
clear: "\x1b[0m"
};
fs.exists(filename, function(exists) {
if(!exists) {
response.writeHead(404, {
"Content-Type": "text/plain"
});
response.write("404 Not Found\n");
response.end();
return;
var server = app.listen(port);
app.use('/index.html', function(req, res){
res.send("<ul>" + tests.map(function(test) {
return "<li><a href='" + test + "'>" + test + "</a></li>";
}).join("") + "</ul>");
});
app.use('/', express.static(__dirname + "/../"));
function mapStat(item) {
return Bacon.combineTemplate({
stat: Bacon.fromNodeCallback(fs.stat, item),
item: item
});
}
function isDirectory(item) {
return item.stat.isDirectory();
}
function getItem(item) {
return item.item;
}
function isFile(item) {
return !isDirectory(item);
}
function arrayStream(arr) {
return Bacon.fromArray(arr);
}
function getTests(path) {
var items = Bacon.fromNodeCallback(fs.readdir, path).flatMap(arrayStream).map(function(name) {
return path + "/" + name;
}).flatMap(mapStat);
return items.filter(isFile).map(getItem).merge(items.filter(isDirectory).map(getItem).flatMap(getTests));
}
function getPixelArray(base64) {
return Bacon.fromCallback(function(callback) {
var arraybuffer = base64_arraybuffer.decode(base64);
(new PNG(arraybuffer)).decode(callback);
});
}
function calculateDifference(h2cPixels, screenPixels) {
var len = h2cPixels.length, index = 0, diff = 0;
for (; index < len; index++) {
if (screenPixels[index] - h2cPixels[index] !== 0) {
diff++;
}
}
return (100 - (Math.round((diff/h2cPixels.length) * 10000) / 100));
}
function canvasToDataUrl(canvas) {
return canvas.toDataURL("image/png").substring(22);
}
function closeServer() {
server.close();
}
function findResult(testName, tests) {
var item = null;
return tests.some(function(testCase) {
item = testCase;
return testCase.test === testName;
}) ? item : null;
}
function compareResults(oldResults, newResults, browser) {
var improved = [],
regressed = [],
newItems = [];
newResults.forEach(function(testCase){
var testResult = testCase.result,
oldResult = findResult(testCase.test, oldResults),
oldResultValue = oldResult ? oldResult.result : null,
dataObject = {
amount: (Math.abs(testResult - oldResultValue) < 0.01) ? 0 : testResult - oldResultValue,
test: testCase.test
};
if (oldResultValue === null) {
newItems.push(dataObject);
} else if (dataObject.amount > 0) {
improved.push(dataObject);
} else if (dataObject.amount < 0) {
regressed.push(dataObject);
}
});
reportChanges(browser, improved, regressed, newItems);
}
function reportChanges(browser, improved, regressed, newItems) {
if (newItems.length > 0 || improved.length > 0 || regressed.length > 0) {
console.log((regressed.length > 0) ? colors.red : colors.green, browser);
regressed.forEach(function(item) {
console.log(colors.red, item.amount + "%", item.test);
});
improved.forEach(function(item) {
console.log(colors.green, item.amount + "%", item.test);
});
newItems.forEach(function(item) {
console.log(colors.blue, "NEW", item.test);
});
}
}
function httpget(options) {
return Bacon.fromCallback(function(callback) {
https.get(options, function(res){
var data = '';
res.on('data', function (chunk){
data += chunk;
});
res.on('end',function(){
callback(data);
});
});
});
}
function parseJSON(str) {
return JSON.parse(str);
}
function writeResults() {
Object.keys(results).forEach(function(browser) {
var filename = "tests/results/" + browser + ".json";
try {
var oldResults = JSON.parse(fs.readFileSync(filename));
compareResults(oldResults, results[browser], browser);
} catch(e) {}
var date = new Date();
var result = JSON.stringify({
browser: browser,
results: results[browser],
timestamp: date.toISOString()
});
if (process.env.MONGOLAB_APIKEY) {
var options = {
host: "api.mongolab.com",
port: 443,
path: "/api/1/databases/html2canvas/collections/webdriver-results?apiKey=" + process.env.MONGOLAB_APIKEY + '&q={"browser":"' + browser + '"}&fo=true&s={"timestamp":-1}'
};
httpget(options).map(parseJSON).onValue(function(data) {
compareResults(data.results, results[browser], browser);
options.method = 'POST';
options.path = "/api/1/databases/html2canvas/collections/webdriver-results?apiKey=" + process.env.MONGOLAB_APIKEY;
options.headers = {
'Content-Type': 'application/json',
'Content-Length': result.length
};
console.log("Sending results for", browser);
var request = https.request(options, function(res) {
console.log(colors.green, "Results sent for", browser);
});
request.write(result);
request.end();
});
}
console.log(colors.violet, "Writing", browser + ".json");
fs.writeFile(filename, result);
});
}
function webdriverOptions(browserName, version, platform) {
var options = {};
if (process.env.SAUCE_USERNAME && process.env.SAUCE_ACCESS_KEY) {
options = {
port: 4445,
hostname: "localhost",
name: process.env.TRAVIS_JOB_ID || "Manual run",
username: process.env.SAUCE_USERNAME,
password: process.env.SAUCE_ACCESS_KEY,
desiredCapabilities: {
browserName: browserName,
version: version,
platform: platform,
"tunnel-identifier": process.env.TRAVIS_JOB_NUMBER
}
};
}
return options;
}
function mapResults(result) {
if (!results[result.browser]) {
results[result.browser] = [];
}
if (fs.statSync(filename).isDirectory()) filename += '/index.html';
fs.readFile(filename, "binary", function(err, file) {
if(err) {
response.writeHead(500, {
"Content-Type": "text/plain"
});
response.write(err + "\n");
response.end();
return;
}
response.writeHead(200);
response.write(file, "binary");
response.end();
results[result.browser].push({
test: result.testCase,
result: result.accuracy
});
});
}
}).listen(port);
}
function formatResultName(navigator) {
return (navigator.browser + "-" + ((navigator.version) ? navigator.version : "release") + "-" + navigator.platform).replace(/ /g, "").toLowerCase();
}
function walkDir(dir, done) {
var results = [];
fs.readdir(dir, function(err, list) {
if (err) return done(err);
var i = 0;
(function next() {
var file = list[i++];
if (!file) return done(null, results);
file = dir + '/' + file;
fs.stat(file, function(err, stat) {
if (stat && stat.isDirectory()) {
walkDir(file, function(err, res) {
results = results.concat(res);
next();
});
} else {
results.push(file);
next();
}
});
})();
});
};
function webdriverStream(navigator) {
var drive = Bacon.fromCallback(discover, "drive", "v2").toProperty();
var auth = Bacon.fromCallback(createToken, "95492219822.apps.googleusercontent.com").toProperty();
function getPixelArray(base64, func) {
var arraybuffer = base64_arraybuffer.decode(base64);
(new PNG(arraybuffer)).decode(func);
}
return Bacon.fromCallback(function(callback) {
new WebDriver.Session(webdriverOptions(navigator.browser, navigator.version, navigator.platform), function() {
var browser = this;
function getBaselineFiles() {
return fs.readdirSync("tests/results/").filter(function(name) {
return /\.baseline$/.test(name);
}).map(function(item) {
return "tests/results/" + item;
});
}
var resultStream = Bacon.fromArray(tests).flatMap(function(testCase) {
console.log(colors.green, "STARTING",formatResultName(navigator), testCase, colors.clear);
browser.url = "http://localhost:" + port + "/" + testCase + "?selenium";
var canvas = browser.element(".html2canvas", 15000);
var dataUrl = Bacon.constant(browser.execute(canvasToDataUrl, canvas));
var screenshot = Bacon.constant(browser.screenshot());
var result = dataUrl.flatMap(getPixelArray).combine(screenshot.flatMap(getPixelArray), calculateDifference);
console.log(colors.green, "COMPLETE", formatResultName(navigator), testCase, colors.clear);
return Bacon.combineTemplate({
browser: formatResultName(navigator),
testCase: testCase,
accuracy: result,
dataUrl: dataUrl,
screenshot: screenshot
});
});
function testPage(browser, url, done) {
browser.url(url)
.$(".html2canvas", 5000, function(){
this.execute(function(){
var canvas = $('.html2canvas')[0];
return canvas.toDataURL("image/png").substring(22);
},[], function(dataurl) {
getPixelArray(dataurl, function(h2cPixels) {
browser.screenshot(function(base64){
getPixelArray(base64, function(screenPixels) {
var len = h2cPixels.length, index = 0, diff = 0;
for (; index < len; index++) {
if (screenPixels[index] - h2cPixels[index] !== 0) {
diff++;
if (fs.existsSync('tests/certificate.pem')) {
Bacon.combineWith(permissionRequest, drive, auth, Bacon.combineWith(uploadRequest, drive, auth, resultStream.doAction(mapResults).flatMap(createImages)).flatMap(executeRequest)).flatMap(executeRequestOriginal).onValue(uploadImages);
}
}
done(100 - (Math.round((diff/h2cPixels.length) * 10000) / 100));
resultStream.onEnd(callback);
});
})
});
});
});
}
}
var writeResultFile = function(filename, json, append) {
fs.writeFile(filename + (append || ""), json);
};
function permissionRequest(client, authClient, images) {
var body = {
value: 'me',
type: 'anyone',
role: 'reader'
};
var openResultFile = function(stats, browser) {
var tests = stats[browser].tests,
filename = "tests/results/" + browser + ".json",
write = writeResultFile.bind(null, filename, JSON.stringify(stats[browser]));
return images.map(function(data) {
var request = client.drive.permissions.insert({fileId: data.id}).withAuthClient(authClient);
request.body = body;
request.fileData = data;
return request;
});
}
fs.exists(filename, function(exists) {
if(exists) {
fs.readFile(filename, "binary", parseResultFile.bind(null, tests, browser, write));
} else {
write();
}
});
};
function executeRequest(requests) {
return Bacon.combineAsArray(requests.map(function(request) {
return Bacon.fromCallback(function(callback) {
request.execute(function(err, result) {
if (!err) {
callback(result);
} else {
console.log("Google drive error", err);
}
});
});
}));
}
var setColor = function(color, text) {
return [color, " * ", ((isNaN(text.amount)) ? "NEW" : text.amount + "%"), " ", text.test].join("");
};
function executeRequestOriginal(requests) {
return Bacon.combineAsArray(requests.map(function(request) {
return Bacon.fromCallback(function(callback) {
request.execute(function(err, result) {
if (!err) {
callback(request.fileData);
} else {
console.log("Google drive error", err);
}
});
});
}));
}
var parseResultFile = function(tests, browser, createResultFile, err, file) {
if (err) throw err;
var data = JSON.parse(file),
improved = [],
regressed = [],
newItems = [],
colors = {
red: "\x1b[1;31m",
blue: "\x1b[1;36m",
violet: "\x1b[0;35m",
green: "\x1b[0;32m"
};
Object.keys(tests).forEach(function(test){
var testResult = tests[test],
dataResult = data.tests[test],
dataObject = {
amount: (Math.abs(testResult - dataResult) < 0.02) ? 0 : testResult - dataResult,
test: test
};
if (dataObject.amount > 0) {
improved.push(dataObject);
} else if (dataObject.amount < 0) {
regressed.push(dataObject);
} else if (dataResult === undefined) {
newItems.push(dataObject);
}
});
if (newItems.length > 0 || improved.length > 0 || regressed.length > 0) {
if (regressed.length === 0) {
createResultFile(".baseline");
}
console.log(colors.violet, "********************");
console.log((regressed.length > 0) ? colors.red : colors.green, browser);
improved.map(setColor.bind(null, colors.green))
.concat(regressed.map(setColor.bind(null, colors.red)))
.concat(newItems.map(setColor.bind(null, colors.blue)))
.forEach(function(item) {
console.log(item);
function createImages(data) {
var dataurlFileName = "tests/results/" + data.browser + "-" + data.testCase.replace(/\//g, "-") + "-html2canvas.png";
var screenshotFileName = "tests/results/" + data.browser + "-" + data.testCase.replace(/\//g, "-") + "-screencapture.png";
return Bacon.combineTemplate({
name: data.testCase,
dataurl: Bacon.fromNodeCallback(fs.writeFile, dataurlFileName, data.dataUrl, "base64").map(function() {
return dataurlFileName;
}),
screenshot: Bacon.fromNodeCallback(fs.writeFile, screenshotFileName, data.screenshot, "base64").map(function() {
return screenshotFileName;
})
});
}
};
function handleResults(stats) {
Object.keys(stats).forEach(openResultFile.bind(null, stats));
}
function runBrowsers(pages){
var port = 5555,
stats = {},
browsers = ["chrome", "firefox", "internet explorer"],
browsersDone = 0,
server = createServer(port),
numPages = pages.length;
var browserDone = function() {
if (++browsersDone >= browsers.length) {
server.close();
handleResults(stats);
}
};
browsers.forEach(function(browserName){
var browser = new webdriver({
browser: browserName
}),
browserType;
browserName = browserName.replace("internet explorer", "iexplorer");
browser.status(function(browserInfo){
browserType = [browserName, browser.version, browserInfo.os.name.replace(/\s+/g, "-").toLowerCase()].join("-");
var date = new Date(),
obj = {
tests: {},
date: date.toISOString(),
version: browser.version
};
stats[browserType] = obj;
stats[browserName] = obj;
processPage(0);
});
function processPage(index) {
var page = pages[index++];
testPage(browser, "http://localhost:" + port + "/" + page + "?selenium", function(result) {
if (numPages > index) {
processPage(index);
} else {
browser.close(browserDone);
}
stats[browserType].tests[page] = result;
function uploadImages(results) {
results.forEach(function(result) {
console.log(result.webContentLink);
});
}
}
});
}
function discover(api, version, callback) {
googleapis.discover(api, version).execute(function(err, client) {
if (!err) {
callback(client);
}
});
}
function createToken(account, callback) {
var payload = {
"iss": '95492219822@developer.gserviceaccount.com',
"scope": 'https://www.googleapis.com/auth/drive',
"aud":"https://accounts.google.com/o/oauth2/token",
"exp": ~~(new Date().getTime() / 1000) + (30 * 60),
"iat": ~~(new Date().getTime() / 1000 - 60)
},
key = fs.readFileSync('tests/certificate.pem', 'utf8'),
transporterTokenRequest = {
method: 'POST',
uri: 'https://accounts.google.com/o/oauth2/token',
form: {
grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
assertion: jwt.sign(payload, key)
},
json: true
},
oauth2Client = new googleapis.OAuth2Client(account, "", "");
oauth2Client.transporter.request(transporterTokenRequest, function(err, result) {
if (!err) {
oauth2Client.credentials = result;
callback(oauth2Client);
}
});
}
function uploadRequest(client, authClient, data) {
return [
client.drive.files.insert({title: data.dataurl, mimeType: 'image/png', description: process.env.TRAVIS_JOB_ID}).withMedia('image/png', fs.readFileSync(data.dataurl)).withAuthClient(authClient),
client.drive.files.insert({title: data.screenshot, mimeType: 'image/png', description: process.env.TRAVIS_JOB_ID}).withMedia('image/png', fs.readFileSync(data.screenshot)).withAuthClient(authClient)
];
}
function runWebDriver() {
var browsers = [
{
browser: "chrome",
platform: "Windows 7"
},{
browser: "firefox",
version: "15",
platform: "Windows 7"
},{
browser: "internet explorer",
version: "9",
platform: "Windows 7"
},{
browser: "internet explorer",
version: "10",
platform: "Windows 8"
},{
browser: "safari",
version: "6",
platform: "OS X 10.8"
},{
browser: "chrome",
platform: "OS X 10.8"
}
];
var testRunnerStream = Bacon.sequentially(1000, browsers).flatMap(webdriverStream);
testRunnerStream.onEnd(writeResults);
testRunnerStream.onEnd(closeServer);
}
var tests = [],
results = {},
testStream = getTests("tests/cases");
testStream.onValue(function(test) {
tests.push(test);
});
exports.tests = function() {
getBaselineFiles().forEach(fs.unlinkSync.bind(fs));
walkDir("tests/cases", function(err, results) {
if (err) throw err;
runBrowsers(results);
});
testStream.onEnd(runWebDriver);
};
exports.baseline = function() {
getBaselineFiles().forEach(function(file) {
var newName = file.substring(0, file.length - 9);
fs.renameSync(file, newName);
console.log(newName, "created");
});
};
exports.markdown = function() {
var data = {}, html = "<table><thead><tr><td></td>",
browsers = ["chrome", "firefox", "iexplorer"];
browsers.forEach(function(browser) {
data[browser] = JSON.parse(fs.readFileSync("tests/results/" + browser + ".json"));
html += "<th>" + browser + "<br />" + data[browser].version + "</th>";
});
html += "</tr></thead><tbody>\n";
Object.keys(data[browsers[0]].tests).forEach(function(testFile) {
html += "<tr><td>" + testFile.substring(12) + "</td>";
browsers.forEach(function(browser) {
html += "<td>" + Math.round(data[browser].tests[testFile] * 100) / 100 + "%</td>";
});
html += "</tr>\n"
});
html += "</tbody></table>";
fs.writeFileSync("tests/readme.md", html);
};
})();