mirror of
https://github.com/niklasvh/html2canvas.git
synced 2023-08-10 21:13:10 +03:00
Compare commits
555 Commits
0.3.4
...
v1.0.0-alp
Author | SHA1 | Date | |
---|---|---|---|
9bc0fb0bd1 | |||
d83bc0247a | |||
13e80cc635 | |||
b239937e00 | |||
e8a4d775e8 | |||
a6a3c1bd0f | |||
850338a76a | |||
63377d47a4 | |||
1d1c74a74e | |||
ef5c59e26d | |||
8b653f89bc | |||
9db8580b97 | |||
4a09264103 | |||
b8178e92b4 | |||
166cbba7c2 | |||
2c8c604f9a | |||
a967475826 | |||
158435761f | |||
0e8a924ea2 | |||
0b4f922405 | |||
7e0b2b5201 | |||
9445b0b598 | |||
f16d581f04 | |||
53dd885279 | |||
ae47d901a1 | |||
929b9de6e0 | |||
57dc7137b2 | |||
90a8422938 | |||
aa47a3a3a6 | |||
c2b7ed9c42 | |||
0f9f8ff494 | |||
1a53643457 | |||
906a66eec7 | |||
c013e49192 | |||
c28263ddc2 | |||
6d9639d0af | |||
badbf52c1c | |||
23b6f29ecf | |||
a41ba8852f | |||
b75fd70042 | |||
5dbb197a82 | |||
bd463d9343 | |||
c093c95881 | |||
f79ae2b73a | |||
4f96abfb7b | |||
26d8d8ea5b | |||
a73dbf8067 | |||
26cdc0441b | |||
fa4a4a4db5 | |||
d77301a353 | |||
8999c76181 | |||
fd1447a6e7 | |||
ea080e0f5d | |||
a101b52685 | |||
ae46010476 | |||
05e5d932f0 | |||
068480f606 | |||
c9fb5d5026 | |||
f3d6d2fdf4 | |||
37a9249a4a | |||
c765e2042f | |||
d327327166 | |||
96cde64c6e | |||
31a9f913ed | |||
af09280c38 | |||
a1b8cbc2fb | |||
18761b7352 | |||
ad487f4585 | |||
fb58d1f0b6 | |||
42a87b8354 | |||
97b0a1f21d | |||
5bd06895e9 | |||
eb380f023f | |||
1c318ab607 | |||
8f575a446d | |||
5969c95481 | |||
82cfcf8704 | |||
c287f51cb6 | |||
edebe082f3 | |||
77393074ba | |||
ed92d2354c | |||
58d1bef3b6 | |||
93f08c7547 | |||
a2895691ba | |||
f7f445c71e | |||
8da77eb689 | |||
965f850e68 | |||
216c290c4b | |||
68900c3087 | |||
018ed765ad | |||
f0fdeac703 | |||
6baa847092 | |||
10ec079762 | |||
6554d4c8c8 | |||
30a2578f38 | |||
82a7349e43 | |||
0224592a96 | |||
12672839f1 | |||
9bdb871307 | |||
56b3b6df27 | |||
adb1f50f00 | |||
e380e2c873 | |||
3977ebeadd | |||
9a7075252b | |||
96fbe954e9 | |||
b8450f4d4a | |||
b3db735415 | |||
959b75a441 | |||
f6a5153d99 | |||
ad1119a76c | |||
fe97851988 | |||
f2b8c16c2c | |||
f278ba4f22 | |||
52a815a13f | |||
213f35f61c | |||
7cc2b856cb | |||
aafb0cfb9c | |||
c5135f4839 | |||
478155af64 | |||
7a3bad2fcb | |||
4f48bc9b0c | |||
6f7a3145fe | |||
ba089b4771 | |||
9f8bae4b09 | |||
f89ba365bd | |||
50579f399e | |||
8a6fb5f733 | |||
83e9b85e1e | |||
3c7c3ddf60 | |||
2ef53b37cc | |||
3b49cba21c | |||
a4aa0c6444 | |||
4ebe9c5fcc | |||
e17bbacd17 | |||
47a7240d6b | |||
6539f9d9c3 | |||
3cb0911de3 | |||
144c9a903e | |||
57dd9b5461 | |||
6d168f46be | |||
318ca48157 | |||
bebb353b3f | |||
eb5ac1122c | |||
ae97dd9a3d | |||
11fdc501b1 | |||
4df19968b5 | |||
9ab7f8cdb1 | |||
5c5531fd47 | |||
e88ac871a3 | |||
2a2ad9bb65 | |||
81e60975cc | |||
a0669300c4 | |||
ba9758cf14 | |||
aa05241ff8 | |||
5b4a6c26ee | |||
364a8aac1c | |||
46078acf71 | |||
4b37909f09 | |||
90f9eeba83 | |||
98ee30643a | |||
a49c3a2320 | |||
4b80102e77 | |||
9201cf7e95 | |||
c2baf42145 | |||
d9a9615ed7 | |||
585a96a918 | |||
9a0d43d60b | |||
3671de81f9 | |||
f3b6df267e | |||
60619dca72 | |||
ed299b3db1 | |||
c7e484af89 | |||
19ecd8bd7f | |||
edb113c230 | |||
33cd2c5ef6 | |||
3400354d78 | |||
fc01263f68 | |||
399ae9f33d | |||
db88784655 | |||
77c73c478f | |||
37b39ec716 | |||
6d53461a68 | |||
33844129e9 | |||
0df71b34f3 | |||
3996449e88 | |||
2699670bf1 | |||
7a58ad019f | |||
9b372a4399 | |||
498527918c | |||
e363f3813e | |||
433dc98177 | |||
b6a73b635a | |||
e2713cd6f9 | |||
4b771d558a | |||
2f5f9f6e59 | |||
60368d4670 | |||
f5e318d968 | |||
612e59c3d3 | |||
fcbcb9bfaa | |||
3b0352a3d7 | |||
313c227a1f | |||
893ce74a33 | |||
e4329c4d37 | |||
069140974b | |||
1e826e32ae | |||
0579804cbb | |||
f15666e156 | |||
d4cb7e8868 | |||
657eb983cf | |||
fc800bff9d | |||
36648afc3d | |||
aa3aafbc0c | |||
525b5c4f36 | |||
706b2abb19 | |||
6b1d116a5e | |||
772767d0d9 | |||
7ebd191488 | |||
6ece2a3d5a | |||
1793b802b1 | |||
8184b7cf51 | |||
04bdb48cba | |||
7e13231807 | |||
199685ebd1 | |||
444869e3ca | |||
717f69d99a | |||
541f6a62d8 | |||
d8dcbdedd8 | |||
5712b621ca | |||
6073928978 | |||
e103ad8219 | |||
d5430070a2 | |||
f3d45e005e | |||
b60b4b2a45 | |||
bd1abe1857 | |||
8a3d1d7f22 | |||
e6d31ada4a | |||
19777c6623 | |||
33d84c82b0 | |||
1e19832171 | |||
fa659ad1df | |||
f517a35781 | |||
3f3424e49c | |||
1d8a316f13 | |||
b1f948bb60 | |||
24d9a22556 | |||
7ee2f411b0 | |||
9406f76a18 | |||
6092629679 | |||
349d0a300c | |||
0a7df6d9b9 | |||
70241a789d | |||
075c836d16 | |||
c9993a7237 | |||
3b8d4dece2 | |||
440120b087 | |||
e80fe312ee | |||
b141c9f0d1 | |||
3a3a61e316 | |||
6c08c3fa04 | |||
d4c9a41873 | |||
6347e7f043 | |||
9220eb6def | |||
af965cc3a6 | |||
9d088fa431 | |||
b8d3688c29 | |||
9907149513 | |||
645fcd60b3 | |||
0325a9b836 | |||
36052c2765 | |||
281efd2d5e | |||
44b958beaf | |||
2a020e5a21 | |||
c20e679f2c | |||
52c669fe5b | |||
382c16a522 | |||
ba9d5201cf | |||
2e2d722e3d | |||
b9edf0b1c5 | |||
07f793b0ed | |||
180b624cb3 | |||
b2280bc8ec | |||
f3f92ab425 | |||
503add6e2f | |||
d2bfb810d4 | |||
649cdd4e37 | |||
73a34493ac | |||
ce1c4c84f5 | |||
44beaf2989 | |||
d716210509 | |||
be5d1f8665 | |||
340b125b19 | |||
ad1f0d418c | |||
08373f0bd4 | |||
6959058560 | |||
d6ed6c0194 | |||
9ee87339a3 | |||
b7595e19e9 | |||
281e6bbedf | |||
fee91055b2 | |||
650ead63e5 | |||
b35fcaeaf9 | |||
81c22866bc | |||
25d892f525 | |||
9db1ecfdfc | |||
12d85e3c04 | |||
3101f2007a | |||
85b77ca49f | |||
bb8c5a973b | |||
0187fcab42 | |||
cfe4137bcc | |||
d2c3378c3e | |||
95f4bcea0a | |||
9bae5b610a | |||
7ce46e95cd | |||
84c1dc6283 | |||
15ca3381eb | |||
18d95d669b | |||
5137e5f35a | |||
314d26f1f1 | |||
82e5a8a7c0 | |||
9af96d3812 | |||
6f2a775841 | |||
f2b662801e | |||
60587c72bf | |||
d9d516d27e | |||
899d5321d4 | |||
5d20493f46 | |||
b5891c49b4 | |||
467ff87482 | |||
9beae48cf0 | |||
17731169e9 | |||
e27c41efd3 | |||
1f90defbfa | |||
b4bb34c95b | |||
64668fe694 | |||
9ebae161e2 | |||
729bc88d1f | |||
b1c2f03ae9 | |||
74cb3466ec | |||
2afdcaff35 | |||
1070cec852 | |||
ba9a33b1bc | |||
f474542382 | |||
0cb259f6cd | |||
1a7f5732bf | |||
2b8389cb64 | |||
8b8c080841 | |||
6201e09118 | |||
517fd8cd1d | |||
e228fc57ce | |||
46cc8b6975 | |||
443fd17a12 | |||
0b213eecef | |||
ae1a15f7c5 | |||
cea3005056 | |||
1d4b1753d6 | |||
f00b23a9ec | |||
e9afe03960 | |||
57d20a9794 | |||
35c5ca3340 | |||
7cc7f80ee2 | |||
4c75d819a4 | |||
838f91e156 | |||
8bea01b81d | |||
e115180731 | |||
e782efa614 | |||
806cd60474 | |||
0515765788 | |||
0fd25f048d | |||
14ff672c6e | |||
38fad5ac17 | |||
a31de83368 | |||
1fb3b53fc0 | |||
4d465116da | |||
74ce2c5062 | |||
fbeb6e72f2 | |||
c097f11ce3 | |||
b6ebf2acf6 | |||
e9c3d9d332 | |||
c232da2595 | |||
c759600c06 | |||
5f45968154 | |||
feb2fd0a63 | |||
fb944d9381 | |||
564634ba97 | |||
dd7468c446 | |||
eb00650b02 | |||
1d03a5f9a4 | |||
ea7d6b485d | |||
fd4fd95429 | |||
10b40821e5 | |||
518dd702a2 | |||
056953f2c1 | |||
9a57a08c72 | |||
26a81da2f0 | |||
57028ab423 | |||
c9e2fc27c8 | |||
2777a3e079 | |||
02ab96dc5f | |||
65746bd2e3 | |||
16d3bef255 | |||
0277c34310 | |||
f35ef0fe6f | |||
2c8dd18d55 | |||
6b5f31eef0 | |||
832b9ee934 | |||
73698e8ceb | |||
37fbd3f90e | |||
5300f20b78 | |||
f0e234a1d8 | |||
30163ab16f | |||
407145da94 | |||
c5e6eaa849 | |||
2d39cd0719 | |||
ebd7828dc8 | |||
877367d499 | |||
fd888bde8d | |||
7d2e12c3dd | |||
a7d3e9c2a2 | |||
f49e147b2f | |||
a902f92a14 | |||
e1573f8aed | |||
655779743b | |||
1a30167f6a | |||
2c58c56fbe | |||
288b851d05 | |||
0afb0fae0e | |||
a4702423cc | |||
f4aef61e5a | |||
cb210b2e61 | |||
a80ef26c42 | |||
2580b48d16 | |||
87d7894d71 | |||
7842768707 | |||
403908c7da | |||
8c8128b80a | |||
0d4b6ba665 | |||
b91fd9bc87 | |||
62d27c20c3 | |||
e811effe2a | |||
6156e28721 | |||
2b000f0061 | |||
843db27f72 | |||
bbdfe8a035 | |||
9da3bd7769 | |||
85fa81ad95 | |||
a5d74bcfd9 | |||
cf735a9fa1 | |||
9b051b8749 | |||
822311ed0c | |||
c39225ceac | |||
763125ce6d | |||
5ac4b42e33 | |||
cacb9a468f | |||
8623e4014b | |||
2d95a761b0 | |||
b2df50a858 | |||
8ddf10fc04 | |||
410537456a | |||
3aa7d69cc7 | |||
7a3dd7572c | |||
1b37c5d1ea | |||
67850f2cee | |||
88dd1e41c0 | |||
554185ed4a | |||
222dfa84b7 | |||
9b0c32c62c | |||
2bb926c7d0 | |||
861a18f977 | |||
cf15c4a59f | |||
2f3f27b672 | |||
3032dc6ce0 | |||
2b0db917e3 | |||
d73e53fbf0 | |||
6d29cc5df3 | |||
dc21fab450 | |||
5492d80135 | |||
a313524aa4 | |||
3edf9fa743 | |||
7d7deca342 | |||
053a0a4787 | |||
65b4bdf282 | |||
56780565f4 | |||
55ed0ffde0 | |||
7da4326885 | |||
eb57b61859 | |||
bb73d3c15e | |||
9b5ae9e191 | |||
2557a83dbe | |||
67ccb33dd5 | |||
85706166cc | |||
57d6003b65 | |||
9ce03d6e86 | |||
3d3f923ed8 | |||
42abcfe5fc | |||
473ff45267 | |||
496c8488bd | |||
df0f436e66 | |||
6ce619f0c0 | |||
3774f3655c | |||
0c66766d55 | |||
ba9ace71ba | |||
b19c200c6c | |||
29bd4c8c05 | |||
0dd2c24ab4 | |||
d93e36d768 | |||
1357057cbf | |||
74e93cbb93 | |||
45853a083c | |||
cb43e09899 | |||
bb1cd21367 | |||
5faa45847e | |||
0c2572b5ce | |||
aa5b3d41c4 | |||
516edbceea | |||
c72a02bf64 | |||
c3e9636e4f | |||
816ff6d3c5 | |||
5f1fedf8f0 | |||
a82234873e | |||
d1ee6e9d64 | |||
e7b4dd17b9 | |||
05f3af4901 | |||
630bed968e | |||
7c870a6fb8 | |||
52033a5d72 | |||
07e80df399 | |||
7e38df782c | |||
7f1cbc70a8 | |||
b81d7473e3 | |||
d7bef66cc5 | |||
04782c1716 | |||
a4b7d04e80 | |||
62cb111956 | |||
3171390f80 | |||
8fe61a43b0 | |||
f0ce6917fa | |||
118b42eb7e | |||
3bbcfe36e0 | |||
cdc7a744e3 | |||
fb0e7ca29d | |||
4e978c60cc | |||
c9ed8d91fa | |||
582d10e00d | |||
4684177df8 | |||
5198028c7b | |||
d6ddb7e29d | |||
9df9426c91 | |||
76aa1e8feb | |||
2841f19647 | |||
c6baabc99c | |||
44023015b6 | |||
bca6458301 | |||
7c0b893564 | |||
084bf4b039 |
4
.babelrc
Normal file
4
.babelrc
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"plugins": ["transform-object-rest-spread"],
|
||||
"presets": ["es2015", "flow"]
|
||||
}
|
17
.editorconfig
Normal file
17
.editorconfig
Normal file
@ -0,0 +1,17 @@
|
||||
# http://editorconfig.org
|
||||
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
[{.travis.yml,package.json}]
|
||||
# The indent size used in the `package.json` file cannot be changed
|
||||
# https://github.com/npm/npm/pull/3180#issuecomment-16336516
|
||||
indent_size = 2
|
23
.eslintrc
Normal file
23
.eslintrc
Normal file
@ -0,0 +1,23 @@
|
||||
{
|
||||
"parser": "babel-eslint",
|
||||
"plugins": [
|
||||
"flowtype",
|
||||
"prettier"
|
||||
],
|
||||
"rules": {
|
||||
"no-console": ["error", { "allow": ["warn", "error"] }],
|
||||
"flowtype/boolean-style": [
|
||||
2,
|
||||
"boolean"
|
||||
],
|
||||
"flowtype/no-weak-types": 2,
|
||||
"flowtype/delimiter-dangle": 2,
|
||||
"prettier/prettier": ["error", {
|
||||
"singleQuote": true,
|
||||
"bracketSpacing": false,
|
||||
"parser": "flow",
|
||||
"tabWidth": 4,
|
||||
"printWidth": 100
|
||||
}]
|
||||
}
|
||||
}
|
7
.flowconfig
Normal file
7
.flowconfig
Normal file
@ -0,0 +1,7 @@
|
||||
[ignore]
|
||||
.*/www/.*
|
||||
[include]
|
||||
[libs]
|
||||
./flow-typed
|
||||
[options]
|
||||
[lints]
|
19
.github/ISSUE_TEMPLATE.md
vendored
Normal file
19
.github/ISSUE_TEMPLATE.md
vendored
Normal file
@ -0,0 +1,19 @@
|
||||
Please make sure you are testing with the latest [release of html2canvas](https://github.com/niklasvh/html2canvas/releases).
|
||||
Old versions are not supported and issues reported for them will be closed.
|
||||
|
||||
# Please follow the general troubleshooting steps first:
|
||||
|
||||
- [ ] You are using the latest [version](https://github.com/niklasvh/html2canvas/releases)
|
||||
- [ ] You are testing using the non-minified version of html2canvas and checked any potential issues reported in the console
|
||||
|
||||
<!-- You can erase any parts of this template not applicable to your Issue. -->
|
||||
|
||||
### Bug reports:
|
||||
|
||||
Please replace this line with a brief summary of your issue **AND** if possible an example on [jsfiddle](https://jsfiddle.net/).
|
||||
|
||||
### Specifications:
|
||||
|
||||
* html2canvas version tested with:
|
||||
* Browser & version:
|
||||
* Operating system:
|
37
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
37
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@ -0,0 +1,37 @@
|
||||
A similar PR may already be submitted!
|
||||
Please search among the [Pull request](https://github.com/niklasvh/html2canvas/pulls) before creating one.
|
||||
|
||||
Thanks for submitting a pull request! Please provide enough information so that others can review your pull request:
|
||||
|
||||
Before opening a pull request, please make sure all the tests pass locally by running `npm test`.
|
||||
|
||||
**Summary**
|
||||
|
||||
<!-- Summary of the PR -->
|
||||
|
||||
This PR fixes/implements the following **bugs/features**
|
||||
|
||||
* [ ] Bug 1
|
||||
* [ ] Bug 2
|
||||
* [ ] Feature 1
|
||||
* [ ] Feature 2
|
||||
* [ ] Breaking changes
|
||||
|
||||
<!-- You can skip this if you're fixing a typo or adding an app to the Showcase. -->
|
||||
|
||||
Explain the **motivation** for making this change. What existing problem does the pull request solve?
|
||||
|
||||
<!-- Example: When "Adding a function to do X", explain why it is necessary to have a way to do X. -->
|
||||
|
||||
**Test plan (required)**
|
||||
|
||||
Demonstrate how the issue/feature can be replicated. For most cases, simply adding an appropriate html/css template into the [reftests](https://github.com/niklasvh/html2canvas/tree/master/tests/reftests) should be sufficient. Please see other tests there for reference.
|
||||
|
||||
**Code formatting**
|
||||
|
||||
Please make sure that code adheres to the project code formatting. Running `npm run format` will automatically format your code correctly.
|
||||
|
||||
**Closing issues**
|
||||
|
||||
<!-- Put `closes #XXXX` in your comment to auto-close the issue that your PR fixes (if such). -->
|
||||
Fixes #
|
18
.gitignore
vendored
18
.gitignore
vendored
@ -1,13 +1,17 @@
|
||||
/dist
|
||||
/build
|
||||
/nbproject/
|
||||
/images/
|
||||
/external/
|
||||
/tests/templates/
|
||||
/tests/cache/
|
||||
/tests/flashcanvas.html
|
||||
/lib/
|
||||
/build/
|
||||
image.jpg
|
||||
/.project
|
||||
/.settings/
|
||||
node_modules/
|
||||
.envrc
|
||||
*.sublime-workspace
|
||||
*.baseline
|
||||
*.iml
|
||||
.idea/
|
||||
.DS_Store
|
||||
npm-debug.log
|
||||
debug.log
|
||||
tests/reftests.js
|
||||
*.log
|
||||
|
15
.npmignore
Normal file
15
.npmignore
Normal file
@ -0,0 +1,15 @@
|
||||
build/
|
||||
examples/
|
||||
scripts/
|
||||
src/
|
||||
tests/
|
||||
.github/
|
||||
*.iml
|
||||
.babelrc
|
||||
.idea/
|
||||
.npmignore
|
||||
.eslintrc
|
||||
.travis.yml
|
||||
karma.js
|
||||
karma.conf.js
|
||||
webpack.config.js
|
46
.travis.yml
Normal file
46
.travis.yml
Normal file
@ -0,0 +1,46 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- '7'
|
||||
env:
|
||||
global:
|
||||
- secure: eW41gIqOizwO4pTgWnAAbW75AP7F+CK9qfSed/fSh4sJ9HWMIY1YRIaY8gjr+6jV/f7XVHcXuym6ZxgINYSkVKbF1JKxBJNLOXtSgNbVHSic58pYFvUjwxIBI9aPig9uux1+DbnpWqXFDTcACJSevQZE0xwmjdrSkDLgB0G34v8=
|
||||
- secure: Y2Av+Gd3z9uQEB36GwdOOuGka0hx0/HeitASEo59z934O8RxnmN9eNTXS7dDT3XtKtwxIyLTOEpS7qlRdWahH28hr/dS4xJj6ao58C+1xMcDs6NAPGmDxUlcJWpcGEsnjmXjQCc3fBioSTdpIBrK/gdvgpNh77UKG74Sk7Z+YGk=
|
||||
addons:
|
||||
chrome: stable
|
||||
firefox: latest
|
||||
dist: trusty
|
||||
sudo: false
|
||||
before_script:
|
||||
- export DISPLAY=:99.0
|
||||
- sh -e /etc/init.d/xvfb start
|
||||
notifications:
|
||||
webhooks:
|
||||
urls:
|
||||
- https://webhooks.gitter.im/e/2b007d4f86de89588804
|
||||
on_success: always
|
||||
on_failure: always
|
||||
on_start: false
|
||||
script:
|
||||
- npm run build
|
||||
- npm test
|
||||
deploy:
|
||||
- provider: npm
|
||||
email: niklasvh@gmail.com
|
||||
api_key:
|
||||
secure: G/Szpr8q4/D6hp+H/Z9yyluUXtHAwf7LLa1Y07X59/Enlj1h7V5fQ7AW4/iAVM3XbIsrCPWR3dJU9g/ZxpxFg4OovIHVpS2Jr/mahtPYWdHR3pWuSmMW8QD+Twnq2VAFwSgg5Oumq3QxhX3YbCOnZox6+6Uviqk8FO7Z5B0RwW4=
|
||||
skip_cleanup: true
|
||||
on:
|
||||
tags: true
|
||||
branch: master
|
||||
repo: niklasvh/html2canvas
|
||||
- provider: releases
|
||||
api_key:
|
||||
secure: "PowO/Jat660k3gHcjgI6DlJz15RM7pLUu11UPsLCtYJ8ZwodppE6Keg0DfVkSFSIZttZor+UssDwP/WOEqfZNLqmXbcj3Gec4xolohet/GOe0KJKKuF/HgggbcxumopxMX6sMVePlMBpkLpHh7tgEAEHBWTlzC1c1a7Xa48fZ7k="
|
||||
file:
|
||||
- "dist/html2canvas.js"
|
||||
- "dist/html2canvas.min.js"
|
||||
skip_cleanup: true
|
||||
on:
|
||||
tags: true
|
||||
branch: master
|
||||
repo: niklasvh/html2canvas
|
102
CHANGELOG.md
Normal file
102
CHANGELOG.md
Normal file
@ -0,0 +1,102 @@
|
||||
### Changelog ###
|
||||
|
||||
#### v1.0.0-alpha3 - TBD ####
|
||||
* Disable `foreignObjectRendering` by default (#1295)
|
||||
* Fix background-size when using background-origin and background-size: cover/contain (#1299)
|
||||
* Added support for background-origin: content-box (#1299)
|
||||
|
||||
#### v1.0.0-alpha2 - 7.12.2017 ####
|
||||
* Fix scroll positions for CanvasRenderer (#1259)
|
||||
* Fix `data-html2canvas-ignore` attribute (#1253)
|
||||
* Fix decimal `letter-spacing` values (#1293)
|
||||
|
||||
#### v1.0.0-alpha1 - 5.12.2017 ####
|
||||
* Complete rewrite of library
|
||||
##### Breaking Changes #####
|
||||
* Remove deprecated onrendered callback, calling `html2canvas` returns a `Promise<HTMLCanvasElement>`
|
||||
* Removed option `type`, same results can be achieved by assigning `x`, `y`, `scrollX`, `scrollY`, `width` and `height` properties.
|
||||
|
||||
##### New featues / fixes #####
|
||||
* Add support for scaling canvas (defaults to device pixel ratio)
|
||||
* Add support for multiple text-shadows
|
||||
* Add support for multiple text-decorations
|
||||
* Add support for text-decoration-color
|
||||
* Add support for percentage values for border-radius
|
||||
* Correctly handle px and percentage values in linear-gradients
|
||||
* Correctly support all angle types for linear-gradients
|
||||
* Add support for multiple values for background-repeat, background-position and background-size
|
||||
|
||||
#### v0.5.0-beta4 - 23.1.2016 ####
|
||||
* Fix logger requiring access to window object
|
||||
* Derequire browserify build
|
||||
* Fix rendering of specific elements when window is scrolled and `type` isn't set to `view`
|
||||
|
||||
#### v0.5.0-beta3 - 6.12.2015 ####
|
||||
* Handle color names in linear gradients
|
||||
|
||||
#### v0.5.0-beta2 - 20.10.2015 ####
|
||||
* Remove Promise polyfill (use native or provide it yourself)
|
||||
|
||||
#### v0.5.0-beta1 - 19.10.2015 ####
|
||||
* Fix bug with unmatched color stops in gradients
|
||||
* Fix scrolling issues with iOS
|
||||
* Correctly handle named colors in gradients
|
||||
* Accept matrix3d transforms
|
||||
* Fix transparent colors breaking gradients
|
||||
* Preserve scrolling positions on render
|
||||
|
||||
#### v0.5.0-alpha2 - 3.2.2015 ####
|
||||
* Switch to using browserify for building
|
||||
* Fix (#517) Chrome stretches background images with 'auto' or single attributes
|
||||
|
||||
#### v0.5.0-alpha - 19.1.2015####
|
||||
* Complete rewrite of library
|
||||
* Switched interface to return Promise
|
||||
* Uses hidden iframe window to perform rendering, allowing async rendering and doesn't force the viewport to be scrolled to the top anymore.
|
||||
* Better support for unicode
|
||||
* Checkbox/radio button rendering
|
||||
* SVG rendering
|
||||
* iframe rendering
|
||||
* Changed format for proxy requests, permitting binary responses with CORS headers as well
|
||||
* Fixed many layering issues (see z-index tests)
|
||||
|
||||
#### 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
|
||||
* Support for border-radius
|
||||
* Support for multiple background images, size, and clipping
|
||||
* Support for :before and :after pseudo elements
|
||||
* Support for placeholder rendering
|
||||
* Reformatted all tests to small units to test specific features
|
||||
|
||||
#### 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>)
|
||||
* Radial gradients (<a href="https://github.com/niklasvh/html2canvas/commit/4f22c18043a73c0c3bbf3b5e4d62714c56acd3c7">SunboX</a>)
|
||||
* 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.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>)
|
||||
* Improved minification saved ~1K! (<a href="https://github.com/cobexer/html2canvas/commit/b82be022b2b9240bd503e078ac980bde2b953e43">cobexer</a>)
|
||||
* 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.3.2 - 20.2.2012 ####
|
||||
|
||||
* Added changelog!
|
||||
* Added bookmarklet (<a href="https://github.com/niklasvh/html2canvas/commit/b320dd306e1a2d32a3bc5a71b6ebf6d8c060cde5">cobexer</a>)
|
||||
* Option to select single element to render (<a href="https://github.com/niklasvh/html2canvas/commit/0cb252ada91c84ef411288b317c03e97da1f12ad">niklasvh</a>)
|
||||
* Fixed closure compiler warnings (<a href="https://github.com/niklasvh/html2canvas/commit/36ff1ec7aadcbdf66851a0b77f0b9e87e4a8e4a1">cobexer</a>)
|
||||
* Enable profiling in FF (<a href="https://github.com/niklasvh/html2canvas/commit/bbd75286a8406cf9e5aea01fdb7950d547edefb9">cobexer</a>)
|
37
LICENSE
37
LICENSE
@ -1,21 +1,22 @@
|
||||
/*
|
||||
The MIT License
|
||||
Copyright (c) 2012 Niklas von Hertzen
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
Permission is hereby granted, free of charge, to any person
|
||||
obtaining a copy of this software and associated documentation
|
||||
files (the "Software"), to deal in the Software without
|
||||
restriction, including without limitation the rights to use,
|
||||
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
*/
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
OTHER DEALINGS IN THE SOFTWARE.
|
7
Makefile
7
Makefile
@ -1,7 +0,0 @@
|
||||
build:
|
||||
ant
|
||||
|
||||
install:
|
||||
brew install https://raw.github.com/Homebrew/homebrew-dupes/master/ant.rb
|
||||
|
||||
.PHONY: install build
|
86
README.md
Normal file
86
README.md
Normal file
@ -0,0 +1,86 @@
|
||||
html2canvas
|
||||
===========
|
||||
|
||||
[Homepage](https://html2canvas.hertzen.com) | [Downloads](https://github.com/niklasvh/html2canvas/releases) | [Questions](http://stackoverflow.com/questions/tagged/html2canvas?sort=newest) | [Donate](https://www.gittip.com/niklasvh/)
|
||||
|
||||
[](https://gitter.im/niklasvh/html2canvas?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) [](https://travis-ci.org/niklasvh/html2canvas)
|
||||
|
||||
#### JavaScript HTML renderer ####
|
||||
|
||||
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 **not require any rendering from the server**, as the whole image is created on the **client's 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.
|
||||
|
||||
### Browser compatibility ###
|
||||
|
||||
The library should work fine on the following browsers (with `Promise` polyfill):
|
||||
|
||||
* Firefox 3.5+
|
||||
* 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 ###
|
||||
|
||||
The html2canvas library utilizes `Promise`s and expects them to be available in the global context. If you wish to
|
||||
support [older browsers](http://caniuse.com/#search=promise) that do not natively support `Promise`s, please include a polyfill such as
|
||||
[es6-promise](https://github.com/jakearchibald/es6-promise) before including `html2canvas`.
|
||||
|
||||
**Note!** These instructions are for using the current dev version of 0.5, for the latest release version (0.4.1), checkout the [old readme](https://github.com/niklasvh/html2canvas/blob/v0.4/readme.md).
|
||||
|
||||
To render an `element` with html2canvas, simply call:
|
||||
` html2canvas(element[, options]);`
|
||||
|
||||
The function returns a [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) containing the `<canvas>` element. Simply add a promise fullfillment handler to the promise using `then`:
|
||||
|
||||
html2canvas(document.body).then(function(canvas) {
|
||||
document.body.appendChild(canvas);
|
||||
});
|
||||
|
||||
### Building ###
|
||||
|
||||
You can download ready builds [here](https://github.com/niklasvh/html2canvas/releases).
|
||||
|
||||
Clone git repository:
|
||||
|
||||
$ git clone git://github.com/niklasvh/html2canvas.git
|
||||
|
||||
Install dependencies:
|
||||
|
||||
$ npm install
|
||||
|
||||
Build browser bundle
|
||||
|
||||
$ npm run 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 [phantomjs](http://phantomjs.org/).
|
||||
|
||||
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 committing changes, these should generally not go decrease from the baseline values.
|
||||
|
||||
Start by downloading the dependencies:
|
||||
|
||||
$ npm install
|
||||
|
||||
Run tests:
|
||||
|
||||
$ npm test
|
||||
|
||||
### Examples ###
|
||||
|
||||
For more information and examples, please visit the [homepage](https://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 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.
|
@ -1,57 +0,0 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>html2canvas Bookmarklet</title>
|
||||
<script type="text/javascript" src="external/jquery-1.6.2.min.js"></script>
|
||||
<script type="text/javascript">
|
||||
var isDebug = false, origBookmarklet = '';
|
||||
function patchLinks() {
|
||||
var bookmarklet = origBookmarklet;
|
||||
if (isDebug) {
|
||||
bookmarklet = bookmarklet.replace('//DEBUG: ', '');
|
||||
}
|
||||
bookmarklet = bookmarklet.replace(/\s\/\/.*/g, ''); // remove single line comments
|
||||
bookmarklet = bookmarklet.replace(/[\u000A\u000D]+/g, ''); // remove all linebreaks
|
||||
bookmarklet = bookmarklet.replace(/\/\*.*?\*\//g, ''); // remove multi line comments
|
||||
bookmarklet = bookmarklet.replace(/\s\s+/g, ' '); // reduce multiple spaces to single spaces
|
||||
bookmarklet = bookmarklet.replace(/\s+=\s+/g, '=');
|
||||
$('a.bookmarklet').each(function(_, el) {
|
||||
el.href = $(el).attr('data-href') + bookmarklet;
|
||||
});
|
||||
}
|
||||
$(function() {
|
||||
$('input[type=checkbox]').bind('change', function() {
|
||||
isDebug = $(this).is(':checked');
|
||||
patchLinks();
|
||||
}).change();
|
||||
$.ajax('src/plugins/bookmarklet.js', {
|
||||
dataType: 'text',
|
||||
success: function(data, status, xhr) {
|
||||
origBookmarklet = data;
|
||||
patchLinks();
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>html2canvas Bookmarklet</h1>
|
||||
<p>
|
||||
If you use a normal browser: drag the normal <a class="bookmarklet" data-href="javascript:">html2canvas</a> bookmarklet to your bookmarks toolbar.<br />
|
||||
If not use the following link: <a class="bookmarklet" data-href="#_remove_this_javascript:">bookmarklet for those special mobile devices</a>
|
||||
click / tap that link and then bookmark the page, edit the bookmark and remove the start up until including <code>#_remove_this_</code>
|
||||
part at the beginning of the URL, it must start with: <code>javascript:</code> to be correct.
|
||||
</p>
|
||||
<p>
|
||||
If you are using Firefox and the NoScript Addon: disable the ABE part of it,
|
||||
took me quite some time to figure out that the reason for an unreliable bookmarklet was in NoScript...
|
||||
</p>
|
||||
<h2>For Developers:</h2>
|
||||
<p>
|
||||
If you are a developer and want to debug locally (you need the source tree of your html2canvas at:
|
||||
<code>http(s)://localhost/html2canvas/</code>)
|
||||
check the following box to get the bookmarklet patched automatically ;)<br />
|
||||
<label>Debug bookmarklet: <input type="checkbox" /></label>
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
9
bower.json
Normal file
9
bower.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "html2canvas",
|
||||
"description": "Screenshots with JavaScript",
|
||||
"main": "dist/html2canvas.js",
|
||||
"ignore": [
|
||||
"tests",
|
||||
".travis.yml"
|
||||
]
|
||||
}
|
167
build.xml
167
build.xml
@ -1,167 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<project name="html2canvas" basedir="." default="build">
|
||||
<property name="src.dir" location="src"/>
|
||||
<property name="lib.dir" location="../lib"/>
|
||||
<property name="build.dir" location="build"/>
|
||||
<property name="dist" location="dist"/>
|
||||
<property name="jquery-externs" value="jquery-1.4.4.externs.js"/>
|
||||
|
||||
<property name="JS_NAME" value="html2canvas.js"/>
|
||||
<property name="JS_NAME_MIN" value="html2canvas.min.js"/>
|
||||
<property name="JQUERY_PLUGIN_NAME" value="jquery.plugin.html2canvas.js"/>
|
||||
<loadfile property="version" srcfile="version.txt" />
|
||||
|
||||
<path id="sourcefiles">
|
||||
<filelist dir="${src.dir}">
|
||||
<file name="LICENSE"/>
|
||||
<file name="html2canvas-pre.txt"/>
|
||||
<file name="Core.js"/>
|
||||
<file name="Generate.js"/>
|
||||
<file name="Parse.js"/>
|
||||
<file name="Preload.js"/>
|
||||
<file name="Queue.js"/>
|
||||
<file name="Renderer.js"/>
|
||||
<file name="Util.js"/>
|
||||
<file name="renderers/Canvas.js"/>
|
||||
<file name="html2canvas-post.txt"/>
|
||||
</filelist>
|
||||
</path>
|
||||
|
||||
<path id="sourcefiles-allrends">
|
||||
<filelist dir="${src.dir}">
|
||||
<file name="LICENSE"/>
|
||||
<file name="html2canvas-pre.txt"/>
|
||||
<file name="Core.js"/>
|
||||
<file name="Generate.js"/>
|
||||
<file name="Parse.js"/>
|
||||
<file name="Preload.js"/>
|
||||
<file name="Queue.js"/>
|
||||
<file name="Renderer.js"/>
|
||||
<file name="Util.js"/>
|
||||
<file name="renderers/Canvas.js"/>
|
||||
<file name="renderers/SVG.js"/>
|
||||
<file name="html2canvas-post.txt"/>
|
||||
</filelist>
|
||||
</path>
|
||||
|
||||
<path id="jquery-plugin">
|
||||
<fileset dir="${src.dir}" includes="LICENSE"/>
|
||||
<fileset dir="${src.dir}/plugins" includes="${JQUERY_PLUGIN_NAME}"/>
|
||||
</path>
|
||||
|
||||
<target name="build-dir">
|
||||
<echo>Creating directory ${build.dir}...</echo>
|
||||
<mkdir dir="${build.dir}"/>
|
||||
</target>
|
||||
|
||||
<target name="plugins" depends="build-dir">
|
||||
<echo>Creating ${JQUERY_PLUGIN_NAME}...</echo>
|
||||
<concat fixlastline="yes" destfile="${build.dir}/${JQUERY_PLUGIN_NAME}">
|
||||
<path refid="jquery-plugin"/>
|
||||
</concat>
|
||||
<replaceregexp match="@VERSION@" replace="${version}" flags="g" byline="true" file="${build.dir}/${JQUERY_PLUGIN_NAME}" />
|
||||
</target>
|
||||
|
||||
<pathconvert property="prettty-sourcefiles" pathsep="${line.separator}" refid="sourcefiles"></pathconvert>
|
||||
|
||||
<pathconvert property="prettty-sourcefiles-allrends" pathsep="${line.separator}" refid="sourcefiles-allrends"></pathconvert>
|
||||
|
||||
<target name="build" depends="build-dir,plugins">
|
||||
<echo>Concatenating files:${line.separator}${prettty-sourcefiles}${line.separator}into ${build.dir}/${JS_NAME}...</echo>
|
||||
<concat fixlastline="yes" destfile="${build.dir}/${JS_NAME}">
|
||||
<path refid="sourcefiles"/>
|
||||
</concat>
|
||||
<replaceregexp match="@VERSION@" replace="${version}" flags="g" byline="true" file="${build.dir}/${JS_NAME}" />
|
||||
</target>
|
||||
|
||||
<target name="build-allrends" depends="build-dir,plugins">
|
||||
<echo>Concatenating files:${line.separator}${prettty-sourcefiles-allrends}${line.separator}into ${build.dir}/${JS_NAME}...</echo>
|
||||
<concat fixlastline="yes" destfile="${build.dir}/${JS_NAME}">
|
||||
<path refid="sourcefiles-allrends"/>
|
||||
</concat>
|
||||
<replaceregexp match="@VERSION@" replace="${version}" flags="g" byline="true" file="${build.dir}/${JS_NAME}" />
|
||||
</target>
|
||||
|
||||
<taskdef name="jscomp" classname="com.google.javascript.jscomp.ant.CompileTask"
|
||||
classpath="${lib.dir}/compiler.jar" onerror="report"/>
|
||||
|
||||
<target name="syntaxcheck" depends="build-dir,build">
|
||||
<jscomp compilationLevel="simple" warning="verbose"
|
||||
debug="false"
|
||||
output="${build.dir}/${JS_NAME_MIN}.tmp">
|
||||
<externs dir="${lib.dir}">
|
||||
<file name="${jquery-externs}"/>
|
||||
</externs>
|
||||
<sources dir="${src.dir}">
|
||||
<!-- need to write them again here since the closure compiler doesn't understand filesets,... -->
|
||||
<file name="LICENSE"/>
|
||||
<file name="Core.js"/>
|
||||
<file name="Generate.js"/>
|
||||
<file name="Parse.js"/>
|
||||
<file name="Preload.js"/>
|
||||
<file name="Queue.js"/>
|
||||
<file name="Renderer.js"/>
|
||||
<file name="Util.js"/>
|
||||
<file name="renderers/Canvas.js"/>
|
||||
</sources>
|
||||
</jscomp>
|
||||
<delete file="${build.dir}/${JS_NAME_MIN}.tmp"></delete>
|
||||
</target>
|
||||
|
||||
<target name="syntaxcheck-allrends" depends="build-dir,build-allrends">
|
||||
<jscomp compilationLevel="simple" warning="verbose"
|
||||
debug="false"
|
||||
output="${build.dir}/${JS_NAME_MIN}.tmp">
|
||||
<externs dir="${lib.dir}">
|
||||
<file name="${jquery-externs}"/>
|
||||
</externs>
|
||||
<sources dir="${src.dir}">
|
||||
<!-- need to write them again here since the closure compiler doesn't understand filesets,... -->
|
||||
<file name="LICENSE"/>
|
||||
<file name="Core.js"/>
|
||||
<file name="Generate.js"/>
|
||||
<file name="Parse.js"/>
|
||||
<file name="Preload.js"/>
|
||||
<file name="Queue.js"/>
|
||||
<file name="Renderer.js"/>
|
||||
<file name="Util.js"/>
|
||||
<file name="renderers/Canvas.js"/>
|
||||
<file name="renderers/SVG.js"/>
|
||||
</sources>
|
||||
</jscomp>
|
||||
<delete file="${build.dir}/${JS_NAME_MIN}.tmp"></delete>
|
||||
</target>
|
||||
|
||||
<target name="release" depends="build-dir,build,syntaxcheck">
|
||||
<jscomp compilationLevel="simple" warning="verbose"
|
||||
debug="false"
|
||||
output="${build.dir}/${JS_NAME_MIN}">
|
||||
<externs dir="${lib.dir}">
|
||||
<file name="${jquery-externs}"/>
|
||||
</externs>
|
||||
<sources dir="${build.dir}">
|
||||
<file name="${JS_NAME}"/>
|
||||
</sources>
|
||||
</jscomp>
|
||||
<replaceregexp match="@VERSION@" replace="${version}" flags="g" byline="true" file="${build.dir}/${JS_NAME_MIN}" />
|
||||
</target>
|
||||
|
||||
<target name="release-allrends" depends="build-dir,build-allrends,syntaxcheck-allrends">
|
||||
<jscomp compilationLevel="simple" warning="verbose"
|
||||
debug="false"
|
||||
output="${build.dir}/${JS_NAME_MIN}">
|
||||
<externs dir="${lib.dir}">
|
||||
<file name="${jquery-externs}"/>
|
||||
</externs>
|
||||
<sources dir="${build.dir}">
|
||||
<file name="${JS_NAME}"/>
|
||||
</sources>
|
||||
</jscomp>
|
||||
<replaceregexp match="@VERSION@" replace="${version}" flags="g" byline="true" file="${build.dir}/${JS_NAME_MIN}" />
|
||||
</target>
|
||||
|
||||
<target name="clean">
|
||||
<delete dir="${build.dir}"></delete>
|
||||
</target>
|
||||
</project>
|
||||
|
186
demo2.html
186
demo2.html
@ -1,186 +0,0 @@
|
||||
<!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="external/jquery-1.6.2.min.js"></script>
|
||||
<script type="text/javascript" src="build/html2canvas.js"></script>
|
||||
<script type="text/javascript" src="build/jquery.plugin.html2canvas.js"></script>
|
||||
<script type="text/javascript">
|
||||
$(document).ready(function() {
|
||||
$('body').html2canvas();
|
||||
// var ss = $('ul').offset();
|
||||
// alert(ss.left);
|
||||
});
|
||||
</script>
|
||||
<title>
|
||||
display/box/float/clear test
|
||||
</title>
|
||||
<style type="text/css">
|
||||
/* last modified: 1 Dec 98 */
|
||||
|
||||
html {
|
||||
font: 10px/1 Verdana, sans-serif;
|
||||
background-color: blue;
|
||||
color: white;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 1.5em;
|
||||
border: .5em solid black;
|
||||
padding: 0;
|
||||
width: 48em;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
dl {
|
||||
margin: 0;
|
||||
border: 0;
|
||||
padding: .5em;
|
||||
}
|
||||
|
||||
dt {
|
||||
background-color: rgb(204,0,0);
|
||||
margin: 0;
|
||||
padding: 1em;
|
||||
width: 10.638%; /* refers to parent element's width of 47em. = 5em or 50px */
|
||||
height: 28em;
|
||||
border: .5em solid black;
|
||||
float: left;
|
||||
}
|
||||
|
||||
dd {
|
||||
float: right;
|
||||
margin: 0 0 0 1em;
|
||||
border: 1em solid black;
|
||||
padding: 1em;
|
||||
width: 34em;
|
||||
height: 27em;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin: 0;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
li {
|
||||
display: block; /* i.e., suppress marker */
|
||||
color: black;
|
||||
height: 9em;
|
||||
width: 5em;
|
||||
margin: 0;
|
||||
border: .5em solid black;
|
||||
padding: 1em;
|
||||
float: left;
|
||||
background-color: #FC0;
|
||||
}
|
||||
|
||||
#bar {
|
||||
background-color: black;
|
||||
color: white;
|
||||
width: 41.17%; /* = 14em */
|
||||
border: 0;
|
||||
margin: 0 1em;
|
||||
}
|
||||
|
||||
#baz {
|
||||
margin: 1em 0;
|
||||
border: 0;
|
||||
padding: 1em;
|
||||
width: 10em;
|
||||
height: 10em;
|
||||
background-color: black;
|
||||
color: white;
|
||||
}
|
||||
|
||||
form {
|
||||
margin: 0;
|
||||
display: inline;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
form p {
|
||||
line-height: 1.9;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin: 1em 1em 1em 2em;
|
||||
border-width: 1em 1.5em 2em .5em;
|
||||
border-style: solid;
|
||||
border-color: black;
|
||||
padding: 1em 0;
|
||||
width: 5em;
|
||||
height: 9em;
|
||||
float: left;
|
||||
background-color: #FC0;
|
||||
color: black;
|
||||
}
|
||||
|
||||
address {
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
h1 {
|
||||
background-color: black;
|
||||
color: white;
|
||||
float: left;
|
||||
margin: 1em 0;
|
||||
border: 0;
|
||||
padding: 1em;
|
||||
width: 10em;
|
||||
height: 10em;
|
||||
font-weight: normal;
|
||||
font-size: 1em;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<dl>
|
||||
<dt>
|
||||
toggle
|
||||
</dt>
|
||||
<dd>
|
||||
<ul>
|
||||
<li>
|
||||
the way
|
||||
</li>
|
||||
<li id="bar">
|
||||
<p>
|
||||
the world ends
|
||||
</p>
|
||||
<form action="./" method="get">
|
||||
<p>
|
||||
bang
|
||||
<input type="radio" name="foo" value="off">
|
||||
</p>
|
||||
<p>
|
||||
whimper
|
||||
<input type="radio" name="foo2" value="on">
|
||||
</p>
|
||||
</form>
|
||||
</li>
|
||||
<li>
|
||||
i grow old
|
||||
</li>
|
||||
<li id="baz">
|
||||
pluot?
|
||||
</li>
|
||||
</ul>
|
||||
<blockquote>
|
||||
<address>
|
||||
bar maids,
|
||||
</address>
|
||||
</blockquote>
|
||||
<h1>
|
||||
sing to me, erbarme dich
|
||||
</h1>
|
||||
</dd>
|
||||
</dl>
|
||||
<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>
|
||||
</body>
|
||||
</html>
|
72
demo3.html
72
demo3.html
@ -1,72 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title></title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
|
||||
|
||||
<script type="text/javascript" src="external/jquery-1.6.2.min.js"></script>
|
||||
<script type="text/javascript" src="build/html2canvas.js"></script>
|
||||
<script type="text/javascript" src="build/jquery.plugin.html2canvas.js"></script>
|
||||
<script type="text/javascript">
|
||||
$(function(){
|
||||
|
||||
|
||||
$('body').html2canvas();
|
||||
|
||||
|
||||
|
||||
});
|
||||
</script>
|
||||
<style>
|
||||
.feedback-overlay-black{
|
||||
background-color:#000;
|
||||
opacity:0.5;
|
||||
position:absolute;
|
||||
top:0;
|
||||
left:0;
|
||||
width:100%;
|
||||
height:100%;
|
||||
margin:0;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<style>
|
||||
div{
|
||||
padding:20px;
|
||||
margin:0 auto;
|
||||
border:5px solid black;
|
||||
}
|
||||
|
||||
h1{
|
||||
border-bottom:2px solid white;
|
||||
}
|
||||
|
||||
h2{
|
||||
background: #efefef;
|
||||
padding:10px;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div style="background:red;">
|
||||
<div style="background:green;">
|
||||
<div style="background:blue;border-color:white;">
|
||||
<div style="background:yellow;"><div style="background:orange;"><h1>Heading</h1>
|
||||
Text that isn't wrapped in anything.
|
||||
<p>Followed by some text wrapped in a <b><p> paragraph.</b> </p>
|
||||
Maybe add a <a href="#">link</a> or a different style of <a href="#" style="background:white;" id="highlight">link with a highlight</a>.
|
||||
<hr />
|
||||
<h2>More content</h2>
|
||||
<div style="width:10px;height:10px;border-width:10px;padding:0;">a</div>
|
||||
</div></div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
38
docs/about.md
Normal file
38
docs/about.md
Normal file
@ -0,0 +1,38 @@
|
||||
---
|
||||
title: "About"
|
||||
---
|
||||
|
||||
Before you get started with the script, there are a few things that are good to know regarding the
|
||||
script and some of its limitations.
|
||||
|
||||
# Introduction
|
||||
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 it works
|
||||
The script traverses through the DOM of the page it is loaded on. It gathers information on all the elements
|
||||
there, which it then uses to build a representation of the page. In other words, it does not actually take a
|
||||
screenshot of the page, but builds a representation of it based on the properties it reads from the DOM.
|
||||
|
||||
As a result, it is only able to render correctly properties that it understands, meaning there are many
|
||||
CSS properties which do not work. For a full list of supported CSS properties, check out the
|
||||
[support features](/features/) page.
|
||||
|
||||
# Limitations
|
||||
All the images that the script uses need to reside under the [same origin](http://en.wikipedia.org/wiki/Same_origin_policy)
|
||||
for it to be able to read them without the assistance of a [proxy](/proxy/). Similarly, if you have other `canvas`
|
||||
elements on the page, which have been tainted with cross-origin content, they will become dirty and no longer readable by html2canvas.
|
||||
|
||||
The script doesn't render plugin content such as Flash or Java applets.
|
||||
|
||||
# Browser compatibility
|
||||
|
||||
The library should work fine on the following browsers (with `Promise` polyfill):
|
||||
- Firefox 3.5+
|
||||
- Google Chrome
|
||||
- Opera 12+
|
||||
- IE9+
|
||||
- Edge
|
||||
- Safari 6+
|
25
docs/configuration.md
Normal file
25
docs/configuration.md
Normal file
@ -0,0 +1,25 @@
|
||||
---
|
||||
title: "Configuration"
|
||||
---
|
||||
|
||||
These are all of the available configuration options.
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
| ------------- |:-------------:| :------: | ----------- |
|
||||
| async | Boolean | `true` | Whether to parse and render the element asynchronously
|
||||
| allowTaint | Boolean | `false` | Whether to allow cross-origin images to taint the canvas
|
||||
| backgroundColor | String | `#ffffff` | Canvas background color, if none is specified in DOM. Set undefined for transparent
|
||||
| canvas | `HTMLCanvasElement` | `null` | Existing `canvas` element to use as a base for drawing on
|
||||
| foreignObjectRendering | Boolean | `false` | Whether to use ForeignObject rendering if the browser supports it
|
||||
| imageTimeout | Number | `15000` | Timeout for loading an image (in milliseconds). Set to `0` to disable timeout.
|
||||
| proxy | String | `null` | Url to the [proxy](/proxy/) which is to be used for loading cross-origin images. If left empty, cross-origin images won't be loaded.
|
||||
| removeContainer | Boolean | `true` | Whether to cleanup the cloned DOM elements html2canvas creates temporarily
|
||||
| scale | Number | `window.devicePixelRatio` | The scale to use for rendering. Defaults to the browsers device pixel ratio.
|
||||
| width | Number | `Element` width | The width of the `canvas`
|
||||
| height | Number | `Element` height | The height of the `canvas`
|
||||
| x | Number | `Element` x-offset | Crop canvas x-coordinate
|
||||
| y | Number | `Element` y-offset| Crop canvas y-coordinate
|
||||
| scrollX | Number | `Element` scrollX | The x-scroll position to used when rendering element, (for example if the Element uses `position: fixed`)
|
||||
| scrollY | Number | `Element` scrollY | The y-scroll position to used when rendering element, (for example if the Element uses `position: fixed`)
|
||||
| windowWidth | Number | `Window.innerWidth` | Window width to use when rendering `Element`, which may affect things like Media queries
|
||||
| windowHeight | Number | `Window.innerHeight` | Window height to use when rendering `Element`, which may affect things like Media queries
|
72
docs/features.md
Normal file
72
docs/features.md
Normal file
@ -0,0 +1,72 @@
|
||||
---
|
||||
title: "Features"
|
||||
---
|
||||
|
||||
Below is a list of all the supported CSS properties and values.
|
||||
|
||||
- background
|
||||
- background-clip (**Does not support `text`**)
|
||||
- background-color
|
||||
- background-image
|
||||
- url()
|
||||
- linear-gradient()
|
||||
- background-origin
|
||||
- background-position
|
||||
- background-size
|
||||
- border
|
||||
- border-color
|
||||
- border-radius
|
||||
- border-style (**Only supports `solid`**)
|
||||
- border-width
|
||||
- bottom
|
||||
- box-sizing
|
||||
- content (**Does not support `attr()`**)
|
||||
- color
|
||||
- display
|
||||
- flex
|
||||
- float
|
||||
- font
|
||||
- font-family
|
||||
- font-size
|
||||
- font-style
|
||||
- font-variant
|
||||
- font-weight
|
||||
- height
|
||||
- left
|
||||
- letter-spacing
|
||||
- margin
|
||||
- max-height
|
||||
- max-width
|
||||
- min-height
|
||||
- min-width
|
||||
- opacity
|
||||
- overflow
|
||||
- padding
|
||||
- position
|
||||
- right
|
||||
- text-align
|
||||
- text-decoration
|
||||
- text-decoration-color
|
||||
- text-decoration-line
|
||||
- text-decoration-style (**Only supports `solid`**)
|
||||
- text-shadow
|
||||
- text-transform
|
||||
- top
|
||||
- transform (**Limited support**)
|
||||
- visibility
|
||||
- white-space
|
||||
- width
|
||||
- word-spacing
|
||||
- z-index
|
||||
|
||||
## Unsupported CSS properties
|
||||
These CSS properties are **NOT** currently supported
|
||||
- [background-blend-mode](https://github.com/niklasvh/html2canvas/issues/966)
|
||||
- [border-image](https://github.com/niklasvh/html2canvas/issues/1287)
|
||||
- [box-shadow](https://github.com/niklasvh/html2canvas/pull/1086)
|
||||
- [filter](https://github.com/niklasvh/html2canvas/issues/493)
|
||||
- [font-variant-ligatures](https://github.com/niklasvh/html2canvas/pull/1085)
|
||||
- [list-style](https://github.com/niklasvh/html2canvas/issues/177)
|
||||
- word-break
|
||||
- [writing-mode](https://github.com/niklasvh/html2canvas/issues/1258)
|
||||
|
25
docs/getting-started.md
Normal file
25
docs/getting-started.md
Normal file
@ -0,0 +1,25 @@
|
||||
---
|
||||
title: "Getting Started"
|
||||
---
|
||||
|
||||
# Installing
|
||||
|
||||
You can install `html2canvas` through npm or [download a built release](https://github.com/niklasvh/html2canvas/releases).
|
||||
|
||||
### npm
|
||||
|
||||
npm install html2canvas
|
||||
|
||||
```javascript
|
||||
import html2canvas from 'html2canvas';
|
||||
```
|
||||
|
||||
# Usage
|
||||
|
||||
To render an `element` with html2canvas with some (optional) [options](/configuration/), simply call `html2canvas(element, options);`
|
||||
|
||||
```javascript
|
||||
html2canvas(document.body).then(function(canvas) {
|
||||
document.body.appendChild(canvas);
|
||||
});
|
||||
```
|
11
docs/proxy.md
Normal file
11
docs/proxy.md
Normal file
@ -0,0 +1,11 @@
|
||||
---
|
||||
title: "Proxy"
|
||||
---
|
||||
|
||||
html2canvas does not get around content policy restrictions set by your browser. Drawing images that reside outside of
|
||||
the origin of the current page taint the canvas that they are drawn upon. If the canvas gets tainted,
|
||||
it cannot be read anymore. If you wish to load images that reside outside of your pages origin, you can use a proxy to load the images.
|
||||
|
||||
# Available proxies
|
||||
|
||||
- [node.js](https://github.com/niklasvh/html2canvas-proxy-nodejs)
|
181
examples/demo.html
Normal file
181
examples/demo.html
Normal file
@ -0,0 +1,181 @@
|
||||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">
|
||||
<html>
|
||||
<head>
|
||||
<title>
|
||||
display/box/float/clear test
|
||||
</title>
|
||||
<style type="text/css">
|
||||
/* last modified: 1 Dec 98 */
|
||||
|
||||
html {
|
||||
font: 10px/1 Verdana, sans-serif;
|
||||
background-color: blue;
|
||||
color: white;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 1.5em;
|
||||
border: .5em solid black;
|
||||
padding: 0;
|
||||
width: 48em;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
dl {
|
||||
margin: 0;
|
||||
border: 0;
|
||||
padding: .5em;
|
||||
}
|
||||
|
||||
dt {
|
||||
background-color: rgb(204,0,0);
|
||||
margin: 0;
|
||||
padding: 1em;
|
||||
width: 10.638%; /* refers to parent element's width of 47em. = 5em or 50px */
|
||||
height: 28em;
|
||||
border: .5em solid black;
|
||||
float: left;
|
||||
}
|
||||
|
||||
dd {
|
||||
float: right;
|
||||
margin: 0 0 0 1em;
|
||||
border: 1em solid black;
|
||||
padding: 1em;
|
||||
width: 34em;
|
||||
height: 27em;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin: 0;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
li {
|
||||
display: block; /* i.e., suppress marker */
|
||||
color: black;
|
||||
height: 9em;
|
||||
width: 5em;
|
||||
margin: 0;
|
||||
border: .5em solid black;
|
||||
padding: 1em;
|
||||
float: left;
|
||||
background-color: #FC0;
|
||||
}
|
||||
|
||||
#bar {
|
||||
background-color: black;
|
||||
color: white;
|
||||
width: 41.17%; /* = 14em */
|
||||
border: 0;
|
||||
margin: 0 1em;
|
||||
}
|
||||
|
||||
#baz {
|
||||
margin: 1em 0;
|
||||
border: 0;
|
||||
padding: 1em;
|
||||
width: 10em;
|
||||
height: 10em;
|
||||
background-color: black;
|
||||
color: white;
|
||||
}
|
||||
|
||||
form {
|
||||
margin: 0;
|
||||
display: inline;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
form p {
|
||||
line-height: 1.9;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin: 1em 1em 1em 2em;
|
||||
border-width: 1em 1.5em 2em .5em;
|
||||
border-style: solid;
|
||||
border-color: black;
|
||||
padding: 1em 0;
|
||||
width: 5em;
|
||||
height: 9em;
|
||||
float: left;
|
||||
background-color: #FC0;
|
||||
color: black;
|
||||
}
|
||||
|
||||
address {
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
h1 {
|
||||
background-color: black;
|
||||
color: white;
|
||||
float: left;
|
||||
margin: 1em 0;
|
||||
border: 0;
|
||||
padding: 1em;
|
||||
width: 10em;
|
||||
height: 10em;
|
||||
font-weight: normal;
|
||||
font-size: 1em;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<dl>
|
||||
<dt>
|
||||
toggle
|
||||
</dt>
|
||||
<dd>
|
||||
<ul>
|
||||
<li>
|
||||
the way
|
||||
</li>
|
||||
<li id="bar">
|
||||
<p>
|
||||
the world ends
|
||||
</p>
|
||||
<form action="./" method="get">
|
||||
<p>
|
||||
bang
|
||||
<input type="radio" name="foo" value="off">
|
||||
</p>
|
||||
<p>
|
||||
whimper
|
||||
<input type="radio" name="foo2" value="on">
|
||||
</p>
|
||||
</form>
|
||||
</li>
|
||||
<li>
|
||||
i grow old
|
||||
</li>
|
||||
<li id="baz">
|
||||
pluot?
|
||||
</li>
|
||||
</ul>
|
||||
<blockquote>
|
||||
<address>
|
||||
bar maids,
|
||||
</address>
|
||||
</blockquote>
|
||||
<h1>
|
||||
sing to me, erbarme dich
|
||||
</h1>
|
||||
</dd>
|
||||
</dl>
|
||||
<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="../dist/html2canvas.js"></script>
|
||||
<script type="text/javascript">
|
||||
html2canvas(document.body).then(function(canvas) {
|
||||
document.body.appendChild(canvas);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
63
examples/demo2.html
Normal file
63
examples/demo2.html
Normal file
@ -0,0 +1,63 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title></title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<style>
|
||||
.feedback-overlay-black{
|
||||
background-color:#000;
|
||||
opacity:0.5;
|
||||
position:absolute;
|
||||
top:0;
|
||||
left:0;
|
||||
width:100%;
|
||||
height:100%;
|
||||
margin:0;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<style>
|
||||
div{
|
||||
padding:20px;
|
||||
margin:0 auto;
|
||||
border:5px solid black;
|
||||
}
|
||||
|
||||
h1{
|
||||
border-bottom:2px solid white;
|
||||
}
|
||||
|
||||
h2{
|
||||
background: #efefef;
|
||||
padding:10px;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div style="background:red;">
|
||||
<div style="background:green;">
|
||||
<div style="background:blue;border-color:white;">
|
||||
<div style="background:yellow;"><div style="background:orange;"><h1>Heading</h1>
|
||||
Text that isn't wrapped in anything.
|
||||
<p>Followed by some text wrapped in a <b><p> paragraph.</b> </p>
|
||||
Maybe add a <a href="#">link</a> or a different style of <a href="#" style="background:white;" id="highlight">link with a highlight</a>.
|
||||
<hr />
|
||||
<h2>More content</h2>
|
||||
<div style="width:10px;height:10px;border-width:10px;padding:0;">a</div>
|
||||
</div></div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<script type="text/javascript" src="../dist/html2canvas.js"></script>
|
||||
<script type="text/javascript">
|
||||
html2canvas(document.body).then(function(canvas) {
|
||||
document.body.appendChild(canvas);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
52
examples/existing_canvas.html
Normal file
52
examples/existing_canvas.html
Normal file
@ -0,0 +1,52 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head lang="en">
|
||||
<meta charset="UTF-8">
|
||||
<title>Using an existing canvas to draw on</title>
|
||||
<style>
|
||||
canvas {
|
||||
border: 1px solid black;
|
||||
}
|
||||
button {
|
||||
clear: both;
|
||||
display: block;
|
||||
}
|
||||
#content {
|
||||
background: rgba(100, 255, 255, 0.5);
|
||||
padding: 50px 10px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div><h1>HTML content to render:</h1>
|
||||
<div id="content">Render the content in this element <strong>only</strong> onto the existing canvas element</div>
|
||||
</div>
|
||||
<h1>Existing canvas:</h1>
|
||||
<canvas width="500" height="200"></canvas>
|
||||
<script type="text/javascript" src="../dist/html2canvas.js"></script>
|
||||
<button>Run html2canvas</button>
|
||||
<script type="text/javascript">
|
||||
var canvas = document.querySelector("canvas");
|
||||
var ctx = canvas.getContext("2d");
|
||||
|
||||
var ctx = canvas.getContext('2d');
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(75,75,50,0,Math.PI*2,true); // Outer circle
|
||||
ctx.moveTo(110,75);
|
||||
ctx.arc(75,75,35,0,Math.PI,false); // Mouth (clockwise)
|
||||
ctx.moveTo(65,65);
|
||||
ctx.arc(60,65,5,0,Math.PI*2,true); // Left eye
|
||||
ctx.moveTo(95,65);
|
||||
ctx.arc(90,65,5,0,Math.PI*2,true); // Right eye
|
||||
ctx.stroke();
|
||||
|
||||
document.querySelector("button").addEventListener("click", function() {
|
||||
html2canvas(document.querySelector("#content"), {canvas: canvas}).then(function(canvas) {
|
||||
console.log('Drew on the existing canvas');
|
||||
});
|
||||
}, false);
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
1194
external/flashcanvas.js
vendored
1194
external/flashcanvas.js
vendored
File diff suppressed because it is too large
Load Diff
28
external/flashcanvas.min.js
vendored
28
external/flashcanvas.min.js
vendored
@ -1,28 +0,0 @@
|
||||
/*
|
||||
* FlashCanvas
|
||||
*
|
||||
* Copyright (c) 2009 Tim Cameron Ryan
|
||||
* Copyright (c) 2009-2011 FlashCanvas Project
|
||||
* Released under the MIT/X License
|
||||
*/
|
||||
window.ActiveXObject&&!window.CanvasRenderingContext2D&&function(h,j){function D(a){this.code=a;this.message=T[a]}function U(a){this.width=a}function E(a){this.id=a.C++}function t(a){this.G=a;this.id=a.C++}function u(a,b){this.canvas=a;this.B=b;this.d=a.uniqueID;this.D();this.C=0;this.t="";var c=this;setInterval(function(){n[c.d]===0&&c.e()},30)}function A(){if(j.readyState==="complete"){j.detachEvent(F,A);for(var a=j.getElementsByTagName(r),b=0,c=a.length;b<c;++b)B.initElement(a[b])}}function G(){var a=
|
||||
event.srcElement,b=a.parentNode;a.blur();b.focus()}function H(){var a=event.propertyName;if(a==="width"||a==="height"){var b=event.srcElement,c=b[a],d=parseInt(c,10);if(isNaN(d)||d<0)d=a==="width"?300:150;if(c===d){b.style[a]=d+"px";b.getContext("2d").I(b.width,b.height)}else b[a]=d}}function I(){h.detachEvent(J,I);for(var a in s){var b=s[a],c=b.firstChild,d;for(d in c)if(typeof c[d]==="function")c[d]=k;for(d in b)if(typeof b[d]==="function")b[d]=k;c.detachEvent(K,G);b.detachEvent(L,H)}h[M]=k;h[N]=
|
||||
k;h[O]=k;h[C]=k;h[P]=k}function V(){var a=j.getElementsByTagName("script");a=a[a.length-1];return j.documentMode>=8?a.src:a.getAttribute("src",4)}function v(a){return(""+a).replace(/&/g,"&").replace(/</g,"<")}function W(a){return a.toLowerCase()}function i(a){throw new D(a);}function Q(a){var b=parseInt(a.width,10),c=parseInt(a.height,10);if(isNaN(b)||b<0)b=300;if(isNaN(c)||c<0)c=150;a.width=b;a.height=c}var k=null,r="canvas",M="CanvasRenderingContext2D",N="CanvasGradient",O="CanvasPattern",
|
||||
C="FlashCanvas",P="G_vmlCanvasManager",K="onfocus",L="onpropertychange",F="onreadystatechange",J="onunload",w=((h[C+"Options"]||{}).swfPath||V().replace(/[^\/]+$/,""))+"flashcanvas.swf",e=new function(a){for(var b=0,c=a.length;b<c;b++)this[a[b]]=b}(["toDataURL","save","restore","scale","rotate","translate","transform","setTransform","globalAlpha","globalCompositeOperation","strokeStyle","fillStyle","createLinearGradient","createRadialGradient","createPattern","lineWidth","lineCap","lineJoin","miterLimit",
|
||||
"shadowOffsetX","shadowOffsetY","shadowBlur","shadowColor","clearRect","fillRect","strokeRect","beginPath","closePath","moveTo","lineTo","quadraticCurveTo","bezierCurveTo","arcTo","rect","arc","fill","stroke","clip","isPointInPath","font","textAlign","textBaseline","fillText","strokeText","measureText","drawImage","createImageData","getImageData","putImageData","addColorStop","direction","resize"]),x={},n={},s={},y={};u.prototype={save:function(){this.b();this.c();this.m();this.l();this.z();this.w();
|
||||
this.F.push([this.f,this.g,this.A,this.u,this.j,this.h,this.i,this.k,this.p,this.q,this.n,this.o,this.v,this.r,this.s]);this.a.push(e.save)},restore:function(){var a=this.F;if(a.length){a=a.pop();this.globalAlpha=a[0];this.globalCompositeOperation=a[1];this.strokeStyle=a[2];this.fillStyle=a[3];this.lineWidth=a[4];this.lineCap=a[5];this.lineJoin=a[6];this.miterLimit=a[7];this.shadowOffsetX=a[8];this.shadowOffsetY=a[9];this.shadowBlur=a[10];this.shadowColor=a[11];this.font=a[12];this.textAlign=a[13];
|
||||
this.textBaseline=a[14]}this.a.push(e.restore)},scale:function(a,b){this.a.push(e.scale,a,b)},rotate:function(a){this.a.push(e.rotate,a)},translate:function(a,b){this.a.push(e.translate,a,b)},transform:function(a,b,c,d,f,g){this.a.push(e.transform,a,b,c,d,f,g)},setTransform:function(a,b,c,d,f,g){this.a.push(e.setTransform,a,b,c,d,f,g)},b:function(){var a=this.a;if(this.f!==this.globalAlpha){this.f=this.globalAlpha;a.push(e.globalAlpha,this.f)}if(this.g!==this.globalCompositeOperation){this.g=this.globalCompositeOperation;
|
||||
a.push(e.globalCompositeOperation,this.g)}},m:function(){if(this.A!==this.strokeStyle){var a=this.A=this.strokeStyle;this.a.push(e.strokeStyle,typeof a==="object"?a.id:a)}},l:function(){if(this.u!==this.fillStyle){var a=this.u=this.fillStyle;this.a.push(e.fillStyle,typeof a==="object"?a.id:a)}},createLinearGradient:function(a,b,c,d){isFinite(a)&&isFinite(b)&&isFinite(c)&&isFinite(d)||i(9);this.a.push(e.createLinearGradient,a,b,c,d);return new t(this)},createRadialGradient:function(a,b,c,d,f,g){isFinite(a)&&
|
||||
isFinite(b)&&isFinite(c)&&isFinite(d)&&isFinite(f)&&isFinite(g)||i(9);if(c<0||g<0)i(1);this.a.push(e.createRadialGradient,a,b,c,d,f,g);return new t(this)},createPattern:function(a,b){a||i(17);var c=a.tagName,d,f=this.d;if(c){c=c.toLowerCase();if(c==="img")d=a.getAttribute("src",2);else if(c===r||c==="video")return;else i(17)}else if(a.src)d=a.src;else i(17);b==="repeat"||b==="no-repeat"||b==="repeat-x"||b==="repeat-y"||b===""||b===k||i(12);this.a.push(e.createPattern,v(d),b);if(x[f]){this.e();++n[f]}return new E(this)},
|
||||
z:function(){var a=this.a;if(this.j!==this.lineWidth){this.j=this.lineWidth;a.push(e.lineWidth,this.j)}if(this.h!==this.lineCap){this.h=this.lineCap;a.push(e.lineCap,this.h)}if(this.i!==this.lineJoin){this.i=this.lineJoin;a.push(e.lineJoin,this.i)}if(this.k!==this.miterLimit){this.k=this.miterLimit;a.push(e.miterLimit,this.k)}},c:function(){var a=this.a;if(this.p!==this.shadowOffsetX){this.p=this.shadowOffsetX;a.push(e.shadowOffsetX,this.p)}if(this.q!==this.shadowOffsetY){this.q=this.shadowOffsetY;
|
||||
a.push(e.shadowOffsetY,this.q)}if(this.n!==this.shadowBlur){this.n=this.shadowBlur;a.push(e.shadowBlur,this.n)}if(this.o!==this.shadowColor){this.o=this.shadowColor;a.push(e.shadowColor,this.o)}},clearRect:function(a,b,c,d){this.a.push(e.clearRect,a,b,c,d)},fillRect:function(a,b,c,d){this.b();this.c();this.l();this.a.push(e.fillRect,a,b,c,d)},strokeRect:function(a,b,c,d){this.b();this.c();this.m();this.z();this.a.push(e.strokeRect,a,b,c,d)},beginPath:function(){this.a.push(e.beginPath)},closePath:function(){this.a.push(e.closePath)},
|
||||
moveTo:function(a,b){this.a.push(e.moveTo,a,b)},lineTo:function(a,b){this.a.push(e.lineTo,a,b)},quadraticCurveTo:function(a,b,c,d){this.a.push(e.quadraticCurveTo,a,b,c,d)},bezierCurveTo:function(a,b,c,d,f,g){this.a.push(e.bezierCurveTo,a,b,c,d,f,g)},arcTo:function(a,b,c,d,f){f<0&&isFinite(f)&&i(1);this.a.push(e.arcTo,a,b,c,d,f)},rect:function(a,b,c,d){this.a.push(e.rect,a,b,c,d)},arc:function(a,b,c,d,f,g){c<0&&isFinite(c)&&i(1);this.a.push(e.arc,a,b,c,d,f,g?1:0)},fill:function(){this.b();this.c();
|
||||
this.l();this.a.push(e.fill)},stroke:function(){this.b();this.c();this.m();this.z();this.a.push(e.stroke)},clip:function(){this.a.push(e.clip)},w:function(){var a=this.a;if(this.v!==this.font)try{var b=y[this.d];b.style.font=this.v=this.font;var c=b.currentStyle;a.push(e.font,[c.fontStyle,c.fontWeight,b.offsetHeight,c.fontFamily].join(" "))}catch(d){}if(this.r!==this.textAlign){this.r=this.textAlign;a.push(e.textAlign,this.r)}if(this.s!==this.textBaseline){this.s=this.textBaseline;a.push(e.textBaseline,
|
||||
this.s)}if(this.t!==this.canvas.currentStyle.direction){this.t=this.canvas.currentStyle.direction;a.push(e.direction,this.t)}},fillText:function(a,b,c,d){this.b();this.l();this.c();this.w();this.a.push(e.fillText,v(a),b,c,d===void 0?Infinity:d)},strokeText:function(a,b,c,d){this.b();this.m();this.c();this.w();this.a.push(e.strokeText,v(a),b,c,d===void 0?Infinity:d)},measureText:function(a){var b=y[this.d];try{b.style.font=this.font}catch(c){}b.innerText=a.replace(/[ \n\f\r]/g,"\t");return new U(b.offsetWidth)},
|
||||
drawImage:function(a,b,c,d,f,g,o,l,z){a||i(17);var p=a.tagName,m,q=arguments.length,R=this.d;if(p){p=p.toLowerCase();if(p==="img")m=a.getAttribute("src",2);else if(p===r||p==="video")return;else i(17)}else if(a.src)m=a.src;else i(17);this.b();this.c();m=v(m);if(q===3)this.a.push(e.drawImage,q,m,b,c);else if(q===5)this.a.push(e.drawImage,q,m,b,c,d,f);else if(q===9){if(d===0||f===0)i(1);this.a.push(e.drawImage,q,m,b,c,d,f,g,o,l,z)}else return;if(x[R]){this.e();++n[R]}},D:function(){this.globalAlpha=
|
||||
this.f=1;this.globalCompositeOperation=this.g="source-over";this.fillStyle=this.u=this.strokeStyle=this.A="#000000";this.lineWidth=this.j=1;this.lineCap=this.h="butt";this.lineJoin=this.i="miter";this.miterLimit=this.k=10;this.shadowBlur=this.n=this.shadowOffsetY=this.q=this.shadowOffsetX=this.p=0;this.shadowColor=this.o="rgba(0, 0, 0, 0.0)";this.font=this.v="10px sans-serif";this.textAlign=this.r="start";this.textBaseline=this.s="alphabetic";this.a=[];this.F=[]},H:function(){var a=this.a;this.a=
|
||||
[];return a},e:function(){var a=this.H();if(a.length>0)return eval(this.B.CallFunction('<invoke name="executeCommand" returntype="javascript"><arguments><string>'+a.join("�")+"</string></arguments></invoke>"))},I:function(a,b){this.e();this.D();if(a>0)this.B.width=a;if(b>0)this.B.height=b;this.a.push(e.resize,a,b)}};t.prototype={addColorStop:function(a,b){if(isNaN(a)||a<0||a>1)i(1);this.G.a.push(e.addColorStop,this.id,a,b)}};D.prototype=Error();var T={1:"INDEX_SIZE_ERR",9:"NOT_SUPPORTED_ERR",11:"INVALID_STATE_ERR",
|
||||
12:"SYNTAX_ERR",17:"TYPE_MISMATCH_ERR",18:"SECURITY_ERR"},B={initElement:function(a){if(a.getContext)return a;var b=a.uniqueID,c="external"+b;x[b]=false;n[b]=1;Q(a);a.innerHTML='<object classid="clsid:d27cdb6e-ae6d-11cf-96b8-444553540000" codebase="'+location.protocol+'//fpdownload.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=9,0,0,0" width="100%" height="100%" id="'+c+'"><param name="allowScriptAccess" value="always"><param name="flashvars" value="id='+c+'"><param name="wmode" value="transparent"></object><span style="margin:0;padding:0;border:0;display:inline-block;position:static;height:1em;overflow:visible;white-space:nowrap"></span>';
|
||||
s[b]=a;var d=a.firstChild;y[b]=a.lastChild;var f=j.body.contains;if(f(a))d.movie=w;else var g=setInterval(function(){if(f(a)){clearInterval(g);d.movie=w}},0);if(j.compatMode==="BackCompat"||!h.XMLHttpRequest)y[b].style.overflow="hidden";var o=new u(a,d);a.getContext=function(l){return l==="2d"?o:k};a.toDataURL=function(l,z){(""+l).replace(/[A-Z]+/g,W)==="image/jpeg"?o.a.push(e.toDataURL,l,typeof z==="number"?z:""):o.a.push(e.toDataURL,l);return o.e()};d.attachEvent(K,G);return a},saveImage:function(a){a.firstChild.saveImage()},
|
||||
setOptions:function(){},trigger:function(a,b){s[a].fireEvent("on"+b)},unlock:function(a,b){n[a]&&--n[a];if(b){var c=s[a],d=c.firstChild,f,g;Q(c);f=c.width;g=c.height;c.style.width=f+"px";c.style.height=g+"px";if(f>0)d.width=f;if(g>0)d.height=g;d.resize(f,g);c.attachEvent(L,H);x[a]=true}}};j.createElement(r);j.createStyleSheet().cssText=r+"{display:inline-block;overflow:hidden;width:300px;height:150px}";j.readyState==="complete"?A():j.attachEvent(F,A);h.attachEvent(J,I);if(w.indexOf(location.protocol+
|
||||
"//"+location.host+"/")===0){var S=new ActiveXObject("Microsoft.XMLHTTP");S.open("GET",w,false);S.send(k)}h[M]=u;h[N]=t;h[O]=E;h[C]=B;h[P]={init:function(){},init_:function(){},initElement:B.initElement};keep=u.measureText}(window,document);
|
BIN
external/flashcanvas.swf
vendored
BIN
external/flashcanvas.swf
vendored
Binary file not shown.
8981
external/jquery-1.6.2.js
vendored
8981
external/jquery-1.6.2.js
vendored
File diff suppressed because it is too large
Load Diff
18
external/jquery-1.6.2.min.js
vendored
18
external/jquery-1.6.2.min.js
vendored
File diff suppressed because one or more lines are too long
11
flow-typed/myLibDef.js
vendored
Normal file
11
flow-typed/myLibDef.js
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
declare var __DEV__: boolean;
|
||||
declare var __VERSION__: string;
|
||||
|
||||
declare class SVGSVGElement extends Element {
|
||||
className: string;
|
||||
style: CSSStyleDeclaration;
|
||||
|
||||
getPresentationAttribute(name: string): any;
|
||||
}
|
||||
|
||||
declare class HTMLBodyElement extends HTMLElement {}
|
@ -1,57 +0,0 @@
|
||||
{
|
||||
"folders":
|
||||
[
|
||||
{
|
||||
"path": ".",
|
||||
"folder_exclude_patterns": [".svn", ".git", ".hg", "CVS", "node_modules"],
|
||||
"file_exclude_patterns": ["*.sublime-workspace"]
|
||||
}
|
||||
],
|
||||
"settings":
|
||||
{
|
||||
// The number of spaces a tab is considered equal to
|
||||
"tab_size": 4,
|
||||
|
||||
// Set to true to insert spaces when tab is pressed
|
||||
"translate_tabs_to_spaces": true,
|
||||
|
||||
// If translate_tabs_to_spaces is true, use_tab_stops will make tab and
|
||||
// backspace insert/delete up to the next tabstop
|
||||
"use_tab_stops": false,
|
||||
|
||||
// Set to false to disable detection of tabs vs. spaces on load
|
||||
"detect_indentation": false,
|
||||
|
||||
// Set to true to removing trailing white space on save
|
||||
"trim_trailing_white_space_on_save": true,
|
||||
|
||||
// Set to true to ensure the last line of the file ends in a newline
|
||||
// character when saving
|
||||
"ensure_newline_at_eof_on_save": false,
|
||||
|
||||
// Linting
|
||||
"jshint_options": {
|
||||
"eqeqeq": false,
|
||||
|
||||
"laxbreak": true,
|
||||
"undef": true,
|
||||
"newcap": true,
|
||||
"noarg": true,
|
||||
"strict": false,
|
||||
"trailing": true,
|
||||
"onecase": true,
|
||||
|
||||
"boss": true,
|
||||
"eqnull": true,
|
||||
|
||||
"onevar": false,
|
||||
|
||||
"evil": true,
|
||||
"regexdash": true,
|
||||
"browser": true,
|
||||
"wsh": true,
|
||||
"trailing": true,
|
||||
"sub": true
|
||||
}
|
||||
}
|
||||
}
|
176
karma.conf.js
Normal file
176
karma.conf.js
Normal file
@ -0,0 +1,176 @@
|
||||
// Karma configuration
|
||||
// Generated on Sat Aug 05 2017 23:42:26 GMT+0800 (Malay Peninsula Standard Time)
|
||||
|
||||
const port = 9876;
|
||||
module.exports = function(config) {
|
||||
const slLaunchers = (!process.env.SAUCE_USERNAME || !process.env.SAUCE_ACCESS_KEY) ? {} : {
|
||||
sl_beta_chrome: {
|
||||
base: 'SauceLabs',
|
||||
browserName: 'chrome',
|
||||
platform: 'Windows 10',
|
||||
version: 'beta'
|
||||
},
|
||||
sl_ie9: {
|
||||
base: 'SauceLabs',
|
||||
browserName: 'internet explorer',
|
||||
version: '9.0',
|
||||
platform: 'Windows 7'
|
||||
},
|
||||
sl_ie10: {
|
||||
base: 'SauceLabs',
|
||||
browserName: 'internet explorer',
|
||||
version: '10.0',
|
||||
platform: 'Windows 7'
|
||||
},
|
||||
sl_ie11: {
|
||||
base: 'SauceLabs',
|
||||
browserName: 'internet explorer',
|
||||
version: '11.0',
|
||||
platform: 'Windows 7'
|
||||
},
|
||||
sl_edge_15: {
|
||||
base: 'SauceLabs',
|
||||
browserName: 'MicrosoftEdge',
|
||||
version: '15.15063',
|
||||
platform: 'Windows 10'
|
||||
},
|
||||
sl_edge_14: {
|
||||
base: 'SauceLabs',
|
||||
browserName: 'MicrosoftEdge',
|
||||
version: '14.14393',
|
||||
platform: 'Windows 10'
|
||||
},
|
||||
sl_safari: {
|
||||
base: 'SauceLabs',
|
||||
browserName: 'safari',
|
||||
version: '10.1',
|
||||
platform: 'macOS 10.12'
|
||||
},
|
||||
'sl_android_4.4': {
|
||||
base: 'SauceLabs',
|
||||
browserName: 'Browser',
|
||||
platform: 'Android',
|
||||
version: '4.4',
|
||||
device: 'Android Emulator',
|
||||
},
|
||||
'sl_ios_10.3_safari': {
|
||||
base: 'SauceLabs',
|
||||
browserName: 'Safari',
|
||||
platform: 'iOS',
|
||||
version: '10.3',
|
||||
device: 'iPhone 7 Plus Simulator'
|
||||
},
|
||||
'sl_ios_9.3_safari': {
|
||||
base: 'SauceLabs',
|
||||
browserName: 'Safari',
|
||||
platform: 'iOS',
|
||||
version: '9.3',
|
||||
device: 'iPhone 6 Plus Simulator'
|
||||
},
|
||||
'sl_ios_8.4_safari': {
|
||||
base: 'SauceLabs',
|
||||
browserName: 'Safari',
|
||||
platform: 'iOS',
|
||||
version: '8.4',
|
||||
device: 'iPhone 5s Simulator'
|
||||
}
|
||||
};
|
||||
|
||||
const customLaunchers = Object.assign({}, slLaunchers, {
|
||||
stable_chrome: {
|
||||
base: 'Chrome'
|
||||
},
|
||||
stable_firefox: {
|
||||
base: 'Firefox'
|
||||
}
|
||||
});
|
||||
|
||||
config.set({
|
||||
|
||||
// base path that will be used to resolve all patterns (eg. files, exclude)
|
||||
basePath: '',
|
||||
|
||||
|
||||
// frameworks to use
|
||||
// available frameworks: https://npmjs.org/browse/keyword/karma-adapter
|
||||
frameworks: ['mocha'],
|
||||
|
||||
|
||||
// list of files / patterns to load in the browser
|
||||
files: [
|
||||
'build/testrunner.js',
|
||||
{ pattern: './tests/**/*', 'watched': true, 'included': false, 'served': true},
|
||||
{ pattern: './dist/**/*', 'watched': true, 'included': false, 'served': true},
|
||||
{ pattern: './node_modules/**/*', 'watched': true, 'included': false, 'served': true}
|
||||
],
|
||||
|
||||
|
||||
// list of files to exclude
|
||||
exclude: [
|
||||
],
|
||||
|
||||
|
||||
// preprocess matching files before serving them to the browser
|
||||
// available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
|
||||
preprocessors: {
|
||||
},
|
||||
|
||||
|
||||
// test results reporter to use
|
||||
// possible values: 'dots', 'progress'
|
||||
// available reporters: https://npmjs.org/browse/keyword/karma-reporter
|
||||
reporters: ['progress', 'saucelabs'],
|
||||
|
||||
// web server port
|
||||
port,
|
||||
|
||||
|
||||
// enable / disable colors in the output (reporters and logs)
|
||||
colors: true,
|
||||
|
||||
|
||||
// level of logging
|
||||
// possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
|
||||
logLevel: config.LOG_INFO,
|
||||
|
||||
|
||||
// enable / disable watching file and executing tests whenever any file changes
|
||||
autoWatch: true,
|
||||
|
||||
|
||||
// start these browsers
|
||||
// available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
|
||||
browsers: Object.keys(customLaunchers),
|
||||
|
||||
|
||||
customLaunchers,
|
||||
|
||||
// Continuous Integration mode
|
||||
// if true, Karma captures browsers, runs the tests and exits
|
||||
singleRun: true,
|
||||
|
||||
// Concurrency level
|
||||
// how many browser should be started simultaneous
|
||||
concurrency: 5,
|
||||
|
||||
proxies: {
|
||||
'/dist': `http://localhost:${port}/base/dist`,
|
||||
'/node_modules': `http://localhost:${port}/base/node_modules`,
|
||||
'/tests': `http://localhost:${port}/base/tests`,
|
||||
'/assets': `http://localhost:${port}/base/tests/assets`
|
||||
},
|
||||
|
||||
client: {
|
||||
mocha: {
|
||||
// change Karma's debug.html to the mocha web reporter
|
||||
reporter: 'html'
|
||||
}
|
||||
},
|
||||
|
||||
captureTimeout: 300000,
|
||||
|
||||
browserDisconnectTimeout: 60000,
|
||||
|
||||
browserNoActivityTimeout: 1200000
|
||||
})
|
||||
};
|
112
karma.js
Normal file
112
karma.js
Normal file
@ -0,0 +1,112 @@
|
||||
const Server = require('karma').Server;
|
||||
const cfg = require('karma').config;
|
||||
const path = require('path');
|
||||
const proxy = require('html2canvas-proxy');
|
||||
const karmaConfig = cfg.parseConfig(path.resolve('./karma.conf.js'));
|
||||
const server = new Server(karmaConfig, (exitCode) => {
|
||||
console.log('Karma has exited with ' + exitCode);
|
||||
process.exit(exitCode)
|
||||
});
|
||||
|
||||
const fs = require('fs');
|
||||
const express = require('express');
|
||||
const bodyParser = require('body-parser');
|
||||
const cors = require('cors');
|
||||
const filenamifyUrl = require('filenamify-url');
|
||||
|
||||
const CORS_PORT = 8081;
|
||||
const corsApp = express();
|
||||
corsApp.use('/proxy', proxy());
|
||||
corsApp.use('/cors', cors(), express.static(path.resolve(__dirname)));
|
||||
corsApp.use('/', express.static(path.resolve(__dirname, '/tests')));
|
||||
corsApp.use((error, req, res, next) => {
|
||||
console.error(error);
|
||||
next();
|
||||
});
|
||||
|
||||
process.on('uncaughtException', (err) => {
|
||||
if(err.errno === 'EADDRINUSE') {
|
||||
console.warn(err);
|
||||
} else {
|
||||
console.log(err);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
corsApp.listen(CORS_PORT, () => {
|
||||
console.log(`CORS server running on port ${CORS_PORT}`);
|
||||
});
|
||||
|
||||
const app = express();
|
||||
app.use(cors());
|
||||
app.use((req, res, next) => {
|
||||
// IE9 doesn't set headers for cross-domain ajax requests
|
||||
if(typeof(req.headers['content-type']) === 'undefined'){
|
||||
req.headers['content-type'] = "application/json";
|
||||
}
|
||||
next();
|
||||
});
|
||||
app.use(
|
||||
bodyParser.json({
|
||||
limit: '15mb',
|
||||
type: '*/*'
|
||||
})
|
||||
);
|
||||
|
||||
const prefix = 'data:image/png;base64,';
|
||||
|
||||
const writeScreenshot = (buffer, body) => {
|
||||
const filename = `${filenamifyUrl(
|
||||
body.test.replace(/^\/tests\/reftests\//, '').replace(/\.html$/, ''),
|
||||
{replacement: '-'}
|
||||
)}!${body.platform.name}-${body.platform.version}.png`;
|
||||
|
||||
fs.writeFileSync(path.resolve(__dirname, './tests/results/', filename), buffer);
|
||||
};
|
||||
|
||||
app.post('/screenshot', (req, res) => {
|
||||
if (!req.body || !req.body.screenshot) {
|
||||
return res.sendStatus(400);
|
||||
}
|
||||
|
||||
const buffer = new Buffer(req.body.screenshot.substring(prefix.length), 'base64');
|
||||
writeScreenshot(buffer, req.body);
|
||||
return res.sendStatus(200);
|
||||
});
|
||||
|
||||
const chunks = {};
|
||||
|
||||
app.post('/screenshot/chunk', (req, res) => {
|
||||
if (!req.body || !req.body.screenshot) {
|
||||
return res.sendStatus(400);
|
||||
}
|
||||
|
||||
const key = `${req.body.platform.name}-${req.body.platform.version}-${req.body.test
|
||||
.replace(/^\/tests\/reftests\//, '')
|
||||
.replace(/\.html$/, '')}`;
|
||||
if (!Array.isArray(chunks[key])) {
|
||||
chunks[key] = Array.from(Array(req.body.totalCount));
|
||||
}
|
||||
|
||||
chunks[key][req.body.part] = req.body.screenshot;
|
||||
|
||||
if (chunks[key].every(s => typeof s === 'string')) {
|
||||
const str = chunks[key].reduce((acc, s) => acc + s, '');
|
||||
const buffer = new Buffer(str.substring(prefix.length), 'base64');
|
||||
delete chunks[key];
|
||||
writeScreenshot(buffer, req.body);
|
||||
}
|
||||
|
||||
return res.sendStatus(200);
|
||||
});
|
||||
|
||||
app.use((error, req, res, next) => {
|
||||
console.error(error);
|
||||
next();
|
||||
});
|
||||
|
||||
const listener = app.listen(8000, () => {
|
||||
server.start();
|
||||
});
|
||||
|
||||
|
82
package.json
Normal file
82
package.json
Normal file
@ -0,0 +1,82 @@
|
||||
{
|
||||
"title": "html2canvas",
|
||||
"name": "html2canvas",
|
||||
"description": "Screenshots with JavaScript",
|
||||
"main": "dist/npm/index.js",
|
||||
"version": "1.0.0-alpha.3",
|
||||
"author": {
|
||||
"name": "Niklas von Hertzen",
|
||||
"email": "niklasvh@gmail.com",
|
||||
"url": "https://hertzen.com"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4.0.0"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git@github.com:niklasvh/html2canvas.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/niklasvh/html2canvas/issues"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-cli": "6.24.1",
|
||||
"babel-core": "6.25.0",
|
||||
"babel-eslint": "7.2.3",
|
||||
"babel-loader": "7.1.1",
|
||||
"babel-plugin-dev-expression": "0.2.1",
|
||||
"babel-plugin-transform-es2015-modules-commonjs": "6.26.0",
|
||||
"babel-plugin-transform-object-rest-spread": "6.23.0",
|
||||
"babel-preset-es2015": "6.24.1",
|
||||
"babel-preset-flow": "6.23.0",
|
||||
"base64-arraybuffer": "0.1.5",
|
||||
"body-parser": "1.17.2",
|
||||
"chai": "4.1.1",
|
||||
"chromeless": "1.2.0",
|
||||
"cors": "2.8.4",
|
||||
"eslint": "4.2.0",
|
||||
"eslint-plugin-flowtype": "2.35.0",
|
||||
"eslint-plugin-prettier": "2.1.2",
|
||||
"express": "4.15.4",
|
||||
"filenamify-url": "1.0.0",
|
||||
"flow-bin": "0.56.0",
|
||||
"glob": "7.1.2",
|
||||
"html2canvas-proxy": "1.0.0",
|
||||
"jquery": "3.2.1",
|
||||
"karma": "1.7.0",
|
||||
"karma-chrome-launcher": "2.2.0",
|
||||
"karma-edge-launcher": "0.4.1",
|
||||
"karma-firefox-launcher": "1.0.1",
|
||||
"karma-ie-launcher": "1.0.0",
|
||||
"karma-mocha": "1.3.0",
|
||||
"karma-sauce-launcher": "1.1.0",
|
||||
"mocha": "3.5.0",
|
||||
"platform": "1.3.4",
|
||||
"prettier": "1.5.3",
|
||||
"promise-polyfill": "6.0.2",
|
||||
"replace-in-file": "^3.0.0",
|
||||
"rimraf": "2.6.1",
|
||||
"serve-index": "1.9.0",
|
||||
"slash": "1.0.0",
|
||||
"uglifyjs-webpack-plugin": "^1.1.2",
|
||||
"webpack": "3.4.1"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "rimraf dist/ && node scripts/create-reftest-list && npm run build:npm && npm run build:browser",
|
||||
"build:npm": "babel src/ -d dist/npm/ --plugins=dev-expression,transform-es2015-modules-commonjs && replace-in-file __VERSION__ '\"$npm_package_version\"' dist/npm/index.js",
|
||||
"build:browser": "webpack",
|
||||
"format": "prettier --single-quote --no-bracket-spacing --tab-width 4 --print-width 100 --write \"{src,www,tests,scripts}/**/*.js\"",
|
||||
"flow": "flow",
|
||||
"lint": "eslint src/**",
|
||||
"test": "npm run flow && npm run lint && npm run test:node && npm run karma",
|
||||
"test:node": "mocha tests/node/*.js",
|
||||
"karma": "node karma",
|
||||
"watch": "webpack --progress --colors --watch",
|
||||
"start": "node tests/server"
|
||||
},
|
||||
"homepage": "https://html2canvas.hertzen.com",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"punycode": "2.1.0"
|
||||
}
|
||||
}
|
78
readme.md
78
readme.md
@ -1,78 +0,0 @@
|
||||
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.
|
||||
|
||||
|
||||
###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. However, as many elements are displayed differently on different browsers and operating systems (such as form elements such as radio buttons or checkboxes) as well as
|
||||
|
||||
It does <b>not require any rendering from the server</b>, as the whole image is created on the <b>clients browser</b>. However, for browsers without <code>canvas</code> support alternatives such as <a href="http://flashcanvas.net/">flashcanvas</a> or <a href="http://excanvas.sourceforge.net/">ExplorerCanvas</a> are necessary to create the image.
|
||||
|
||||
Additionally, to render <code>iframe</code> content or images situated outside of the <a href="http://en.wikipedia.org/wiki/Same_origin_policy">same origin policy</a> a proxy will be necessary to load the content to the users browser.
|
||||
|
||||
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. However, please do test it out and report your findings, especially if something should be working, but is displaying it incorrectly.
|
||||
|
||||
###Browser compatibility###
|
||||
|
||||
The script should work fine on the following browsers:
|
||||
|
||||
* Firefox 3.5+
|
||||
* Google Chrome
|
||||
* Newer versions of Opera (exactly how new is yet to be determined)
|
||||
* >=IE9 (Older versions compatible with the use of flashcanvas)
|
||||
|
||||
Note that the compatibility will most likely be increased in future builds, as many of the current restrictions have at least partial work arounds, which can be used with older browser versions.
|
||||
|
||||
###So what isn't included yet?###
|
||||
|
||||
There are still a lot of CSS properties missing, including most CSS3 properties such as <code>text-shadow</code>, <code>box-radius</code> etc. as well as all elements created by the browser, such as radio and checkbox buttons and list icons. I will compile a full list of supported elements and CSS properties soon.
|
||||
There is no support for <code>frame</code> and <code>object</code> content such as Flash.
|
||||
|
||||
### Usage ###
|
||||
To render an `element` with html2canvas, simply call:
|
||||
` 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 ], {
|
||||
onrendered: function( canvas ) {
|
||||
/* canvas is the actual canvas element,
|
||||
to append it to the page call for example
|
||||
document.body.appendChild( canvas );
|
||||
*/
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
### 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>.
|
||||
|
||||
### Changelog ###
|
||||
|
||||
v0.34 - 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>)
|
||||
* Radial gradients (<a href="https://github.com/niklasvh/html2canvas/commit/4f22c18043a73c0c3bbf3b5e4d62714c56acd3c7">SunboX</a>)
|
||||
* 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
|
||||
|
||||
* 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>)
|
||||
* Improved minification saved ~1K! (<a href="https://github.com/cobexer/html2canvas/commit/b82be022b2b9240bd503e078ac980bde2b953e43">cobexer</a>)
|
||||
* 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
|
||||
|
||||
* Added changelog!
|
||||
* Added bookmarklet (<a href="https://github.com/niklasvh/html2canvas/commit/b320dd306e1a2d32a3bc5a71b6ebf6d8c060cde5">cobexer</a>)
|
||||
* Option to select single element to render (<a href="https://github.com/niklasvh/html2canvas/commit/0cb252ada91c84ef411288b317c03e97da1f12ad">niklasvh</a>)
|
||||
* Fixed closure compiler warnings (<a href="https://github.com/niklasvh/html2canvas/commit/36ff1ec7aadcbdf66851a0b77f0b9e87e4a8e4a1">cobexer</a>)
|
||||
* Enable profiling in FF (<a href="https://github.com/niklasvh/html2canvas/commit/bbd75286a8406cf9e5aea01fdb7950d547edefb9">cobexer</a>)
|
53
scripts/create-reftest-list.js
Normal file
53
scripts/create-reftest-list.js
Normal file
@ -0,0 +1,53 @@
|
||||
'use strict';
|
||||
|
||||
const path = require('path');
|
||||
const glob = require('glob');
|
||||
const fs = require('fs');
|
||||
const slash = require('slash');
|
||||
const parseRefTest = require('./parse-reftest');
|
||||
const outputPath = 'tests/reftests.js';
|
||||
|
||||
const ignoredTests = fs
|
||||
.readFileSync(path.resolve(__dirname, `../tests/reftests/ignore.txt`))
|
||||
.toString()
|
||||
.split('\n')
|
||||
.filter(l => l.length)
|
||||
.reduce((acc, l) => {
|
||||
const m = l.match(/^(\[(.+)\])?(.+)$/i);
|
||||
acc[m[3]] = m[2] ? m[2].split(',') : [];
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
glob(
|
||||
'../tests/reftests/**/*.html',
|
||||
{
|
||||
cwd: __dirname,
|
||||
root: path.resolve(__dirname, '../../')
|
||||
},
|
||||
(err, files) => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const testList = files.reduce((acc, filename) => {
|
||||
const refTestFilename = path.resolve(__dirname, filename.replace(/\.html$/, '.txt'));
|
||||
const name = `/${slash(path.relative('../', filename))}`;
|
||||
if (!Array.isArray(ignoredTests[name]) || ignoredTests[name].length) {
|
||||
acc[name] = fs.existsSync(refTestFilename)
|
||||
? parseRefTest(fs.readFileSync(refTestFilename).toString())
|
||||
: null;
|
||||
} else {
|
||||
console.log(`IGNORED: ${name}`);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
fs.writeFileSync(
|
||||
path.resolve(__dirname, `../${outputPath}`),
|
||||
`module.exports = ${JSON.stringify({testList, ignoredTests}, null, 4)};`
|
||||
);
|
||||
|
||||
console.log(`${outputPath} updated`);
|
||||
}
|
||||
);
|
37
scripts/create-reftests.js
Normal file
37
scripts/create-reftests.js
Normal file
@ -0,0 +1,37 @@
|
||||
const {Chromeless} = require('chromeless');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const express = require('express');
|
||||
const reftests = require('../tests/reftests');
|
||||
|
||||
const app = express();
|
||||
app.use('/', express.static(path.resolve(__dirname, '../')));
|
||||
|
||||
const listener = app.listen(0, () => {
|
||||
async function run() {
|
||||
const chromeless = new Chromeless();
|
||||
const tests = Object.keys(reftests.testList);
|
||||
let i = 0;
|
||||
while (tests[i]) {
|
||||
const filename = tests[i];
|
||||
i++;
|
||||
const reftest = await chromeless
|
||||
.goto(`http://localhost:${listener.address().port}${filename}?reftest&run=false`)
|
||||
.evaluate(() =>
|
||||
html2canvas(document.documentElement, {
|
||||
windowWidth: 800,
|
||||
windowHeight: 600,
|
||||
target: new RefTestRenderer()
|
||||
})
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.resolve(__dirname, `..${filename.replace(/\.html$/i, '.txt')}`),
|
||||
reftest
|
||||
);
|
||||
}
|
||||
|
||||
await chromeless.end();
|
||||
}
|
||||
|
||||
run().catch(console.error.bind(console)).then(() => process.exit(0));
|
||||
});
|
155
scripts/parse-reftest.js
Normal file
155
scripts/parse-reftest.js
Normal file
@ -0,0 +1,155 @@
|
||||
const ACTION = /^\s*(\w+ ?\w*):\s+(.+)/;
|
||||
const TEXT = /^\s*\[(-?\d+), (-?\d+)\]:\s+(.+)/;
|
||||
const WINDOW_SIZE = /^\[(-?\d+), (-?\d+)\]$/;
|
||||
const RECTANGLE = /^\[(-?\d+), (-?\d+), (-?\d+), (-?\d+)\]\s+(.+)$/;
|
||||
const REPEAT = /^Image\s+\("(.+)"\)\s+\[(-?\d+), (-?\d+)\]\s+Size\s+\((-?\d+), (-?\d+)\)\s+(.+)$/;
|
||||
const PATH = /^Path \((.+)\)$/;
|
||||
const VECTOR = /^Vector\(x: (-?\d+), y: (-?\d+)\)$/;
|
||||
const BEZIER_CURVE = /^BezierCurve\(x0: (-?\d+), y0: (-?\d+), x1: (-?\d+), y1: (-?\d+), cx0: (-?\d+), cy0: (-?\d+), cx1: (-?\d+), cy1: (-?\d+)\)$/;
|
||||
const SHAPE = /^(rgba?\((:?.+)\)) (Path .+)$/;
|
||||
const CIRCLE = /^(rgba?\((:?.+)\)) Circle\(x: (-?\d+), y: (-?\d+), r: (-?\d+)\)$/;
|
||||
const IMAGE = /^Image\s+\("(.+)"\)\s+\(source:\s+\[(-?\d+), (-?\d+), (-?\d+), (-?\d+)\]\)\s+\(destination:\s+\[(-?\d+), (-?\d+), (-?\d+), (-?\d+)\]\)$/;
|
||||
const CANVAS = /^(Canvas)\s+\(source:\s+\[(-?\d+), (-?\d+), (-?\d+), (-?\d+)\]\)\s+\(destination:\s+\[(-?\d+), (-?\d+), (-?\d+), (-?\d+)\]\)$/;
|
||||
const GRADIENT = /^\[(-?\d+), (-?\d+), (-?\d+), (-?\d+)\]\s+linear-gradient\(x0: (-?\d+), x1: (-?\d+), y0: (-?\d+), y1: (-?\d+) (.+)\)$/;
|
||||
const TRANSFORM = /^\((-?\d+), (-?\d+)\) \[(.+)\]$/;
|
||||
|
||||
function parsePath(path) {
|
||||
const parts = path.match(PATH)[1];
|
||||
return parts.split(' > ').map(p => {
|
||||
const vector = p.match(VECTOR);
|
||||
if (vector) {
|
||||
return {
|
||||
type: 'Vector',
|
||||
x: parseInt(vector[1], 10),
|
||||
y: parseInt(vector[2], 10)
|
||||
};
|
||||
} else {
|
||||
const bezier = p.match(BEZIER_CURVE);
|
||||
return {
|
||||
type: 'BezierCurve',
|
||||
x0: parseInt(bezier[1], 10),
|
||||
y0: parseInt(bezier[2], 10),
|
||||
x1: parseInt(bezier[3], 10),
|
||||
y1: parseInt(bezier[4], 10),
|
||||
cx0: parseInt(bezier[5], 10),
|
||||
cy0: parseInt(bezier[6], 10),
|
||||
cx1: parseInt(bezier[7], 10),
|
||||
cy1: parseInt(bezier[8], 10)
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function parseRefTest(txt) {
|
||||
return txt.split(/\n/g).filter(l => l.length > 0).map((l, i) => {
|
||||
const parseAction = l.match(ACTION);
|
||||
if (!parseAction) {
|
||||
const text = l.match(TEXT);
|
||||
return {
|
||||
action: 'T',
|
||||
x: parseInt(text[1], 10),
|
||||
y: parseInt(text[2], 10),
|
||||
text: text[3],
|
||||
line: i + 1
|
||||
};
|
||||
}
|
||||
const args = parseAction[2];
|
||||
|
||||
const data = {
|
||||
action: parseAction[1],
|
||||
line: i + 1
|
||||
};
|
||||
|
||||
switch (data.action) {
|
||||
case 'Opacity':
|
||||
data.opacity = parseFloat(args);
|
||||
break;
|
||||
case 'Fill':
|
||||
data.color = args;
|
||||
break;
|
||||
case 'Clip':
|
||||
data.path = args.split(' | ').map(path => parsePath(path));
|
||||
break;
|
||||
case 'Window':
|
||||
const windowSize = args.match(WINDOW_SIZE);
|
||||
data.width = parseInt(windowSize[1], 10);
|
||||
data.height = parseInt(windowSize[2], 10);
|
||||
break;
|
||||
case 'Rectangle':
|
||||
const rectangle = args.match(RECTANGLE);
|
||||
data.x = parseInt(rectangle[1], 10);
|
||||
data.y = parseInt(rectangle[2], 10);
|
||||
data.width = parseInt(rectangle[3], 10);
|
||||
data.height = parseInt(rectangle[4], 10);
|
||||
data.color = rectangle[5];
|
||||
break;
|
||||
case 'Repeat':
|
||||
const repeat = args.match(REPEAT);
|
||||
data.imageSrc = repeat[1];
|
||||
data.x = parseInt(repeat[2], 10);
|
||||
data.y = parseInt(repeat[3], 10);
|
||||
data.width = parseInt(repeat[4], 10);
|
||||
data.height = parseInt(repeat[5], 10);
|
||||
data.path = parsePath(repeat[6]);
|
||||
break;
|
||||
case 'Shape':
|
||||
const shape = args.match(SHAPE);
|
||||
if (!shape) {
|
||||
const circle = args.match(CIRCLE);
|
||||
data.color = circle[1];
|
||||
data.path = [
|
||||
{
|
||||
type: 'Circle',
|
||||
x: parseInt(circle[2], 10),
|
||||
y: parseInt(circle[3], 10),
|
||||
r: parseInt(circle[4], 10)
|
||||
}
|
||||
];
|
||||
} else {
|
||||
data.color = shape[1];
|
||||
data.path = parsePath(shape[3]);
|
||||
}
|
||||
break;
|
||||
case 'Text':
|
||||
data.font = args;
|
||||
break;
|
||||
case 'Draw image':
|
||||
const image = args.match(IMAGE) ? args.match(IMAGE) : args.match(CANVAS);
|
||||
data.imageSrc = image[1];
|
||||
data.sx = parseInt(image[2], 10);
|
||||
data.xy = parseInt(image[3], 10);
|
||||
data.sw = parseInt(image[4], 10);
|
||||
data.sh = parseInt(image[5], 10);
|
||||
data.dx = parseInt(image[6], 10);
|
||||
data.dy = parseInt(image[7], 10);
|
||||
data.dw = parseInt(image[8], 10);
|
||||
data.dh = parseInt(image[9], 10);
|
||||
break;
|
||||
case 'Gradient':
|
||||
const gradient = args.match(GRADIENT);
|
||||
data.x = parseInt(gradient[1], 10);
|
||||
data.y = parseInt(gradient[2], 10);
|
||||
data.width = parseInt(gradient[3], 10);
|
||||
data.height = parseInt(gradient[4], 10);
|
||||
data.x0 = parseInt(gradient[5], 10);
|
||||
data.x1 = parseInt(gradient[6], 10);
|
||||
data.y0 = parseInt(gradient[7], 10);
|
||||
data.y1 = parseInt(gradient[8], 10);
|
||||
data.stops = gradient[9];
|
||||
break;
|
||||
case 'Transform':
|
||||
const transform = args.match(TRANSFORM);
|
||||
data.x = parseInt(transform[1], 10);
|
||||
data.y = parseInt(transform[2], 10);
|
||||
data.matrix = transform[3];
|
||||
break;
|
||||
default:
|
||||
console.log(args);
|
||||
throw new Error('Unhandled action ' + data.action);
|
||||
}
|
||||
|
||||
return data;
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = parseRefTest;
|
24
src/Angle.js
Normal file
24
src/Angle.js
Normal file
@ -0,0 +1,24 @@
|
||||
/* @flow */
|
||||
'use strict';
|
||||
|
||||
const ANGLE = /([+-]?\d*\.?\d+)(deg|grad|rad|turn)/i;
|
||||
|
||||
export const parseAngle = (angle: string): number | null => {
|
||||
const match = angle.match(ANGLE);
|
||||
|
||||
if (match) {
|
||||
const value = parseFloat(match[1]);
|
||||
switch (match[2].toLowerCase()) {
|
||||
case 'deg':
|
||||
return Math.PI * value / 180;
|
||||
case 'grad':
|
||||
return Math.PI / 200 * value;
|
||||
case 'rad':
|
||||
return value;
|
||||
case 'turn':
|
||||
return Math.PI * 2 * value;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
378
src/Bounds.js
Normal file
378
src/Bounds.js
Normal file
@ -0,0 +1,378 @@
|
||||
/* @flow */
|
||||
'use strict';
|
||||
|
||||
import type {Border, BorderSide} from './parsing/border';
|
||||
import type {BorderRadius} from './parsing/borderRadius';
|
||||
import type {Padding} from './parsing/padding';
|
||||
import type {Path} from './drawing/Path';
|
||||
|
||||
import Vector from './drawing/Vector';
|
||||
import BezierCurve from './drawing/BezierCurve';
|
||||
|
||||
const TOP = 0;
|
||||
const RIGHT = 1;
|
||||
const BOTTOM = 2;
|
||||
const LEFT = 3;
|
||||
|
||||
const H = 0;
|
||||
const V = 1;
|
||||
|
||||
export type BoundCurves = {
|
||||
topLeftOuter: BezierCurve | Vector,
|
||||
topLeftInner: BezierCurve | Vector,
|
||||
topRightOuter: BezierCurve | Vector,
|
||||
topRightInner: BezierCurve | Vector,
|
||||
bottomRightOuter: BezierCurve | Vector,
|
||||
bottomRightInner: BezierCurve | Vector,
|
||||
bottomLeftOuter: BezierCurve | Vector,
|
||||
bottomLeftInner: BezierCurve | Vector
|
||||
};
|
||||
|
||||
export class Bounds {
|
||||
top: number;
|
||||
left: number;
|
||||
width: number;
|
||||
height: number;
|
||||
|
||||
constructor(x: number, y: number, w: number, h: number) {
|
||||
this.left = x;
|
||||
this.top = y;
|
||||
this.width = w;
|
||||
this.height = h;
|
||||
}
|
||||
|
||||
static fromClientRect(clientRect: ClientRect, scrollX: number, scrollY: number): Bounds {
|
||||
return new Bounds(
|
||||
clientRect.left + scrollX,
|
||||
clientRect.top + scrollY,
|
||||
clientRect.width,
|
||||
clientRect.height
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const parseBounds = (
|
||||
node: HTMLElement | SVGSVGElement,
|
||||
scrollX: number,
|
||||
scrollY: number
|
||||
): Bounds => {
|
||||
return Bounds.fromClientRect(node.getBoundingClientRect(), scrollX, scrollY);
|
||||
};
|
||||
|
||||
export const calculatePaddingBox = (bounds: Bounds, borders: Array<Border>): Bounds => {
|
||||
return new Bounds(
|
||||
bounds.left + borders[LEFT].borderWidth,
|
||||
bounds.top + borders[TOP].borderWidth,
|
||||
bounds.width - (borders[RIGHT].borderWidth + borders[LEFT].borderWidth),
|
||||
bounds.height - (borders[TOP].borderWidth + borders[BOTTOM].borderWidth)
|
||||
);
|
||||
};
|
||||
|
||||
export const calculateContentBox = (
|
||||
bounds: Bounds,
|
||||
padding: Padding,
|
||||
borders: Array<Border>
|
||||
): Bounds => {
|
||||
// TODO support percentage paddings
|
||||
const paddingTop = padding[TOP].value;
|
||||
const paddingRight = padding[RIGHT].value;
|
||||
const paddingBottom = padding[BOTTOM].value;
|
||||
const paddingLeft = padding[LEFT].value;
|
||||
|
||||
return new Bounds(
|
||||
bounds.left + paddingLeft + borders[LEFT].borderWidth,
|
||||
bounds.top + paddingTop + borders[TOP].borderWidth,
|
||||
bounds.width -
|
||||
(borders[RIGHT].borderWidth + borders[LEFT].borderWidth + paddingLeft + paddingRight),
|
||||
bounds.height -
|
||||
(borders[TOP].borderWidth + borders[BOTTOM].borderWidth + paddingTop + paddingBottom)
|
||||
);
|
||||
};
|
||||
|
||||
export const parseDocumentSize = (document: Document): Bounds => {
|
||||
const body = document.body;
|
||||
const documentElement = document.documentElement;
|
||||
|
||||
if (!body || !documentElement) {
|
||||
throw new Error(__DEV__ ? `Unable to get document size` : '');
|
||||
}
|
||||
const width = Math.max(
|
||||
Math.max(body.scrollWidth, documentElement.scrollWidth),
|
||||
Math.max(body.offsetWidth, documentElement.offsetWidth),
|
||||
Math.max(body.clientWidth, documentElement.clientWidth)
|
||||
);
|
||||
|
||||
const height = Math.max(
|
||||
Math.max(body.scrollHeight, documentElement.scrollHeight),
|
||||
Math.max(body.offsetHeight, documentElement.offsetHeight),
|
||||
Math.max(body.clientHeight, documentElement.clientHeight)
|
||||
);
|
||||
|
||||
return new Bounds(0, 0, width, height);
|
||||
};
|
||||
|
||||
export const parsePathForBorder = (curves: BoundCurves, borderSide: BorderSide): Path => {
|
||||
switch (borderSide) {
|
||||
case TOP:
|
||||
return createPathFromCurves(
|
||||
curves.topLeftOuter,
|
||||
curves.topLeftInner,
|
||||
curves.topRightOuter,
|
||||
curves.topRightInner
|
||||
);
|
||||
case RIGHT:
|
||||
return createPathFromCurves(
|
||||
curves.topRightOuter,
|
||||
curves.topRightInner,
|
||||
curves.bottomRightOuter,
|
||||
curves.bottomRightInner
|
||||
);
|
||||
case BOTTOM:
|
||||
return createPathFromCurves(
|
||||
curves.bottomRightOuter,
|
||||
curves.bottomRightInner,
|
||||
curves.bottomLeftOuter,
|
||||
curves.bottomLeftInner
|
||||
);
|
||||
case LEFT:
|
||||
default:
|
||||
return createPathFromCurves(
|
||||
curves.bottomLeftOuter,
|
||||
curves.bottomLeftInner,
|
||||
curves.topLeftOuter,
|
||||
curves.topLeftInner
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const createPathFromCurves = (
|
||||
outer1: BezierCurve | Vector,
|
||||
inner1: BezierCurve | Vector,
|
||||
outer2: BezierCurve | Vector,
|
||||
inner2: BezierCurve | Vector
|
||||
): Path => {
|
||||
const path = [];
|
||||
if (outer1 instanceof BezierCurve) {
|
||||
path.push(outer1.subdivide(0.5, false));
|
||||
} else {
|
||||
path.push(outer1);
|
||||
}
|
||||
|
||||
if (outer2 instanceof BezierCurve) {
|
||||
path.push(outer2.subdivide(0.5, true));
|
||||
} else {
|
||||
path.push(outer2);
|
||||
}
|
||||
|
||||
if (inner2 instanceof BezierCurve) {
|
||||
path.push(inner2.subdivide(0.5, true).reverse());
|
||||
} else {
|
||||
path.push(inner2);
|
||||
}
|
||||
|
||||
if (inner1 instanceof BezierCurve) {
|
||||
path.push(inner1.subdivide(0.5, false).reverse());
|
||||
} else {
|
||||
path.push(inner1);
|
||||
}
|
||||
|
||||
return path;
|
||||
};
|
||||
|
||||
export const calculateBorderBoxPath = (curves: BoundCurves): Path => {
|
||||
return [
|
||||
curves.topLeftOuter,
|
||||
curves.topRightOuter,
|
||||
curves.bottomRightOuter,
|
||||
curves.bottomLeftOuter
|
||||
];
|
||||
};
|
||||
|
||||
export const calculatePaddingBoxPath = (curves: BoundCurves): Path => {
|
||||
return [
|
||||
curves.topLeftInner,
|
||||
curves.topRightInner,
|
||||
curves.bottomRightInner,
|
||||
curves.bottomLeftInner
|
||||
];
|
||||
};
|
||||
|
||||
export const parseBoundCurves = (
|
||||
bounds: Bounds,
|
||||
borders: Array<Border>,
|
||||
borderRadius: Array<BorderRadius>
|
||||
): BoundCurves => {
|
||||
const HALF_WIDTH = bounds.width / 2;
|
||||
const HALF_HEIGHT = bounds.height / 2;
|
||||
const tlh =
|
||||
borderRadius[CORNER.TOP_LEFT][H].getAbsoluteValue(bounds.width) < HALF_WIDTH
|
||||
? borderRadius[CORNER.TOP_LEFT][H].getAbsoluteValue(bounds.width)
|
||||
: HALF_WIDTH;
|
||||
const tlv =
|
||||
borderRadius[CORNER.TOP_LEFT][V].getAbsoluteValue(bounds.height) < HALF_HEIGHT
|
||||
? borderRadius[CORNER.TOP_LEFT][V].getAbsoluteValue(bounds.height)
|
||||
: HALF_HEIGHT;
|
||||
const trh =
|
||||
borderRadius[CORNER.TOP_RIGHT][H].getAbsoluteValue(bounds.width) < HALF_WIDTH
|
||||
? borderRadius[CORNER.TOP_RIGHT][H].getAbsoluteValue(bounds.width)
|
||||
: HALF_WIDTH;
|
||||
const trv =
|
||||
borderRadius[CORNER.TOP_RIGHT][V].getAbsoluteValue(bounds.height) < HALF_HEIGHT
|
||||
? borderRadius[CORNER.TOP_RIGHT][V].getAbsoluteValue(bounds.height)
|
||||
: HALF_HEIGHT;
|
||||
const brh =
|
||||
borderRadius[CORNER.BOTTOM_RIGHT][H].getAbsoluteValue(bounds.width) < HALF_WIDTH
|
||||
? borderRadius[CORNER.BOTTOM_RIGHT][H].getAbsoluteValue(bounds.width)
|
||||
: HALF_WIDTH;
|
||||
const brv =
|
||||
borderRadius[CORNER.BOTTOM_RIGHT][V].getAbsoluteValue(bounds.height) < HALF_HEIGHT
|
||||
? borderRadius[CORNER.BOTTOM_RIGHT][V].getAbsoluteValue(bounds.height)
|
||||
: HALF_HEIGHT;
|
||||
const blh =
|
||||
borderRadius[CORNER.BOTTOM_LEFT][H].getAbsoluteValue(bounds.width) < HALF_WIDTH
|
||||
? borderRadius[CORNER.BOTTOM_LEFT][H].getAbsoluteValue(bounds.width)
|
||||
: HALF_WIDTH;
|
||||
const blv =
|
||||
borderRadius[CORNER.BOTTOM_LEFT][V].getAbsoluteValue(bounds.height) < HALF_HEIGHT
|
||||
? borderRadius[CORNER.BOTTOM_LEFT][V].getAbsoluteValue(bounds.height)
|
||||
: HALF_HEIGHT;
|
||||
|
||||
const topWidth = bounds.width - trh;
|
||||
const rightHeight = bounds.height - brv;
|
||||
const bottomWidth = bounds.width - brh;
|
||||
const leftHeight = bounds.height - blv;
|
||||
|
||||
return {
|
||||
topLeftOuter:
|
||||
tlh > 0 || tlv > 0
|
||||
? getCurvePoints(bounds.left, bounds.top, tlh, tlv, CORNER.TOP_LEFT)
|
||||
: new Vector(bounds.left, bounds.top),
|
||||
topLeftInner:
|
||||
tlh > 0 || tlv > 0
|
||||
? getCurvePoints(
|
||||
bounds.left + borders[LEFT].borderWidth,
|
||||
bounds.top + borders[TOP].borderWidth,
|
||||
Math.max(0, tlh - borders[LEFT].borderWidth),
|
||||
Math.max(0, tlv - borders[TOP].borderWidth),
|
||||
CORNER.TOP_LEFT
|
||||
)
|
||||
: new Vector(
|
||||
bounds.left + borders[LEFT].borderWidth,
|
||||
bounds.top + borders[TOP].borderWidth
|
||||
),
|
||||
topRightOuter:
|
||||
trh > 0 || trv > 0
|
||||
? getCurvePoints(bounds.left + topWidth, bounds.top, trh, trv, CORNER.TOP_RIGHT)
|
||||
: new Vector(bounds.left + bounds.width, bounds.top),
|
||||
topRightInner:
|
||||
trh > 0 || trv > 0
|
||||
? getCurvePoints(
|
||||
bounds.left + Math.min(topWidth, bounds.width + borders[LEFT].borderWidth),
|
||||
bounds.top + borders[TOP].borderWidth,
|
||||
topWidth > bounds.width + borders[LEFT].borderWidth
|
||||
? 0
|
||||
: trh - borders[LEFT].borderWidth,
|
||||
trv - borders[TOP].borderWidth,
|
||||
CORNER.TOP_RIGHT
|
||||
)
|
||||
: new Vector(
|
||||
bounds.left + bounds.width - borders[RIGHT].borderWidth,
|
||||
bounds.top + borders[TOP].borderWidth
|
||||
),
|
||||
bottomRightOuter:
|
||||
brh > 0 || brv > 0
|
||||
? getCurvePoints(
|
||||
bounds.left + bottomWidth,
|
||||
bounds.top + rightHeight,
|
||||
brh,
|
||||
brv,
|
||||
CORNER.BOTTOM_RIGHT
|
||||
)
|
||||
: new Vector(bounds.left + bounds.width, bounds.top + bounds.height),
|
||||
bottomRightInner:
|
||||
brh > 0 || brv > 0
|
||||
? getCurvePoints(
|
||||
bounds.left + Math.min(bottomWidth, bounds.width - borders[LEFT].borderWidth),
|
||||
bounds.top + Math.min(rightHeight, bounds.height + borders[TOP].borderWidth),
|
||||
Math.max(0, brh - borders[RIGHT].borderWidth),
|
||||
brv - borders[BOTTOM].borderWidth,
|
||||
CORNER.BOTTOM_RIGHT
|
||||
)
|
||||
: new Vector(
|
||||
bounds.left + bounds.width - borders[RIGHT].borderWidth,
|
||||
bounds.top + bounds.height - borders[BOTTOM].borderWidth
|
||||
),
|
||||
bottomLeftOuter:
|
||||
blh > 0 || blv > 0
|
||||
? getCurvePoints(bounds.left, bounds.top + leftHeight, blh, blv, CORNER.BOTTOM_LEFT)
|
||||
: new Vector(bounds.left, bounds.top + bounds.height),
|
||||
bottomLeftInner:
|
||||
blh > 0 || blv > 0
|
||||
? getCurvePoints(
|
||||
bounds.left + borders[LEFT].borderWidth,
|
||||
bounds.top + leftHeight,
|
||||
Math.max(0, blh - borders[LEFT].borderWidth),
|
||||
blv - borders[BOTTOM].borderWidth,
|
||||
CORNER.BOTTOM_LEFT
|
||||
)
|
||||
: new Vector(
|
||||
bounds.left + borders[LEFT].borderWidth,
|
||||
bounds.top + bounds.height - borders[BOTTOM].borderWidth
|
||||
)
|
||||
};
|
||||
};
|
||||
|
||||
const CORNER = {
|
||||
TOP_LEFT: 0,
|
||||
TOP_RIGHT: 1,
|
||||
BOTTOM_RIGHT: 2,
|
||||
BOTTOM_LEFT: 3
|
||||
};
|
||||
|
||||
type Corner = $Values<typeof CORNER>;
|
||||
|
||||
const getCurvePoints = (
|
||||
x: number,
|
||||
y: number,
|
||||
r1: number,
|
||||
r2: number,
|
||||
position: Corner
|
||||
): BezierCurve => {
|
||||
const kappa = 4 * ((Math.sqrt(2) - 1) / 3);
|
||||
const ox = r1 * kappa; // control point offset horizontal
|
||||
const oy = r2 * kappa; // control point offset vertical
|
||||
const xm = x + r1; // x-middle
|
||||
const ym = y + r2; // y-middle
|
||||
|
||||
switch (position) {
|
||||
case CORNER.TOP_LEFT:
|
||||
return new BezierCurve(
|
||||
new Vector(x, ym),
|
||||
new Vector(x, ym - oy),
|
||||
new Vector(xm - ox, y),
|
||||
new Vector(xm, y)
|
||||
);
|
||||
case CORNER.TOP_RIGHT:
|
||||
return new BezierCurve(
|
||||
new Vector(x, y),
|
||||
new Vector(x + ox, y),
|
||||
new Vector(xm, ym - oy),
|
||||
new Vector(xm, ym)
|
||||
);
|
||||
case CORNER.BOTTOM_RIGHT:
|
||||
return new BezierCurve(
|
||||
new Vector(xm, y),
|
||||
new Vector(xm, y + oy),
|
||||
new Vector(x + ox, ym),
|
||||
new Vector(x, ym)
|
||||
);
|
||||
case CORNER.BOTTOM_LEFT:
|
||||
default:
|
||||
return new BezierCurve(
|
||||
new Vector(xm, ym),
|
||||
new Vector(xm - ox, ym),
|
||||
new Vector(x, y + oy),
|
||||
new Vector(x, y)
|
||||
);
|
||||
}
|
||||
};
|
591
src/Clone.js
Normal file
591
src/Clone.js
Normal file
@ -0,0 +1,591 @@
|
||||
/* @flow */
|
||||
'use strict';
|
||||
import type {Bounds} from './Bounds';
|
||||
import type {Options} from './index';
|
||||
import type Logger from './Logger';
|
||||
|
||||
import {parseBounds} from './Bounds';
|
||||
import {Proxy} from './Proxy';
|
||||
import ResourceLoader from './ResourceLoader';
|
||||
import {copyCSSStyles} from './Util';
|
||||
import {parseBackgroundImage} from './parsing/background';
|
||||
import CanvasRenderer from './renderer/CanvasRenderer';
|
||||
|
||||
const IGNORE_ATTRIBUTE = 'data-html2canvas-ignore';
|
||||
|
||||
export class DocumentCloner {
|
||||
scrolledElements: Array<[HTMLElement, number, number]>;
|
||||
referenceElement: HTMLElement;
|
||||
clonedReferenceElement: HTMLElement;
|
||||
documentElement: HTMLElement;
|
||||
resourceLoader: ResourceLoader;
|
||||
logger: Logger;
|
||||
options: Options;
|
||||
inlineImages: boolean;
|
||||
copyStyles: boolean;
|
||||
renderer: (element: HTMLElement, options: Options, logger: Logger) => Promise<*>;
|
||||
|
||||
constructor(
|
||||
element: HTMLElement,
|
||||
options: Options,
|
||||
logger: Logger,
|
||||
copyInline: boolean,
|
||||
renderer: (element: HTMLElement, options: Options, logger: Logger) => Promise<*>
|
||||
) {
|
||||
this.referenceElement = element;
|
||||
this.scrolledElements = [];
|
||||
this.copyStyles = copyInline;
|
||||
this.inlineImages = copyInline;
|
||||
this.logger = logger;
|
||||
this.options = options;
|
||||
this.renderer = renderer;
|
||||
this.resourceLoader = new ResourceLoader(options, logger, window);
|
||||
// $FlowFixMe
|
||||
this.documentElement = this.cloneNode(element.ownerDocument.documentElement);
|
||||
}
|
||||
|
||||
inlineAllImages(node: ?HTMLElement) {
|
||||
if (this.inlineImages && node) {
|
||||
const style = node.style;
|
||||
Promise.all(
|
||||
parseBackgroundImage(style.backgroundImage).map(backgroundImage => {
|
||||
if (backgroundImage.method === 'url') {
|
||||
return this.resourceLoader
|
||||
.inlineImage(backgroundImage.args[0])
|
||||
.then(
|
||||
img =>
|
||||
img && typeof img.src === 'string'
|
||||
? `url("${img.src}")`
|
||||
: 'none'
|
||||
)
|
||||
.catch(e => {
|
||||
if (__DEV__) {
|
||||
this.logger.log(`Unable to load image`, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
return Promise.resolve(
|
||||
`${backgroundImage.prefix}${backgroundImage.method}(${backgroundImage.args.join(
|
||||
','
|
||||
)})`
|
||||
);
|
||||
})
|
||||
).then(backgroundImages => {
|
||||
if (backgroundImages.length > 1) {
|
||||
// TODO Multiple backgrounds somehow broken in Chrome
|
||||
style.backgroundColor = '';
|
||||
}
|
||||
style.backgroundImage = backgroundImages.join(',');
|
||||
});
|
||||
|
||||
if (node instanceof HTMLImageElement) {
|
||||
this.resourceLoader
|
||||
.inlineImage(node.src)
|
||||
.then(img => {
|
||||
if (img && node instanceof HTMLImageElement && node.parentNode) {
|
||||
const parentNode = node.parentNode;
|
||||
const clonedChild = copyCSSStyles(node.style, img.cloneNode(false));
|
||||
parentNode.replaceChild(clonedChild, node);
|
||||
}
|
||||
})
|
||||
.catch(e => {
|
||||
if (__DEV__) {
|
||||
this.logger.log(`Unable to load image`, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inlineFonts(document: Document): Promise<void> {
|
||||
return Promise.all(
|
||||
Array.from(document.styleSheets).map(sheet => {
|
||||
if (sheet.href) {
|
||||
return fetch(sheet.href)
|
||||
.then(res => res.text())
|
||||
.then(text => createStyleSheetFontsFromText(text, sheet.href))
|
||||
.catch(e => {
|
||||
if (__DEV__) {
|
||||
this.logger.log(`Unable to load stylesheet`, e);
|
||||
}
|
||||
return [];
|
||||
});
|
||||
}
|
||||
return getSheetFonts(sheet, document);
|
||||
})
|
||||
)
|
||||
.then(fonts => fonts.reduce((acc, font) => acc.concat(font), []))
|
||||
.then(fonts =>
|
||||
Promise.all(
|
||||
fonts.map(font =>
|
||||
fetch(font.formats[0].src)
|
||||
.then(response => response.blob())
|
||||
.then(
|
||||
blob =>
|
||||
new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onerror = reject;
|
||||
reader.onload = () => {
|
||||
// $FlowFixMe
|
||||
const result: string = reader.result;
|
||||
resolve(result);
|
||||
};
|
||||
reader.readAsDataURL(blob);
|
||||
})
|
||||
)
|
||||
.then(dataUri => {
|
||||
font.fontFace.setProperty('src', `url("${dataUri}")`);
|
||||
return `@font-face {${font.fontFace.cssText} `;
|
||||
})
|
||||
)
|
||||
)
|
||||
)
|
||||
.then(fontCss => {
|
||||
const style = document.createElement('style');
|
||||
style.textContent = fontCss.join('\n');
|
||||
this.documentElement.appendChild(style);
|
||||
});
|
||||
}
|
||||
|
||||
createElementClone(node: Node) {
|
||||
if (this.copyStyles && node instanceof HTMLCanvasElement) {
|
||||
const img = node.ownerDocument.createElement('img');
|
||||
try {
|
||||
img.src = node.toDataURL();
|
||||
return img;
|
||||
} catch (e) {
|
||||
if (__DEV__) {
|
||||
this.logger.log(`Unable to clone canvas contents, canvas is tainted`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (node instanceof HTMLIFrameElement) {
|
||||
const tempIframe = node.cloneNode(false);
|
||||
const iframeKey = generateIframeKey();
|
||||
tempIframe.setAttribute('data-html2canvas-internal-iframe-key', iframeKey);
|
||||
|
||||
const {width, height} = parseBounds(node, 0, 0);
|
||||
|
||||
this.resourceLoader.cache[iframeKey] = getIframeDocumentElement(node, this.options)
|
||||
.then(documentElement => {
|
||||
return this.renderer(
|
||||
documentElement,
|
||||
{
|
||||
async: this.options.async,
|
||||
allowTaint: this.options.allowTaint,
|
||||
backgroundColor: '#ffffff',
|
||||
canvas: null,
|
||||
imageTimeout: this.options.imageTimeout,
|
||||
proxy: this.options.proxy,
|
||||
removeContainer: this.options.removeContainer,
|
||||
scale: this.options.scale,
|
||||
foreignObjectRendering: this.options.foreignObjectRendering,
|
||||
target: new CanvasRenderer(),
|
||||
width,
|
||||
height,
|
||||
x: 0,
|
||||
y: 0,
|
||||
windowWidth: documentElement.ownerDocument.defaultView.innerWidth,
|
||||
windowHeight: documentElement.ownerDocument.defaultView.innerHeight,
|
||||
scrollX: documentElement.ownerDocument.defaultView.pageXOffset,
|
||||
scrollY: documentElement.ownerDocument.defaultView.pageYOffset
|
||||
},
|
||||
this.logger.child(iframeKey)
|
||||
);
|
||||
})
|
||||
.then(
|
||||
canvas =>
|
||||
new Promise((resolve, reject) => {
|
||||
const iframeCanvas = document.createElement('img');
|
||||
iframeCanvas.onload = () => resolve(canvas);
|
||||
iframeCanvas.onerror = reject;
|
||||
iframeCanvas.src = canvas.toDataURL();
|
||||
if (tempIframe.parentNode) {
|
||||
tempIframe.parentNode.replaceChild(
|
||||
copyCSSStyles(
|
||||
node.ownerDocument.defaultView.getComputedStyle(node),
|
||||
iframeCanvas
|
||||
),
|
||||
tempIframe
|
||||
);
|
||||
}
|
||||
})
|
||||
);
|
||||
return tempIframe;
|
||||
}
|
||||
|
||||
return node.cloneNode(false);
|
||||
}
|
||||
|
||||
cloneNode(node: Node): Node {
|
||||
const clone =
|
||||
node.nodeType === Node.TEXT_NODE
|
||||
? document.createTextNode(node.nodeValue)
|
||||
: this.createElementClone(node);
|
||||
|
||||
const window = node.ownerDocument.defaultView;
|
||||
|
||||
if (this.referenceElement === node && clone instanceof window.HTMLElement) {
|
||||
this.clonedReferenceElement = clone;
|
||||
}
|
||||
|
||||
if (clone instanceof window.HTMLBodyElement) {
|
||||
createPseudoHideStyles(clone);
|
||||
}
|
||||
|
||||
for (let child = node.firstChild; child; child = child.nextSibling) {
|
||||
if (
|
||||
child.nodeType !== Node.ELEMENT_NODE ||
|
||||
// $FlowFixMe
|
||||
(child.nodeName !== 'SCRIPT' && !child.hasAttribute(IGNORE_ATTRIBUTE))
|
||||
) {
|
||||
if (!this.copyStyles || child.nodeName !== 'STYLE') {
|
||||
clone.appendChild(this.cloneNode(child));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (node instanceof window.HTMLElement && clone instanceof window.HTMLElement) {
|
||||
this.inlineAllImages(inlinePseudoElement(node, clone, PSEUDO_BEFORE));
|
||||
this.inlineAllImages(inlinePseudoElement(node, clone, PSEUDO_AFTER));
|
||||
if (this.copyStyles && !(node instanceof HTMLIFrameElement)) {
|
||||
copyCSSStyles(node.ownerDocument.defaultView.getComputedStyle(node), clone);
|
||||
}
|
||||
this.inlineAllImages(clone);
|
||||
if (node.scrollTop !== 0 || node.scrollLeft !== 0) {
|
||||
this.scrolledElements.push([clone, node.scrollLeft, node.scrollTop]);
|
||||
}
|
||||
switch (node.nodeName) {
|
||||
case 'CANVAS':
|
||||
if (!this.copyStyles) {
|
||||
cloneCanvasContents(node, clone);
|
||||
}
|
||||
break;
|
||||
case 'TEXTAREA':
|
||||
case 'SELECT':
|
||||
clone.value = node.value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return clone;
|
||||
}
|
||||
}
|
||||
|
||||
type Font = {
|
||||
src: string,
|
||||
format: string
|
||||
};
|
||||
|
||||
type FontFamily = {
|
||||
formats: Array<Font>,
|
||||
fontFace: CSSStyleDeclaration
|
||||
};
|
||||
|
||||
const getSheetFonts = (sheet: StyleSheet, document: Document): Array<FontFamily> => {
|
||||
// $FlowFixMe
|
||||
return (sheet.cssRules ? Array.from(sheet.cssRules) : [])
|
||||
.filter(rule => rule.type === CSSRule.FONT_FACE_RULE)
|
||||
.map(rule => {
|
||||
const src = parseBackgroundImage(rule.style.getPropertyValue('src'));
|
||||
const formats = [];
|
||||
for (let i = 0; i < src.length; i++) {
|
||||
if (src[i].method === 'url' && src[i + 1] && src[i + 1].method === 'format') {
|
||||
const a = document.createElement('a');
|
||||
a.href = src[i].args[0];
|
||||
if (document.body) {
|
||||
document.body.appendChild(a);
|
||||
}
|
||||
|
||||
const font = {
|
||||
src: a.href,
|
||||
format: src[i + 1].args[0]
|
||||
};
|
||||
formats.push(font);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
// TODO select correct format for browser),
|
||||
|
||||
formats: formats.filter(font => /^woff/i.test(font.format)),
|
||||
fontFace: rule.style
|
||||
};
|
||||
})
|
||||
.filter(font => font.formats.length);
|
||||
};
|
||||
|
||||
const createStyleSheetFontsFromText = (text: string, baseHref: string): Array<FontFamily> => {
|
||||
const doc = document.implementation.createHTMLDocument('');
|
||||
const base = document.createElement('base');
|
||||
// $FlowFixMe
|
||||
base.href = baseHref;
|
||||
const style = document.createElement('style');
|
||||
|
||||
style.textContent = text;
|
||||
if (doc.head) {
|
||||
doc.head.appendChild(base);
|
||||
}
|
||||
if (doc.body) {
|
||||
doc.body.appendChild(style);
|
||||
}
|
||||
|
||||
return style.sheet ? getSheetFonts(style.sheet, doc) : [];
|
||||
};
|
||||
|
||||
const restoreOwnerScroll = (ownerDocument: Document, x: number, y: number) => {
|
||||
if (
|
||||
ownerDocument.defaultView &&
|
||||
(x !== ownerDocument.defaultView.pageXOffset || y !== ownerDocument.defaultView.pageYOffset)
|
||||
) {
|
||||
ownerDocument.defaultView.scrollTo(x, y);
|
||||
}
|
||||
};
|
||||
|
||||
const cloneCanvasContents = (canvas: HTMLCanvasElement, clonedCanvas: HTMLCanvasElement) => {
|
||||
try {
|
||||
if (clonedCanvas) {
|
||||
clonedCanvas.width = canvas.width;
|
||||
clonedCanvas.height = canvas.height;
|
||||
clonedCanvas
|
||||
.getContext('2d')
|
||||
.putImageData(
|
||||
canvas.getContext('2d').getImageData(0, 0, canvas.width, canvas.height),
|
||||
0,
|
||||
0
|
||||
);
|
||||
}
|
||||
} catch (e) {}
|
||||
};
|
||||
|
||||
const inlinePseudoElement = (
|
||||
node: HTMLElement,
|
||||
clone: HTMLElement,
|
||||
pseudoElt: ':before' | ':after'
|
||||
): ?HTMLElement => {
|
||||
const style = node.ownerDocument.defaultView.getComputedStyle(node, pseudoElt);
|
||||
if (
|
||||
!style ||
|
||||
!style.content ||
|
||||
style.content === 'none' ||
|
||||
style.content === '-moz-alt-content' ||
|
||||
style.display === 'none'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const content = stripQuotes(style.content);
|
||||
const image = content.match(URL_REGEXP);
|
||||
const anonymousReplacedElement = clone.ownerDocument.createElement(
|
||||
image ? 'img' : 'html2canvaspseudoelement'
|
||||
);
|
||||
if (image) {
|
||||
// $FlowFixMe
|
||||
anonymousReplacedElement.src = stripQuotes(image[1]);
|
||||
} else {
|
||||
anonymousReplacedElement.textContent = content;
|
||||
}
|
||||
|
||||
copyCSSStyles(style, anonymousReplacedElement);
|
||||
|
||||
anonymousReplacedElement.className = `${PSEUDO_HIDE_ELEMENT_CLASS_BEFORE} ${PSEUDO_HIDE_ELEMENT_CLASS_AFTER}`;
|
||||
clone.className +=
|
||||
pseudoElt === PSEUDO_BEFORE
|
||||
? ` ${PSEUDO_HIDE_ELEMENT_CLASS_BEFORE}`
|
||||
: ` ${PSEUDO_HIDE_ELEMENT_CLASS_AFTER}`;
|
||||
if (pseudoElt === PSEUDO_BEFORE) {
|
||||
clone.insertBefore(anonymousReplacedElement, clone.firstChild);
|
||||
} else {
|
||||
clone.appendChild(anonymousReplacedElement);
|
||||
}
|
||||
|
||||
return anonymousReplacedElement;
|
||||
};
|
||||
|
||||
const stripQuotes = (content: string): string => {
|
||||
const first = content.substr(0, 1);
|
||||
return first === content.substr(content.length - 1) && first.match(/['"]/)
|
||||
? content.substr(1, content.length - 2)
|
||||
: content;
|
||||
};
|
||||
|
||||
const URL_REGEXP = /^url\((.+)\)$/i;
|
||||
const PSEUDO_BEFORE = ':before';
|
||||
const PSEUDO_AFTER = ':after';
|
||||
const PSEUDO_HIDE_ELEMENT_CLASS_BEFORE = '___html2canvas___pseudoelement_before';
|
||||
const PSEUDO_HIDE_ELEMENT_CLASS_AFTER = '___html2canvas___pseudoelement_after';
|
||||
|
||||
const PSEUDO_HIDE_ELEMENT_STYLE = `{
|
||||
content: "" !important;
|
||||
display: none !important;
|
||||
}`;
|
||||
|
||||
const createPseudoHideStyles = (body: HTMLElement) => {
|
||||
createStyles(
|
||||
body,
|
||||
`.${PSEUDO_HIDE_ELEMENT_CLASS_BEFORE}${PSEUDO_BEFORE}${PSEUDO_HIDE_ELEMENT_STYLE}
|
||||
.${PSEUDO_HIDE_ELEMENT_CLASS_AFTER}${PSEUDO_AFTER}${PSEUDO_HIDE_ELEMENT_STYLE}`
|
||||
);
|
||||
};
|
||||
|
||||
const createStyles = (body: HTMLElement, styles) => {
|
||||
const style = body.ownerDocument.createElement('style');
|
||||
style.innerHTML = styles;
|
||||
body.appendChild(style);
|
||||
};
|
||||
|
||||
const initNode = ([element, x, y]: [HTMLElement, number, number]) => {
|
||||
element.scrollLeft = x;
|
||||
element.scrollTop = y;
|
||||
};
|
||||
|
||||
const generateIframeKey = (): string =>
|
||||
Math.ceil(Date.now() + Math.random() * 10000000).toString(16);
|
||||
|
||||
const DATA_URI_REGEXP = /^data:text\/(.+);(base64)?,(.*)$/i;
|
||||
|
||||
const getIframeDocumentElement = (
|
||||
node: HTMLIFrameElement,
|
||||
options: Options
|
||||
): Promise<HTMLElement> => {
|
||||
try {
|
||||
return Promise.resolve(node.contentWindow.document.documentElement);
|
||||
} catch (e) {
|
||||
return options.proxy
|
||||
? Proxy(node.src, options)
|
||||
.then(html => {
|
||||
const match = html.match(DATA_URI_REGEXP);
|
||||
if (!match) {
|
||||
return Promise.reject();
|
||||
}
|
||||
|
||||
return match[2] === 'base64'
|
||||
? window.atob(decodeURIComponent(match[3]))
|
||||
: decodeURIComponent(match[3]);
|
||||
})
|
||||
.then(html =>
|
||||
createIframeContainer(
|
||||
node.ownerDocument,
|
||||
parseBounds(node, 0, 0)
|
||||
).then(cloneIframeContainer => {
|
||||
const cloneWindow = cloneIframeContainer.contentWindow;
|
||||
const documentClone = cloneWindow.document;
|
||||
|
||||
documentClone.open();
|
||||
documentClone.write(html);
|
||||
const iframeLoad = iframeLoader(cloneIframeContainer).then(
|
||||
() => documentClone.documentElement
|
||||
);
|
||||
|
||||
documentClone.close();
|
||||
return iframeLoad;
|
||||
})
|
||||
)
|
||||
: Promise.reject();
|
||||
}
|
||||
};
|
||||
|
||||
const createIframeContainer = (
|
||||
ownerDocument: Document,
|
||||
bounds: Bounds
|
||||
): Promise<HTMLIFrameElement> => {
|
||||
const cloneIframeContainer = ownerDocument.createElement('iframe');
|
||||
|
||||
cloneIframeContainer.className = 'html2canvas-container';
|
||||
cloneIframeContainer.style.visibility = 'hidden';
|
||||
cloneIframeContainer.style.position = 'fixed';
|
||||
cloneIframeContainer.style.left = '-10000px';
|
||||
cloneIframeContainer.style.top = '0px';
|
||||
cloneIframeContainer.style.border = '0';
|
||||
cloneIframeContainer.width = bounds.width.toString();
|
||||
cloneIframeContainer.height = bounds.height.toString();
|
||||
cloneIframeContainer.scrolling = 'no'; // ios won't scroll without it
|
||||
cloneIframeContainer.setAttribute(IGNORE_ATTRIBUTE, 'true');
|
||||
if (!ownerDocument.body) {
|
||||
return Promise.reject(
|
||||
__DEV__ ? `Body element not found in Document that is getting rendered` : ''
|
||||
);
|
||||
}
|
||||
|
||||
ownerDocument.body.appendChild(cloneIframeContainer);
|
||||
|
||||
return Promise.resolve(cloneIframeContainer);
|
||||
};
|
||||
|
||||
const iframeLoader = (cloneIframeContainer: HTMLIFrameElement): Promise<HTMLIFrameElement> => {
|
||||
const cloneWindow = cloneIframeContainer.contentWindow;
|
||||
const documentClone = cloneWindow.document;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
cloneWindow.onload = cloneIframeContainer.onload = documentClone.onreadystatechange = () => {
|
||||
const interval = setInterval(() => {
|
||||
if (
|
||||
documentClone.body.childNodes.length > 0 &&
|
||||
documentClone.readyState === 'complete'
|
||||
) {
|
||||
clearInterval(interval);
|
||||
resolve(cloneIframeContainer);
|
||||
}
|
||||
}, 50);
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
export const cloneWindow = (
|
||||
ownerDocument: Document,
|
||||
bounds: Bounds,
|
||||
referenceElement: HTMLElement,
|
||||
options: Options,
|
||||
logger: Logger,
|
||||
renderer: (element: HTMLElement, options: Options, logger: Logger) => Promise<*>
|
||||
): Promise<[HTMLIFrameElement, HTMLElement, ResourceLoader]> => {
|
||||
const cloner = new DocumentCloner(referenceElement, options, logger, false, renderer);
|
||||
const scrollX = ownerDocument.defaultView.pageXOffset;
|
||||
const scrollY = ownerDocument.defaultView.pageYOffset;
|
||||
|
||||
return createIframeContainer(ownerDocument, bounds).then(cloneIframeContainer => {
|
||||
const cloneWindow = cloneIframeContainer.contentWindow;
|
||||
const documentClone = cloneWindow.document;
|
||||
|
||||
/* Chrome doesn't detect relative background-images assigned in inline <style> sheets when fetched through getComputedStyle
|
||||
if window url is about:blank, we can assign the url to current by writing onto the document
|
||||
*/
|
||||
|
||||
const iframeLoad = iframeLoader(cloneIframeContainer).then(() => {
|
||||
cloner.scrolledElements.forEach(initNode);
|
||||
cloneWindow.scrollTo(bounds.left, bounds.top);
|
||||
if (
|
||||
/(iPad|iPhone|iPod)/g.test(navigator.userAgent) &&
|
||||
(cloneWindow.scrollY !== bounds.top || cloneWindow.scrollX !== bounds.left)
|
||||
) {
|
||||
documentClone.documentElement.style.top = -bounds.top + 'px';
|
||||
documentClone.documentElement.style.left = -bounds.left + 'px';
|
||||
documentClone.documentElement.style.position = 'absolute';
|
||||
}
|
||||
return cloner.clonedReferenceElement instanceof cloneWindow.HTMLElement ||
|
||||
cloner.clonedReferenceElement instanceof ownerDocument.defaultView.HTMLElement ||
|
||||
cloner.clonedReferenceElement instanceof HTMLElement
|
||||
? Promise.resolve([
|
||||
cloneIframeContainer,
|
||||
cloner.clonedReferenceElement,
|
||||
cloner.resourceLoader
|
||||
])
|
||||
: Promise.reject(
|
||||
__DEV__
|
||||
? `Error finding the ${referenceElement.nodeName} in the cloned document`
|
||||
: ''
|
||||
);
|
||||
});
|
||||
|
||||
documentClone.open();
|
||||
documentClone.write('<!DOCTYPE html><html></html>');
|
||||
// Chrome scrolls the parent document for some reason after the write to the cloned window???
|
||||
restoreOwnerScroll(referenceElement.ownerDocument, scrollX, scrollY);
|
||||
documentClone.replaceChild(
|
||||
documentClone.adoptNode(cloner.documentElement),
|
||||
documentClone.documentElement
|
||||
);
|
||||
documentClone.close();
|
||||
|
||||
return iframeLoad;
|
||||
});
|
||||
};
|
251
src/Color.js
Normal file
251
src/Color.js
Normal file
@ -0,0 +1,251 @@
|
||||
/* @flow */
|
||||
'use strict';
|
||||
|
||||
// http://dev.w3.org/csswg/css-color/
|
||||
|
||||
type ColorArray = [number, number, number, number | null];
|
||||
|
||||
const HEX3 = /^#([a-f0-9]{3})$/i;
|
||||
const hex3 = (value: string): ColorArray | false => {
|
||||
const match = value.match(HEX3);
|
||||
if (match) {
|
||||
return [
|
||||
parseInt(match[1][0] + match[1][0], 16),
|
||||
parseInt(match[1][1] + match[1][1], 16),
|
||||
parseInt(match[1][2] + match[1][2], 16),
|
||||
null
|
||||
];
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const HEX6 = /^#([a-f0-9]{6})$/i;
|
||||
const hex6 = (value: string): ColorArray | false => {
|
||||
const match = value.match(HEX6);
|
||||
if (match) {
|
||||
return [
|
||||
parseInt(match[1].substring(0, 2), 16),
|
||||
parseInt(match[1].substring(2, 4), 16),
|
||||
parseInt(match[1].substring(4, 6), 16),
|
||||
null
|
||||
];
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const RGB = /^rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)$/;
|
||||
const rgb = (value: string): ColorArray | false => {
|
||||
const match = value.match(RGB);
|
||||
if (match) {
|
||||
return [Number(match[1]), Number(match[2]), Number(match[3]), null];
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const RGBA = /^rgba\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d?\.?\d+)\s*\)$/;
|
||||
const rgba = (value: string): ColorArray | false => {
|
||||
const match = value.match(RGBA);
|
||||
if (match && match.length > 4) {
|
||||
return [Number(match[1]), Number(match[2]), Number(match[3]), Number(match[4])];
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const fromArray = (array: Array<number>): ColorArray => {
|
||||
return [
|
||||
Math.min(array[0], 255),
|
||||
Math.min(array[1], 255),
|
||||
Math.min(array[2], 255),
|
||||
array.length > 3 ? array[3] : null
|
||||
];
|
||||
};
|
||||
|
||||
const namedColor = (name: string): ColorArray | false => {
|
||||
const color: ColorArray | void = NAMED_COLORS[name.toLowerCase()];
|
||||
return color ? color : false;
|
||||
};
|
||||
|
||||
export default class Color {
|
||||
r: number;
|
||||
g: number;
|
||||
b: number;
|
||||
a: number | null;
|
||||
|
||||
constructor(value: string | Array<number>) {
|
||||
const [r, g, b, a] = Array.isArray(value)
|
||||
? fromArray(value)
|
||||
: hex3(value) ||
|
||||
rgb(value) ||
|
||||
rgba(value) ||
|
||||
namedColor(value) ||
|
||||
hex6(value) || [0, 0, 0, null];
|
||||
this.r = r;
|
||||
this.g = g;
|
||||
this.b = b;
|
||||
this.a = a;
|
||||
}
|
||||
|
||||
isTransparent(): boolean {
|
||||
return this.a === 0;
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return this.a !== null && this.a !== 1
|
||||
? `rgba(${this.r},${this.g},${this.b},${this.a})`
|
||||
: `rgb(${this.r},${this.g},${this.b})`;
|
||||
}
|
||||
}
|
||||
|
||||
const NAMED_COLORS = {
|
||||
transparent: [0, 0, 0, 0],
|
||||
aliceblue: [240, 248, 255, null],
|
||||
antiquewhite: [250, 235, 215, null],
|
||||
aqua: [0, 255, 255, null],
|
||||
aquamarine: [127, 255, 212, null],
|
||||
azure: [240, 255, 255, null],
|
||||
beige: [245, 245, 220, null],
|
||||
bisque: [255, 228, 196, null],
|
||||
black: [0, 0, 0, null],
|
||||
blanchedalmond: [255, 235, 205, null],
|
||||
blue: [0, 0, 255, null],
|
||||
blueviolet: [138, 43, 226, null],
|
||||
brown: [165, 42, 42, null],
|
||||
burlywood: [222, 184, 135, null],
|
||||
cadetblue: [95, 158, 160, null],
|
||||
chartreuse: [127, 255, 0, null],
|
||||
chocolate: [210, 105, 30, null],
|
||||
coral: [255, 127, 80, null],
|
||||
cornflowerblue: [100, 149, 237, null],
|
||||
cornsilk: [255, 248, 220, null],
|
||||
crimson: [220, 20, 60, null],
|
||||
cyan: [0, 255, 255, null],
|
||||
darkblue: [0, 0, 139, null],
|
||||
darkcyan: [0, 139, 139, null],
|
||||
darkgoldenrod: [184, 134, 11, null],
|
||||
darkgray: [169, 169, 169, null],
|
||||
darkgreen: [0, 100, 0, null],
|
||||
darkgrey: [169, 169, 169, null],
|
||||
darkkhaki: [189, 183, 107, null],
|
||||
darkmagenta: [139, 0, 139, null],
|
||||
darkolivegreen: [85, 107, 47, null],
|
||||
darkorange: [255, 140, 0, null],
|
||||
darkorchid: [153, 50, 204, null],
|
||||
darkred: [139, 0, 0, null],
|
||||
darksalmon: [233, 150, 122, null],
|
||||
darkseagreen: [143, 188, 143, null],
|
||||
darkslateblue: [72, 61, 139, null],
|
||||
darkslategray: [47, 79, 79, null],
|
||||
darkslategrey: [47, 79, 79, null],
|
||||
darkturquoise: [0, 206, 209, null],
|
||||
darkviolet: [148, 0, 211, null],
|
||||
deeppink: [255, 20, 147, null],
|
||||
deepskyblue: [0, 191, 255, null],
|
||||
dimgray: [105, 105, 105, null],
|
||||
dimgrey: [105, 105, 105, null],
|
||||
dodgerblue: [30, 144, 255, null],
|
||||
firebrick: [178, 34, 34, null],
|
||||
floralwhite: [255, 250, 240, null],
|
||||
forestgreen: [34, 139, 34, null],
|
||||
fuchsia: [255, 0, 255, null],
|
||||
gainsboro: [220, 220, 220, null],
|
||||
ghostwhite: [248, 248, 255, null],
|
||||
gold: [255, 215, 0, null],
|
||||
goldenrod: [218, 165, 32, null],
|
||||
gray: [128, 128, 128, null],
|
||||
green: [0, 128, 0, null],
|
||||
greenyellow: [173, 255, 47, null],
|
||||
grey: [128, 128, 128, null],
|
||||
honeydew: [240, 255, 240, null],
|
||||
hotpink: [255, 105, 180, null],
|
||||
indianred: [205, 92, 92, null],
|
||||
indigo: [75, 0, 130, null],
|
||||
ivory: [255, 255, 240, null],
|
||||
khaki: [240, 230, 140, null],
|
||||
lavender: [230, 230, 250, null],
|
||||
lavenderblush: [255, 240, 245, null],
|
||||
lawngreen: [124, 252, 0, null],
|
||||
lemonchiffon: [255, 250, 205, null],
|
||||
lightblue: [173, 216, 230, null],
|
||||
lightcoral: [240, 128, 128, null],
|
||||
lightcyan: [224, 255, 255, null],
|
||||
lightgoldenrodyellow: [250, 250, 210, null],
|
||||
lightgray: [211, 211, 211, null],
|
||||
lightgreen: [144, 238, 144, null],
|
||||
lightgrey: [211, 211, 211, null],
|
||||
lightpink: [255, 182, 193, null],
|
||||
lightsalmon: [255, 160, 122, null],
|
||||
lightseagreen: [32, 178, 170, null],
|
||||
lightskyblue: [135, 206, 250, null],
|
||||
lightslategray: [119, 136, 153, null],
|
||||
lightslategrey: [119, 136, 153, null],
|
||||
lightsteelblue: [176, 196, 222, null],
|
||||
lightyellow: [255, 255, 224, null],
|
||||
lime: [0, 255, 0, null],
|
||||
limegreen: [50, 205, 50, null],
|
||||
linen: [250, 240, 230, null],
|
||||
magenta: [255, 0, 255, null],
|
||||
maroon: [128, 0, 0, null],
|
||||
mediumaquamarine: [102, 205, 170, null],
|
||||
mediumblue: [0, 0, 205, null],
|
||||
mediumorchid: [186, 85, 211, null],
|
||||
mediumpurple: [147, 112, 219, null],
|
||||
mediumseagreen: [60, 179, 113, null],
|
||||
mediumslateblue: [123, 104, 238, null],
|
||||
mediumspringgreen: [0, 250, 154, null],
|
||||
mediumturquoise: [72, 209, 204, null],
|
||||
mediumvioletred: [199, 21, 133, null],
|
||||
midnightblue: [25, 25, 112, null],
|
||||
mintcream: [245, 255, 250, null],
|
||||
mistyrose: [255, 228, 225, null],
|
||||
moccasin: [255, 228, 181, null],
|
||||
navajowhite: [255, 222, 173, null],
|
||||
navy: [0, 0, 128, null],
|
||||
oldlace: [253, 245, 230, null],
|
||||
olive: [128, 128, 0, null],
|
||||
olivedrab: [107, 142, 35, null],
|
||||
orange: [255, 165, 0, null],
|
||||
orangered: [255, 69, 0, null],
|
||||
orchid: [218, 112, 214, null],
|
||||
palegoldenrod: [238, 232, 170, null],
|
||||
palegreen: [152, 251, 152, null],
|
||||
paleturquoise: [175, 238, 238, null],
|
||||
palevioletred: [219, 112, 147, null],
|
||||
papayawhip: [255, 239, 213, null],
|
||||
peachpuff: [255, 218, 185, null],
|
||||
peru: [205, 133, 63, null],
|
||||
pink: [255, 192, 203, null],
|
||||
plum: [221, 160, 221, null],
|
||||
powderblue: [176, 224, 230, null],
|
||||
purple: [128, 0, 128, null],
|
||||
rebeccapurple: [102, 51, 153, null],
|
||||
red: [255, 0, 0, null],
|
||||
rosybrown: [188, 143, 143, null],
|
||||
royalblue: [65, 105, 225, null],
|
||||
saddlebrown: [139, 69, 19, null],
|
||||
salmon: [250, 128, 114, null],
|
||||
sandybrown: [244, 164, 96, null],
|
||||
seagreen: [46, 139, 87, null],
|
||||
seashell: [255, 245, 238, null],
|
||||
sienna: [160, 82, 45, null],
|
||||
silver: [192, 192, 192, null],
|
||||
skyblue: [135, 206, 235, null],
|
||||
slateblue: [106, 90, 205, null],
|
||||
slategray: [112, 128, 144, null],
|
||||
slategrey: [112, 128, 144, null],
|
||||
snow: [255, 250, 250, null],
|
||||
springgreen: [0, 255, 127, null],
|
||||
steelblue: [70, 130, 180, null],
|
||||
tan: [210, 180, 140, null],
|
||||
teal: [0, 128, 128, null],
|
||||
thistle: [216, 191, 216, null],
|
||||
tomato: [255, 99, 71, null],
|
||||
turquoise: [64, 224, 208, null],
|
||||
violet: [238, 130, 238, null],
|
||||
wheat: [245, 222, 179, null],
|
||||
white: [255, 255, 255, null],
|
||||
whitesmoke: [245, 245, 245, null],
|
||||
yellow: [255, 255, 0, null],
|
||||
yellowgreen: [154, 205, 50, null]
|
||||
};
|
||||
|
||||
export const TRANSPARENT = new Color([0, 0, 0, 0]);
|
278
src/Core.js
278
src/Core.js
@ -1,278 +0,0 @@
|
||||
/*
|
||||
html2canvas @VERSION@ <http://html2canvas.hertzen.com>
|
||||
Copyright (c) 2011 Niklas von Hertzen. All rights reserved.
|
||||
http://www.twitter.com/niklasvh
|
||||
|
||||
Released under MIT License
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
var _html2canvas = {},
|
||||
previousElement,
|
||||
computedCSS,
|
||||
html2canvas;
|
||||
|
||||
|
||||
function h2clog(a) {
|
||||
if (_html2canvas.logging && window.console && window.console.log) {
|
||||
window.console.log(a);
|
||||
}
|
||||
}
|
||||
|
||||
_html2canvas.Util = {};
|
||||
|
||||
_html2canvas.Util.backgroundImage = function (src) {
|
||||
|
||||
if (/data:image\/.*;base64,/i.test( src ) || /^(-webkit|-moz|linear-gradient|-o-)/.test( src )) {
|
||||
return src;
|
||||
}
|
||||
|
||||
if (src.toLowerCase().substr( 0, 5 ) === 'url("') {
|
||||
src = src.substr( 5 );
|
||||
src = src.substr( 0, src.length - 2 );
|
||||
} else {
|
||||
src = src.substr( 4 );
|
||||
src = src.substr( 0, src.length - 1 );
|
||||
}
|
||||
|
||||
return src;
|
||||
};
|
||||
|
||||
_html2canvas.Util.Bounds = function getBounds (el) {
|
||||
var clientRect,
|
||||
bounds = {};
|
||||
|
||||
if (el.getBoundingClientRect){
|
||||
clientRect = el.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;
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
_html2canvas.Util.getCSS = function (el, attribute) {
|
||||
// return $(el).css(attribute);
|
||||
|
||||
var val;
|
||||
|
||||
function toPX( attribute, val ) {
|
||||
var rsLeft = el.runtimeStyle && el.runtimeStyle[ attribute ],
|
||||
left,
|
||||
style = el.style;
|
||||
|
||||
// Check if we are not dealing with pixels, (Opera has issues with this)
|
||||
// Ported from jQuery css.js
|
||||
// From the awesome hack by Dean Edwards
|
||||
// http://erik.eae.net/archives/2007/07/27/18.54.15/#comment-102291
|
||||
|
||||
// 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 ) ) {
|
||||
|
||||
// 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 ( window.getComputedStyle ) {
|
||||
if ( previousElement !== el ) {
|
||||
computedCSS = document.defaultView.getComputedStyle(el, null);
|
||||
}
|
||||
val = computedCSS[ attribute ];
|
||||
|
||||
if ( attribute === "backgroundPosition" ) {
|
||||
|
||||
val = (val.split(",")[0] || "0 0").split(" ");
|
||||
|
||||
val[ 0 ] = ( val[0].indexOf( "%" ) === -1 ) ? toPX( attribute + "X", val[ 0 ] ) : val[ 0 ];
|
||||
val[ 1 ] = ( val[1] === undefined ) ? val[0] : val[1]; // IE 9 doesn't return double digit always
|
||||
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;
|
||||
}
|
||||
|
||||
} else if ( el.currentStyle ) {
|
||||
// IE 9>
|
||||
if (attribute === "backgroundPosition") {
|
||||
// Older IE uses -x and -y
|
||||
val = [ toPX( attribute + "X", el.currentStyle[ attribute + "X" ] ), toPX( attribute + "Y", el.currentStyle[ attribute + "Y" ] ) ];
|
||||
} else {
|
||||
|
||||
val = toPX( attribute, el.currentStyle[ attribute ] );
|
||||
|
||||
if (/^(border)/i.test( attribute ) && /^(medium|thin|thick)$/i.test( val )) {
|
||||
switch (val) {
|
||||
case "thin":
|
||||
val = "1px";
|
||||
break;
|
||||
case "medium":
|
||||
val = "0px"; // this is wrong, it should be 3px but IE uses medium for no border as well.. TODO find a work around
|
||||
break;
|
||||
case "thick":
|
||||
val = "5px";
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
return val;
|
||||
|
||||
|
||||
|
||||
//return $(el).css(attribute);
|
||||
|
||||
|
||||
};
|
||||
|
||||
|
||||
_html2canvas.Util.BackgroundPosition = function ( el, bounds, image ) {
|
||||
// TODO add support for multi image backgrounds
|
||||
|
||||
var bgposition = _html2canvas.Util.getCSS( el, "backgroundPosition" ) ,
|
||||
topPos,
|
||||
left,
|
||||
percentage,
|
||||
val;
|
||||
|
||||
if (bgposition.length === 1){
|
||||
val = bgposition;
|
||||
|
||||
bgposition = [];
|
||||
|
||||
bgposition[0] = val;
|
||||
bgposition[1] = val;
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (bgposition[0].toString().indexOf("%") !== -1){
|
||||
percentage = (parseFloat(bgposition[0])/100);
|
||||
left = ((bounds.width * percentage)-(image.width*percentage));
|
||||
|
||||
}else{
|
||||
left = parseInt(bgposition[0],10);
|
||||
}
|
||||
|
||||
if (bgposition[1].toString().indexOf("%") !== -1){
|
||||
|
||||
percentage = (parseFloat(bgposition[1])/100);
|
||||
topPos = ((bounds.height * percentage)-(image.height*percentage));
|
||||
}else{
|
||||
topPos = parseInt(bgposition[1],10);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
return {
|
||||
top: topPos,
|
||||
left: left
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
_html2canvas.Util.Extend = function (options, defaults) {
|
||||
for (var key in options) {
|
||||
if (options.hasOwnProperty(key)) {
|
||||
defaults[key] = options[key];
|
||||
}
|
||||
}
|
||||
return defaults;
|
||||
};
|
||||
|
||||
|
||||
/*
|
||||
* Derived from jQuery.contents()
|
||||
* Copyright 2010, John Resig
|
||||
* Dual licensed under the MIT or GPL Version 2 licenses.
|
||||
* 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 ){
|
||||
var ret = [];
|
||||
|
||||
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 ];
|
||||
}
|
||||
|
||||
} else {
|
||||
while ( second[j] !== undefined ) {
|
||||
first[ i++ ] = second[ j++ ];
|
||||
}
|
||||
}
|
||||
|
||||
first.length = i;
|
||||
|
||||
return first;
|
||||
})( ret, array );
|
||||
|
||||
}
|
||||
|
||||
return ret;
|
||||
})( elem.childNodes );
|
||||
|
||||
} catch (ex) {
|
||||
h2clog("html2canvas.Util.Children failed with exception: " + ex.message);
|
||||
children = [];
|
||||
}
|
||||
return children;
|
||||
};
|
179
src/Feature.js
Normal file
179
src/Feature.js
Normal file
@ -0,0 +1,179 @@
|
||||
/* @flow */
|
||||
'use strict';
|
||||
|
||||
import {createForeignObjectSVG, loadSerializedSVG} from './renderer/ForeignObjectRenderer';
|
||||
|
||||
const testRangeBounds = document => {
|
||||
const TEST_HEIGHT = 123;
|
||||
|
||||
if (document.createRange) {
|
||||
const range = document.createRange();
|
||||
if (range.getBoundingClientRect) {
|
||||
const testElement = document.createElement('boundtest');
|
||||
testElement.style.height = `${TEST_HEIGHT}px`;
|
||||
testElement.style.display = 'block';
|
||||
document.body.appendChild(testElement);
|
||||
|
||||
range.selectNode(testElement);
|
||||
const rangeBounds = range.getBoundingClientRect();
|
||||
const rangeHeight = Math.round(rangeBounds.height);
|
||||
document.body.removeChild(testElement);
|
||||
if (rangeHeight === TEST_HEIGHT) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
// iOS 10.3 taints canvas with base64 images unless crossOrigin = 'anonymous'
|
||||
const testBase64 = (document: Document, src: string): Promise<boolean> => {
|
||||
const img = new Image();
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
return new Promise(resolve => {
|
||||
// Single pixel base64 image renders fine on iOS 10.3???
|
||||
img.src = src;
|
||||
|
||||
const onload = () => {
|
||||
try {
|
||||
ctx.drawImage(img, 0, 0);
|
||||
canvas.toDataURL();
|
||||
} catch (e) {
|
||||
return resolve(false);
|
||||
}
|
||||
|
||||
return resolve(true);
|
||||
};
|
||||
|
||||
img.onload = onload;
|
||||
img.onerror = () => resolve(false);
|
||||
|
||||
if (img.complete === true) {
|
||||
setTimeout(() => {
|
||||
onload();
|
||||
}, 500);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const testCORS = () => typeof new Image().crossOrigin !== 'undefined';
|
||||
|
||||
const testResponseType = () => typeof new XMLHttpRequest().responseType === 'string';
|
||||
|
||||
const testSVG = document => {
|
||||
const img = new Image();
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
img.src = `data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg'></svg>`;
|
||||
|
||||
try {
|
||||
ctx.drawImage(img, 0, 0);
|
||||
canvas.toDataURL();
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const isGreenPixel = data => data[0] === 0 && data[1] === 255 && data[2] === 0 && data[3] === 255;
|
||||
|
||||
const testForeignObject = document => {
|
||||
const canvas = document.createElement('canvas');
|
||||
const size = 100;
|
||||
canvas.width = size;
|
||||
canvas.height = size;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.fillStyle = 'rgb(0, 255, 0)';
|
||||
ctx.fillRect(0, 0, size, size);
|
||||
|
||||
const img = new Image();
|
||||
const greenImageSrc = canvas.toDataURL();
|
||||
img.src = greenImageSrc;
|
||||
const svg = createForeignObjectSVG(size, size, 0, 0, img);
|
||||
ctx.fillStyle = 'red';
|
||||
ctx.fillRect(0, 0, size, size);
|
||||
|
||||
return loadSerializedSVG(svg)
|
||||
.then(img => {
|
||||
ctx.drawImage(img, 0, 0);
|
||||
const data = ctx.getImageData(0, 0, size, size).data;
|
||||
ctx.fillStyle = 'red';
|
||||
ctx.fillRect(0, 0, size, size);
|
||||
|
||||
const node = document.createElement('div');
|
||||
node.style.backgroundImage = `url(${greenImageSrc})`;
|
||||
node.style.height = `${size}px`;
|
||||
// Firefox 55 does not render inline <img /> tags
|
||||
return isGreenPixel(data)
|
||||
? loadSerializedSVG(createForeignObjectSVG(size, size, 0, 0, node))
|
||||
: Promise.reject(false);
|
||||
})
|
||||
.then(img => {
|
||||
ctx.drawImage(img, 0, 0);
|
||||
// Edge does not render background-images
|
||||
return isGreenPixel(ctx.getImageData(0, 0, size, size).data);
|
||||
})
|
||||
.catch(e => false);
|
||||
};
|
||||
|
||||
const FEATURES = {
|
||||
// $FlowFixMe - get/set properties not yet supported
|
||||
get SUPPORT_RANGE_BOUNDS() {
|
||||
'use strict';
|
||||
const value = testRangeBounds(document);
|
||||
Object.defineProperty(FEATURES, 'SUPPORT_RANGE_BOUNDS', {value});
|
||||
return value;
|
||||
},
|
||||
// $FlowFixMe - get/set properties not yet supported
|
||||
get SUPPORT_SVG_DRAWING() {
|
||||
'use strict';
|
||||
const value = testSVG(document);
|
||||
Object.defineProperty(FEATURES, 'SUPPORT_SVG_DRAWING', {value});
|
||||
return value;
|
||||
},
|
||||
// $FlowFixMe - get/set properties not yet supported
|
||||
get SUPPORT_BASE64_DRAWING() {
|
||||
'use strict';
|
||||
return (src: string) => {
|
||||
const value = testBase64(document, src);
|
||||
Object.defineProperty(FEATURES, 'SUPPORT_BASE64_DRAWING', {value: () => value});
|
||||
return value;
|
||||
};
|
||||
},
|
||||
// $FlowFixMe - get/set properties not yet supported
|
||||
get SUPPORT_FOREIGNOBJECT_DRAWING() {
|
||||
'use strict';
|
||||
const value =
|
||||
typeof Array.from === 'function' && typeof window.fetch === 'function'
|
||||
? testForeignObject(document)
|
||||
: Promise.resolve(false);
|
||||
Object.defineProperty(FEATURES, 'SUPPORT_FOREIGNOBJECT_DRAWING', {value});
|
||||
return value;
|
||||
},
|
||||
// $FlowFixMe - get/set properties not yet supported
|
||||
get SUPPORT_CORS_IMAGES() {
|
||||
'use strict';
|
||||
const value = testCORS();
|
||||
Object.defineProperty(FEATURES, 'SUPPORT_CORS_IMAGES', {value});
|
||||
return value;
|
||||
},
|
||||
// $FlowFixMe - get/set properties not yet supported
|
||||
get SUPPORT_RESPONSE_TYPE() {
|
||||
'use strict';
|
||||
const value = testResponseType();
|
||||
Object.defineProperty(FEATURES, 'SUPPORT_RESPONSE_TYPE', {value});
|
||||
return value;
|
||||
},
|
||||
// $FlowFixMe - get/set properties not yet supported
|
||||
get SUPPORT_CORS_XHR() {
|
||||
'use strict';
|
||||
const value = 'withCredentials' in new XMLHttpRequest();
|
||||
Object.defineProperty(FEATURES, 'SUPPORT_CORS_XHR', {value});
|
||||
return value;
|
||||
}
|
||||
};
|
||||
|
||||
export default FEATURES;
|
73
src/Font.js
Normal file
73
src/Font.js
Normal file
@ -0,0 +1,73 @@
|
||||
/* @flow */
|
||||
'use strict';
|
||||
|
||||
import type {Font} from './parsing/font';
|
||||
|
||||
const SAMPLE_TEXT = 'Hidden Text';
|
||||
import {SMALL_IMAGE} from './Util';
|
||||
|
||||
export class FontMetrics {
|
||||
_data: {};
|
||||
_document: Document;
|
||||
|
||||
constructor(document: Document) {
|
||||
this._data = {};
|
||||
this._document = document;
|
||||
}
|
||||
_parseMetrics(font: Font) {
|
||||
const container = this._document.createElement('div');
|
||||
const img = this._document.createElement('img');
|
||||
const span = this._document.createElement('span');
|
||||
|
||||
const body = this._document.body;
|
||||
if (!body) {
|
||||
throw new Error(__DEV__ ? 'No document found for font metrics' : '');
|
||||
}
|
||||
|
||||
container.style.visibility = 'hidden';
|
||||
container.style.fontFamily = font.fontFamily;
|
||||
container.style.fontSize = font.fontSize;
|
||||
container.style.margin = '0';
|
||||
container.style.padding = '0';
|
||||
|
||||
body.appendChild(container);
|
||||
|
||||
img.src = SMALL_IMAGE;
|
||||
img.width = 1;
|
||||
img.height = 1;
|
||||
|
||||
img.style.margin = '0';
|
||||
img.style.padding = '0';
|
||||
img.style.verticalAlign = 'baseline';
|
||||
|
||||
span.style.fontFamily = font.fontFamily;
|
||||
span.style.fontSize = font.fontSize;
|
||||
span.style.margin = '0';
|
||||
span.style.padding = '0';
|
||||
|
||||
span.appendChild(this._document.createTextNode(SAMPLE_TEXT));
|
||||
container.appendChild(span);
|
||||
container.appendChild(img);
|
||||
const baseline = img.offsetTop - span.offsetTop + 2;
|
||||
|
||||
container.removeChild(span);
|
||||
container.appendChild(this._document.createTextNode(SAMPLE_TEXT));
|
||||
|
||||
container.style.lineHeight = 'normal';
|
||||
img.style.verticalAlign = 'super';
|
||||
|
||||
const middle = img.offsetTop - container.offsetTop + 2;
|
||||
|
||||
body.removeChild(container);
|
||||
|
||||
return {baseline, middle};
|
||||
}
|
||||
getMetrics(font: Font) {
|
||||
const key = `${font.fontFamily} ${font.fontSize}`;
|
||||
if (this._data[key] === undefined) {
|
||||
this._data[key] = this._parseMetrics(font);
|
||||
}
|
||||
|
||||
return this._data[key];
|
||||
}
|
||||
}
|
454
src/Generate.js
454
src/Generate.js
@ -1,454 +0,0 @@
|
||||
/*
|
||||
html2canvas @VERSION@ <http://html2canvas.hertzen.com>
|
||||
Copyright (c) 2011 Niklas von Hertzen. All rights reserved.
|
||||
http://www.twitter.com/niklasvh
|
||||
|
||||
Contributor(s):
|
||||
Niklas von Hertzen <http://www.twitter.com/niklasvh>
|
||||
André Fiedler <http://www.twitter.com/sonnenkiste>
|
||||
|
||||
Released under MIT License
|
||||
*/
|
||||
|
||||
(function(){
|
||||
|
||||
_html2canvas.Generate = {};
|
||||
|
||||
var reGradients = [
|
||||
/^(-webkit-linear-gradient)\(([a-z\s]+)([\w\d\.\s,%\(\)]+)\)$/,
|
||||
/^(-o-linear-gradient)\(([a-z\s]+)([\w\d\.\s,%\(\)]+)\)$/,
|
||||
/^(-webkit-gradient)\((linear|radial),\s((?:\d{1,3}%?)\s(?:\d{1,3}%?),\s(?:\d{1,3}%?)\s(?:\d{1,3}%?))([\w\d\.\s,%\(\)-]+)\)$/,
|
||||
/^(-moz-linear-gradient)\(((?:\d{1,3}%?)\s(?:\d{1,3}%?))([\w\d\.\s,%\(\)]+)\)$/,
|
||||
/^(-webkit-radial-gradient)\(((?:\d{1,3}%?)\s(?:\d{1,3}%?)),\s(\w+)\s([a-z-]+)([\w\d\.\s,%\(\)]+)\)$/,
|
||||
/^(-moz-radial-gradient)\(((?:\d{1,3}%?)\s(?:\d{1,3}%?)),\s(\w+)\s?([a-z-]*)([\w\d\.\s,%\(\)]+)\)$/,
|
||||
/^(-o-radial-gradient)\(((?:\d{1,3}%?)\s(?:\d{1,3}%?)),\s(\w+)\s([a-z-]+)([\w\d\.\s,%\(\)]+)\)$/
|
||||
];
|
||||
|
||||
/*
|
||||
* TODO: Add IE10 vendor prefix (-ms) support
|
||||
* TODO: Add W3C gradient (linear-gradient) support
|
||||
* TODO: Add old Webkit -webkit-gradient(radial, ...) support
|
||||
* TODO: Maybe some RegExp optimizations are possible ;o)
|
||||
*/
|
||||
_html2canvas.Generate.parseGradient = function(css, bounds) {
|
||||
var gradient, i, len = reGradients.length, m1, stop, m2, m2Len, step, m3;
|
||||
|
||||
for(i = 0; i < len; i+=1){
|
||||
m1 = css.match(reGradients[i]);
|
||||
if(m1) break;
|
||||
}
|
||||
|
||||
if(m1) {
|
||||
switch(m1[1]) {
|
||||
case '-webkit-linear-gradient':
|
||||
case '-o-linear-gradient':
|
||||
|
||||
gradient = {
|
||||
type: 'linear',
|
||||
x0: null,
|
||||
y0: null,
|
||||
x1: null,
|
||||
y1: null,
|
||||
colorStops: []
|
||||
};
|
||||
|
||||
// get coordinates
|
||||
m2 = m1[2].match(/\w+/g);
|
||||
if(m2){
|
||||
m2Len = m2.length;
|
||||
for(i = 0; i < m2Len; i+=1){
|
||||
switch(m2[i]) {
|
||||
case 'top':
|
||||
gradient.y0 = 0;
|
||||
gradient.y1 = bounds.height;
|
||||
break;
|
||||
|
||||
case 'right':
|
||||
gradient.x0 = bounds.width;
|
||||
gradient.x1 = 0;
|
||||
break;
|
||||
|
||||
case 'bottom':
|
||||
gradient.y0 = bounds.height;
|
||||
gradient.y1 = 0;
|
||||
break;
|
||||
|
||||
case 'left':
|
||||
gradient.x0 = 0;
|
||||
gradient.x1 = bounds.width;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if(gradient.x0 === null && gradient.x1 === null){ // center
|
||||
gradient.x0 = gradient.x1 = bounds.width / 2;
|
||||
}
|
||||
if(gradient.y0 === null && gradient.y1 === null){ // center
|
||||
gradient.y0 = gradient.y1 = bounds.height / 2;
|
||||
}
|
||||
|
||||
// get colors and stops
|
||||
m2 = m1[3].match(/((?:rgb|rgba)\(\d{1,3},\s\d{1,3},\s\d{1,3}(?:,\s[0-9\.]+)?\)(?:\s\d{1,3}(?:%|px))?)+/g);
|
||||
if(m2){
|
||||
m2Len = m2.length;
|
||||
step = 1 / Math.max(m2Len - 1, 1);
|
||||
for(i = 0; i < m2Len; i+=1){
|
||||
m3 = m2[i].match(/((?:rgb|rgba)\(\d{1,3},\s\d{1,3},\s\d{1,3}(?:,\s[0-9\.]+)?\))\s*(\d{1,3})?(%|px)?/);
|
||||
if(m3[2]){
|
||||
stop = parseFloat(m3[2]);
|
||||
if(m3[3] === '%'){
|
||||
stop /= 100;
|
||||
} else { // px - stupid opera
|
||||
stop /= bounds.width;
|
||||
}
|
||||
} else {
|
||||
stop = i * step;
|
||||
}
|
||||
gradient.colorStops.push({
|
||||
color: m3[1],
|
||||
stop: stop
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case '-webkit-gradient':
|
||||
|
||||
gradient = {
|
||||
type: m1[2] === 'radial' ? 'circle' : m1[2], // TODO: Add radial gradient support for older mozilla definitions
|
||||
x0: 0,
|
||||
y0: 0,
|
||||
x1: 0,
|
||||
y1: 0,
|
||||
colorStops: []
|
||||
};
|
||||
|
||||
// get coordinates
|
||||
m2 = m1[3].match(/(\d{1,3})%?\s(\d{1,3})%?,\s(\d{1,3})%?\s(\d{1,3})%?/);
|
||||
if(m2){
|
||||
gradient.x0 = (m2[1] * bounds.width) / 100;
|
||||
gradient.y0 = (m2[2] * bounds.height) / 100;
|
||||
gradient.x1 = (m2[3] * bounds.width) / 100;
|
||||
gradient.y1 = (m2[4] * bounds.height) / 100;
|
||||
}
|
||||
|
||||
// get colors and stops
|
||||
m2 = m1[4].match(/((?:from|to|color-stop)\((?:[0-9\.]+,\s)?(?:rgb|rgba)\(\d{1,3},\s\d{1,3},\s\d{1,3}(?:,\s[0-9\.]+)?\)\))+/g);
|
||||
if(m2){
|
||||
m2Len = m2.length;
|
||||
for(i = 0; i < m2Len; i+=1){
|
||||
m3 = m2[i].match(/(from|to|color-stop)\(([0-9\.]+)?(?:,\s)?((?:rgb|rgba)\(\d{1,3},\s\d{1,3},\s\d{1,3}(?:,\s[0-9\.]+)?\))\)/);
|
||||
stop = parseFloat(m3[2]);
|
||||
if(m3[1] === 'from') stop = 0.0;
|
||||
if(m3[1] === 'to') stop = 1.0;
|
||||
gradient.colorStops.push({
|
||||
color: m3[3],
|
||||
stop: stop
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case '-moz-linear-gradient':
|
||||
|
||||
gradient = {
|
||||
type: 'linear',
|
||||
x0: 0,
|
||||
y0: 0,
|
||||
x1: 0,
|
||||
y1: 0,
|
||||
colorStops: []
|
||||
};
|
||||
|
||||
// get coordinates
|
||||
m2 = m1[2].match(/(\d{1,3})%?\s(\d{1,3})%?/);
|
||||
|
||||
// m2[1] == 0% -> left
|
||||
// m2[1] == 50% -> center
|
||||
// m2[1] == 100% -> right
|
||||
|
||||
// m2[2] == 0% -> top
|
||||
// m2[2] == 50% -> center
|
||||
// m2[2] == 100% -> bottom
|
||||
|
||||
if(m2){
|
||||
gradient.x0 = (m2[1] * bounds.width) / 100;
|
||||
gradient.y0 = (m2[2] * bounds.height) / 100;
|
||||
gradient.x1 = bounds.width - gradient.x0;
|
||||
gradient.y1 = bounds.height - gradient.y0;
|
||||
}
|
||||
|
||||
// get colors and stops
|
||||
m2 = m1[3].match(/((?:rgb|rgba)\(\d{1,3},\s\d{1,3},\s\d{1,3}(?:,\s[0-9\.]+)?\)(?:\s\d{1,3}%)?)+/g);
|
||||
if(m2){
|
||||
m2Len = m2.length;
|
||||
step = 1 / Math.max(m2Len - 1, 1);
|
||||
for(i = 0; i < m2Len; i+=1){
|
||||
m3 = m2[i].match(/((?:rgb|rgba)\(\d{1,3},\s\d{1,3},\s\d{1,3}(?:,\s[0-9\.]+)?\))\s*(\d{1,3})?(%)?/);
|
||||
if(m3[2]){
|
||||
stop = parseFloat(m3[2]);
|
||||
if(m3[3]){ // percentage
|
||||
stop /= 100;
|
||||
}
|
||||
} else {
|
||||
stop = i * step;
|
||||
}
|
||||
gradient.colorStops.push({
|
||||
color: m3[1],
|
||||
stop: stop
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case '-webkit-radial-gradient':
|
||||
case '-moz-radial-gradient':
|
||||
case '-o-radial-gradient':
|
||||
|
||||
gradient = {
|
||||
type: 'circle',
|
||||
x0: 0,
|
||||
y0: 0,
|
||||
x1: bounds.width,
|
||||
y1: bounds.height,
|
||||
cx: 0,
|
||||
cy: 0,
|
||||
rx: 0,
|
||||
ry: 0,
|
||||
colorStops: []
|
||||
};
|
||||
|
||||
// center
|
||||
m2 = m1[2].match(/(\d{1,3})%?\s(\d{1,3})%?/);
|
||||
if(m2){
|
||||
gradient.cx = (m2[1] * bounds.width) / 100;
|
||||
gradient.cy = (m2[2] * bounds.height) / 100;
|
||||
}
|
||||
|
||||
// size
|
||||
m2 = m1[3].match(/\w+/);
|
||||
m3 = m1[4].match(/[a-z-]*/);
|
||||
if(m2 && m3){
|
||||
switch(m3[0]){
|
||||
case 'farthest-corner':
|
||||
case 'cover': // is equivalent to farthest-corner
|
||||
case '': // mozilla removes "cover" from definition :(
|
||||
var tl = Math.sqrt(Math.pow(gradient.cx, 2) + Math.pow(gradient.cy, 2));
|
||||
var tr = Math.sqrt(Math.pow(gradient.cx, 2) + Math.pow(gradient.y1 - gradient.cy, 2));
|
||||
var br = Math.sqrt(Math.pow(gradient.x1 - gradient.cx, 2) + Math.pow(gradient.y1 - gradient.cy, 2));
|
||||
var bl = Math.sqrt(Math.pow(gradient.x1 - gradient.cx, 2) + Math.pow(gradient.cy, 2));
|
||||
gradient.rx = gradient.ry = Math.max(tl, tr, br, bl);
|
||||
break;
|
||||
case 'closest-corner':
|
||||
var tl = Math.sqrt(Math.pow(gradient.cx, 2) + Math.pow(gradient.cy, 2));
|
||||
var tr = Math.sqrt(Math.pow(gradient.cx, 2) + Math.pow(gradient.y1 - gradient.cy, 2));
|
||||
var br = Math.sqrt(Math.pow(gradient.x1 - gradient.cx, 2) + Math.pow(gradient.y1 - gradient.cy, 2));
|
||||
var bl = Math.sqrt(Math.pow(gradient.x1 - gradient.cx, 2) + Math.pow(gradient.cy, 2));
|
||||
gradient.rx = gradient.ry = Math.min(tl, tr, br, bl);
|
||||
break;
|
||||
case 'farthest-side':
|
||||
if(m2[0] === 'circle'){
|
||||
gradient.rx = gradient.ry = Math.max(
|
||||
gradient.cx,
|
||||
gradient.cy,
|
||||
gradient.x1 - gradient.cx,
|
||||
gradient.y1 - gradient.cy
|
||||
);
|
||||
} else { // ellipse
|
||||
|
||||
gradient.type = m2[0];
|
||||
|
||||
gradient.rx = Math.max(
|
||||
gradient.cx,
|
||||
gradient.x1 - gradient.cx
|
||||
);
|
||||
gradient.ry = Math.max(
|
||||
gradient.cy,
|
||||
gradient.y1 - gradient.cy
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'closest-side':
|
||||
case 'contain': // is equivalent to closest-side
|
||||
if(m2[0] === 'circle'){
|
||||
gradient.rx = gradient.ry = Math.min(
|
||||
gradient.cx,
|
||||
gradient.cy,
|
||||
gradient.x1 - gradient.cx,
|
||||
gradient.y1 - gradient.cy
|
||||
);
|
||||
} else { // ellipse
|
||||
|
||||
gradient.type = m2[0];
|
||||
|
||||
gradient.rx = Math.min(
|
||||
gradient.cx,
|
||||
gradient.x1 - gradient.cx
|
||||
);
|
||||
gradient.ry = Math.min(
|
||||
gradient.cy,
|
||||
gradient.y1 - gradient.cy
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
// TODO: add support for "30px 40px" sizes (webkit only)
|
||||
}
|
||||
}
|
||||
|
||||
// color stops
|
||||
m2 = m1[5].match(/((?:rgb|rgba)\(\d{1,3},\s\d{1,3},\s\d{1,3}(?:,\s[0-9\.]+)?\)(?:\s\d{1,3}(?:%|px))?)+/g);
|
||||
if(m2){
|
||||
m2Len = m2.length;
|
||||
step = 1 / Math.max(m2Len - 1, 1);
|
||||
for(i = 0; i < m2Len; i+=1){
|
||||
m3 = m2[i].match(/((?:rgb|rgba)\(\d{1,3},\s\d{1,3},\s\d{1,3}(?:,\s[0-9\.]+)?\))\s*(\d{1,3})?(%|px)?/);
|
||||
if(m3[2]){
|
||||
stop = parseFloat(m3[2]);
|
||||
if(m3[3] === '%'){
|
||||
stop /= 100;
|
||||
} else { // px - stupid opera
|
||||
stop /= bounds.width;
|
||||
}
|
||||
} else {
|
||||
stop = i * step;
|
||||
}
|
||||
gradient.colorStops.push({
|
||||
color: m3[1],
|
||||
stop: stop
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return gradient;
|
||||
};
|
||||
|
||||
_html2canvas.Generate.Gradient = function(src, bounds) {
|
||||
var canvas = document.createElement('canvas'),
|
||||
ctx = canvas.getContext('2d'),
|
||||
gradient, grad, i, len, img;
|
||||
|
||||
canvas.width = bounds.width;
|
||||
canvas.height = bounds.height;
|
||||
|
||||
// TODO: add support for multi defined background gradients (like radial gradient example in background.html)
|
||||
gradient = _html2canvas.Generate.parseGradient(src, bounds);
|
||||
|
||||
img = new Image();
|
||||
|
||||
if(gradient){
|
||||
if(gradient.type === 'linear'){
|
||||
grad = ctx.createLinearGradient(gradient.x0, gradient.y0, gradient.x1, gradient.y1);
|
||||
|
||||
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);
|
||||
|
||||
img.src = canvas.toDataURL();
|
||||
} else if(gradient.type === 'circle'){
|
||||
|
||||
grad = ctx.createRadialGradient(gradient.cx, gradient.cy, 0, gradient.cx, gradient.cy, gradient.rx);
|
||||
|
||||
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);
|
||||
|
||||
img.src = canvas.toDataURL();
|
||||
} 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);
|
||||
|
||||
imgRadial = new Image();
|
||||
imgRadial.onload = function() { // wait until the image is filled
|
||||
|
||||
// transform circle to ellipse
|
||||
ctx.drawImage(imgRadial, gradient.cx - gradient.rx, gradient.cy - gradient.ry, 2 * gradient.rx, 2 * gradient.ry);
|
||||
|
||||
img.src = canvas.toDataURL();
|
||||
|
||||
}
|
||||
imgRadial.src = canvasRadial.toDataURL();
|
||||
}
|
||||
}
|
||||
|
||||
return img;
|
||||
};
|
||||
|
||||
_html2canvas.Generate.ListAlpha = function(number) {
|
||||
var tmp = "",
|
||||
modulus;
|
||||
|
||||
do {
|
||||
modulus = number % 26;
|
||||
tmp = String.fromCharCode((modulus) + 64) + tmp;
|
||||
number = number / 26;
|
||||
}while((number*26) > 26);
|
||||
|
||||
return tmp;
|
||||
};
|
||||
|
||||
_html2canvas.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 = "",
|
||||
v,
|
||||
len = romanArray.length;
|
||||
|
||||
if (number <= 0 || number >= 4000) {
|
||||
return number;
|
||||
}
|
||||
|
||||
for (v=0; v < len; v+=1) {
|
||||
while (number >= decimal[v]) {
|
||||
number -= decimal[v];
|
||||
roman += romanArray[v];
|
||||
}
|
||||
}
|
||||
|
||||
return roman;
|
||||
|
||||
};
|
||||
|
||||
})();
|
191
src/Gradient.js
Normal file
191
src/Gradient.js
Normal file
@ -0,0 +1,191 @@
|
||||
/* @flow */
|
||||
'use strict';
|
||||
|
||||
import type {BackgroundSource} from './parsing/background';
|
||||
import type {Bounds} from './Bounds';
|
||||
import {parseAngle} from './Angle';
|
||||
import Color from './Color';
|
||||
import Length, {LENGTH_TYPE} from './Length';
|
||||
|
||||
const SIDE_OR_CORNER = /^(to )?(left|top|right|bottom)( (left|top|right|bottom))?$/i;
|
||||
const PERCENTAGE_ANGLES = /^([+-]?\d*\.?\d+)% ([+-]?\d*\.?\d+)%$/i;
|
||||
const ENDS_WITH_LENGTH = /(px)|%|( 0)$/i;
|
||||
const FROM_TO = /^(from|to)\((.+)\)$/i;
|
||||
|
||||
export type Direction = {
|
||||
x0: number,
|
||||
x1: number,
|
||||
y0: number,
|
||||
y1: number
|
||||
};
|
||||
|
||||
export type ColorStop = {
|
||||
color: Color,
|
||||
stop: number
|
||||
};
|
||||
|
||||
export type Gradient = {
|
||||
direction: Direction,
|
||||
colorStops: Array<ColorStop>
|
||||
};
|
||||
|
||||
export const parseGradient = (
|
||||
{args, method, prefix}: BackgroundSource,
|
||||
bounds: Bounds
|
||||
): ?Gradient => {
|
||||
if (method === 'linear-gradient') {
|
||||
return parseLinearGradient(args, bounds);
|
||||
} else if (method === 'gradient' && args[0] === 'linear') {
|
||||
// TODO handle correct angle
|
||||
return parseLinearGradient(
|
||||
['to bottom'].concat(
|
||||
args
|
||||
.slice(3)
|
||||
.map(color => color.match(FROM_TO))
|
||||
.filter(v => v !== null)
|
||||
// $FlowFixMe
|
||||
.map(v => v[2])
|
||||
),
|
||||
bounds
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const parseLinearGradient = (args: Array<string>, bounds: Bounds): Gradient => {
|
||||
const angle = parseAngle(args[0]);
|
||||
const HAS_SIDE_OR_CORNER = SIDE_OR_CORNER.test(args[0]);
|
||||
const HAS_DIRECTION = HAS_SIDE_OR_CORNER || angle !== null || PERCENTAGE_ANGLES.test(args[0]);
|
||||
const direction = HAS_DIRECTION
|
||||
? angle !== null
|
||||
? calculateGradientDirection(angle, bounds)
|
||||
: HAS_SIDE_OR_CORNER
|
||||
? parseSideOrCorner(args[0], bounds)
|
||||
: parsePercentageAngle(args[0], bounds)
|
||||
: calculateGradientDirection(Math.PI, bounds);
|
||||
const colorStops = [];
|
||||
const firstColorStopIndex = HAS_DIRECTION ? 1 : 0;
|
||||
|
||||
for (let i = firstColorStopIndex; i < args.length; i++) {
|
||||
const value = args[i];
|
||||
const HAS_LENGTH = ENDS_WITH_LENGTH.test(value);
|
||||
const lastSpaceIndex = value.lastIndexOf(' ');
|
||||
const color = new Color(HAS_LENGTH ? value.substring(0, lastSpaceIndex) : value);
|
||||
const stop = HAS_LENGTH
|
||||
? new Length(value.substring(lastSpaceIndex + 1))
|
||||
: i === firstColorStopIndex
|
||||
? new Length('0%')
|
||||
: i === args.length - 1 ? new Length('100%') : null;
|
||||
colorStops.push({color, stop});
|
||||
}
|
||||
|
||||
// TODO: Fix some inaccuracy with color stops with px values
|
||||
const lineLength = Math.min(
|
||||
Math.sqrt(
|
||||
Math.pow(Math.abs(direction.x0) + Math.abs(direction.x1), 2) +
|
||||
Math.pow(Math.abs(direction.y0) + Math.abs(direction.y1), 2)
|
||||
),
|
||||
bounds.width * 2,
|
||||
bounds.height * 2
|
||||
);
|
||||
|
||||
const absoluteValuedColorStops = colorStops.map(({color, stop}) => {
|
||||
return {
|
||||
color,
|
||||
// $FlowFixMe
|
||||
stop: stop ? stop.getAbsoluteValue(lineLength) / lineLength : null
|
||||
};
|
||||
});
|
||||
|
||||
let previousColorStop = absoluteValuedColorStops[0].stop;
|
||||
for (let i = 0; i < absoluteValuedColorStops.length; i++) {
|
||||
if (previousColorStop !== null) {
|
||||
const stop = absoluteValuedColorStops[i].stop;
|
||||
if (stop === null) {
|
||||
let n = i;
|
||||
while (absoluteValuedColorStops[n].stop === null) {
|
||||
n++;
|
||||
}
|
||||
const steps = n - i + 1;
|
||||
const nextColorStep = absoluteValuedColorStops[n].stop;
|
||||
const stepSize = (nextColorStep - previousColorStop) / steps;
|
||||
for (; i < n; i++) {
|
||||
previousColorStop = absoluteValuedColorStops[i].stop =
|
||||
previousColorStop + stepSize;
|
||||
}
|
||||
} else {
|
||||
previousColorStop = stop;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
direction,
|
||||
colorStops: absoluteValuedColorStops
|
||||
};
|
||||
};
|
||||
|
||||
const calculateGradientDirection = (radian: number, bounds: Bounds): Direction => {
|
||||
const width = bounds.width;
|
||||
const height = bounds.height;
|
||||
const HALF_WIDTH = width * 0.5;
|
||||
const HALF_HEIGHT = height * 0.5;
|
||||
const lineLength = Math.abs(width * Math.sin(radian)) + Math.abs(height * Math.cos(radian));
|
||||
const HALF_LINE_LENGTH = lineLength / 2;
|
||||
|
||||
const x0 = HALF_WIDTH + Math.sin(radian) * HALF_LINE_LENGTH;
|
||||
const y0 = HALF_HEIGHT - Math.cos(radian) * HALF_LINE_LENGTH;
|
||||
const x1 = width - x0;
|
||||
const y1 = height - y0;
|
||||
|
||||
return {x0, x1, y0, y1};
|
||||
};
|
||||
|
||||
const parseTopRight = (bounds: Bounds) =>
|
||||
Math.acos(
|
||||
bounds.width / 2 / (Math.sqrt(Math.pow(bounds.width, 2) + Math.pow(bounds.height, 2)) / 2)
|
||||
);
|
||||
|
||||
const parseSideOrCorner = (side: string, bounds: Bounds): Direction => {
|
||||
switch (side) {
|
||||
case 'bottom':
|
||||
case 'to top':
|
||||
return calculateGradientDirection(0, bounds);
|
||||
case 'left':
|
||||
case 'to right':
|
||||
return calculateGradientDirection(Math.PI / 2, bounds);
|
||||
case 'right':
|
||||
case 'to left':
|
||||
return calculateGradientDirection(3 * Math.PI / 2, bounds);
|
||||
case 'top right':
|
||||
case 'right top':
|
||||
case 'to bottom left':
|
||||
case 'to left bottom':
|
||||
return calculateGradientDirection(Math.PI + parseTopRight(bounds), bounds);
|
||||
case 'top left':
|
||||
case 'left top':
|
||||
case 'to bottom right':
|
||||
case 'to right bottom':
|
||||
return calculateGradientDirection(Math.PI - parseTopRight(bounds), bounds);
|
||||
case 'bottom left':
|
||||
case 'left bottom':
|
||||
case 'to top right':
|
||||
case 'to right top':
|
||||
return calculateGradientDirection(parseTopRight(bounds), bounds);
|
||||
case 'bottom right':
|
||||
case 'right bottom':
|
||||
case 'to top left':
|
||||
case 'to left top':
|
||||
return calculateGradientDirection(2 * Math.PI - parseTopRight(bounds), bounds);
|
||||
case 'top':
|
||||
case 'to bottom':
|
||||
default:
|
||||
return calculateGradientDirection(Math.PI, bounds);
|
||||
}
|
||||
};
|
||||
|
||||
const parsePercentageAngle = (angle: string, bounds: Bounds): Direction => {
|
||||
const [left, top] = angle.split(' ').map(parseFloat);
|
||||
const ratio = left / 100 * bounds.width / (top / 100 * bounds.height);
|
||||
|
||||
return calculateGradientDirection(Math.atan(isNaN(ratio) ? 1 : ratio) + Math.PI / 2, bounds);
|
||||
};
|
155
src/Input.js
Normal file
155
src/Input.js
Normal file
@ -0,0 +1,155 @@
|
||||
/* @flow */
|
||||
'use strict';
|
||||
import type NodeContainer from './NodeContainer';
|
||||
import TextContainer from './TextContainer';
|
||||
|
||||
import {BACKGROUND_CLIP, BACKGROUND_ORIGIN} from './parsing/background';
|
||||
import {BORDER_STYLE} from './parsing/border';
|
||||
|
||||
import Circle from './drawing/Circle';
|
||||
import Vector from './drawing/Vector';
|
||||
import Color from './Color';
|
||||
import Length from './Length';
|
||||
import {Bounds} from './Bounds';
|
||||
import {TextBounds} from './TextBounds';
|
||||
import {copyCSSStyles} from './Util';
|
||||
|
||||
export const INPUT_COLOR = new Color([42, 42, 42]);
|
||||
const INPUT_BORDER_COLOR = new Color([165, 165, 165]);
|
||||
const INPUT_BACKGROUND_COLOR = new Color([222, 222, 222]);
|
||||
const INPUT_BORDER = {
|
||||
borderWidth: 1,
|
||||
borderColor: INPUT_BORDER_COLOR,
|
||||
borderStyle: BORDER_STYLE.SOLID
|
||||
};
|
||||
export const INPUT_BORDERS = [INPUT_BORDER, INPUT_BORDER, INPUT_BORDER, INPUT_BORDER];
|
||||
export const INPUT_BACKGROUND = {
|
||||
backgroundColor: INPUT_BACKGROUND_COLOR,
|
||||
backgroundImage: [],
|
||||
backgroundClip: BACKGROUND_CLIP.PADDING_BOX,
|
||||
backgroundOrigin: BACKGROUND_ORIGIN.PADDING_BOX
|
||||
};
|
||||
|
||||
const RADIO_BORDER_RADIUS = new Length('50%');
|
||||
const RADIO_BORDER_RADIUS_TUPLE = [RADIO_BORDER_RADIUS, RADIO_BORDER_RADIUS];
|
||||
const INPUT_RADIO_BORDER_RADIUS = [
|
||||
RADIO_BORDER_RADIUS_TUPLE,
|
||||
RADIO_BORDER_RADIUS_TUPLE,
|
||||
RADIO_BORDER_RADIUS_TUPLE,
|
||||
RADIO_BORDER_RADIUS_TUPLE
|
||||
];
|
||||
|
||||
const CHECKBOX_BORDER_RADIUS = new Length('3px');
|
||||
const CHECKBOX_BORDER_RADIUS_TUPLE = [CHECKBOX_BORDER_RADIUS, CHECKBOX_BORDER_RADIUS];
|
||||
const INPUT_CHECKBOX_BORDER_RADIUS = [
|
||||
CHECKBOX_BORDER_RADIUS_TUPLE,
|
||||
CHECKBOX_BORDER_RADIUS_TUPLE,
|
||||
CHECKBOX_BORDER_RADIUS_TUPLE,
|
||||
CHECKBOX_BORDER_RADIUS_TUPLE
|
||||
];
|
||||
|
||||
export const getInputBorderRadius = (node: HTMLInputElement) => {
|
||||
return node.type === 'radio' ? INPUT_RADIO_BORDER_RADIUS : INPUT_CHECKBOX_BORDER_RADIUS;
|
||||
};
|
||||
|
||||
export const inlineInputElement = (node: HTMLInputElement, container: NodeContainer): void => {
|
||||
if (node.type === 'radio' || node.type === 'checkbox') {
|
||||
if (node.checked) {
|
||||
const size = Math.min(container.bounds.width, container.bounds.height);
|
||||
container.childNodes.push(
|
||||
node.type === 'checkbox'
|
||||
? [
|
||||
new Vector(
|
||||
container.bounds.left + size * 0.39363,
|
||||
container.bounds.top + size * 0.79
|
||||
),
|
||||
new Vector(
|
||||
container.bounds.left + size * 0.16,
|
||||
container.bounds.top + size * 0.5549
|
||||
),
|
||||
new Vector(
|
||||
container.bounds.left + size * 0.27347,
|
||||
container.bounds.top + size * 0.44071
|
||||
),
|
||||
new Vector(
|
||||
container.bounds.left + size * 0.39694,
|
||||
container.bounds.top + size * 0.5649
|
||||
),
|
||||
new Vector(
|
||||
container.bounds.left + size * 0.72983,
|
||||
container.bounds.top + size * 0.23
|
||||
),
|
||||
new Vector(
|
||||
container.bounds.left + size * 0.84,
|
||||
container.bounds.top + size * 0.34085
|
||||
),
|
||||
new Vector(
|
||||
container.bounds.left + size * 0.39363,
|
||||
container.bounds.top + size * 0.79
|
||||
)
|
||||
]
|
||||
: new Circle(
|
||||
container.bounds.left + size / 4,
|
||||
container.bounds.top + size / 4,
|
||||
size / 4
|
||||
)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
inlineFormElement(getInputValue(node), node, container, false);
|
||||
}
|
||||
};
|
||||
|
||||
export const inlineTextAreaElement = (
|
||||
node: HTMLTextAreaElement,
|
||||
container: NodeContainer
|
||||
): void => {
|
||||
inlineFormElement(node.value, node, container, true);
|
||||
};
|
||||
|
||||
export const inlineSelectElement = (node: HTMLSelectElement, container: NodeContainer): void => {
|
||||
const option = node.options[node.selectedIndex || 0];
|
||||
inlineFormElement(option ? option.text || '' : '', node, container, false);
|
||||
};
|
||||
|
||||
export const reformatInputBounds = (bounds: Bounds): Bounds => {
|
||||
if (bounds.width > bounds.height) {
|
||||
bounds.left += (bounds.width - bounds.height) / 2;
|
||||
bounds.width = bounds.height;
|
||||
} else if (bounds.width < bounds.height) {
|
||||
bounds.top += (bounds.height - bounds.width) / 2;
|
||||
bounds.height = bounds.width;
|
||||
}
|
||||
return bounds;
|
||||
};
|
||||
|
||||
const inlineFormElement = (
|
||||
value: string,
|
||||
node: HTMLElement,
|
||||
container: NodeContainer,
|
||||
allowLinebreak: boolean
|
||||
): void => {
|
||||
const body = node.ownerDocument.body;
|
||||
if (value.length > 0 && body) {
|
||||
const wrapper = node.ownerDocument.createElement('html2canvaswrapper');
|
||||
copyCSSStyles(node.ownerDocument.defaultView.getComputedStyle(node, null), wrapper);
|
||||
wrapper.style.position = 'fixed';
|
||||
wrapper.style.left = `${container.bounds.left}px`;
|
||||
wrapper.style.top = `${container.bounds.top}px`;
|
||||
if (!allowLinebreak) {
|
||||
wrapper.style.whiteSpace = 'nowrap';
|
||||
}
|
||||
const text = node.ownerDocument.createTextNode(value);
|
||||
wrapper.appendChild(text);
|
||||
body.appendChild(wrapper);
|
||||
container.childNodes.push(TextContainer.fromTextNode(text, container));
|
||||
body.removeChild(wrapper);
|
||||
}
|
||||
};
|
||||
|
||||
const getInputValue = (node: HTMLInputElement): string => {
|
||||
const value =
|
||||
node.type === 'password' ? new Array(node.value.length + 1).join('\u2022') : node.value;
|
||||
|
||||
return value.length === 0 ? node.placeholder || '' : value;
|
||||
};
|
@ -1,7 +0,0 @@
|
||||
/**
|
||||
@license html2canvas @VERSION@ <http://html2canvas.hertzen.com>
|
||||
Copyright (c) 2011 Niklas von Hertzen. All rights reserved.
|
||||
http://www.twitter.com/niklasvh
|
||||
|
||||
Released under MIT License
|
||||
*/
|
36
src/Length.js
Normal file
36
src/Length.js
Normal file
@ -0,0 +1,36 @@
|
||||
/* @flow */
|
||||
'use strict';
|
||||
|
||||
export const LENGTH_TYPE = {
|
||||
PX: 0,
|
||||
PERCENTAGE: 1
|
||||
};
|
||||
|
||||
export type LengthType = $Values<typeof LENGTH_TYPE>;
|
||||
|
||||
export default class Length {
|
||||
type: LengthType;
|
||||
value: number;
|
||||
|
||||
constructor(value: string) {
|
||||
this.type =
|
||||
value.substr(value.length - 1) === '%' ? LENGTH_TYPE.PERCENTAGE : LENGTH_TYPE.PX;
|
||||
const parsedValue = parseFloat(value);
|
||||
if (__DEV__ && isNaN(parsedValue)) {
|
||||
console.error(`Invalid value given for Length: "${value}"`);
|
||||
}
|
||||
this.value = isNaN(parsedValue) ? 0 : parsedValue;
|
||||
}
|
||||
|
||||
isPercentage(): boolean {
|
||||
return this.type === LENGTH_TYPE.PERCENTAGE;
|
||||
}
|
||||
|
||||
getAbsoluteValue(parentLength: number): number {
|
||||
return this.isPercentage() ? parentLength * (this.value / 100) : this.value;
|
||||
}
|
||||
|
||||
static create(v): Length {
|
||||
return new Length(v);
|
||||
}
|
||||
}
|
46
src/Logger.js
Normal file
46
src/Logger.js
Normal file
@ -0,0 +1,46 @@
|
||||
/* @flow */
|
||||
'use strict';
|
||||
|
||||
export default class Logger {
|
||||
start: number;
|
||||
id: ?string;
|
||||
|
||||
constructor(id: ?string, start: ?number) {
|
||||
this.start = start ? start : Date.now();
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
child(id: string) {
|
||||
return new Logger(id, this.start);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line flowtype/no-weak-types
|
||||
log(...args: any) {
|
||||
if (window.console && window.console.log) {
|
||||
Function.prototype.bind
|
||||
.call(window.console.log, window.console)
|
||||
.apply(
|
||||
window.console,
|
||||
[
|
||||
Date.now() - this.start + 'ms',
|
||||
this.id ? `html2canvas (${this.id}):` : 'html2canvas:'
|
||||
].concat([].slice.call(args, 0))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line flowtype/no-weak-types
|
||||
error(...args: any) {
|
||||
if (window.console && window.console.error) {
|
||||
Function.prototype.bind
|
||||
.call(window.console.error, window.console)
|
||||
.apply(
|
||||
window.console,
|
||||
[
|
||||
Date.now() - this.start + 'ms',
|
||||
this.id ? `html2canvas (${this.id}):` : 'html2canvas:'
|
||||
].concat([].slice.call(args, 0))
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
254
src/NodeContainer.js
Normal file
254
src/NodeContainer.js
Normal file
@ -0,0 +1,254 @@
|
||||
/* @flow */
|
||||
'use strict';
|
||||
|
||||
import type {Background} from './parsing/background';
|
||||
import type {Border} from './parsing/border';
|
||||
import type {BorderRadius} from './parsing/borderRadius';
|
||||
import type {DisplayBit} from './parsing/display';
|
||||
import type {Float} from './parsing/float';
|
||||
import type {Font} from './parsing/font';
|
||||
import type {Overflow} from './parsing/overflow';
|
||||
import type {Padding} from './parsing/padding';
|
||||
import type {Position} from './parsing/position';
|
||||
import type {TextShadow} from './parsing/textShadow';
|
||||
import type {TextTransform} from './parsing/textTransform';
|
||||
import type {TextDecoration} from './parsing/textDecoration';
|
||||
import type {Transform} from './parsing/transform';
|
||||
import type {Visibility} from './parsing/visibility';
|
||||
import type {zIndex} from './parsing/zIndex';
|
||||
|
||||
import type {Bounds, BoundCurves} from './Bounds';
|
||||
import type ResourceLoader, {ImageElement} from './ResourceLoader';
|
||||
import type {Path} from './drawing/Path';
|
||||
import type TextContainer from './TextContainer';
|
||||
|
||||
import Color from './Color';
|
||||
|
||||
import {contains} from './Util';
|
||||
import {parseBackground} from './parsing/background';
|
||||
import {parseBorder} from './parsing/border';
|
||||
import {parseBorderRadius} from './parsing/borderRadius';
|
||||
import {parseDisplay, DISPLAY} from './parsing/display';
|
||||
import {parseCSSFloat, FLOAT} from './parsing/float';
|
||||
import {parseFont} from './parsing/font';
|
||||
import {parseLetterSpacing} from './parsing/letterSpacing';
|
||||
import {parseOverflow, OVERFLOW} from './parsing/overflow';
|
||||
import {parsePadding} from './parsing/padding';
|
||||
import {parsePosition, POSITION} from './parsing/position';
|
||||
import {parseTextDecoration} from './parsing/textDecoration';
|
||||
import {parseTextShadow} from './parsing/textShadow';
|
||||
import {parseTextTransform} from './parsing/textTransform';
|
||||
import {parseTransform} from './parsing/transform';
|
||||
import {parseVisibility, VISIBILITY} from './parsing/visibility';
|
||||
import {parseZIndex} from './parsing/zIndex';
|
||||
|
||||
import {parseBounds, parseBoundCurves, calculatePaddingBoxPath} from './Bounds';
|
||||
import {
|
||||
INPUT_BACKGROUND,
|
||||
INPUT_BORDERS,
|
||||
INPUT_COLOR,
|
||||
getInputBorderRadius,
|
||||
reformatInputBounds
|
||||
} from './Input';
|
||||
|
||||
type StyleDeclaration = {
|
||||
background: Background,
|
||||
border: Array<Border>,
|
||||
borderRadius: Array<BorderRadius>,
|
||||
color: Color,
|
||||
display: DisplayBit,
|
||||
float: Float,
|
||||
font: Font,
|
||||
letterSpacing: number,
|
||||
opacity: number,
|
||||
overflow: Overflow,
|
||||
padding: Padding,
|
||||
position: Position,
|
||||
textDecoration: TextDecoration | null,
|
||||
textShadow: Array<TextShadow> | null,
|
||||
textTransform: TextTransform,
|
||||
transform: Transform,
|
||||
visibility: Visibility,
|
||||
zIndex: zIndex
|
||||
};
|
||||
|
||||
const INPUT_TAGS = ['INPUT', 'TEXTAREA', 'SELECT'];
|
||||
|
||||
export default class NodeContainer {
|
||||
name: ?string;
|
||||
parent: ?NodeContainer;
|
||||
style: StyleDeclaration;
|
||||
childNodes: Array<TextContainer | Path>;
|
||||
bounds: Bounds;
|
||||
curvedBounds: BoundCurves;
|
||||
image: ?string;
|
||||
index: number;
|
||||
|
||||
constructor(
|
||||
node: HTMLElement | SVGSVGElement,
|
||||
parent: ?NodeContainer,
|
||||
resourceLoader: ResourceLoader,
|
||||
index: number
|
||||
) {
|
||||
this.parent = parent;
|
||||
this.index = index;
|
||||
this.childNodes = [];
|
||||
const defaultView = node.ownerDocument.defaultView;
|
||||
const scrollX = defaultView.pageXOffset;
|
||||
const scrollY = defaultView.pageYOffset;
|
||||
const style = defaultView.getComputedStyle(node, null);
|
||||
const display = parseDisplay(style.display);
|
||||
|
||||
const IS_INPUT = node.type === 'radio' || node.type === 'checkbox';
|
||||
|
||||
const position = parsePosition(style.position);
|
||||
|
||||
this.style = {
|
||||
background: IS_INPUT ? INPUT_BACKGROUND : parseBackground(style, resourceLoader),
|
||||
border: IS_INPUT ? INPUT_BORDERS : parseBorder(style),
|
||||
borderRadius:
|
||||
(node instanceof defaultView.HTMLInputElement ||
|
||||
node instanceof HTMLInputElement) &&
|
||||
IS_INPUT
|
||||
? getInputBorderRadius(node)
|
||||
: parseBorderRadius(style),
|
||||
color: IS_INPUT ? INPUT_COLOR : new Color(style.color),
|
||||
display: display,
|
||||
float: parseCSSFloat(style.float),
|
||||
font: parseFont(style),
|
||||
letterSpacing: parseLetterSpacing(style.letterSpacing),
|
||||
opacity: parseFloat(style.opacity),
|
||||
overflow:
|
||||
INPUT_TAGS.indexOf(node.tagName) === -1
|
||||
? parseOverflow(style.overflow)
|
||||
: OVERFLOW.HIDDEN,
|
||||
padding: parsePadding(style),
|
||||
position: position,
|
||||
textDecoration: parseTextDecoration(style),
|
||||
textShadow: parseTextShadow(style.textShadow),
|
||||
textTransform: parseTextTransform(style.textTransform),
|
||||
transform: parseTransform(style),
|
||||
visibility: parseVisibility(style.visibility),
|
||||
zIndex: parseZIndex(position !== POSITION.STATIC ? style.zIndex : 'auto')
|
||||
};
|
||||
|
||||
if (this.isTransformed()) {
|
||||
// getBoundingClientRect provides values post-transform, we want them without the transformation
|
||||
node.style.transform = 'matrix(1,0,0,1,0,0)';
|
||||
}
|
||||
|
||||
// TODO move bound retrieval for all nodes to a later stage?
|
||||
if (node.tagName === 'IMG') {
|
||||
node.addEventListener('load', () => {
|
||||
this.bounds = parseBounds(node, scrollX, scrollY);
|
||||
this.curvedBounds = parseBoundCurves(
|
||||
this.bounds,
|
||||
this.style.border,
|
||||
this.style.borderRadius
|
||||
);
|
||||
});
|
||||
}
|
||||
this.image = getImage(node, resourceLoader);
|
||||
this.bounds = IS_INPUT
|
||||
? reformatInputBounds(parseBounds(node, scrollX, scrollY))
|
||||
: parseBounds(node, scrollX, scrollY);
|
||||
this.curvedBounds = parseBoundCurves(
|
||||
this.bounds,
|
||||
this.style.border,
|
||||
this.style.borderRadius
|
||||
);
|
||||
|
||||
if (__DEV__) {
|
||||
this.name = `${node.tagName.toLowerCase()}${node.id
|
||||
? `#${node.id}`
|
||||
: ''}${node.className
|
||||
.toString()
|
||||
.split(' ')
|
||||
.map(s => (s.length ? `.${s}` : ''))
|
||||
.join('')}`;
|
||||
}
|
||||
}
|
||||
getClipPaths(): Array<Path> {
|
||||
const parentClips = this.parent ? this.parent.getClipPaths() : [];
|
||||
const isClipped =
|
||||
this.style.overflow === OVERFLOW.HIDDEN || this.style.overflow === OVERFLOW.SCROLL;
|
||||
|
||||
return isClipped
|
||||
? parentClips.concat([calculatePaddingBoxPath(this.curvedBounds)])
|
||||
: parentClips;
|
||||
}
|
||||
isInFlow(): boolean {
|
||||
return this.isRootElement() && !this.isFloating() && !this.isAbsolutelyPositioned();
|
||||
}
|
||||
isVisible(): boolean {
|
||||
return (
|
||||
!contains(this.style.display, DISPLAY.NONE) &&
|
||||
this.style.opacity > 0 &&
|
||||
this.style.visibility === VISIBILITY.VISIBLE
|
||||
);
|
||||
}
|
||||
isAbsolutelyPositioned(): boolean {
|
||||
return this.style.position !== POSITION.STATIC && this.style.position !== POSITION.RELATIVE;
|
||||
}
|
||||
isPositioned(): boolean {
|
||||
return this.style.position !== POSITION.STATIC;
|
||||
}
|
||||
isFloating(): boolean {
|
||||
return this.style.float !== FLOAT.NONE;
|
||||
}
|
||||
isRootElement(): boolean {
|
||||
return this.parent === null;
|
||||
}
|
||||
isTransformed(): boolean {
|
||||
return this.style.transform !== null;
|
||||
}
|
||||
isPositionedWithZIndex(): boolean {
|
||||
return this.isPositioned() && !this.style.zIndex.auto;
|
||||
}
|
||||
isInlineLevel(): boolean {
|
||||
return (
|
||||
contains(this.style.display, DISPLAY.INLINE) ||
|
||||
contains(this.style.display, DISPLAY.INLINE_BLOCK) ||
|
||||
contains(this.style.display, DISPLAY.INLINE_FLEX) ||
|
||||
contains(this.style.display, DISPLAY.INLINE_GRID) ||
|
||||
contains(this.style.display, DISPLAY.INLINE_LIST_ITEM) ||
|
||||
contains(this.style.display, DISPLAY.INLINE_TABLE)
|
||||
);
|
||||
}
|
||||
isInlineBlockOrInlineTable(): boolean {
|
||||
return (
|
||||
contains(this.style.display, DISPLAY.INLINE_BLOCK) ||
|
||||
contains(this.style.display, DISPLAY.INLINE_TABLE)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const getImage = (node: HTMLElement | SVGSVGElement, resourceLoader: ResourceLoader): ?string => {
|
||||
if (
|
||||
node instanceof node.ownerDocument.defaultView.SVGSVGElement ||
|
||||
node instanceof SVGSVGElement
|
||||
) {
|
||||
const s = new XMLSerializer();
|
||||
return resourceLoader.loadImage(
|
||||
`data:image/svg+xml,${encodeURIComponent(s.serializeToString(node))}`
|
||||
);
|
||||
}
|
||||
switch (node.tagName) {
|
||||
case 'IMG':
|
||||
// $FlowFixMe
|
||||
const img: HTMLImageElement = node;
|
||||
return resourceLoader.loadImage(img.currentSrc || img.src);
|
||||
case 'CANVAS':
|
||||
// $FlowFixMe
|
||||
const canvas: HTMLCanvasElement = node;
|
||||
return resourceLoader.loadCanvas(canvas);
|
||||
case 'IFRAME':
|
||||
const iframeKey = node.getAttribute('data-html2canvas-internal-iframe-key');
|
||||
if (iframeKey) {
|
||||
return iframeKey;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
158
src/NodeParser.js
Normal file
158
src/NodeParser.js
Normal file
@ -0,0 +1,158 @@
|
||||
/* @flow */
|
||||
'use strict';
|
||||
import type ResourceLoader, {ImageElement} from './ResourceLoader';
|
||||
import type Logger from './Logger';
|
||||
import StackingContext from './StackingContext';
|
||||
import NodeContainer from './NodeContainer';
|
||||
import TextContainer from './TextContainer';
|
||||
import {inlineInputElement, inlineTextAreaElement, inlineSelectElement} from './Input';
|
||||
|
||||
export const NodeParser = (
|
||||
node: HTMLElement,
|
||||
resourceLoader: ResourceLoader,
|
||||
logger: Logger
|
||||
): StackingContext => {
|
||||
if (__DEV__) {
|
||||
logger.log(`Starting node parsing`);
|
||||
}
|
||||
|
||||
let index = 0;
|
||||
|
||||
const container = new NodeContainer(node, null, resourceLoader, index++);
|
||||
const stack = new StackingContext(container, null, true);
|
||||
|
||||
parseNodeTree(node, container, stack, resourceLoader, index);
|
||||
|
||||
if (__DEV__) {
|
||||
logger.log(`Finished parsing node tree`);
|
||||
}
|
||||
|
||||
return stack;
|
||||
};
|
||||
|
||||
const IGNORED_NODE_NAMES = ['SCRIPT', 'HEAD', 'TITLE', 'OBJECT', 'BR', 'OPTION'];
|
||||
|
||||
const parseNodeTree = (
|
||||
node: HTMLElement,
|
||||
parent: NodeContainer,
|
||||
stack: StackingContext,
|
||||
resourceLoader: ResourceLoader,
|
||||
index: number
|
||||
): void => {
|
||||
if (__DEV__ && index > 50000) {
|
||||
throw new Error(`Recursion error while parsing node tree`);
|
||||
}
|
||||
|
||||
for (let childNode = node.firstChild, nextNode; childNode; childNode = nextNode) {
|
||||
nextNode = childNode.nextSibling;
|
||||
const defaultView = childNode.ownerDocument.defaultView;
|
||||
if (
|
||||
childNode instanceof defaultView.Text ||
|
||||
childNode instanceof Text ||
|
||||
(defaultView.parent && childNode instanceof defaultView.parent.Text)
|
||||
) {
|
||||
if (childNode.data.trim().length > 0) {
|
||||
parent.childNodes.push(TextContainer.fromTextNode(childNode, parent));
|
||||
}
|
||||
} else if (
|
||||
childNode instanceof defaultView.HTMLElement ||
|
||||
childNode instanceof HTMLElement ||
|
||||
(defaultView.parent && childNode instanceof defaultView.parent.HTMLElement)
|
||||
) {
|
||||
if (IGNORED_NODE_NAMES.indexOf(childNode.nodeName) === -1) {
|
||||
const container = new NodeContainer(childNode, parent, resourceLoader, index++);
|
||||
if (container.isVisible()) {
|
||||
if (childNode.tagName === 'INPUT') {
|
||||
// $FlowFixMe
|
||||
inlineInputElement(childNode, container);
|
||||
} else if (childNode.tagName === 'TEXTAREA') {
|
||||
// $FlowFixMe
|
||||
inlineTextAreaElement(childNode, container);
|
||||
} else if (childNode.tagName === 'SELECT') {
|
||||
// $FlowFixMe
|
||||
inlineSelectElement(childNode, container);
|
||||
}
|
||||
|
||||
const SHOULD_TRAVERSE_CHILDREN = childNode.tagName !== 'TEXTAREA';
|
||||
const treatAsRealStackingContext = createsRealStackingContext(
|
||||
container,
|
||||
childNode
|
||||
);
|
||||
if (treatAsRealStackingContext || createsStackingContext(container)) {
|
||||
// for treatAsRealStackingContext:false, any positioned descendants and descendants
|
||||
// which actually create a new stacking context should be considered part of the parent stacking context
|
||||
const parentStack =
|
||||
treatAsRealStackingContext || container.isPositioned()
|
||||
? stack.getRealParentStackingContext()
|
||||
: stack;
|
||||
const childStack = new StackingContext(
|
||||
container,
|
||||
parentStack,
|
||||
treatAsRealStackingContext
|
||||
);
|
||||
parentStack.contexts.push(childStack);
|
||||
if (SHOULD_TRAVERSE_CHILDREN) {
|
||||
parseNodeTree(childNode, container, childStack, resourceLoader, index);
|
||||
}
|
||||
} else {
|
||||
stack.children.push(container);
|
||||
if (SHOULD_TRAVERSE_CHILDREN) {
|
||||
parseNodeTree(childNode, container, stack, resourceLoader, index);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
childNode instanceof defaultView.SVGSVGElement ||
|
||||
childNode instanceof SVGSVGElement ||
|
||||
(defaultView.parent && childNode instanceof defaultView.parent.SVGSVGElement)
|
||||
) {
|
||||
const container = new NodeContainer(childNode, parent, resourceLoader, index++);
|
||||
const treatAsRealStackingContext = createsRealStackingContext(container, childNode);
|
||||
if (treatAsRealStackingContext || createsStackingContext(container)) {
|
||||
// for treatAsRealStackingContext:false, any positioned descendants and descendants
|
||||
// which actually create a new stacking context should be considered part of the parent stacking context
|
||||
const parentStack =
|
||||
treatAsRealStackingContext || container.isPositioned()
|
||||
? stack.getRealParentStackingContext()
|
||||
: stack;
|
||||
const childStack = new StackingContext(
|
||||
container,
|
||||
parentStack,
|
||||
treatAsRealStackingContext
|
||||
);
|
||||
parentStack.contexts.push(childStack);
|
||||
} else {
|
||||
stack.children.push(container);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const createsRealStackingContext = (
|
||||
container: NodeContainer,
|
||||
node: HTMLElement | SVGSVGElement
|
||||
): boolean => {
|
||||
return (
|
||||
container.isRootElement() ||
|
||||
container.isPositionedWithZIndex() ||
|
||||
container.style.opacity < 1 ||
|
||||
container.isTransformed() ||
|
||||
isBodyWithTransparentRoot(container, node)
|
||||
);
|
||||
};
|
||||
|
||||
const createsStackingContext = (container: NodeContainer): boolean => {
|
||||
return container.isPositioned() || container.isFloating();
|
||||
};
|
||||
|
||||
const isBodyWithTransparentRoot = (
|
||||
container: NodeContainer,
|
||||
node: HTMLElement | SVGSVGElement
|
||||
): boolean => {
|
||||
return (
|
||||
node.nodeName === 'BODY' &&
|
||||
container.parent instanceof NodeContainer &&
|
||||
container.parent.style.background.backgroundColor.isTransparent()
|
||||
);
|
||||
};
|
1399
src/Parse.js
1399
src/Parse.js
File diff suppressed because it is too large
Load Diff
360
src/Preload.js
360
src/Preload.js
@ -1,360 +0,0 @@
|
||||
/*
|
||||
html2canvas @VERSION@ <http://html2canvas.hertzen.com>
|
||||
Copyright (c) 2011 Niklas von Hertzen. All rights reserved.
|
||||
http://www.twitter.com/niklasvh
|
||||
|
||||
Released under MIT License
|
||||
*/
|
||||
|
||||
_html2canvas.Preload = function( options ) {
|
||||
|
||||
var images = {
|
||||
numLoaded: 0, // also failed are counted here
|
||||
numFailed: 0,
|
||||
numTotal: 0,
|
||||
cleanupDone: false
|
||||
},
|
||||
pageOrigin,
|
||||
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
|
||||
imgLen = domImages.length,
|
||||
link = doc.createElement("a"),
|
||||
supportCORS = (function( img ){
|
||||
return (img.crossOrigin !== undefined);
|
||||
})(new Image()),
|
||||
timeoutTimer;
|
||||
|
||||
link.href = window.location.href;
|
||||
pageOrigin = link.protocol + link.host;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
function isSameOrigin(url){
|
||||
link.href = url;
|
||||
link.href = link.href; // YES, BELIEVE IT OR NOT, that is required for IE9 - http://jsfiddle.net/niklasvh/2e48b/
|
||||
var origin = link.protocol + link.host;
|
||||
return (origin === pageOrigin);
|
||||
}
|
||||
|
||||
function start(){
|
||||
h2clog("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 + ")");
|
||||
|
||||
if (typeof options.complete === "function"){
|
||||
options.complete(images);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// TODO modify proxy to serve images with CORS enabled, where available
|
||||
function proxyGetImage(url, img, imageObj){
|
||||
var callback_name,
|
||||
scriptUrl = options.proxy,
|
||||
script;
|
||||
|
||||
link.href = url;
|
||||
url = link.href; // work around for pages with base href="" set - WARNING: this may change the url
|
||||
|
||||
callback_name = 'html2canvas_' + (count++);
|
||||
imageObj.callbackname = callback_name;
|
||||
|
||||
if (scriptUrl.indexOf("?") > -1) {
|
||||
scriptUrl += "&";
|
||||
} else {
|
||||
scriptUrl += "?";
|
||||
}
|
||||
scriptUrl += 'url=' + encodeURIComponent(url) + '&callback=' + callback_name;
|
||||
script = doc.createElement("script");
|
||||
|
||||
window[callback_name] = function(a){
|
||||
if (a.substring(0,6) === "error:"){
|
||||
imageObj.succeeded = false;
|
||||
images.numLoaded++;
|
||||
images.numFailed++;
|
||||
start();
|
||||
} else {
|
||||
setImageLoadHandlers(img, imageObj);
|
||||
img.src = a;
|
||||
}
|
||||
window[callback_name] = undefined; // to work with IE<9 // NOTE: that the undefined callback property-name still exists on the window object (for IE<9)
|
||||
try {
|
||||
delete window[callback_name]; // for all browser that support this
|
||||
} catch(ex) {}
|
||||
script.parentNode.removeChild(script);
|
||||
script = null;
|
||||
delete imageObj.script;
|
||||
delete imageObj.callbackname;
|
||||
};
|
||||
|
||||
script.setAttribute("type", "text/javascript");
|
||||
script.setAttribute("src", scriptUrl);
|
||||
imageObj.script = script;
|
||||
window.document.body.appendChild(script);
|
||||
|
||||
}
|
||||
|
||||
function getImages (el) {
|
||||
|
||||
|
||||
|
||||
// if (!this.ignoreRe.test(el.nodeName)){
|
||||
//
|
||||
|
||||
var contents = _html2canvas.Util.Children(el),
|
||||
i,
|
||||
background_image,
|
||||
src,
|
||||
img,
|
||||
elNodeType = false;
|
||||
|
||||
// Firefox fails with permission denied on pages with iframes
|
||||
try {
|
||||
var contentsLen = contents.length;
|
||||
for (i = 0; i < contentsLen; i+=1 ){
|
||||
// var ignRe = new RegExp("("+this.ignoreElements+")");
|
||||
// if (!ignRe.test(element.nodeName)){
|
||||
getImages(contents[i]);
|
||||
// }
|
||||
}
|
||||
}
|
||||
catch( e ) {}
|
||||
|
||||
|
||||
// }
|
||||
try {
|
||||
elNodeType = el.nodeType;
|
||||
} catch (ex) {
|
||||
elNodeType = false;
|
||||
h2clog("html2canvas: failed to access some element's nodeType - Exception: " + ex.message);
|
||||
}
|
||||
|
||||
if (elNodeType === 1 || elNodeType === undefined){
|
||||
|
||||
// opera throws exception on external-content.html
|
||||
try {
|
||||
background_image = _html2canvas.Util.getCSS(el, 'backgroundImage');
|
||||
}catch(e) {
|
||||
h2clog("html2canvas: failed to get background-image - Exception: " + e.message);
|
||||
}
|
||||
if ( background_image && background_image !== "1" && background_image !== "none" ) {
|
||||
|
||||
// TODO add multi image background support
|
||||
|
||||
if (/^(-webkit|-o|-moz|-ms|linear)-/.test( background_image )) {
|
||||
|
||||
img = _html2canvas.Generate.Gradient( background_image, _html2canvas.Util.Bounds( el ) );
|
||||
|
||||
if ( img !== undefined ){
|
||||
images[background_image] = {
|
||||
img: img,
|
||||
succeeded: true
|
||||
};
|
||||
images.numTotal++;
|
||||
images.numLoaded++;
|
||||
start();
|
||||
|
||||
}
|
||||
|
||||
} else {
|
||||
src = _html2canvas.Util.backgroundImage(background_image.match(/data:image\/.*;base64,/i) ? background_image : background_image.split(",")[0]);
|
||||
methods.loadImage(src);
|
||||
}
|
||||
|
||||
/*
|
||||
if (background_image && background_image !== "1" && background_image !== "none" && background_image.substring(0,7) !== "-webkit" && background_image.substring(0,3)!== "-o-" && background_image.substring(0,4) !== "-moz"){
|
||||
// TODO add multi image background support
|
||||
src = _html2canvas.Util.backgroundImage(background_image.split(",")[0]);
|
||||
methods.loadImage(src); */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setImageLoadHandlers(img, imageObj) {
|
||||
img.onload = function() {
|
||||
if ( imageObj.timer !== undefined ) {
|
||||
// CORS succeeded
|
||||
window.clearTimeout( imageObj.timer );
|
||||
}
|
||||
|
||||
images.numLoaded++;
|
||||
imageObj.succeeded = true;
|
||||
img.onerror = img.onload = null;
|
||||
start();
|
||||
};
|
||||
img.onerror = function() {
|
||||
|
||||
if (img.crossOrigin === "anonymous") {
|
||||
// CORS failed
|
||||
window.clearTimeout( imageObj.timer );
|
||||
|
||||
// let's try with proxy instead
|
||||
if ( options.proxy ) {
|
||||
var src = img.src;
|
||||
img = new Image();
|
||||
imageObj.img = img;
|
||||
img.src = src;
|
||||
|
||||
proxyGetImage( img.src, img, imageObj );
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
images.numLoaded++;
|
||||
images.numFailed++;
|
||||
imageObj.succeeded = false;
|
||||
img.onerror = img.onload = null;
|
||||
start();
|
||||
|
||||
};
|
||||
|
||||
// TODO Opera has no load/error event for SVG images
|
||||
|
||||
// Opera ninja onload's cached images
|
||||
/*
|
||||
window.setTimeout(function(){
|
||||
if ( img.width !== 0 && imageObj.succeeded === undefined ) {
|
||||
img.onload();
|
||||
}
|
||||
}, 100); // needs a reflow for base64 encoded images? interestingly timeout of 0 doesn't work but 1 does.
|
||||
*/
|
||||
}
|
||||
|
||||
|
||||
methods = {
|
||||
loadImage: function( src ) {
|
||||
var img, imageObj;
|
||||
if ( src && images[src] === undefined ) {
|
||||
img = new Image();
|
||||
if ( src.match(/data:image\/.*;base64,/i) ) {
|
||||
img.src = src.replace(/url\(['"]{0,}|['"]{0,}\)$/ig, '');
|
||||
imageObj = images[src] = {
|
||||
img: img
|
||||
};
|
||||
images.numTotal++;
|
||||
setImageLoadHandlers(img, imageObj);
|
||||
} else if ( isSameOrigin( src ) || options.allowTaint === true ) {
|
||||
imageObj = images[src] = {
|
||||
img: img
|
||||
};
|
||||
images.numTotal++;
|
||||
setImageLoadHandlers(img, imageObj);
|
||||
img.src = src;
|
||||
} else if ( supportCORS && !options.allowTaint && options.useCORS ) {
|
||||
// attempt to load with CORS
|
||||
|
||||
img.crossOrigin = "anonymous";
|
||||
imageObj = images[src] = {
|
||||
img: img
|
||||
};
|
||||
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
|
||||
};
|
||||
images.numTotal++;
|
||||
proxyGetImage( src, img, imageObj );
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
cleanupDOM: function(cause) {
|
||||
var img, src;
|
||||
if (!images.cleanupDone) {
|
||||
if (cause && typeof cause === "string") {
|
||||
h2clog("html2canvas: Cleanup because: " + cause);
|
||||
} else {
|
||||
h2clog("html2canvas: Cleanup after timeout: " + options.timeout + " ms.");
|
||||
}
|
||||
|
||||
for (src in images) {
|
||||
if (images.hasOwnProperty(src)) {
|
||||
img = images[src];
|
||||
if (typeof img === "object" && img.callbackname && img.succeeded === undefined) {
|
||||
// cancel proxy image request
|
||||
window[img.callbackname] = undefined; // to work with IE<9 // NOTE: that the undefined callback property-name still exists on the window object (for IE<9)
|
||||
try {
|
||||
delete window[img.callbackname]; // for all browser that support this
|
||||
} catch(ex) {}
|
||||
if (img.script && img.script.parentNode) {
|
||||
img.script.setAttribute("src", "about:blank"); // try to cancel running request
|
||||
img.script.parentNode.removeChild(img.script);
|
||||
}
|
||||
images.numLoaded++;
|
||||
images.numFailed++;
|
||||
h2clog("html2canvas: Cleaned up failed img: '" + src + "' Steps: " + images.numLoaded + " / " + images.numTotal);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// cancel any pending requests
|
||||
if(window.stop !== undefined) {
|
||||
window.stop();
|
||||
} else if(document.execCommand !== undefined) {
|
||||
document.execCommand("Stop", false);
|
||||
}
|
||||
if (document.close !== undefined) {
|
||||
document.close();
|
||||
}
|
||||
images.cleanupDone = true;
|
||||
if (!(cause && typeof cause === "string")) {
|
||||
start();
|
||||
}
|
||||
}
|
||||
},
|
||||
renderingDone: function() {
|
||||
if (timeoutTimer) {
|
||||
window.clearTimeout(timeoutTimer);
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
if (options.timeout > 0) {
|
||||
timeoutTimer = window.setTimeout(methods.cleanupDOM, options.timeout);
|
||||
}
|
||||
h2clog('html2canvas: Preload starts: finding background-images');
|
||||
images.firstRun = true;
|
||||
|
||||
getImages( element );
|
||||
|
||||
h2clog('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 ) {
|
||||
start();
|
||||
}
|
||||
|
||||
return methods;
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
62
src/Proxy.js
Normal file
62
src/Proxy.js
Normal file
@ -0,0 +1,62 @@
|
||||
/* @flow */
|
||||
'use strict';
|
||||
|
||||
import type Options from './index';
|
||||
|
||||
import FEATURES from './Feature';
|
||||
|
||||
export const Proxy = (src: string, options: Options): Promise<string> => {
|
||||
if (!options.proxy) {
|
||||
return Promise.reject(__DEV__ ? 'No proxy defined' : null);
|
||||
}
|
||||
const proxy = options.proxy;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const responseType =
|
||||
FEATURES.SUPPORT_CORS_XHR && FEATURES.SUPPORT_RESPONSE_TYPE ? 'blob' : 'text';
|
||||
const xhr = FEATURES.SUPPORT_CORS_XHR ? new XMLHttpRequest() : new XDomainRequest();
|
||||
xhr.onload = () => {
|
||||
if (xhr instanceof XMLHttpRequest) {
|
||||
if (xhr.status === 200) {
|
||||
if (responseType === 'text') {
|
||||
resolve(xhr.response);
|
||||
} else {
|
||||
const reader = new FileReader();
|
||||
// $FlowFixMe
|
||||
reader.addEventListener('load', () => resolve(reader.result), false);
|
||||
// $FlowFixMe
|
||||
reader.addEventListener('error', e => reject(e), false);
|
||||
reader.readAsDataURL(xhr.response);
|
||||
}
|
||||
} else {
|
||||
reject(
|
||||
__DEV__
|
||||
? `Failed to proxy resource ${src.substring(
|
||||
0,
|
||||
256
|
||||
)} with status code ${xhr.status}`
|
||||
: ''
|
||||
);
|
||||
}
|
||||
} else {
|
||||
resolve(xhr.responseText);
|
||||
}
|
||||
};
|
||||
|
||||
xhr.onerror = reject;
|
||||
xhr.open('GET', `${proxy}?url=${encodeURIComponent(src)}&responseType=${responseType}`);
|
||||
|
||||
if (responseType !== 'text' && xhr instanceof XMLHttpRequest) {
|
||||
xhr.responseType = responseType;
|
||||
}
|
||||
|
||||
if (options.imageTimeout) {
|
||||
const timeout = options.imageTimeout;
|
||||
xhr.timeout = timeout;
|
||||
xhr.ontimeout = () =>
|
||||
reject(__DEV__ ? `Timed out (${timeout}ms) proxying ${src.substring(0, 256)}` : '');
|
||||
}
|
||||
|
||||
xhr.send();
|
||||
});
|
||||
};
|
81
src/Queue.js
81
src/Queue.js
@ -1,81 +0,0 @@
|
||||
/*
|
||||
html2canvas @VERSION@ <http://html2canvas.hertzen.com>
|
||||
Copyright (c) 2011 Niklas von Hertzen. All rights reserved.
|
||||
http://www.twitter.com/niklasvh
|
||||
|
||||
Released under MIT License
|
||||
*/
|
||||
function h2cRenderContext(width, height) {
|
||||
var storage = [];
|
||||
return {
|
||||
storage: storage,
|
||||
width: width,
|
||||
height: height,
|
||||
fillRect: function () {
|
||||
storage.push({
|
||||
type: "function",
|
||||
name: "fillRect",
|
||||
'arguments': arguments
|
||||
});
|
||||
},
|
||||
drawShape: function() {
|
||||
|
||||
var shape = [];
|
||||
|
||||
storage.push({
|
||||
type: "function",
|
||||
name: "drawShape",
|
||||
'arguments': shape
|
||||
});
|
||||
|
||||
return {
|
||||
moveTo: function() {
|
||||
shape.push({
|
||||
name: "moveTo",
|
||||
'arguments': arguments
|
||||
});
|
||||
},
|
||||
lineTo: function() {
|
||||
shape.push({
|
||||
name: "lineTo",
|
||||
'arguments': arguments
|
||||
});
|
||||
},
|
||||
bezierCurveTo: function() {
|
||||
shape.push({
|
||||
name: "bezierCurveTo",
|
||||
'arguments': arguments
|
||||
});
|
||||
},
|
||||
quadraticCurveTo: function() {
|
||||
shape.push({
|
||||
name: "quadraticCurveTo",
|
||||
'arguments': arguments
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
},
|
||||
drawImage: function () {
|
||||
storage.push({
|
||||
type: "function",
|
||||
name: "drawImage",
|
||||
'arguments': arguments
|
||||
});
|
||||
},
|
||||
fillText: function () {
|
||||
storage.push({
|
||||
type: "function",
|
||||
name: "fillText",
|
||||
'arguments': arguments
|
||||
});
|
||||
},
|
||||
setVariable: function (variable, value) {
|
||||
storage.push({
|
||||
type: "variable",
|
||||
name: variable,
|
||||
'arguments': value
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
436
src/Renderer.js
436
src/Renderer.js
@ -1,66 +1,408 @@
|
||||
/*
|
||||
html2canvas @VERSION@ <http://html2canvas.hertzen.com>
|
||||
Copyright (c) 2011 Niklas von Hertzen. All rights reserved.
|
||||
http://www.twitter.com/niklasvh
|
||||
/* @flow */
|
||||
'use strict';
|
||||
|
||||
Released under MIT License
|
||||
*/
|
||||
_html2canvas.Renderer = function(parseQueue, options){
|
||||
import type Color from './Color';
|
||||
import type {Path} from './drawing/Path';
|
||||
import type Size from './drawing/Size';
|
||||
import type Logger from './Logger';
|
||||
|
||||
import type {BackgroundImage} from './parsing/background';
|
||||
import type {Border, BorderSide} from './parsing/border';
|
||||
import type {Font} from './parsing/font';
|
||||
import type {TextDecoration} from './parsing/textDecoration';
|
||||
import type {TextShadow} from './parsing/textShadow';
|
||||
import type {Matrix} from './parsing/transform';
|
||||
|
||||
var queue = [];
|
||||
import type {BoundCurves} from './Bounds';
|
||||
import type {Gradient} from './Gradient';
|
||||
import type {ResourceStore, ImageElement} from './ResourceLoader';
|
||||
import type NodeContainer from './NodeContainer';
|
||||
import type StackingContext from './StackingContext';
|
||||
import type {TextBounds} from './TextBounds';
|
||||
|
||||
function sortZ(zStack){
|
||||
var subStacks = [],
|
||||
stackValues = [],
|
||||
zStackChildren = zStack.children,
|
||||
s,
|
||||
i,
|
||||
stackLen,
|
||||
zValue,
|
||||
zLen,
|
||||
stackChild,
|
||||
b,
|
||||
subStackLen;
|
||||
import {Bounds, parsePathForBorder, calculateContentBox, calculatePaddingBoxPath} from './Bounds';
|
||||
import {FontMetrics} from './Font';
|
||||
import {parseGradient} from './Gradient';
|
||||
import TextContainer from './TextContainer';
|
||||
|
||||
import {
|
||||
calculateBackgroungPositioningArea,
|
||||
calculateBackgroungPaintingArea,
|
||||
calculateBackgroundPosition,
|
||||
calculateBackgroundRepeatPath,
|
||||
calculateBackgroundSize
|
||||
} from './parsing/background';
|
||||
import {BORDER_STYLE} from './parsing/border';
|
||||
|
||||
for (s = 0, zLen = zStackChildren.length; s < zLen; s+=1){
|
||||
export type RenderOptions = {
|
||||
scale: number,
|
||||
backgroundColor: ?Color,
|
||||
imageStore: ResourceStore,
|
||||
fontMetrics: FontMetrics,
|
||||
logger: Logger,
|
||||
x: number,
|
||||
y: number,
|
||||
width: number,
|
||||
height: number
|
||||
};
|
||||
|
||||
stackChild = zStackChildren[s];
|
||||
export interface RenderTarget<Output> {
|
||||
clip(clipPaths: Array<Path>, callback: () => void): void,
|
||||
|
||||
if (stackChild.children && stackChild.children.length > 0){
|
||||
subStacks.push(stackChild);
|
||||
stackValues.push(stackChild.zindex);
|
||||
}else{
|
||||
queue.push(stackChild);
|
||||
drawImage(image: ImageElement, source: Bounds, destination: Bounds): void,
|
||||
|
||||
drawShape(path: Path, color: Color): void,
|
||||
|
||||
fill(color: Color): void,
|
||||
|
||||
getTarget(): Promise<Output>,
|
||||
|
||||
rectangle(x: number, y: number, width: number, height: number, color: Color): void,
|
||||
|
||||
render(options: RenderOptions): void,
|
||||
|
||||
renderLinearGradient(bounds: Bounds, gradient: Gradient): void,
|
||||
|
||||
renderRepeat(
|
||||
path: Path,
|
||||
image: ImageElement,
|
||||
imageSize: Size,
|
||||
offsetX: number,
|
||||
offsetY: number
|
||||
): void,
|
||||
|
||||
renderTextNode(
|
||||
textBounds: Array<TextBounds>,
|
||||
color: Color,
|
||||
font: Font,
|
||||
textDecoration: TextDecoration | null,
|
||||
textShadows: Array<TextShadow> | null
|
||||
): void,
|
||||
|
||||
setOpacity(opacity: number): void,
|
||||
|
||||
transform(offsetX: number, offsetY: number, matrix: Matrix, callback: () => void): void
|
||||
}
|
||||
|
||||
export default class Renderer {
|
||||
target: RenderTarget<*>;
|
||||
options: RenderOptions;
|
||||
_opacity: ?number;
|
||||
|
||||
constructor(target: RenderTarget<*>, options: RenderOptions) {
|
||||
this.target = target;
|
||||
this.options = options;
|
||||
target.render(options);
|
||||
}
|
||||
|
||||
renderNode(container: NodeContainer) {
|
||||
if (container.isVisible()) {
|
||||
this.renderNodeBackgroundAndBorders(container);
|
||||
this.renderNodeContent(container);
|
||||
}
|
||||
}
|
||||
|
||||
renderNodeContent(container: NodeContainer) {
|
||||
const callback = () => {
|
||||
if (container.childNodes.length) {
|
||||
container.childNodes.forEach(child => {
|
||||
if (child instanceof TextContainer) {
|
||||
const style = child.parent.style;
|
||||
this.target.renderTextNode(
|
||||
child.bounds,
|
||||
style.color,
|
||||
style.font,
|
||||
style.textDecoration,
|
||||
style.textShadow
|
||||
);
|
||||
} else {
|
||||
this.target.drawShape(child, container.style.color);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
stackValues.sort(function(a, b) {
|
||||
return a - b;
|
||||
});
|
||||
|
||||
for (i = 0, stackLen = stackValues.length; i < stackLen; i+=1){
|
||||
zValue = stackValues[i];
|
||||
for (b = 0, subStackLen = subStacks.length; b <= subStackLen; b+=1){
|
||||
|
||||
if (subStacks[b].zindex === zValue){
|
||||
stackChild = subStacks.splice(b, 1);
|
||||
sortZ(stackChild[0]);
|
||||
break;
|
||||
|
||||
if (container.image) {
|
||||
const image = this.options.imageStore.get(container.image);
|
||||
if (image) {
|
||||
const contentBox = calculateContentBox(
|
||||
container.bounds,
|
||||
container.style.padding,
|
||||
container.style.border
|
||||
);
|
||||
const width =
|
||||
typeof image.width === 'number' && image.width > 0
|
||||
? image.width
|
||||
: contentBox.width;
|
||||
const height =
|
||||
typeof image.height === 'number' && image.height > 0
|
||||
? image.height
|
||||
: contentBox.height;
|
||||
if (width > 0 && height > 0) {
|
||||
this.target.clip([calculatePaddingBoxPath(container.curvedBounds)], () => {
|
||||
this.target.drawImage(
|
||||
image,
|
||||
new Bounds(0, 0, width, height),
|
||||
contentBox
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
const paths = container.getClipPaths();
|
||||
if (paths.length) {
|
||||
this.target.clip(paths, callback);
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
renderNodeBackgroundAndBorders(container: NodeContainer) {
|
||||
const HAS_BACKGROUND =
|
||||
!container.style.background.backgroundColor.isTransparent() ||
|
||||
container.style.background.backgroundImage.length;
|
||||
|
||||
sortZ(parseQueue.zIndex);
|
||||
if ( typeof options._renderer._create !== "function" ) {
|
||||
throw new Error("Invalid renderer defined");
|
||||
const renderableBorders = container.style.border.filter(
|
||||
border =>
|
||||
border.borderStyle !== BORDER_STYLE.NONE && !border.borderColor.isTransparent()
|
||||
);
|
||||
|
||||
const callback = () => {
|
||||
const backgroundPaintingArea = calculateBackgroungPaintingArea(
|
||||
container.curvedBounds,
|
||||
container.style.background.backgroundClip
|
||||
);
|
||||
|
||||
if (HAS_BACKGROUND) {
|
||||
this.target.clip([backgroundPaintingArea], () => {
|
||||
if (!container.style.background.backgroundColor.isTransparent()) {
|
||||
this.target.fill(container.style.background.backgroundColor);
|
||||
}
|
||||
|
||||
this.renderBackgroundImage(container);
|
||||
});
|
||||
}
|
||||
|
||||
renderableBorders.forEach((border, side) => {
|
||||
this.renderBorder(border, side, container.curvedBounds);
|
||||
});
|
||||
};
|
||||
|
||||
if (HAS_BACKGROUND || renderableBorders.length) {
|
||||
const paths = container.parent ? container.parent.getClipPaths() : [];
|
||||
if (paths.length) {
|
||||
this.target.clip(paths, callback);
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
}
|
||||
return options._renderer._create( parseQueue, options, document, queue, _html2canvas );
|
||||
|
||||
renderBackgroundImage(container: NodeContainer) {
|
||||
container.style.background.backgroundImage.slice(0).reverse().forEach(backgroundImage => {
|
||||
if (backgroundImage.source.method === 'url' && backgroundImage.source.args.length) {
|
||||
this.renderBackgroundRepeat(container, backgroundImage);
|
||||
} else {
|
||||
const gradient = parseGradient(backgroundImage.source, container.bounds);
|
||||
if (gradient) {
|
||||
const bounds = container.bounds;
|
||||
this.target.renderLinearGradient(bounds, gradient);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
renderBackgroundRepeat(container: NodeContainer, background: BackgroundImage) {
|
||||
const image = this.options.imageStore.get(background.source.args[0]);
|
||||
if (image) {
|
||||
const backgroundPositioningArea = calculateBackgroungPositioningArea(
|
||||
container.style.background.backgroundOrigin,
|
||||
container.bounds,
|
||||
container.style.padding,
|
||||
container.style.border
|
||||
);
|
||||
const backgroundImageSize = calculateBackgroundSize(
|
||||
background,
|
||||
image,
|
||||
backgroundPositioningArea
|
||||
);
|
||||
const position = calculateBackgroundPosition(
|
||||
background.position,
|
||||
backgroundImageSize,
|
||||
backgroundPositioningArea
|
||||
);
|
||||
const path = calculateBackgroundRepeatPath(
|
||||
background,
|
||||
position,
|
||||
backgroundImageSize,
|
||||
backgroundPositioningArea,
|
||||
container.bounds
|
||||
);
|
||||
|
||||
const offsetX = Math.round(backgroundPositioningArea.left + position.x);
|
||||
const offsetY = Math.round(backgroundPositioningArea.top + position.y);
|
||||
this.target.renderRepeat(path, image, backgroundImageSize, offsetX, offsetY);
|
||||
}
|
||||
}
|
||||
|
||||
renderBorder(border: Border, side: BorderSide, curvePoints: BoundCurves) {
|
||||
this.target.drawShape(parsePathForBorder(curvePoints, side), border.borderColor);
|
||||
}
|
||||
|
||||
renderStack(stack: StackingContext) {
|
||||
if (stack.container.isVisible()) {
|
||||
const opacity = stack.getOpacity();
|
||||
if (opacity !== this._opacity) {
|
||||
this.target.setOpacity(stack.getOpacity());
|
||||
this._opacity = opacity;
|
||||
}
|
||||
|
||||
const transform = stack.container.style.transform;
|
||||
if (transform !== null) {
|
||||
this.target.transform(
|
||||
stack.container.bounds.left + transform.transformOrigin[0].value,
|
||||
stack.container.bounds.top + transform.transformOrigin[1].value,
|
||||
transform.transform,
|
||||
() => this.renderStackContent(stack)
|
||||
);
|
||||
} else {
|
||||
this.renderStackContent(stack);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
renderStackContent(stack: StackingContext) {
|
||||
const [
|
||||
negativeZIndex,
|
||||
zeroOrAutoZIndexOrTransformedOrOpacity,
|
||||
positiveZIndex,
|
||||
nonPositionedFloats,
|
||||
nonPositionedInlineLevel
|
||||
] = splitStackingContexts(stack);
|
||||
const [inlineLevel, nonInlineLevel] = splitDescendants(stack);
|
||||
|
||||
// https://www.w3.org/TR/css-position-3/#painting-order
|
||||
// 1. the background and borders of the element forming the stacking context.
|
||||
this.renderNodeBackgroundAndBorders(stack.container);
|
||||
// 2. the child stacking contexts with negative stack levels (most negative first).
|
||||
negativeZIndex.sort(sortByZIndex).forEach(this.renderStack, this);
|
||||
// 3. For all its in-flow, non-positioned, block-level descendants in tree order:
|
||||
this.renderNodeContent(stack.container);
|
||||
nonInlineLevel.forEach(this.renderNode, this);
|
||||
// 4. All non-positioned floating descendants, in tree order. For each one of these,
|
||||
// treat the element as if it created a new stacking context, but any positioned descendants and descendants
|
||||
// which actually create a new stacking context should be considered part of the parent stacking context,
|
||||
// not this new one.
|
||||
nonPositionedFloats.forEach(this.renderStack, this);
|
||||
// 5. the in-flow, inline-level, non-positioned descendants, including inline tables and inline blocks.
|
||||
nonPositionedInlineLevel.forEach(this.renderStack, this);
|
||||
inlineLevel.forEach(this.renderNode, this);
|
||||
// 6. All positioned, opacity or transform descendants, in tree order that fall into the following categories:
|
||||
// All positioned descendants with 'z-index: auto' or 'z-index: 0', in tree order.
|
||||
// For those with 'z-index: auto', treat the element as if it created a new stacking context,
|
||||
// but any positioned descendants and descendants which actually create a new stacking context should be
|
||||
// considered part of the parent stacking context, not this new one. For those with 'z-index: 0',
|
||||
// treat the stacking context generated atomically.
|
||||
//
|
||||
// All opacity descendants with opacity less than 1
|
||||
//
|
||||
// All transform descendants with transform other than none
|
||||
zeroOrAutoZIndexOrTransformedOrOpacity.forEach(this.renderStack, this);
|
||||
// 7. Stacking contexts formed by positioned descendants with z-indices greater than or equal to 1 in z-index
|
||||
// order (smallest first) then tree order.
|
||||
positiveZIndex.sort(sortByZIndex).forEach(this.renderStack, this);
|
||||
}
|
||||
|
||||
render(stack: StackingContext): Promise<*> {
|
||||
if (this.options.backgroundColor) {
|
||||
this.target.rectangle(
|
||||
0,
|
||||
0,
|
||||
this.options.width,
|
||||
this.options.height,
|
||||
this.options.backgroundColor
|
||||
);
|
||||
}
|
||||
this.renderStack(stack);
|
||||
const target = this.target.getTarget();
|
||||
if (__DEV__) {
|
||||
return target.then(output => {
|
||||
this.options.logger.log(`Render completed`);
|
||||
return output;
|
||||
});
|
||||
}
|
||||
return target;
|
||||
}
|
||||
}
|
||||
|
||||
const splitDescendants = (stack: StackingContext): [Array<NodeContainer>, Array<NodeContainer>] => {
|
||||
const inlineLevel = [];
|
||||
const nonInlineLevel = [];
|
||||
|
||||
const length = stack.children.length;
|
||||
for (let i = 0; i < length; i++) {
|
||||
let child = stack.children[i];
|
||||
if (child.isInlineLevel()) {
|
||||
inlineLevel.push(child);
|
||||
} else {
|
||||
nonInlineLevel.push(child);
|
||||
}
|
||||
}
|
||||
return [inlineLevel, nonInlineLevel];
|
||||
};
|
||||
|
||||
const splitStackingContexts = (
|
||||
stack: StackingContext
|
||||
): [
|
||||
Array<StackingContext>,
|
||||
Array<StackingContext>,
|
||||
Array<StackingContext>,
|
||||
Array<StackingContext>,
|
||||
Array<StackingContext>
|
||||
] => {
|
||||
const negativeZIndex = [];
|
||||
const zeroOrAutoZIndexOrTransformedOrOpacity = [];
|
||||
const positiveZIndex = [];
|
||||
const nonPositionedFloats = [];
|
||||
const nonPositionedInlineLevel = [];
|
||||
const length = stack.contexts.length;
|
||||
for (let i = 0; i < length; i++) {
|
||||
let child = stack.contexts[i];
|
||||
if (
|
||||
child.container.isPositioned() ||
|
||||
child.container.style.opacity < 1 ||
|
||||
child.container.isTransformed()
|
||||
) {
|
||||
if (child.container.style.zIndex.order < 0) {
|
||||
negativeZIndex.push(child);
|
||||
} else if (child.container.style.zIndex.order > 0) {
|
||||
positiveZIndex.push(child);
|
||||
} else {
|
||||
zeroOrAutoZIndexOrTransformedOrOpacity.push(child);
|
||||
}
|
||||
} else {
|
||||
if (child.container.isFloating()) {
|
||||
nonPositionedFloats.push(child);
|
||||
} else {
|
||||
nonPositionedInlineLevel.push(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
return [
|
||||
negativeZIndex,
|
||||
zeroOrAutoZIndexOrTransformedOrOpacity,
|
||||
positiveZIndex,
|
||||
nonPositionedFloats,
|
||||
nonPositionedInlineLevel
|
||||
];
|
||||
};
|
||||
|
||||
const sortByZIndex = (a: StackingContext, b: StackingContext): number => {
|
||||
if (a.container.style.zIndex.order > b.container.style.zIndex.order) {
|
||||
return 1;
|
||||
} else if (a.container.style.zIndex.order < b.container.style.zIndex.order) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return a.container.index > b.container.index ? 1 : -1;
|
||||
};
|
||||
|
249
src/ResourceLoader.js
Normal file
249
src/ResourceLoader.js
Normal file
@ -0,0 +1,249 @@
|
||||
/* @flow */
|
||||
'use strict';
|
||||
|
||||
import type Options from './index';
|
||||
import type Logger from './Logger';
|
||||
|
||||
export type ImageElement = Image | HTMLCanvasElement;
|
||||
export type Resource = ImageElement;
|
||||
type ResourceCache = {[string]: Promise<Resource>};
|
||||
|
||||
import FEATURES from './Feature';
|
||||
import {Proxy} from './Proxy';
|
||||
|
||||
export default class ResourceLoader {
|
||||
origin: string;
|
||||
options: Options;
|
||||
_link: HTMLAnchorElement;
|
||||
cache: ResourceCache;
|
||||
logger: Logger;
|
||||
_index: number;
|
||||
_window: WindowProxy;
|
||||
|
||||
constructor(options: Options, logger: Logger, window: WindowProxy) {
|
||||
this.options = options;
|
||||
this._window = window;
|
||||
this.origin = this.getOrigin(window.location.href);
|
||||
this.cache = {};
|
||||
this.logger = logger;
|
||||
this._index = 0;
|
||||
}
|
||||
|
||||
loadImage(src: string): ?string {
|
||||
if (this.hasResourceInCache(src)) {
|
||||
return src;
|
||||
}
|
||||
|
||||
if (isSVG(src)) {
|
||||
if (this.options.allowTaint === true || FEATURES.SUPPORT_SVG_DRAWING) {
|
||||
return this.addImage(src, src, false);
|
||||
}
|
||||
} else {
|
||||
if (
|
||||
this.options.allowTaint === true ||
|
||||
isInlineBase64Image(src) ||
|
||||
this.isSameOrigin(src)
|
||||
) {
|
||||
return this.addImage(src, src, false);
|
||||
} else if (!this.isSameOrigin(src)) {
|
||||
if (typeof this.options.proxy === 'string') {
|
||||
this.cache[src] = Proxy(src, this.options).then(src =>
|
||||
loadImage(src, this.options.imageTimeout || 0)
|
||||
);
|
||||
return src;
|
||||
} else if (this.options.useCORS === true && FEATURES.SUPPORT_CORS_IMAGES) {
|
||||
return this.addImage(src, src, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inlineImage(src: string): Promise<Resource> {
|
||||
if (isInlineImage(src)) {
|
||||
return loadImage(src, this.options.imageTimeout || 0);
|
||||
}
|
||||
if (this.hasResourceInCache(src)) {
|
||||
return this.cache[src];
|
||||
}
|
||||
if (!this.isSameOrigin(src) && typeof this.options.proxy === 'string') {
|
||||
return (this.cache[src] = Proxy(src, this.options).then(src =>
|
||||
loadImage(src, this.options.imageTimeout || 0)
|
||||
));
|
||||
}
|
||||
|
||||
return this.xhrImage(src);
|
||||
}
|
||||
|
||||
xhrImage(src: string): Promise<Resource> {
|
||||
this.cache[src] = new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.onreadystatechange = () => {
|
||||
if (xhr.readyState === 4) {
|
||||
if (xhr.status !== 200) {
|
||||
reject(
|
||||
`Failed to fetch image ${src.substring(
|
||||
0,
|
||||
256
|
||||
)} with status code ${xhr.status}`
|
||||
);
|
||||
} else {
|
||||
const reader = new FileReader();
|
||||
reader.addEventListener(
|
||||
'load',
|
||||
() => {
|
||||
// $FlowFixMe
|
||||
const result: string = reader.result;
|
||||
resolve(result);
|
||||
},
|
||||
false
|
||||
);
|
||||
reader.addEventListener('error', (e: Event) => reject(e), false);
|
||||
reader.readAsDataURL(xhr.response);
|
||||
}
|
||||
}
|
||||
};
|
||||
xhr.responseType = 'blob';
|
||||
if (this.options.imageTimeout) {
|
||||
const timeout = this.options.imageTimeout;
|
||||
xhr.timeout = timeout;
|
||||
xhr.ontimeout = () =>
|
||||
reject(
|
||||
__DEV__ ? `Timed out (${timeout}ms) fetching ${src.substring(0, 256)}` : ''
|
||||
);
|
||||
}
|
||||
xhr.open('GET', src, true);
|
||||
xhr.send();
|
||||
}).then(src => loadImage(src, this.options.imageTimeout || 0));
|
||||
|
||||
return this.cache[src];
|
||||
}
|
||||
|
||||
loadCanvas(node: HTMLCanvasElement): string {
|
||||
const key = String(this._index++);
|
||||
this.cache[key] = Promise.resolve(node);
|
||||
return key;
|
||||
}
|
||||
|
||||
hasResourceInCache(key: string): boolean {
|
||||
return typeof this.cache[key] !== 'undefined';
|
||||
}
|
||||
|
||||
addImage(key: string, src: string, useCORS: boolean): string {
|
||||
if (__DEV__) {
|
||||
this.logger.log(`Added image ${key.substring(0, 256)}`);
|
||||
}
|
||||
|
||||
const imageLoadHandler = (supportsDataImages: boolean): Promise<Image> =>
|
||||
new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
img.onload = () => resolve(img);
|
||||
//ios safari 10.3 taints canvas with data urls unless crossOrigin is set to anonymous
|
||||
if (!supportsDataImages || useCORS) {
|
||||
img.crossOrigin = 'anonymous';
|
||||
}
|
||||
|
||||
img.onerror = reject;
|
||||
img.src = src;
|
||||
if (img.complete === true) {
|
||||
// Inline XML images may fail to parse, throwing an Error later on
|
||||
setTimeout(() => {
|
||||
resolve(img);
|
||||
}, 500);
|
||||
}
|
||||
if (this.options.imageTimeout) {
|
||||
const timeout = this.options.imageTimeout;
|
||||
setTimeout(
|
||||
() =>
|
||||
reject(
|
||||
__DEV__
|
||||
? `Timed out (${timeout}ms) fetching ${src.substring(0, 256)}`
|
||||
: ''
|
||||
),
|
||||
timeout
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
this.cache[key] =
|
||||
isInlineBase64Image(src) && !isSVG(src)
|
||||
? // $FlowFixMe
|
||||
FEATURES.SUPPORT_BASE64_DRAWING(src).then(imageLoadHandler)
|
||||
: imageLoadHandler(true);
|
||||
return key;
|
||||
}
|
||||
|
||||
isSameOrigin(url: string): boolean {
|
||||
return this.getOrigin(url) === this.origin;
|
||||
}
|
||||
|
||||
getOrigin(url: string): string {
|
||||
const link = this._link || (this._link = this._window.document.createElement('a'));
|
||||
link.href = url;
|
||||
link.href = link.href; // IE9, LOL! - http://jsfiddle.net/niklasvh/2e48b/
|
||||
return link.protocol + link.hostname + link.port;
|
||||
}
|
||||
|
||||
ready(): Promise<ResourceStore> {
|
||||
const keys: Array<string> = Object.keys(this.cache);
|
||||
const values: Array<Promise<?Resource>> = keys.map(str =>
|
||||
this.cache[str].catch(e => {
|
||||
if (__DEV__) {
|
||||
this.logger.log(`Unable to load image`, e);
|
||||
}
|
||||
return null;
|
||||
})
|
||||
);
|
||||
return Promise.all(values).then((images: Array<?Resource>) => {
|
||||
if (__DEV__) {
|
||||
this.logger.log(`Finished loading ${images.length} images`, images);
|
||||
}
|
||||
return new ResourceStore(keys, images);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class ResourceStore {
|
||||
_keys: Array<string>;
|
||||
_resources: Array<?Resource>;
|
||||
|
||||
constructor(keys: Array<string>, resources: Array<?Resource>) {
|
||||
this._keys = keys;
|
||||
this._resources = resources;
|
||||
}
|
||||
|
||||
get(key: string): ?Resource {
|
||||
const index = this._keys.indexOf(key);
|
||||
return index === -1 ? null : this._resources[index];
|
||||
}
|
||||
}
|
||||
|
||||
const INLINE_SVG = /^data:image\/svg\+xml/i;
|
||||
const INLINE_BASE64 = /^data:image\/.*;base64,/i;
|
||||
const INLINE_IMG = /^data:image\/.*/i;
|
||||
|
||||
const isInlineImage = (src: string): boolean => INLINE_IMG.test(src);
|
||||
const isInlineBase64Image = (src: string): boolean => INLINE_BASE64.test(src);
|
||||
|
||||
const isSVG = (src: string): boolean =>
|
||||
src.substr(-3).toLowerCase() === 'svg' || INLINE_SVG.test(src);
|
||||
|
||||
const loadImage = (src: string, timeout: number): Promise<Image> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
img.onload = () => resolve(img);
|
||||
img.onerror = reject;
|
||||
img.src = src;
|
||||
if (img.complete === true) {
|
||||
// Inline XML images may fail to parse, throwing an Error later on
|
||||
setTimeout(() => {
|
||||
resolve(img);
|
||||
}, 500);
|
||||
}
|
||||
if (timeout) {
|
||||
setTimeout(
|
||||
() => reject(__DEV__ ? `Timed out (${timeout}ms) loading image` : ''),
|
||||
timeout
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
37
src/StackingContext.js
Normal file
37
src/StackingContext.js
Normal file
@ -0,0 +1,37 @@
|
||||
/* @flow */
|
||||
'use strict';
|
||||
|
||||
import NodeContainer from './NodeContainer';
|
||||
import {POSITION} from './parsing/position';
|
||||
|
||||
export default class StackingContext {
|
||||
container: NodeContainer;
|
||||
parent: ?StackingContext;
|
||||
contexts: Array<StackingContext>;
|
||||
children: Array<NodeContainer>;
|
||||
treatAsRealStackingContext: boolean;
|
||||
|
||||
constructor(
|
||||
container: NodeContainer,
|
||||
parent: ?StackingContext,
|
||||
treatAsRealStackingContext: boolean
|
||||
) {
|
||||
this.container = container;
|
||||
this.parent = parent;
|
||||
this.contexts = [];
|
||||
this.children = [];
|
||||
this.treatAsRealStackingContext = treatAsRealStackingContext;
|
||||
}
|
||||
|
||||
getOpacity(): number {
|
||||
return this.parent
|
||||
? this.container.style.opacity * this.parent.getOpacity()
|
||||
: this.container.style.opacity;
|
||||
}
|
||||
|
||||
getRealParentStackingContext(): StackingContext {
|
||||
return !this.parent || this.treatAsRealStackingContext
|
||||
? this
|
||||
: this.parent.getRealParentStackingContext();
|
||||
}
|
||||
}
|
129
src/TextBounds.js
Normal file
129
src/TextBounds.js
Normal file
@ -0,0 +1,129 @@
|
||||
/* @flow */
|
||||
'use strict';
|
||||
|
||||
import {ucs2} from 'punycode';
|
||||
import type NodeContainer from './NodeContainer';
|
||||
import {Bounds, parseBounds} from './Bounds';
|
||||
import {TEXT_DECORATION} from './parsing/textDecoration';
|
||||
|
||||
import FEATURES from './Feature';
|
||||
|
||||
const UNICODE = /[^\u0000-\u00ff]/;
|
||||
|
||||
const hasUnicodeCharacters = (text: string): boolean => UNICODE.test(text);
|
||||
|
||||
const encodeCodePoint = (codePoint: number): string => ucs2.encode([codePoint]);
|
||||
|
||||
export class TextBounds {
|
||||
text: string;
|
||||
bounds: Bounds;
|
||||
|
||||
constructor(text: string, bounds: Bounds) {
|
||||
this.text = text;
|
||||
this.bounds = bounds;
|
||||
}
|
||||
}
|
||||
|
||||
export const parseTextBounds = (
|
||||
value: string,
|
||||
parent: NodeContainer,
|
||||
node: Text
|
||||
): Array<TextBounds> => {
|
||||
const codePoints = ucs2.decode(value);
|
||||
const letterRendering = parent.style.letterSpacing !== 0 || hasUnicodeCharacters(value);
|
||||
const textList = letterRendering ? codePoints.map(encodeCodePoint) : splitWords(codePoints);
|
||||
const length = textList.length;
|
||||
const defaultView = node.parentNode ? node.parentNode.ownerDocument.defaultView : null;
|
||||
const scrollX = defaultView ? defaultView.pageXOffset : 0;
|
||||
const scrollY = defaultView ? defaultView.pageYOffset : 0;
|
||||
const textBounds = [];
|
||||
let offset = 0;
|
||||
for (let i = 0; i < length; i++) {
|
||||
let text = textList[i];
|
||||
if (parent.style.textDecoration !== TEXT_DECORATION.NONE || text.trim().length > 0) {
|
||||
if (FEATURES.SUPPORT_RANGE_BOUNDS) {
|
||||
textBounds.push(
|
||||
new TextBounds(
|
||||
text,
|
||||
getRangeBounds(node, offset, text.length, scrollX, scrollY)
|
||||
)
|
||||
);
|
||||
} else {
|
||||
const replacementNode = node.splitText(text.length);
|
||||
textBounds.push(new TextBounds(text, getWrapperBounds(node, scrollX, scrollY)));
|
||||
node = replacementNode;
|
||||
}
|
||||
} else if (!FEATURES.SUPPORT_RANGE_BOUNDS) {
|
||||
node = node.splitText(text.length);
|
||||
}
|
||||
offset += text.length;
|
||||
}
|
||||
return textBounds;
|
||||
};
|
||||
|
||||
const getWrapperBounds = (node: Text, scrollX: number, scrollY: number): Bounds => {
|
||||
const wrapper = node.ownerDocument.createElement('html2canvaswrapper');
|
||||
wrapper.appendChild(node.cloneNode(true));
|
||||
const parentNode = node.parentNode;
|
||||
if (parentNode) {
|
||||
parentNode.replaceChild(wrapper, node);
|
||||
const bounds = parseBounds(wrapper, scrollX, scrollY);
|
||||
if (wrapper.firstChild) {
|
||||
parentNode.replaceChild(wrapper.firstChild, wrapper);
|
||||
}
|
||||
return bounds;
|
||||
}
|
||||
return new Bounds(0, 0, 0, 0);
|
||||
};
|
||||
|
||||
const getRangeBounds = (
|
||||
node: Text,
|
||||
offset: number,
|
||||
length: number,
|
||||
scrollX: number,
|
||||
scrollY: number
|
||||
): Bounds => {
|
||||
const range = node.ownerDocument.createRange();
|
||||
range.setStart(node, offset);
|
||||
range.setEnd(node, offset + length);
|
||||
return Bounds.fromClientRect(range.getBoundingClientRect(), scrollX, scrollY);
|
||||
};
|
||||
|
||||
const splitWords = (codePoints: Array<number>): Array<string> => {
|
||||
const words = [];
|
||||
let i = 0;
|
||||
let onWordBoundary = false;
|
||||
let word;
|
||||
while (codePoints.length) {
|
||||
if (isWordBoundary(codePoints[i]) === onWordBoundary) {
|
||||
word = codePoints.splice(0, i);
|
||||
if (word.length) {
|
||||
words.push(ucs2.encode(word));
|
||||
}
|
||||
onWordBoundary = !onWordBoundary;
|
||||
i = 0;
|
||||
} else {
|
||||
i++;
|
||||
}
|
||||
|
||||
if (i >= codePoints.length) {
|
||||
word = codePoints.splice(0, i);
|
||||
if (word.length) {
|
||||
words.push(ucs2.encode(word));
|
||||
}
|
||||
}
|
||||
}
|
||||
return words;
|
||||
};
|
||||
|
||||
const isWordBoundary = (characterCode: number): boolean => {
|
||||
return (
|
||||
[
|
||||
32, // <space>
|
||||
13, // \r
|
||||
10, // \n
|
||||
9, // \t
|
||||
45 // -
|
||||
].indexOf(characterCode) !== -1
|
||||
);
|
||||
};
|
48
src/TextContainer.js
Normal file
48
src/TextContainer.js
Normal file
@ -0,0 +1,48 @@
|
||||
/* @flow */
|
||||
'use strict';
|
||||
|
||||
import type NodeContainer from './NodeContainer';
|
||||
import type {TextTransform} from './parsing/textTransform';
|
||||
import type {TextBounds} from './TextBounds';
|
||||
import {TEXT_TRANSFORM} from './parsing/textTransform';
|
||||
import {parseTextBounds} from './TextBounds';
|
||||
|
||||
export default class TextContainer {
|
||||
text: string;
|
||||
parent: NodeContainer;
|
||||
bounds: Array<TextBounds>;
|
||||
|
||||
constructor(text: string, parent: NodeContainer, bounds: Array<TextBounds>) {
|
||||
this.text = text;
|
||||
this.parent = parent;
|
||||
this.bounds = bounds;
|
||||
}
|
||||
|
||||
static fromTextNode(node: Text, parent: NodeContainer) {
|
||||
const text = transform(node.data, parent.style.textTransform);
|
||||
return new TextContainer(text, parent, parseTextBounds(text, parent, node));
|
||||
}
|
||||
}
|
||||
|
||||
const CAPITALIZE = /(^|\s|:|-|\(|\))([a-z])/g;
|
||||
|
||||
const transform = (text: string, transform: TextTransform) => {
|
||||
switch (transform) {
|
||||
case TEXT_TRANSFORM.LOWERCASE:
|
||||
return text.toLowerCase();
|
||||
case TEXT_TRANSFORM.CAPITALIZE:
|
||||
return text.replace(CAPITALIZE, capitalize);
|
||||
case TEXT_TRANSFORM.UPPERCASE:
|
||||
return text.toUpperCase();
|
||||
default:
|
||||
return text;
|
||||
}
|
||||
};
|
||||
|
||||
function capitalize(m, p1, p2) {
|
||||
if (m.length > 0) {
|
||||
return p1 + p2.toUpperCase();
|
||||
}
|
||||
|
||||
return m;
|
||||
}
|
107
src/Util.js
107
src/Util.js
@ -1,98 +1,19 @@
|
||||
/*
|
||||
html2canvas @VERSION@ <http://html2canvas.hertzen.com>
|
||||
Copyright (c) 2011 Niklas von Hertzen. All rights reserved.
|
||||
http://www.twitter.com/niklasvh
|
||||
/* @flow */
|
||||
'use strict';
|
||||
|
||||
Released under MIT License
|
||||
*/
|
||||
export const contains = (bit: number, value: number): boolean => (bit & value) !== 0;
|
||||
|
||||
|
||||
html2canvas = function( elements, opts ) {
|
||||
|
||||
var queue,
|
||||
canvas,
|
||||
options = {
|
||||
// general
|
||||
logging: false,
|
||||
elements: elements,
|
||||
|
||||
// preload options
|
||||
proxy: "http://html2canvas.appspot.com/",
|
||||
timeout: 0, // no timeout
|
||||
useCORS: false, // try to load images as CORS (where available), before falling back to proxy
|
||||
allowTaint: false, // whether to allow images to taint the canvas, won't need proxy if set to true
|
||||
|
||||
// parse options
|
||||
svgRendering: false, // use svg powered rendering where available (FF11+)
|
||||
iframeDefault: "default",
|
||||
ignoreElements: "IFRAME|OBJECT|PARAM",
|
||||
useOverflow: true,
|
||||
letterRendering: false,
|
||||
|
||||
// render options
|
||||
|
||||
flashcanvas: undefined, // path to flashcanvas
|
||||
width: null,
|
||||
height: null,
|
||||
taintTest: true, // do a taint test with all images before applying to canvas
|
||||
renderer: "Canvas"
|
||||
}, renderer;
|
||||
|
||||
options = _html2canvas.Util.Extend(opts, options);
|
||||
|
||||
if (typeof options.renderer === "string" && _html2canvas.Renderer[options.renderer] !== undefined) {
|
||||
options._renderer = _html2canvas.Renderer[options.renderer]( options );
|
||||
} else if (typeof options.renderer === "function") {
|
||||
options._renderer = options.renderer( options );
|
||||
} else {
|
||||
throw("Unknown renderer");
|
||||
}
|
||||
|
||||
_html2canvas.logging = options.logging;
|
||||
options.complete = function( images ) {
|
||||
|
||||
if (typeof options.onpreloaded === "function") {
|
||||
if ( options.onpreloaded( images ) === false ) {
|
||||
return;
|
||||
}
|
||||
export const copyCSSStyles = (style: CSSStyleDeclaration, target: HTMLElement): HTMLElement => {
|
||||
// Edge does not provide value for cssText
|
||||
for (let i = style.length - 1; i >= 0; i--) {
|
||||
const property = style.item(i);
|
||||
// Safari shows pseudoelements if content is set
|
||||
if (property !== 'content') {
|
||||
target.style.setProperty(property, style.getPropertyValue(property));
|
||||
}
|
||||
queue = _html2canvas.Parse( images, options );
|
||||
|
||||
if (typeof options.onparsed === "function") {
|
||||
if ( options.onparsed( queue ) === false ) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
canvas = _html2canvas.Renderer( queue, options );
|
||||
|
||||
if (typeof options.onrendered === "function") {
|
||||
options.onrendered( canvas );
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
||||
// for pages without images, we still want this to be async, i.e. return methods before executing
|
||||
window.setTimeout( function(){
|
||||
_html2canvas.Preload( options );
|
||||
}, 0 );
|
||||
|
||||
return {
|
||||
render: function( queue, opts ) {
|
||||
return _html2canvas.Renderer( queue, _html2canvas.Util.Extend(opts, options) );
|
||||
},
|
||||
parse: function( images, opts ) {
|
||||
return _html2canvas.Parse( images, _html2canvas.Util.Extend(opts, options) );
|
||||
},
|
||||
preload: function( opts ) {
|
||||
return _html2canvas.Preload( _html2canvas.Util.Extend(opts, options) );
|
||||
},
|
||||
log: h2clog
|
||||
};
|
||||
}
|
||||
return target;
|
||||
};
|
||||
|
||||
html2canvas.log = h2clog; // for renderers
|
||||
html2canvas.Renderer = {
|
||||
Canvas: undefined // We are assuming this will be used
|
||||
};
|
||||
export const SMALL_IMAGE =
|
||||
'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
|
||||
|
141
src/Window.js
Normal file
141
src/Window.js
Normal file
@ -0,0 +1,141 @@
|
||||
/* @flow */
|
||||
'use strict';
|
||||
|
||||
import type {Options} from './index';
|
||||
|
||||
import Logger from './Logger';
|
||||
|
||||
import {NodeParser} from './NodeParser';
|
||||
import Renderer from './Renderer';
|
||||
import ForeignObjectRenderer from './renderer/ForeignObjectRenderer';
|
||||
|
||||
import Feature from './Feature';
|
||||
import {Bounds} from './Bounds';
|
||||
import {cloneWindow, DocumentCloner} from './Clone';
|
||||
import {FontMetrics} from './Font';
|
||||
import Color, {TRANSPARENT} from './Color';
|
||||
|
||||
export const renderElement = (
|
||||
element: HTMLElement,
|
||||
options: Options,
|
||||
logger: Logger
|
||||
): Promise<*> => {
|
||||
const ownerDocument = element.ownerDocument;
|
||||
|
||||
const windowBounds = new Bounds(
|
||||
options.scrollX,
|
||||
options.scrollY,
|
||||
options.windowWidth,
|
||||
options.windowHeight
|
||||
);
|
||||
|
||||
// http://www.w3.org/TR/css3-background/#special-backgrounds
|
||||
const documentBackgroundColor = ownerDocument.documentElement
|
||||
? new Color(getComputedStyle(ownerDocument.documentElement).backgroundColor)
|
||||
: TRANSPARENT;
|
||||
const bodyBackgroundColor = ownerDocument.body
|
||||
? new Color(getComputedStyle(ownerDocument.body).backgroundColor)
|
||||
: TRANSPARENT;
|
||||
|
||||
const backgroundColor =
|
||||
element === ownerDocument.documentElement
|
||||
? documentBackgroundColor.isTransparent()
|
||||
? bodyBackgroundColor.isTransparent()
|
||||
? options.backgroundColor ? new Color(options.backgroundColor) : null
|
||||
: bodyBackgroundColor
|
||||
: documentBackgroundColor
|
||||
: options.backgroundColor ? new Color(options.backgroundColor) : null;
|
||||
|
||||
return (options.foreignObjectRendering
|
||||
? // $FlowFixMe
|
||||
Feature.SUPPORT_FOREIGNOBJECT_DRAWING
|
||||
: Promise.resolve(false)).then(
|
||||
supportForeignObject =>
|
||||
supportForeignObject
|
||||
? (cloner => {
|
||||
if (__DEV__) {
|
||||
logger.log(`Document cloned, using foreignObject rendering`);
|
||||
}
|
||||
|
||||
return cloner
|
||||
.inlineFonts(ownerDocument)
|
||||
.then(() => cloner.resourceLoader.ready())
|
||||
.then(() => {
|
||||
const renderer = new ForeignObjectRenderer(cloner.documentElement);
|
||||
return renderer.render({
|
||||
backgroundColor,
|
||||
logger,
|
||||
scale: options.scale,
|
||||
x: options.x,
|
||||
y: options.y,
|
||||
width: options.width,
|
||||
height: options.height,
|
||||
windowWidth: options.windowWidth,
|
||||
windowHeight: options.windowHeight,
|
||||
scrollX: options.scrollX,
|
||||
scrollY: options.scrollY
|
||||
});
|
||||
});
|
||||
})(new DocumentCloner(element, options, logger, true, renderElement))
|
||||
: cloneWindow(
|
||||
ownerDocument,
|
||||
windowBounds,
|
||||
element,
|
||||
options,
|
||||
logger,
|
||||
renderElement
|
||||
).then(([container, clonedElement, resourceLoader]) => {
|
||||
if (__DEV__) {
|
||||
logger.log(`Document cloned, using computed rendering`);
|
||||
}
|
||||
|
||||
const stack = NodeParser(clonedElement, resourceLoader, logger);
|
||||
const clonedDocument = clonedElement.ownerDocument;
|
||||
|
||||
if (backgroundColor === stack.container.style.background.backgroundColor) {
|
||||
stack.container.style.background.backgroundColor = TRANSPARENT;
|
||||
}
|
||||
|
||||
return resourceLoader.ready().then(imageStore => {
|
||||
if (options.removeContainer === true) {
|
||||
if (container.parentNode) {
|
||||
container.parentNode.removeChild(container);
|
||||
} else if (__DEV__) {
|
||||
logger.log(
|
||||
`Cannot detach cloned iframe as it is not in the DOM anymore`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const fontMetrics = new FontMetrics(clonedDocument);
|
||||
if (__DEV__) {
|
||||
logger.log(`Starting renderer`);
|
||||
}
|
||||
|
||||
const renderOptions = {
|
||||
backgroundColor,
|
||||
fontMetrics,
|
||||
imageStore,
|
||||
logger,
|
||||
scale: options.scale,
|
||||
x: options.x,
|
||||
y: options.y,
|
||||
width: options.width,
|
||||
height: options.height
|
||||
};
|
||||
|
||||
if (Array.isArray(options.target)) {
|
||||
return Promise.all(
|
||||
options.target.map(target => {
|
||||
const renderer = new Renderer(target, renderOptions);
|
||||
return renderer.render(stack);
|
||||
})
|
||||
);
|
||||
} else {
|
||||
const renderer = new Renderer(options.target, renderOptions);
|
||||
return renderer.render(stack);
|
||||
}
|
||||
});
|
||||
})
|
||||
);
|
||||
};
|
41
src/drawing/BezierCurve.js
Normal file
41
src/drawing/BezierCurve.js
Normal file
@ -0,0 +1,41 @@
|
||||
/* @flow */
|
||||
'use strict';
|
||||
import type {Drawable} from './Path';
|
||||
import {PATH} from './Path';
|
||||
import Vector from './Vector';
|
||||
|
||||
const lerp = (a: Vector, b: Vector, t: number): Vector => {
|
||||
return new Vector(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t);
|
||||
};
|
||||
|
||||
export default class BezierCurve implements Drawable<1> {
|
||||
type: 1;
|
||||
start: Vector;
|
||||
startControl: Vector;
|
||||
endControl: Vector;
|
||||
end: Vector;
|
||||
|
||||
constructor(start: Vector, startControl: Vector, endControl: Vector, end: Vector) {
|
||||
this.type = PATH.BEZIER_CURVE;
|
||||
this.start = start;
|
||||
this.startControl = startControl;
|
||||
this.endControl = endControl;
|
||||
this.end = end;
|
||||
}
|
||||
|
||||
subdivide(t: number, firstHalf: boolean): BezierCurve {
|
||||
const ab = lerp(this.start, this.startControl, t);
|
||||
const bc = lerp(this.startControl, this.endControl, t);
|
||||
const cd = lerp(this.endControl, this.end, t);
|
||||
const abbc = lerp(ab, bc, t);
|
||||
const bccd = lerp(bc, cd, t);
|
||||
const dest = lerp(abbc, bccd, t);
|
||||
return firstHalf
|
||||
? new BezierCurve(this.start, ab, abbc, dest)
|
||||
: new BezierCurve(dest, bccd, cd, this.end);
|
||||
}
|
||||
|
||||
reverse(): BezierCurve {
|
||||
return new BezierCurve(this.end, this.endControl, this.startControl, this.start);
|
||||
}
|
||||
}
|
29
src/drawing/Circle.js
Normal file
29
src/drawing/Circle.js
Normal file
@ -0,0 +1,29 @@
|
||||
/* @flow */
|
||||
'use strict';
|
||||
import type {Drawable} from './Path';
|
||||
import {PATH} from './Path';
|
||||
|
||||
export default class Circle implements Drawable<2> {
|
||||
type: 2;
|
||||
x: number;
|
||||
y: number;
|
||||
radius: number;
|
||||
|
||||
constructor(x: number, y: number, radius: number) {
|
||||
this.type = PATH.CIRCLE;
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.radius = radius;
|
||||
if (__DEV__) {
|
||||
if (isNaN(x)) {
|
||||
console.error(`Invalid x value given for Circle`);
|
||||
}
|
||||
if (isNaN(y)) {
|
||||
console.error(`Invalid y value given for Circle`);
|
||||
}
|
||||
if (isNaN(radius)) {
|
||||
console.error(`Invalid radius value given for Circle`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
20
src/drawing/Path.js
Normal file
20
src/drawing/Path.js
Normal file
@ -0,0 +1,20 @@
|
||||
/* @flow */
|
||||
'use strict';
|
||||
|
||||
import type Vector from './Vector';
|
||||
import type BezierCurve from './BezierCurve';
|
||||
import type Circle from './Circle';
|
||||
|
||||
export const PATH = {
|
||||
VECTOR: 0,
|
||||
BEZIER_CURVE: 1,
|
||||
CIRCLE: 2
|
||||
};
|
||||
|
||||
export type PathType = $Values<typeof PATH>;
|
||||
|
||||
export interface Drawable<A> {
|
||||
type: A
|
||||
}
|
||||
|
||||
export type Path = Array<Vector | BezierCurve> | Circle;
|
12
src/drawing/Size.js
Normal file
12
src/drawing/Size.js
Normal file
@ -0,0 +1,12 @@
|
||||
/* @flow */
|
||||
'use strict';
|
||||
|
||||
export default class Size {
|
||||
width: number;
|
||||
height: number;
|
||||
|
||||
constructor(width: number, height: number) {
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
}
|
||||
}
|
24
src/drawing/Vector.js
Normal file
24
src/drawing/Vector.js
Normal file
@ -0,0 +1,24 @@
|
||||
/* @flow */
|
||||
'use strict';
|
||||
import type {Drawable} from './Path';
|
||||
import {PATH} from './Path';
|
||||
|
||||
export default class Vector implements Drawable<0> {
|
||||
type: 0;
|
||||
x: number;
|
||||
y: number;
|
||||
|
||||
constructor(x: number, y: number) {
|
||||
this.type = PATH.VECTOR;
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
if (__DEV__) {
|
||||
if (isNaN(x)) {
|
||||
console.error(`Invalid x value given for Vector`);
|
||||
}
|
||||
if (isNaN(y)) {
|
||||
console.error(`Invalid y value given for Vector`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
window.html2canvas = html2canvas;
|
||||
}(window, document));
|
@ -1 +0,0 @@
|
||||
(function(window, document, undefined){
|
95
src/index.js
Normal file
95
src/index.js
Normal file
@ -0,0 +1,95 @@
|
||||
/* @flow */
|
||||
'use strict';
|
||||
|
||||
import type {RenderTarget} from './Renderer';
|
||||
|
||||
import CanvasRenderer from './renderer/CanvasRenderer';
|
||||
import Logger from './Logger';
|
||||
import {renderElement} from './Window';
|
||||
import {parseBounds, parseDocumentSize} from './Bounds';
|
||||
|
||||
export type Options = {
|
||||
async: ?boolean,
|
||||
allowTaint: ?boolean,
|
||||
backgroundColor: string,
|
||||
canvas: ?HTMLCanvasElement,
|
||||
foreignObjectRendering: boolean,
|
||||
imageTimeout: number,
|
||||
proxy: ?string,
|
||||
removeContainer: ?boolean,
|
||||
scale: number,
|
||||
target: RenderTarget<*>,
|
||||
width: number,
|
||||
height: number,
|
||||
x: number,
|
||||
y: number,
|
||||
scrollX: number,
|
||||
scrollY: number,
|
||||
windowWidth: number,
|
||||
windowHeight: number
|
||||
};
|
||||
|
||||
const html2canvas = (element: HTMLElement, conf: ?Options): Promise<*> => {
|
||||
// eslint-disable-next-line no-console
|
||||
if (typeof console === 'object' && typeof console.log === 'function') {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`html2canvas ${__VERSION__}`);
|
||||
}
|
||||
|
||||
const config = conf || {};
|
||||
const logger = new Logger();
|
||||
|
||||
if (__DEV__ && typeof config.onrendered === 'function') {
|
||||
logger.error(
|
||||
`onrendered option is deprecated, html2canvas returns a Promise with the canvas as the value`
|
||||
);
|
||||
}
|
||||
|
||||
const ownerDocument = element.ownerDocument;
|
||||
if (!ownerDocument) {
|
||||
return Promise.reject(`Provided element is not within a Document`);
|
||||
}
|
||||
const defaultView = ownerDocument.defaultView;
|
||||
|
||||
const scrollX = defaultView.pageXOffset;
|
||||
const scrollY = defaultView.pageYOffset;
|
||||
|
||||
const isDocument = element.tagName === 'HTML' || element.tagName === 'BODY';
|
||||
|
||||
const {width, height, left, top} = isDocument
|
||||
? parseDocumentSize(ownerDocument)
|
||||
: parseBounds(element, scrollX, scrollY);
|
||||
|
||||
const defaultOptions = {
|
||||
async: true,
|
||||
allowTaint: false,
|
||||
imageTimeout: 15000,
|
||||
proxy: null,
|
||||
removeContainer: true,
|
||||
foreignObjectRendering: false,
|
||||
scale: defaultView.devicePixelRatio || 1,
|
||||
target: new CanvasRenderer(config.canvas),
|
||||
x: left,
|
||||
y: top,
|
||||
width: Math.ceil(width),
|
||||
height: Math.ceil(height),
|
||||
windowWidth: defaultView.innerWidth,
|
||||
windowHeight: defaultView.innerHeight,
|
||||
scrollX: defaultView.pageXOffset,
|
||||
scrollY: defaultView.pageYOffset
|
||||
};
|
||||
|
||||
const result = renderElement(element, {...defaultOptions, ...config}, logger);
|
||||
|
||||
if (__DEV__) {
|
||||
return result.catch(e => {
|
||||
logger.error(e);
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
html2canvas.CanvasRenderer = CanvasRenderer;
|
||||
|
||||
module.exports = html2canvas;
|
462
src/parsing/background.js
Normal file
462
src/parsing/background.js
Normal file
@ -0,0 +1,462 @@
|
||||
/* @flow */
|
||||
'use strict';
|
||||
import type {Path} from '../drawing/Path';
|
||||
import type {BoundCurves} from '../Bounds';
|
||||
import type ResourceLoader, {ImageElement} from '../ResourceLoader';
|
||||
import type {Border} from './border';
|
||||
import type {Padding} from './padding';
|
||||
|
||||
import Color from '../Color';
|
||||
import Length from '../Length';
|
||||
import Size from '../drawing/Size';
|
||||
import Vector from '../drawing/Vector';
|
||||
import {
|
||||
calculateBorderBoxPath,
|
||||
calculatePaddingBoxPath,
|
||||
calculatePaddingBox,
|
||||
Bounds
|
||||
} from '../Bounds';
|
||||
import {PADDING_SIDES} from './padding';
|
||||
|
||||
export type Background = {
|
||||
backgroundImage: Array<BackgroundImage>,
|
||||
backgroundClip: BackgroundClip,
|
||||
backgroundColor: Color,
|
||||
backgroundOrigin: BackgroundOrigin
|
||||
};
|
||||
|
||||
export type BackgroundClip = $Values<typeof BACKGROUND_CLIP>;
|
||||
export type BackgroundOrigin = $Values<typeof BACKGROUND_ORIGIN>;
|
||||
export type BackgroundRepeat = $Values<typeof BACKGROUND_REPEAT>;
|
||||
export type BackgroundSizeTypes = $Values<typeof BACKGROUND_SIZE>;
|
||||
|
||||
export type BackgroundSource = {
|
||||
prefix: string,
|
||||
method: string,
|
||||
args: Array<string>
|
||||
};
|
||||
|
||||
export type BackgroundImage = {
|
||||
source: BackgroundSource,
|
||||
position: [Length, Length],
|
||||
size: [BackgroundSize, BackgroundSize],
|
||||
repeat: BackgroundRepeat
|
||||
};
|
||||
|
||||
export const BACKGROUND_REPEAT = {
|
||||
REPEAT: 0,
|
||||
NO_REPEAT: 1,
|
||||
REPEAT_X: 2,
|
||||
REPEAT_Y: 3
|
||||
};
|
||||
|
||||
export const BACKGROUND_SIZE = {
|
||||
AUTO: 0,
|
||||
CONTAIN: 1,
|
||||
COVER: 2,
|
||||
LENGTH: 3
|
||||
};
|
||||
|
||||
export const BACKGROUND_CLIP = {
|
||||
BORDER_BOX: 0,
|
||||
PADDING_BOX: 1,
|
||||
CONTENT_BOX: 2
|
||||
};
|
||||
|
||||
export const BACKGROUND_ORIGIN = BACKGROUND_CLIP;
|
||||
|
||||
const AUTO = 'auto';
|
||||
|
||||
class BackgroundSize {
|
||||
size: ?BackgroundSizeTypes;
|
||||
value: ?Length;
|
||||
|
||||
constructor(size: string) {
|
||||
switch (size) {
|
||||
case 'contain':
|
||||
this.size = BACKGROUND_SIZE.CONTAIN;
|
||||
break;
|
||||
case 'cover':
|
||||
this.size = BACKGROUND_SIZE.COVER;
|
||||
break;
|
||||
case 'auto':
|
||||
this.size = BACKGROUND_SIZE.AUTO;
|
||||
break;
|
||||
default:
|
||||
this.value = new Length(size);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const calculateBackgroundSize = (
|
||||
backgroundImage: BackgroundImage,
|
||||
image: ImageElement,
|
||||
bounds: Bounds
|
||||
): Size => {
|
||||
let width = 0;
|
||||
let height = 0;
|
||||
const size = backgroundImage.size;
|
||||
if (size[0].size === BACKGROUND_SIZE.CONTAIN || size[0].size === BACKGROUND_SIZE.COVER) {
|
||||
const targetRatio = bounds.width / bounds.height;
|
||||
const currentRatio = image.width / image.height;
|
||||
return targetRatio < currentRatio !== (size[0].size === BACKGROUND_SIZE.COVER)
|
||||
? new Size(bounds.width, bounds.width / currentRatio)
|
||||
: new Size(bounds.height * currentRatio, bounds.height);
|
||||
}
|
||||
|
||||
if (size[0].value) {
|
||||
width = size[0].value.getAbsoluteValue(bounds.width);
|
||||
}
|
||||
|
||||
if (size[0].size === BACKGROUND_SIZE.AUTO && size[1].size === BACKGROUND_SIZE.AUTO) {
|
||||
height = image.height;
|
||||
} else if (size[1].size === BACKGROUND_SIZE.AUTO) {
|
||||
height = width / image.width * image.height;
|
||||
} else if (size[1].value) {
|
||||
height = size[1].value.getAbsoluteValue(bounds.height);
|
||||
}
|
||||
|
||||
if (size[0].size === BACKGROUND_SIZE.AUTO) {
|
||||
width = height / image.height * image.width;
|
||||
}
|
||||
|
||||
return new Size(width, height);
|
||||
};
|
||||
|
||||
const AUTO_SIZE = new BackgroundSize(AUTO);
|
||||
|
||||
export const calculateBackgroungPaintingArea = (
|
||||
curves: BoundCurves,
|
||||
clip: BackgroundClip
|
||||
): Path => {
|
||||
switch (clip) {
|
||||
case BACKGROUND_CLIP.BORDER_BOX:
|
||||
return calculateBorderBoxPath(curves);
|
||||
case BACKGROUND_CLIP.PADDING_BOX:
|
||||
default:
|
||||
return calculatePaddingBoxPath(curves);
|
||||
}
|
||||
};
|
||||
|
||||
export const calculateBackgroungPositioningArea = (
|
||||
backgroundOrigin: BackgroundOrigin,
|
||||
bounds: Bounds,
|
||||
padding: Padding,
|
||||
border: Array<Border>
|
||||
): Bounds => {
|
||||
const paddingBox = calculatePaddingBox(bounds, border);
|
||||
|
||||
switch (backgroundOrigin) {
|
||||
case BACKGROUND_ORIGIN.BORDER_BOX:
|
||||
return bounds;
|
||||
case BACKGROUND_ORIGIN.CONTENT_BOX:
|
||||
const paddingLeft = padding[PADDING_SIDES.LEFT].getAbsoluteValue(bounds.width);
|
||||
const paddingRight = padding[PADDING_SIDES.RIGHT].getAbsoluteValue(bounds.width);
|
||||
const paddingTop = padding[PADDING_SIDES.TOP].getAbsoluteValue(bounds.width);
|
||||
const paddingBottom = padding[PADDING_SIDES.BOTTOM].getAbsoluteValue(bounds.width);
|
||||
return new Bounds(
|
||||
paddingBox.left + paddingLeft,
|
||||
paddingBox.top + paddingTop,
|
||||
paddingBox.width - paddingLeft - paddingRight,
|
||||
paddingBox.height - paddingTop - paddingBottom
|
||||
);
|
||||
case BACKGROUND_ORIGIN.PADDING_BOX:
|
||||
default:
|
||||
return paddingBox;
|
||||
}
|
||||
};
|
||||
|
||||
export const calculateBackgroundPosition = (
|
||||
position: [Length, Length],
|
||||
size: Size,
|
||||
bounds: Bounds
|
||||
): Vector => {
|
||||
return new Vector(
|
||||
position[0].getAbsoluteValue(bounds.width - size.width),
|
||||
position[1].getAbsoluteValue(bounds.height - size.height)
|
||||
);
|
||||
};
|
||||
|
||||
export const calculateBackgroundRepeatPath = (
|
||||
background: BackgroundImage,
|
||||
position: Vector,
|
||||
size: Size,
|
||||
backgroundPositioningArea: Bounds,
|
||||
bounds: Bounds
|
||||
) => {
|
||||
const repeat = background.repeat;
|
||||
switch (repeat) {
|
||||
case BACKGROUND_REPEAT.REPEAT_X:
|
||||
return [
|
||||
new Vector(
|
||||
Math.round(bounds.left),
|
||||
Math.round(backgroundPositioningArea.top + position.y)
|
||||
),
|
||||
new Vector(
|
||||
Math.round(bounds.left + bounds.width),
|
||||
Math.round(backgroundPositioningArea.top + position.y)
|
||||
),
|
||||
new Vector(
|
||||
Math.round(bounds.left + bounds.width),
|
||||
Math.round(size.height + backgroundPositioningArea.top + position.y)
|
||||
),
|
||||
new Vector(
|
||||
Math.round(bounds.left),
|
||||
Math.round(size.height + backgroundPositioningArea.top + position.y)
|
||||
)
|
||||
];
|
||||
case BACKGROUND_REPEAT.REPEAT_Y:
|
||||
return [
|
||||
new Vector(
|
||||
Math.round(backgroundPositioningArea.left + position.x),
|
||||
Math.round(bounds.top)
|
||||
),
|
||||
new Vector(
|
||||
Math.round(backgroundPositioningArea.left + position.x + size.width),
|
||||
Math.round(bounds.top)
|
||||
),
|
||||
new Vector(
|
||||
Math.round(backgroundPositioningArea.left + position.x + size.width),
|
||||
Math.round(bounds.height + bounds.top)
|
||||
),
|
||||
new Vector(
|
||||
Math.round(backgroundPositioningArea.left + position.x),
|
||||
Math.round(bounds.height + bounds.top)
|
||||
)
|
||||
];
|
||||
case BACKGROUND_REPEAT.NO_REPEAT:
|
||||
return [
|
||||
new Vector(
|
||||
Math.round(backgroundPositioningArea.left + position.x),
|
||||
Math.round(backgroundPositioningArea.top + position.y)
|
||||
),
|
||||
new Vector(
|
||||
Math.round(backgroundPositioningArea.left + position.x + size.width),
|
||||
Math.round(backgroundPositioningArea.top + position.y)
|
||||
),
|
||||
new Vector(
|
||||
Math.round(backgroundPositioningArea.left + position.x + size.width),
|
||||
Math.round(backgroundPositioningArea.top + position.y + size.height)
|
||||
),
|
||||
new Vector(
|
||||
Math.round(backgroundPositioningArea.left + position.x),
|
||||
Math.round(backgroundPositioningArea.top + position.y + size.height)
|
||||
)
|
||||
];
|
||||
default:
|
||||
return [
|
||||
new Vector(Math.round(bounds.left), Math.round(bounds.top)),
|
||||
new Vector(Math.round(bounds.left + bounds.width), Math.round(bounds.top)),
|
||||
new Vector(
|
||||
Math.round(bounds.left + bounds.width),
|
||||
Math.round(bounds.height + bounds.top)
|
||||
),
|
||||
new Vector(Math.round(bounds.left), Math.round(bounds.height + bounds.top))
|
||||
];
|
||||
}
|
||||
};
|
||||
|
||||
export const parseBackground = (
|
||||
style: CSSStyleDeclaration,
|
||||
resourceLoader: ResourceLoader
|
||||
): Background => {
|
||||
return {
|
||||
backgroundColor: new Color(style.backgroundColor),
|
||||
backgroundImage: parseBackgroundImages(style, resourceLoader),
|
||||
backgroundClip: parseBackgroundClip(style.backgroundClip),
|
||||
backgroundOrigin: parseBackgroundOrigin(style.backgroundOrigin)
|
||||
};
|
||||
};
|
||||
|
||||
const parseBackgroundClip = (backgroundClip: string): BackgroundClip => {
|
||||
switch (backgroundClip) {
|
||||
case 'padding-box':
|
||||
return BACKGROUND_CLIP.PADDING_BOX;
|
||||
case 'content-box':
|
||||
return BACKGROUND_CLIP.CONTENT_BOX;
|
||||
}
|
||||
return BACKGROUND_CLIP.BORDER_BOX;
|
||||
};
|
||||
|
||||
const parseBackgroundOrigin = (backgroundOrigin: string): BackgroundOrigin => {
|
||||
switch (backgroundOrigin) {
|
||||
case 'padding-box':
|
||||
return BACKGROUND_ORIGIN.PADDING_BOX;
|
||||
case 'content-box':
|
||||
return BACKGROUND_ORIGIN.CONTENT_BOX;
|
||||
}
|
||||
return BACKGROUND_ORIGIN.BORDER_BOX;
|
||||
};
|
||||
|
||||
const parseBackgroundRepeat = (backgroundRepeat: string): BackgroundRepeat => {
|
||||
switch (backgroundRepeat.trim()) {
|
||||
case 'no-repeat':
|
||||
return BACKGROUND_REPEAT.NO_REPEAT;
|
||||
case 'repeat-x':
|
||||
case 'repeat no-repeat':
|
||||
return BACKGROUND_REPEAT.REPEAT_X;
|
||||
case 'repeat-y':
|
||||
case 'no-repeat repeat':
|
||||
return BACKGROUND_REPEAT.REPEAT_Y;
|
||||
case 'repeat':
|
||||
return BACKGROUND_REPEAT.REPEAT;
|
||||
}
|
||||
|
||||
if (__DEV__) {
|
||||
console.error(`Invalid background-repeat value "${backgroundRepeat}"`);
|
||||
}
|
||||
|
||||
return BACKGROUND_REPEAT.REPEAT;
|
||||
};
|
||||
|
||||
const parseBackgroundImages = (
|
||||
style: CSSStyleDeclaration,
|
||||
resourceLoader: ResourceLoader
|
||||
): Array<BackgroundImage> => {
|
||||
const sources: Array<BackgroundSource> = parseBackgroundImage(
|
||||
style.backgroundImage
|
||||
).map(backgroundImage => {
|
||||
if (backgroundImage.method === 'url') {
|
||||
const key = resourceLoader.loadImage(backgroundImage.args[0]);
|
||||
backgroundImage.args = key ? [key] : [];
|
||||
}
|
||||
return backgroundImage;
|
||||
});
|
||||
const positions = style.backgroundPosition.split(',');
|
||||
const repeats = style.backgroundRepeat.split(',');
|
||||
const sizes = style.backgroundSize.split(',');
|
||||
|
||||
return sources.map((source, index) => {
|
||||
const size = (sizes[index] || AUTO).trim().split(' ').map(parseBackgroundSize);
|
||||
const position = (positions[index] || AUTO).trim().split(' ').map(parseBackgoundPosition);
|
||||
|
||||
return {
|
||||
source,
|
||||
repeat: parseBackgroundRepeat(
|
||||
typeof repeats[index] === 'string' ? repeats[index] : repeats[0]
|
||||
),
|
||||
size: size.length < 2 ? [size[0], AUTO_SIZE] : [size[0], size[1]],
|
||||
position: position.length < 2 ? [position[0], position[0]] : [position[0], position[1]]
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const parseBackgroundSize = (size: string): BackgroundSize =>
|
||||
size === 'auto' ? AUTO_SIZE : new BackgroundSize(size);
|
||||
|
||||
const parseBackgoundPosition = (position: string): Length => {
|
||||
switch (position) {
|
||||
case 'bottom':
|
||||
case 'right':
|
||||
return new Length('100%');
|
||||
case 'left':
|
||||
case 'top':
|
||||
return new Length('0%');
|
||||
case 'auto':
|
||||
return new Length('0');
|
||||
}
|
||||
return new Length(position);
|
||||
};
|
||||
|
||||
export const parseBackgroundImage = (image: string): Array<BackgroundSource> => {
|
||||
const whitespace = /^\s$/;
|
||||
const results = [];
|
||||
|
||||
let args = [];
|
||||
let method = '';
|
||||
let quote = null;
|
||||
let definition = '';
|
||||
let mode = 0;
|
||||
let numParen = 0;
|
||||
|
||||
const appendResult = () => {
|
||||
let prefix = '';
|
||||
if (method) {
|
||||
if (definition.substr(0, 1) === '"') {
|
||||
definition = definition.substr(1, definition.length - 2);
|
||||
}
|
||||
|
||||
if (definition) {
|
||||
args.push(definition.trim());
|
||||
}
|
||||
|
||||
const prefix_i = method.indexOf('-', 1) + 1;
|
||||
if (method.substr(0, 1) === '-' && prefix_i > 0) {
|
||||
prefix = method.substr(0, prefix_i).toLowerCase();
|
||||
method = method.substr(prefix_i);
|
||||
}
|
||||
method = method.toLowerCase();
|
||||
if (method !== 'none') {
|
||||
results.push({
|
||||
prefix,
|
||||
method,
|
||||
args
|
||||
});
|
||||
}
|
||||
}
|
||||
args = [];
|
||||
method = definition = '';
|
||||
};
|
||||
|
||||
image.split('').forEach(c => {
|
||||
if (mode === 0 && whitespace.test(c)) {
|
||||
return;
|
||||
}
|
||||
switch (c) {
|
||||
case '"':
|
||||
if (!quote) {
|
||||
quote = c;
|
||||
} else if (quote === c) {
|
||||
quote = null;
|
||||
}
|
||||
break;
|
||||
case '(':
|
||||
if (quote) {
|
||||
break;
|
||||
} else if (mode === 0) {
|
||||
mode = 1;
|
||||
return;
|
||||
} else {
|
||||
numParen++;
|
||||
}
|
||||
break;
|
||||
case ')':
|
||||
if (quote) {
|
||||
break;
|
||||
} else if (mode === 1) {
|
||||
if (numParen === 0) {
|
||||
mode = 0;
|
||||
appendResult();
|
||||
return;
|
||||
} else {
|
||||
numParen--;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case ',':
|
||||
if (quote) {
|
||||
break;
|
||||
} else if (mode === 0) {
|
||||
appendResult();
|
||||
return;
|
||||
} else if (mode === 1) {
|
||||
if (numParen === 0 && !method.match(/^url$/i)) {
|
||||
args.push(definition.trim());
|
||||
definition = '';
|
||||
return;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (mode === 0) {
|
||||
method += c;
|
||||
} else {
|
||||
definition += c;
|
||||
}
|
||||
});
|
||||
|
||||
appendResult();
|
||||
return results;
|
||||
};
|
49
src/parsing/border.js
Normal file
49
src/parsing/border.js
Normal file
@ -0,0 +1,49 @@
|
||||
/* @flow */
|
||||
'use strict';
|
||||
|
||||
import Color from '../Color';
|
||||
|
||||
export const BORDER_STYLE = {
|
||||
NONE: 0,
|
||||
SOLID: 1
|
||||
};
|
||||
|
||||
export type BorderStyle = $Values<typeof BORDER_STYLE>;
|
||||
|
||||
export type Border = {
|
||||
borderColor: Color,
|
||||
borderStyle: BorderStyle,
|
||||
borderWidth: number
|
||||
};
|
||||
|
||||
export const BORDER_SIDES = {
|
||||
TOP: 0,
|
||||
RIGHT: 1,
|
||||
BOTTOM: 2,
|
||||
LEFT: 3
|
||||
};
|
||||
|
||||
export type BorderSide = $Values<typeof BORDER_SIDES>;
|
||||
|
||||
const SIDES = Object.keys(BORDER_SIDES).map(s => s.toLowerCase());
|
||||
|
||||
const parseBorderStyle = (style: string): BorderStyle => {
|
||||
switch (style) {
|
||||
case 'none':
|
||||
return BORDER_STYLE.NONE;
|
||||
}
|
||||
return BORDER_STYLE.SOLID;
|
||||
};
|
||||
|
||||
export const parseBorder = (style: CSSStyleDeclaration): Array<Border> => {
|
||||
return SIDES.map(side => {
|
||||
const borderColor = new Color(style.getPropertyValue(`border-${side}-color`));
|
||||
const borderStyle = parseBorderStyle(style.getPropertyValue(`border-${side}-style`));
|
||||
const borderWidth = parseFloat(style.getPropertyValue(`border-${side}-width`));
|
||||
return {
|
||||
borderColor,
|
||||
borderStyle,
|
||||
borderWidth: isNaN(borderWidth) ? 0 : borderWidth
|
||||
};
|
||||
});
|
||||
};
|
15
src/parsing/borderRadius.js
Normal file
15
src/parsing/borderRadius.js
Normal file
@ -0,0 +1,15 @@
|
||||
/* @flow */
|
||||
'use strict';
|
||||
import Length from '../Length';
|
||||
|
||||
const SIDES = ['top-left', 'top-right', 'bottom-right', 'bottom-left'];
|
||||
|
||||
export type BorderRadius = [Length, Length];
|
||||
|
||||
export const parseBorderRadius = (style: CSSStyleDeclaration): Array<BorderRadius> => {
|
||||
return SIDES.map(side => {
|
||||
const value = style.getPropertyValue(`border-${side}-radius`);
|
||||
const [horizontal, vertical] = value.split(' ').map(Length.create);
|
||||
return typeof vertical === 'undefined' ? [horizontal, horizontal] : [horizontal, vertical];
|
||||
});
|
||||
};
|
111
src/parsing/display.js
Normal file
111
src/parsing/display.js
Normal file
@ -0,0 +1,111 @@
|
||||
/* @flow */
|
||||
'use strict';
|
||||
|
||||
export const DISPLAY = {
|
||||
NONE: 1 << 0,
|
||||
BLOCK: 1 << 1,
|
||||
INLINE: 1 << 2,
|
||||
RUN_IN: 1 << 3,
|
||||
FLOW: 1 << 4,
|
||||
FLOW_ROOT: 1 << 5,
|
||||
TABLE: 1 << 6,
|
||||
FLEX: 1 << 7,
|
||||
GRID: 1 << 8,
|
||||
RUBY: 1 << 9,
|
||||
SUBGRID: 1 << 10,
|
||||
LIST_ITEM: 1 << 11,
|
||||
TABLE_ROW_GROUP: 1 << 12,
|
||||
TABLE_HEADER_GROUP: 1 << 13,
|
||||
TABLE_FOOTER_GROUP: 1 << 14,
|
||||
TABLE_ROW: 1 << 15,
|
||||
TABLE_CELL: 1 << 16,
|
||||
TABLE_COLUMN_GROUP: 1 << 17,
|
||||
TABLE_COLUMN: 1 << 18,
|
||||
TABLE_CAPTION: 1 << 19,
|
||||
RUBY_BASE: 1 << 20,
|
||||
RUBY_TEXT: 1 << 21,
|
||||
RUBY_BASE_CONTAINER: 1 << 22,
|
||||
RUBY_TEXT_CONTAINER: 1 << 23,
|
||||
CONTENTS: 1 << 24,
|
||||
INLINE_BLOCK: 1 << 25,
|
||||
INLINE_LIST_ITEM: 1 << 26,
|
||||
INLINE_TABLE: 1 << 27,
|
||||
INLINE_FLEX: 1 << 28,
|
||||
INLINE_GRID: 1 << 29
|
||||
};
|
||||
|
||||
export type Display = $Values<typeof DISPLAY>;
|
||||
export type DisplayBit = number;
|
||||
|
||||
const parseDisplayValue = (display: string): Display => {
|
||||
switch (display) {
|
||||
case 'block':
|
||||
return DISPLAY.BLOCK;
|
||||
case 'inline':
|
||||
return DISPLAY.INLINE;
|
||||
case 'run-in':
|
||||
return DISPLAY.RUN_IN;
|
||||
case 'flow':
|
||||
return DISPLAY.FLOW;
|
||||
case 'flow-root':
|
||||
return DISPLAY.FLOW_ROOT;
|
||||
case 'table':
|
||||
return DISPLAY.TABLE;
|
||||
case 'flex':
|
||||
return DISPLAY.FLEX;
|
||||
case 'grid':
|
||||
return DISPLAY.GRID;
|
||||
case 'ruby':
|
||||
return DISPLAY.RUBY;
|
||||
case 'subgrid':
|
||||
return DISPLAY.SUBGRID;
|
||||
case 'list-item':
|
||||
return DISPLAY.LIST_ITEM;
|
||||
case 'table-row-group':
|
||||
return DISPLAY.TABLE_ROW_GROUP;
|
||||
case 'table-header-group':
|
||||
return DISPLAY.TABLE_HEADER_GROUP;
|
||||
case 'table-footer-group':
|
||||
return DISPLAY.TABLE_FOOTER_GROUP;
|
||||
case 'table-row':
|
||||
return DISPLAY.TABLE_ROW;
|
||||
case 'table-cell':
|
||||
return DISPLAY.TABLE_CELL;
|
||||
case 'table-column-group':
|
||||
return DISPLAY.TABLE_COLUMN_GROUP;
|
||||
case 'table-column':
|
||||
return DISPLAY.TABLE_COLUMN;
|
||||
case 'table-caption':
|
||||
return DISPLAY.TABLE_CAPTION;
|
||||
case 'ruby-base':
|
||||
return DISPLAY.RUBY_BASE;
|
||||
case 'ruby-text':
|
||||
return DISPLAY.RUBY_TEXT;
|
||||
case 'ruby-base-container':
|
||||
return DISPLAY.RUBY_BASE_CONTAINER;
|
||||
case 'ruby-text-container':
|
||||
return DISPLAY.RUBY_TEXT_CONTAINER;
|
||||
case 'contents':
|
||||
return DISPLAY.CONTENTS;
|
||||
case 'inline-block':
|
||||
return DISPLAY.INLINE_BLOCK;
|
||||
case 'inline-list-item':
|
||||
return DISPLAY.INLINE_LIST_ITEM;
|
||||
case 'inline-table':
|
||||
return DISPLAY.INLINE_TABLE;
|
||||
case 'inline-flex':
|
||||
return DISPLAY.INLINE_FLEX;
|
||||
case 'inline-grid':
|
||||
return DISPLAY.INLINE_GRID;
|
||||
}
|
||||
|
||||
return DISPLAY.NONE;
|
||||
};
|
||||
|
||||
const setDisplayBit = (bit: DisplayBit, display: string): DisplayBit => {
|
||||
return bit | parseDisplayValue(display);
|
||||
};
|
||||
|
||||
export const parseDisplay = (display: string): Display => {
|
||||
return display.split(' ').reduce(setDisplayBit, 0);
|
||||
};
|
26
src/parsing/float.js
Normal file
26
src/parsing/float.js
Normal file
@ -0,0 +1,26 @@
|
||||
/* @flow */
|
||||
'use strict';
|
||||
|
||||
export const FLOAT = {
|
||||
NONE: 0,
|
||||
LEFT: 1,
|
||||
RIGHT: 2,
|
||||
INLINE_START: 3,
|
||||
INLINE_END: 4
|
||||
};
|
||||
|
||||
export type Float = $Values<typeof FLOAT>;
|
||||
|
||||
export const parseCSSFloat = (float: string): Float => {
|
||||
switch (float) {
|
||||
case 'left':
|
||||
return FLOAT.LEFT;
|
||||
case 'right':
|
||||
return FLOAT.RIGHT;
|
||||
case 'inline-start':
|
||||
return FLOAT.INLINE_START;
|
||||
case 'inline-end':
|
||||
return FLOAT.INLINE_END;
|
||||
}
|
||||
return FLOAT.NONE;
|
||||
};
|
38
src/parsing/font.js
Normal file
38
src/parsing/font.js
Normal file
@ -0,0 +1,38 @@
|
||||
/* @flow */
|
||||
'use strict';
|
||||
|
||||
export type Font = {
|
||||
fontFamily: string,
|
||||
fontSize: string,
|
||||
fontStyle: string,
|
||||
fontVariant: string,
|
||||
fontWeight: number
|
||||
};
|
||||
|
||||
const parseFontWeight = (weight: string): number => {
|
||||
switch (weight) {
|
||||
case 'normal':
|
||||
return 400;
|
||||
case 'bold':
|
||||
return 700;
|
||||
}
|
||||
|
||||
const value = parseInt(weight, 10);
|
||||
return isNaN(value) ? 400 : value;
|
||||
};
|
||||
|
||||
export const parseFont = (style: CSSStyleDeclaration): Font => {
|
||||
const fontFamily = style.fontFamily;
|
||||
const fontSize = style.fontSize;
|
||||
const fontStyle = style.fontStyle;
|
||||
const fontVariant = style.fontVariant;
|
||||
const fontWeight = parseFontWeight(style.fontWeight);
|
||||
|
||||
return {
|
||||
fontFamily,
|
||||
fontSize,
|
||||
fontStyle,
|
||||
fontVariant,
|
||||
fontWeight
|
||||
};
|
||||
};
|
10
src/parsing/letterSpacing.js
Normal file
10
src/parsing/letterSpacing.js
Normal file
@ -0,0 +1,10 @@
|
||||
/* @flow */
|
||||
'use strict';
|
||||
|
||||
export const parseLetterSpacing = (letterSpacing: string): number => {
|
||||
if (letterSpacing === 'normal') {
|
||||
return 0;
|
||||
}
|
||||
const value = parseFloat(letterSpacing);
|
||||
return isNaN(value) ? 0 : value;
|
||||
};
|
25
src/parsing/overflow.js
Normal file
25
src/parsing/overflow.js
Normal file
@ -0,0 +1,25 @@
|
||||
/* @flow */
|
||||
'use strict';
|
||||
|
||||
export const OVERFLOW = {
|
||||
VISIBLE: 0,
|
||||
HIDDEN: 1,
|
||||
SCROLL: 2,
|
||||
AUTO: 3
|
||||
};
|
||||
|
||||
export type Overflow = $Values<typeof OVERFLOW>;
|
||||
|
||||
export const parseOverflow = (overflow: string): Overflow => {
|
||||
switch (overflow) {
|
||||
case 'hidden':
|
||||
return OVERFLOW.HIDDEN;
|
||||
case 'scroll':
|
||||
return OVERFLOW.SCROLL;
|
||||
case 'auto':
|
||||
return OVERFLOW.AUTO;
|
||||
case 'visible':
|
||||
default:
|
||||
return OVERFLOW.VISIBLE;
|
||||
}
|
||||
};
|
18
src/parsing/padding.js
Normal file
18
src/parsing/padding.js
Normal file
@ -0,0 +1,18 @@
|
||||
/* @flow */
|
||||
'use strict';
|
||||
import Length from '../Length';
|
||||
|
||||
export const PADDING_SIDES = {
|
||||
TOP: 0,
|
||||
RIGHT: 1,
|
||||
BOTTOM: 2,
|
||||
LEFT: 3
|
||||
};
|
||||
|
||||
const SIDES = ['top', 'right', 'bottom', 'left'];
|
||||
|
||||
export type Padding = Array<Length>;
|
||||
|
||||
export const parsePadding = (style: CSSStyleDeclaration): Padding => {
|
||||
return SIDES.map(side => new Length(style.getPropertyValue(`padding-${side}`)));
|
||||
};
|
27
src/parsing/position.js
Normal file
27
src/parsing/position.js
Normal file
@ -0,0 +1,27 @@
|
||||
/* @flow */
|
||||
'use strict';
|
||||
|
||||
export const POSITION = {
|
||||
STATIC: 0,
|
||||
RELATIVE: 1,
|
||||
ABSOLUTE: 2,
|
||||
FIXED: 3,
|
||||
STICKY: 4
|
||||
};
|
||||
|
||||
export type Position = $Values<typeof POSITION>;
|
||||
|
||||
export const parsePosition = (position: string): Position => {
|
||||
switch (position) {
|
||||
case 'relative':
|
||||
return POSITION.RELATIVE;
|
||||
case 'absolute':
|
||||
return POSITION.ABSOLUTE;
|
||||
case 'fixed':
|
||||
return POSITION.FIXED;
|
||||
case 'sticky':
|
||||
return POSITION.STICKY;
|
||||
}
|
||||
|
||||
return POSITION.STATIC;
|
||||
};
|
86
src/parsing/textDecoration.js
Normal file
86
src/parsing/textDecoration.js
Normal file
@ -0,0 +1,86 @@
|
||||
/* @flow */
|
||||
'use strict';
|
||||
|
||||
import Color from '../Color';
|
||||
|
||||
export const TEXT_DECORATION_STYLE = {
|
||||
SOLID: 0,
|
||||
DOUBLE: 1,
|
||||
DOTTED: 2,
|
||||
DASHED: 3,
|
||||
WAVY: 4
|
||||
};
|
||||
|
||||
export const TEXT_DECORATION = {
|
||||
NONE: null
|
||||
};
|
||||
|
||||
export const TEXT_DECORATION_LINE = {
|
||||
UNDERLINE: 1,
|
||||
OVERLINE: 2,
|
||||
LINE_THROUGH: 3,
|
||||
BLINK: 4
|
||||
};
|
||||
|
||||
export type TextDecorationStyle = $Values<typeof TEXT_DECORATION_STYLE>;
|
||||
export type TextDecorationLine = $Values<typeof TEXT_DECORATION_LINE>;
|
||||
type TextDecorationLineType = Array<TextDecorationLine> | null;
|
||||
export type TextDecoration = {
|
||||
textDecorationLine: Array<TextDecorationLine>,
|
||||
textDecorationStyle: TextDecorationStyle,
|
||||
textDecorationColor: Color | null
|
||||
};
|
||||
|
||||
const parseLine = (line: string): TextDecorationLine => {
|
||||
switch (line) {
|
||||
case 'underline':
|
||||
return TEXT_DECORATION_LINE.UNDERLINE;
|
||||
case 'overline':
|
||||
return TEXT_DECORATION_LINE.OVERLINE;
|
||||
case 'line-through':
|
||||
return TEXT_DECORATION_LINE.LINE_THROUGH;
|
||||
}
|
||||
return TEXT_DECORATION_LINE.BLINK;
|
||||
};
|
||||
|
||||
const parseTextDecorationLine = (line: string): TextDecorationLineType => {
|
||||
if (line === 'none') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return line.split(' ').map(parseLine);
|
||||
};
|
||||
|
||||
const parseTextDecorationStyle = (style: string): TextDecorationStyle => {
|
||||
switch (style) {
|
||||
case 'double':
|
||||
return TEXT_DECORATION_STYLE.DOUBLE;
|
||||
case 'dotted':
|
||||
return TEXT_DECORATION_STYLE.DOTTED;
|
||||
case 'dashed':
|
||||
return TEXT_DECORATION_STYLE.DASHED;
|
||||
case 'wavy':
|
||||
return TEXT_DECORATION_STYLE.WAVY;
|
||||
}
|
||||
return TEXT_DECORATION_STYLE.SOLID;
|
||||
};
|
||||
|
||||
export const parseTextDecoration = (style: CSSStyleDeclaration): TextDecoration | null => {
|
||||
const textDecorationLine = parseTextDecorationLine(
|
||||
style.textDecorationLine ? style.textDecorationLine : style.textDecoration
|
||||
);
|
||||
if (textDecorationLine === null) {
|
||||
return TEXT_DECORATION.NONE;
|
||||
}
|
||||
|
||||
const textDecorationColor = style.textDecorationColor
|
||||
? new Color(style.textDecorationColor)
|
||||
: null;
|
||||
const textDecorationStyle = parseTextDecorationStyle(style.textDecorationStyle);
|
||||
|
||||
return {
|
||||
textDecorationLine,
|
||||
textDecorationColor,
|
||||
textDecorationStyle
|
||||
};
|
||||
};
|
94
src/parsing/textShadow.js
Normal file
94
src/parsing/textShadow.js
Normal file
@ -0,0 +1,94 @@
|
||||
/* @flow */
|
||||
'use strict';
|
||||
|
||||
import Color from '../Color';
|
||||
|
||||
export type TextShadow = {
|
||||
color: Color,
|
||||
offsetX: number,
|
||||
offsetY: number,
|
||||
blur: number
|
||||
};
|
||||
|
||||
const NUMBER = /^([+-]|\d|\.)$/i;
|
||||
|
||||
export const parseTextShadow = (textShadow: ?string): Array<TextShadow> | null => {
|
||||
if (textShadow === 'none' || typeof textShadow !== 'string') {
|
||||
return null;
|
||||
}
|
||||
|
||||
let currentValue = '';
|
||||
let isLength = false;
|
||||
const values = [];
|
||||
const shadows = [];
|
||||
let numParens = 0;
|
||||
let color = null;
|
||||
|
||||
const appendValue = () => {
|
||||
if (currentValue.length) {
|
||||
if (isLength) {
|
||||
values.push(parseFloat(currentValue));
|
||||
} else {
|
||||
color = new Color(currentValue);
|
||||
}
|
||||
}
|
||||
isLength = false;
|
||||
currentValue = '';
|
||||
};
|
||||
|
||||
const appendShadow = () => {
|
||||
if (values.length && color !== null) {
|
||||
shadows.push({
|
||||
color,
|
||||
offsetX: values[0] || 0,
|
||||
offsetY: values[1] || 0,
|
||||
blur: values[2] || 0
|
||||
});
|
||||
}
|
||||
values.splice(0, values.length);
|
||||
color = null;
|
||||
};
|
||||
|
||||
for (let i = 0; i < textShadow.length; i++) {
|
||||
const c = textShadow[i];
|
||||
switch (c) {
|
||||
case '(':
|
||||
currentValue += c;
|
||||
numParens++;
|
||||
break;
|
||||
case ')':
|
||||
currentValue += c;
|
||||
numParens--;
|
||||
break;
|
||||
case ',':
|
||||
if (numParens === 0) {
|
||||
appendValue();
|
||||
appendShadow();
|
||||
} else {
|
||||
currentValue += c;
|
||||
}
|
||||
break;
|
||||
case ' ':
|
||||
if (numParens === 0) {
|
||||
appendValue();
|
||||
} else {
|
||||
currentValue += c;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if (currentValue.length === 0 && NUMBER.test(c)) {
|
||||
isLength = true;
|
||||
}
|
||||
currentValue += c;
|
||||
}
|
||||
}
|
||||
|
||||
appendValue();
|
||||
appendShadow();
|
||||
|
||||
if (shadows.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return shadows;
|
||||
};
|
24
src/parsing/textTransform.js
Normal file
24
src/parsing/textTransform.js
Normal file
@ -0,0 +1,24 @@
|
||||
/* @flow */
|
||||
'use strict';
|
||||
|
||||
export const TEXT_TRANSFORM = {
|
||||
NONE: 0,
|
||||
LOWERCASE: 1,
|
||||
UPPERCASE: 2,
|
||||
CAPITALIZE: 3
|
||||
};
|
||||
|
||||
export type TextTransform = $Values<typeof TEXT_TRANSFORM>;
|
||||
|
||||
export const parseTextTransform = (textTransform: string): TextTransform => {
|
||||
switch (textTransform) {
|
||||
case 'uppercase':
|
||||
return TEXT_TRANSFORM.UPPERCASE;
|
||||
case 'lowercase':
|
||||
return TEXT_TRANSFORM.LOWERCASE;
|
||||
case 'capitalize':
|
||||
return TEXT_TRANSFORM.CAPITALIZE;
|
||||
}
|
||||
|
||||
return TEXT_TRANSFORM.NONE;
|
||||
};
|
71
src/parsing/transform.js
Normal file
71
src/parsing/transform.js
Normal file
@ -0,0 +1,71 @@
|
||||
/* @flow */
|
||||
'use strict';
|
||||
import Length from '../Length';
|
||||
|
||||
const toFloat = (s: string): number => parseFloat(s.trim());
|
||||
|
||||
export type Matrix = [number, number, number, number, number, number];
|
||||
export type TransformOrigin = [Length, Length];
|
||||
export type Transform = {
|
||||
transform: Matrix,
|
||||
transformOrigin: TransformOrigin
|
||||
} | null;
|
||||
|
||||
const MATRIX = /(matrix|matrix3d)\((.+)\)/;
|
||||
|
||||
export const parseTransform = (style: CSSStyleDeclaration): Transform => {
|
||||
const transform = parseTransformMatrix(
|
||||
style.transform ||
|
||||
style.webkitTransform ||
|
||||
style.mozTransform ||
|
||||
// $FlowFixMe
|
||||
style.msTransform ||
|
||||
// $FlowFixMe
|
||||
style.oTransform
|
||||
);
|
||||
if (transform === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
transform,
|
||||
transformOrigin: parseTransformOrigin(
|
||||
style.transformOrigin ||
|
||||
style.webkitTransformOrigin ||
|
||||
style.mozTransformOrigin ||
|
||||
// $FlowFixMe
|
||||
style.msTransformOrigin ||
|
||||
// $FlowFixMe
|
||||
style.oTransformOrigin
|
||||
)
|
||||
};
|
||||
};
|
||||
|
||||
// $FlowFixMe
|
||||
const parseTransformOrigin = (origin: ?string): TransformOrigin => {
|
||||
if (typeof origin !== 'string') {
|
||||
const v = new Length('0');
|
||||
return [v, v];
|
||||
}
|
||||
const values = origin.split(' ').map(Length.create);
|
||||
return [values[0], values[1]];
|
||||
};
|
||||
|
||||
// $FlowFixMe
|
||||
const parseTransformMatrix = (transform: ?string): Matrix | null => {
|
||||
if (transform === 'none' || typeof transform !== 'string') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const match = transform.match(MATRIX);
|
||||
if (match) {
|
||||
if (match[1] === 'matrix') {
|
||||
const matrix = match[2].split(',').map(toFloat);
|
||||
return [matrix[0], matrix[1], matrix[2], matrix[3], matrix[4], matrix[5]];
|
||||
} else {
|
||||
const matrix3d = match[2].split(',').map(toFloat);
|
||||
return [matrix3d[0], matrix3d[1], matrix3d[4], matrix3d[5], matrix3d[12], matrix3d[13]];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
22
src/parsing/visibility.js
Normal file
22
src/parsing/visibility.js
Normal file
@ -0,0 +1,22 @@
|
||||
/* @flow */
|
||||
'use strict';
|
||||
|
||||
export const VISIBILITY = {
|
||||
VISIBLE: 0,
|
||||
HIDDEN: 1,
|
||||
COLLAPSE: 2
|
||||
};
|
||||
|
||||
export type Visibility = $Values<typeof VISIBILITY>;
|
||||
|
||||
export const parseVisibility = (visibility: string): Visibility => {
|
||||
switch (visibility) {
|
||||
case 'hidden':
|
||||
return VISIBILITY.HIDDEN;
|
||||
case 'collapse':
|
||||
return VISIBILITY.COLLAPSE;
|
||||
case 'visible':
|
||||
default:
|
||||
return VISIBILITY.VISIBLE;
|
||||
}
|
||||
};
|
15
src/parsing/zIndex.js
Normal file
15
src/parsing/zIndex.js
Normal file
@ -0,0 +1,15 @@
|
||||
/* @flow */
|
||||
'use strict';
|
||||
|
||||
export type zIndex = {
|
||||
auto: boolean,
|
||||
order: number
|
||||
};
|
||||
|
||||
export const parseZIndex = (zIndex: string): zIndex => {
|
||||
const auto = zIndex === 'auto';
|
||||
return {
|
||||
auto,
|
||||
order: auto ? 0 : parseInt(zIndex, 10)
|
||||
};
|
||||
};
|
@ -1,76 +0,0 @@
|
||||
(function() {
|
||||
/* options, customize to your needs */
|
||||
var server = '//html2canvas.hertzen.com/js',
|
||||
proxy = '//html2canvas.appspot.com',
|
||||
debug = false,
|
||||
profile = false;
|
||||
//DEBUG: server = 'http://localhost/html2canvas'; debug = true;
|
||||
var debugFiles = [
|
||||
'external/jquery-1.6.2.min',
|
||||
'src/Core',
|
||||
'src/Generate',
|
||||
'src/Parse',
|
||||
'src/Preload',
|
||||
'src/Queue',
|
||||
'src/Renderer',
|
||||
'src/Util',
|
||||
'src/renderers/Canvas',
|
||||
'src/plugins/jquery.plugin.html2canvas'
|
||||
],
|
||||
relFiles = [
|
||||
'//code.jquery.com/jquery-1.6.4.js',
|
||||
'html2canvas',
|
||||
'jquery.plugin.html2canvas'
|
||||
];
|
||||
var loader = {
|
||||
index: 0,
|
||||
head: document.getElementsByTagName('head')[0],
|
||||
statusline: document.createElement('div'),
|
||||
files: (debug ? debugFiles : relFiles),
|
||||
onload: function () {
|
||||
var _ = this;
|
||||
if (_.index < _.files.length) {
|
||||
var el = document.createElement('script');
|
||||
el.type = 'text/javascript';
|
||||
el.onload = function() {
|
||||
_.onload();
|
||||
};
|
||||
el.onerror = function() {
|
||||
_.statusline.style.color = 'red';
|
||||
_.statusline.innerHTML = _.statusline.innerHTML + ' failed';
|
||||
_.statusline.onclick = function() {
|
||||
_.statusline.parentNode.removeChild(_.statusline);
|
||||
};
|
||||
};
|
||||
if (_.files[_.index].substr(0, 2) === '//') {
|
||||
el.src = _.files[_.index];
|
||||
}
|
||||
else {
|
||||
el.src = server + '/' + _.files[_.index] + '.js';
|
||||
}
|
||||
_.head.appendChild(el);
|
||||
++_.index;
|
||||
_.statusline.innerHTML = 'html2canvas: loading "' + el.src + '" ' + _.index + ' / ' + _.files.length + '...';
|
||||
}
|
||||
else {
|
||||
_.statusline.parentNode.removeChild(_.statusline);
|
||||
delete _.statusline;
|
||||
$(document.documentElement).html2canvas({
|
||||
logging: debug,
|
||||
profile: profile,
|
||||
proxy: proxy
|
||||
});
|
||||
}
|
||||
}
|
||||
}, statusline = loader.statusline;
|
||||
statusline.style.position = 'fixed';
|
||||
statusline.style.bottom = '0px';
|
||||
statusline.style.right = '20px';
|
||||
statusline.style.backgroundColor = 'white';
|
||||
statusline.style.border = '1px solid black';
|
||||
statusline.style.borderBottomWidth = '0px';
|
||||
statusline.style.padding = '2px 5px';
|
||||
statusline.style.zIndex = 9999999;
|
||||
document.body.appendChild(statusline);
|
||||
loader.onload();
|
||||
}());
|
@ -1,80 +0,0 @@
|
||||
/*
|
||||
* jQuery helper plugin for examples and tests
|
||||
*/
|
||||
(function( $ ){
|
||||
$.fn.html2canvas = function(options) {
|
||||
if (options && options.profile && window.console && window.console.profile) {
|
||||
console.profile();
|
||||
}
|
||||
var date = new Date(),
|
||||
html2obj,
|
||||
$message = null,
|
||||
timeoutTimer = false,
|
||||
timer = date.getTime();
|
||||
options = options || {};
|
||||
|
||||
options.onrendered = options.onrendered || function( canvas ) {
|
||||
var $canvas = $(canvas),
|
||||
finishTime = new Date();
|
||||
|
||||
if (options && options.profile && window.console && window.console.profileEnd) {
|
||||
console.profileEnd();
|
||||
}
|
||||
$canvas.css({
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: 0
|
||||
}).appendTo(document.body);
|
||||
$canvas.siblings().toggle();
|
||||
|
||||
$(window).click(function(){
|
||||
$canvas.toggle().siblings().toggle();
|
||||
throwMessage("Canvas Render " + ($canvas.is(':visible') ? "visible" : "hidden"));
|
||||
});
|
||||
throwMessage('Screenshot created in '+ ((finishTime.getTime()-timer)) + " ms<br />",4000);
|
||||
|
||||
// test if canvas is read-able
|
||||
try {
|
||||
$canvas[0].toDataURL();
|
||||
} catch(e) {
|
||||
if ($canvas[0].nodeName.toLowerCase() === "canvas") {
|
||||
// TODO, maybe add a bit less offensive way to present this, but still something that can easily be noticed
|
||||
alert("Canvas is tainted, unable to read data");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
html2obj = html2canvas(this, options);
|
||||
|
||||
function throwMessage(msg,duration){
|
||||
window.clearTimeout(timeoutTimer);
|
||||
timeoutTimer = window.setTimeout(function(){
|
||||
$message.fadeOut(function(){
|
||||
$message.remove();
|
||||
$message = null;
|
||||
});
|
||||
},duration || 2000);
|
||||
if ($message)
|
||||
$message.remove();
|
||||
$message = $('<div />').html(msg).css({
|
||||
margin:0,
|
||||
padding:10,
|
||||
background: "#000",
|
||||
opacity:0.7,
|
||||
position:"fixed",
|
||||
top:10,
|
||||
right:10,
|
||||
fontFamily: 'Tahoma',
|
||||
color:'#fff',
|
||||
fontSize:12,
|
||||
borderRadius:12,
|
||||
width:'auto',
|
||||
height:'auto',
|
||||
textAlign:'center',
|
||||
textDecoration:'none',
|
||||
display:'none'
|
||||
}).appendTo(document.body).fadeIn();
|
||||
html2obj.log(msg);
|
||||
}
|
||||
};
|
||||
})( jQuery );
|
274
src/renderer/CanvasRenderer.js
Normal file
274
src/renderer/CanvasRenderer.js
Normal file
@ -0,0 +1,274 @@
|
||||
/* @flow */
|
||||
'use strict';
|
||||
|
||||
import type {RenderTarget, RenderOptions} from '../Renderer';
|
||||
import type Color from '../Color';
|
||||
import type {Path} from '../drawing/Path';
|
||||
import type Size from '../drawing/Size';
|
||||
|
||||
import type {Font} from '../parsing/font';
|
||||
import type {TextDecoration} from '../parsing/textDecoration';
|
||||
import type {TextShadow} from '../parsing/textShadow';
|
||||
import type {Matrix} from '../parsing/transform';
|
||||
|
||||
import type {Bounds} from '../Bounds';
|
||||
import type {ImageElement} from '../ResourceLoader';
|
||||
import type {Gradient} from '../Gradient';
|
||||
import type {TextBounds} from '../TextBounds';
|
||||
|
||||
import {PATH} from '../drawing/Path';
|
||||
import {TEXT_DECORATION_LINE} from '../parsing/textDecoration';
|
||||
|
||||
export default class CanvasRenderer implements RenderTarget<HTMLCanvasElement> {
|
||||
canvas: HTMLCanvasElement;
|
||||
ctx: CanvasRenderingContext2D;
|
||||
options: RenderOptions;
|
||||
|
||||
constructor(canvas: ?HTMLCanvasElement) {
|
||||
this.canvas = canvas ? canvas : document.createElement('canvas');
|
||||
}
|
||||
|
||||
render(options: RenderOptions) {
|
||||
this.ctx = this.canvas.getContext('2d');
|
||||
this.options = options;
|
||||
this.canvas.width = Math.floor(options.width * options.scale);
|
||||
this.canvas.height = Math.floor(options.height * options.scale);
|
||||
this.canvas.style.width = `${options.width}px`;
|
||||
this.canvas.style.height = `${options.height}px`;
|
||||
|
||||
this.ctx.scale(this.options.scale, this.options.scale);
|
||||
this.ctx.translate(-options.x, -options.y);
|
||||
this.ctx.textBaseline = 'bottom';
|
||||
options.logger.log(
|
||||
`Canvas renderer initialized (${options.width}x${options.height} at ${options.x},${options.y}) with scale ${this
|
||||
.options.scale}`
|
||||
);
|
||||
}
|
||||
|
||||
clip(clipPaths: Array<Path>, callback: () => void) {
|
||||
if (clipPaths.length) {
|
||||
this.ctx.save();
|
||||
clipPaths.forEach(path => {
|
||||
this.path(path);
|
||||
this.ctx.clip();
|
||||
});
|
||||
}
|
||||
|
||||
callback();
|
||||
|
||||
if (clipPaths.length) {
|
||||
this.ctx.restore();
|
||||
}
|
||||
}
|
||||
|
||||
drawImage(image: ImageElement, source: Bounds, destination: Bounds) {
|
||||
this.ctx.drawImage(
|
||||
image,
|
||||
source.left,
|
||||
source.top,
|
||||
source.width,
|
||||
source.height,
|
||||
destination.left,
|
||||
destination.top,
|
||||
destination.width,
|
||||
destination.height
|
||||
);
|
||||
}
|
||||
|
||||
drawShape(path: Path, color: Color) {
|
||||
this.path(path);
|
||||
this.ctx.fillStyle = color.toString();
|
||||
this.ctx.fill();
|
||||
}
|
||||
|
||||
fill(color: Color) {
|
||||
this.ctx.fillStyle = color.toString();
|
||||
this.ctx.fill();
|
||||
}
|
||||
|
||||
getTarget(): Promise<HTMLCanvasElement> {
|
||||
return Promise.resolve(this.canvas);
|
||||
}
|
||||
|
||||
path(path: Path) {
|
||||
this.ctx.beginPath();
|
||||
if (Array.isArray(path)) {
|
||||
path.forEach((point, index) => {
|
||||
const start = point.type === PATH.VECTOR ? point : point.start;
|
||||
if (index === 0) {
|
||||
this.ctx.moveTo(start.x, start.y);
|
||||
} else {
|
||||
this.ctx.lineTo(start.x, start.y);
|
||||
}
|
||||
|
||||
if (point.type === PATH.BEZIER_CURVE) {
|
||||
this.ctx.bezierCurveTo(
|
||||
point.startControl.x,
|
||||
point.startControl.y,
|
||||
point.endControl.x,
|
||||
point.endControl.y,
|
||||
point.end.x,
|
||||
point.end.y
|
||||
);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.ctx.arc(
|
||||
path.x + path.radius,
|
||||
path.y + path.radius,
|
||||
path.radius,
|
||||
0,
|
||||
Math.PI * 2,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
this.ctx.closePath();
|
||||
}
|
||||
|
||||
rectangle(x: number, y: number, width: number, height: number, color: Color) {
|
||||
this.ctx.fillStyle = color.toString();
|
||||
this.ctx.fillRect(x, y, width, height);
|
||||
}
|
||||
|
||||
renderLinearGradient(bounds: Bounds, gradient: Gradient) {
|
||||
const linearGradient = this.ctx.createLinearGradient(
|
||||
bounds.left + gradient.direction.x1,
|
||||
bounds.top + gradient.direction.y1,
|
||||
bounds.left + gradient.direction.x0,
|
||||
bounds.top + gradient.direction.y0
|
||||
);
|
||||
|
||||
gradient.colorStops.forEach(colorStop => {
|
||||
linearGradient.addColorStop(colorStop.stop, colorStop.color.toString());
|
||||
});
|
||||
|
||||
this.ctx.fillStyle = linearGradient;
|
||||
this.ctx.fillRect(bounds.left, bounds.top, bounds.width, bounds.height);
|
||||
}
|
||||
|
||||
renderRepeat(
|
||||
path: Path,
|
||||
image: ImageElement,
|
||||
imageSize: Size,
|
||||
offsetX: number,
|
||||
offsetY: number
|
||||
) {
|
||||
this.path(path);
|
||||
this.ctx.fillStyle = this.ctx.createPattern(this.resizeImage(image, imageSize), 'repeat');
|
||||
this.ctx.translate(offsetX, offsetY);
|
||||
this.ctx.fill();
|
||||
this.ctx.translate(-offsetX, -offsetY);
|
||||
}
|
||||
|
||||
renderTextNode(
|
||||
textBounds: Array<TextBounds>,
|
||||
color: Color,
|
||||
font: Font,
|
||||
textDecoration: TextDecoration | null,
|
||||
textShadows: Array<TextShadow> | null
|
||||
) {
|
||||
this.ctx.font = [
|
||||
font.fontStyle,
|
||||
font.fontVariant,
|
||||
font.fontWeight,
|
||||
font.fontSize,
|
||||
font.fontFamily
|
||||
]
|
||||
.join(' ')
|
||||
.split(',')[0];
|
||||
|
||||
textBounds.forEach(text => {
|
||||
this.ctx.fillStyle = color.toString();
|
||||
if (textShadows && text.text.trim().length) {
|
||||
textShadows.slice(0).reverse().forEach(textShadow => {
|
||||
this.ctx.shadowColor = textShadow.color.toString();
|
||||
this.ctx.shadowOffsetX = textShadow.offsetX * this.options.scale;
|
||||
this.ctx.shadowOffsetY = textShadow.offsetY * this.options.scale;
|
||||
this.ctx.shadowBlur = textShadow.blur;
|
||||
|
||||
this.ctx.fillText(
|
||||
text.text,
|
||||
text.bounds.left,
|
||||
text.bounds.top + text.bounds.height
|
||||
);
|
||||
});
|
||||
} else {
|
||||
this.ctx.fillText(
|
||||
text.text,
|
||||
text.bounds.left,
|
||||
text.bounds.top + text.bounds.height
|
||||
);
|
||||
}
|
||||
|
||||
if (textDecoration !== null) {
|
||||
const textDecorationColor = textDecoration.textDecorationColor || color;
|
||||
textDecoration.textDecorationLine.forEach(textDecorationLine => {
|
||||
switch (textDecorationLine) {
|
||||
case TEXT_DECORATION_LINE.UNDERLINE:
|
||||
// Draws a line at the baseline of the font
|
||||
// TODO As some browsers display the line as more than 1px if the font-size is big,
|
||||
// need to take that into account both in position and size
|
||||
const {baseline} = this.options.fontMetrics.getMetrics(font);
|
||||
this.rectangle(
|
||||
text.bounds.left,
|
||||
Math.round(text.bounds.top + baseline),
|
||||
text.bounds.width,
|
||||
1,
|
||||
textDecorationColor
|
||||
);
|
||||
break;
|
||||
case TEXT_DECORATION_LINE.OVERLINE:
|
||||
this.rectangle(
|
||||
text.bounds.left,
|
||||
Math.round(text.bounds.top),
|
||||
text.bounds.width,
|
||||
1,
|
||||
textDecorationColor
|
||||
);
|
||||
break;
|
||||
case TEXT_DECORATION_LINE.LINE_THROUGH:
|
||||
// TODO try and find exact position for line-through
|
||||
const {middle} = this.options.fontMetrics.getMetrics(font);
|
||||
this.rectangle(
|
||||
text.bounds.left,
|
||||
Math.ceil(text.bounds.top + middle),
|
||||
text.bounds.width,
|
||||
1,
|
||||
textDecorationColor
|
||||
);
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
resizeImage(image: ImageElement, size: Size): ImageElement {
|
||||
if (image.width === size.width && image.height === size.height) {
|
||||
return image;
|
||||
}
|
||||
|
||||
const canvas = this.canvas.ownerDocument.createElement('canvas');
|
||||
canvas.width = size.width;
|
||||
canvas.height = size.height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.drawImage(image, 0, 0, image.width, image.height, 0, 0, size.width, size.height);
|
||||
return canvas;
|
||||
}
|
||||
|
||||
setOpacity(opacity: number) {
|
||||
this.ctx.globalAlpha = opacity;
|
||||
}
|
||||
|
||||
transform(offsetX: number, offsetY: number, matrix: Matrix, callback: () => void) {
|
||||
this.ctx.save();
|
||||
this.ctx.translate(offsetX, offsetY);
|
||||
this.ctx.transform(matrix[0], matrix[1], matrix[2], matrix[3], matrix[4], matrix[5]);
|
||||
this.ctx.translate(-offsetX, -offsetY);
|
||||
|
||||
callback();
|
||||
|
||||
this.ctx.restore();
|
||||
}
|
||||
}
|
83
src/renderer/ForeignObjectRenderer.js
Normal file
83
src/renderer/ForeignObjectRenderer.js
Normal file
@ -0,0 +1,83 @@
|
||||
import type {RenderOptions} from '../Renderer';
|
||||
|
||||
export default class ForeignObjectRenderer {
|
||||
options: RenderOptions;
|
||||
element: HTMLElement;
|
||||
|
||||
constructor(element: HTMLElement) {
|
||||
this.element = element;
|
||||
}
|
||||
|
||||
render(options) {
|
||||
this.options = options;
|
||||
this.canvas = document.createElement('canvas');
|
||||
this.ctx = this.canvas.getContext('2d');
|
||||
this.canvas.width = Math.floor(options.width) * options.scale;
|
||||
this.canvas.height = Math.floor(options.height) * options.scale;
|
||||
this.canvas.style.width = `${options.width}px`;
|
||||
this.canvas.style.height = `${options.height}px`;
|
||||
|
||||
options.logger.log(
|
||||
`ForeignObject renderer initialized (${options.width}x${options.height} at ${options.x},${options.y}) with scale ${options.scale}`
|
||||
);
|
||||
const svg = createForeignObjectSVG(
|
||||
Math.max(options.windowWidth, options.width) * options.scale,
|
||||
Math.max(options.windowHeight, options.height) * options.scale,
|
||||
options.scrollX * options.scale,
|
||||
options.scrollY * options.scale,
|
||||
this.element
|
||||
);
|
||||
|
||||
return loadSerializedSVG(svg).then(img => {
|
||||
if (options.backgroundColor) {
|
||||
this.ctx.fillStyle = options.backgroundColor.toString();
|
||||
this.ctx.fillRect(
|
||||
0,
|
||||
0,
|
||||
options.width * options.scale,
|
||||
options.height * options.scale
|
||||
);
|
||||
}
|
||||
|
||||
this.ctx.drawImage(img, -options.x * options.scale, -options.y * options.scale);
|
||||
return this.canvas;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const createForeignObjectSVG = (
|
||||
width: number,
|
||||
height: number,
|
||||
x: number,
|
||||
y: number,
|
||||
node: Node
|
||||
) => {
|
||||
const xmlns = 'http://www.w3.org/2000/svg';
|
||||
const svg = document.createElementNS(xmlns, 'svg');
|
||||
const foreignObject = document.createElementNS(xmlns, 'foreignObject');
|
||||
svg.setAttributeNS(null, 'width', width);
|
||||
svg.setAttributeNS(null, 'height', height);
|
||||
|
||||
foreignObject.setAttributeNS(null, 'width', '100%');
|
||||
foreignObject.setAttributeNS(null, 'height', '100%');
|
||||
foreignObject.setAttributeNS(null, 'x', x);
|
||||
foreignObject.setAttributeNS(null, 'y', y);
|
||||
foreignObject.setAttributeNS(null, 'externalResourcesRequired', 'true');
|
||||
svg.appendChild(foreignObject);
|
||||
|
||||
foreignObject.appendChild(node);
|
||||
|
||||
return svg;
|
||||
};
|
||||
|
||||
export const loadSerializedSVG = (svg: Node) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
img.onload = () => resolve(img);
|
||||
img.onerror = reject;
|
||||
|
||||
img.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(
|
||||
new XMLSerializer().serializeToString(svg)
|
||||
)}`;
|
||||
});
|
||||
};
|
259
src/renderer/RefTestRenderer.js
Normal file
259
src/renderer/RefTestRenderer.js
Normal file
@ -0,0 +1,259 @@
|
||||
/* @flow */
|
||||
'use strict';
|
||||
import {parse} from 'url';
|
||||
|
||||
import type {RenderTarget, RenderOptions} from '../Renderer';
|
||||
import type Color from '../Color';
|
||||
import type {Path} from '../drawing/Path';
|
||||
import type Size from '../drawing/Size';
|
||||
|
||||
import type {Font} from '../parsing/font';
|
||||
import type {
|
||||
TextDecoration,
|
||||
TextDecorationStyle,
|
||||
TextDecorationLine
|
||||
} from '../parsing/textDecoration';
|
||||
import type {TextShadow} from '../parsing/textShadow';
|
||||
import type {Matrix} from '../parsing/transform';
|
||||
|
||||
import type {Bounds} from '../Bounds';
|
||||
import type {ImageElement} from '../ResourceLoader';
|
||||
import type {Gradient} from '../Gradient';
|
||||
import type {TextBounds} from '../TextBounds';
|
||||
|
||||
import {TEXT_DECORATION_STYLE, TEXT_DECORATION_LINE} from '../parsing/textDecoration';
|
||||
import {PATH} from '../drawing/Path';
|
||||
|
||||
class RefTestRenderer implements RenderTarget<string> {
|
||||
options: RenderOptions;
|
||||
indent: number;
|
||||
lines: Array<string>;
|
||||
|
||||
render(options: RenderOptions) {
|
||||
this.options = options;
|
||||
this.indent = 0;
|
||||
this.lines = [];
|
||||
options.logger.log(`RefTest renderer initialized`);
|
||||
this.writeLine(`Window: [${options.width}, ${options.height}]`);
|
||||
}
|
||||
|
||||
clip(clipPaths: Array<Path>, callback: () => void) {
|
||||
this.writeLine(`Clip: ${clipPaths.map(this.formatPath, this).join(' | ')}`);
|
||||
this.indent += 2;
|
||||
callback();
|
||||
this.indent -= 2;
|
||||
}
|
||||
|
||||
drawImage(image: ImageElement, source: Bounds, destination: Bounds) {
|
||||
this.writeLine(
|
||||
`Draw image: ${this.formatImage(image)} (source: ${this.formatBounds(
|
||||
source
|
||||
)}) (destination: ${this.formatBounds(source)})`
|
||||
);
|
||||
}
|
||||
|
||||
drawShape(path: Path, color: Color) {
|
||||
this.writeLine(`Shape: ${color.toString()} ${this.formatPath(path)}`);
|
||||
}
|
||||
|
||||
fill(color: Color) {
|
||||
this.writeLine(`Fill: ${color.toString()}`);
|
||||
}
|
||||
|
||||
getTarget(): Promise<string> {
|
||||
return Promise.resolve(this.lines.join('\n'));
|
||||
}
|
||||
|
||||
rectangle(x: number, y: number, width: number, height: number, color: Color) {
|
||||
const list = [x, y, width, height].map(v => Math.round(v)).join(', ');
|
||||
this.writeLine(`Rectangle: [${list}] ${color.toString()}`);
|
||||
}
|
||||
|
||||
formatBounds(bounds: Bounds): string {
|
||||
const list = [bounds.left, bounds.top, bounds.width, bounds.height];
|
||||
return `[${list.map(v => Math.round(v)).join(', ')}]`;
|
||||
}
|
||||
|
||||
formatImage(image: ImageElement): string {
|
||||
return image.tagName === 'CANVAS'
|
||||
? 'Canvas'
|
||||
: // $FlowFixMe
|
||||
`Image ("${parse(image.src).pathname.substring(0, 100)}")`;
|
||||
}
|
||||
|
||||
formatPath(path: Path): string {
|
||||
if (!Array.isArray(path)) {
|
||||
return `Circle(x: ${Math.round(path.x)}, y: ${Math.round(path.y)}, r: ${Math.round(
|
||||
path.radius
|
||||
)})`;
|
||||
}
|
||||
const string = path
|
||||
.map(v => {
|
||||
if (v.type === PATH.VECTOR) {
|
||||
return `Vector(x: ${Math.round(v.x)}, y: ${Math.round(v.y)})`;
|
||||
}
|
||||
if (v.type === PATH.BEZIER_CURVE) {
|
||||
const values = [
|
||||
`x0: ${Math.round(v.start.x)}`,
|
||||
`y0: ${Math.round(v.start.y)}`,
|
||||
`x1: ${Math.round(v.end.x)}`,
|
||||
`y1: ${Math.round(v.end.y)}`,
|
||||
`cx0: ${Math.round(v.startControl.x)}`,
|
||||
`cy0: ${Math.round(v.startControl.y)}`,
|
||||
`cx1: ${Math.round(v.endControl.x)}`,
|
||||
`cy1: ${Math.round(v.endControl.y)}`
|
||||
];
|
||||
return `BezierCurve(${values.join(', ')})`;
|
||||
}
|
||||
})
|
||||
.join(' > ');
|
||||
return `Path (${string})`;
|
||||
}
|
||||
|
||||
renderLinearGradient(bounds: Bounds, gradient: Gradient) {
|
||||
const direction = [
|
||||
`x0: ${Math.round(gradient.direction.x0)}`,
|
||||
`x1: ${Math.round(gradient.direction.x1)}`,
|
||||
`y0: ${Math.round(gradient.direction.y0)}`,
|
||||
`y1: ${Math.round(gradient.direction.y1)}`
|
||||
];
|
||||
|
||||
const stops = gradient.colorStops.map(
|
||||
stop => `${stop.color.toString()} ${Math.ceil(stop.stop * 100) / 100}`
|
||||
);
|
||||
|
||||
this.writeLine(
|
||||
`Gradient: ${this.formatBounds(bounds)} linear-gradient(${direction.join(
|
||||
', '
|
||||
)} ${stops.join(', ')})`
|
||||
);
|
||||
}
|
||||
|
||||
renderRepeat(
|
||||
path: Path,
|
||||
image: ImageElement,
|
||||
imageSize: Size,
|
||||
offsetX: number,
|
||||
offsetY: number
|
||||
) {
|
||||
this.writeLine(
|
||||
`Repeat: ${this.formatImage(image)} [${Math.round(offsetX)}, ${Math.round(
|
||||
offsetY
|
||||
)}] Size (${Math.round(imageSize.width)}, ${Math.round(
|
||||
imageSize.height
|
||||
)}) ${this.formatPath(path)}`
|
||||
);
|
||||
}
|
||||
|
||||
renderTextNode(
|
||||
textBounds: Array<TextBounds>,
|
||||
color: Color,
|
||||
font: Font,
|
||||
textDecoration: TextDecoration | null,
|
||||
textShadows: Array<TextShadow> | null
|
||||
) {
|
||||
const fontString = [
|
||||
font.fontStyle,
|
||||
font.fontVariant,
|
||||
font.fontWeight,
|
||||
parseInt(font.fontSize, 10),
|
||||
font.fontFamily.replace(/"/g, '')
|
||||
]
|
||||
.join(' ')
|
||||
.split(',')[0];
|
||||
|
||||
const textDecorationString = this.textDecoration(textDecoration, color);
|
||||
const shadowString = textShadows
|
||||
? ` Shadows: (${textShadows
|
||||
.map(
|
||||
shadow =>
|
||||
`${shadow.color.toString()} ${shadow.offsetX}px ${shadow.offsetY}px ${shadow.blur}px`
|
||||
)
|
||||
.join(', ')})`
|
||||
: '';
|
||||
|
||||
this.writeLine(
|
||||
`Text: ${color.toString()} ${fontString}${shadowString}${textDecorationString}`
|
||||
);
|
||||
|
||||
this.indent += 2;
|
||||
textBounds.forEach(textBound => {
|
||||
this.writeLine(
|
||||
`[${Math.round(textBound.bounds.left)}, ${Math.round(
|
||||
textBound.bounds.top
|
||||
)}]: ${textBound.text}`
|
||||
);
|
||||
});
|
||||
this.indent -= 2;
|
||||
}
|
||||
|
||||
textDecoration(textDecoration: TextDecoration | null, color: Color): string {
|
||||
if (textDecoration) {
|
||||
const textDecorationColor = (textDecoration.textDecorationColor
|
||||
? textDecoration.textDecorationColor
|
||||
: color).toString();
|
||||
const textDecorationLines = textDecoration.textDecorationLine.map(
|
||||
this.textDecorationLine,
|
||||
this
|
||||
);
|
||||
return textDecoration
|
||||
? ` ${this.textDecorationStyle(
|
||||
textDecoration.textDecorationStyle
|
||||
)} ${textDecorationColor} ${textDecorationLines.join(', ')}`
|
||||
: '';
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
textDecorationLine(textDecorationLine: TextDecorationLine): string {
|
||||
switch (textDecorationLine) {
|
||||
case TEXT_DECORATION_LINE.LINE_THROUGH:
|
||||
return 'line-through';
|
||||
case TEXT_DECORATION_LINE.OVERLINE:
|
||||
return 'overline';
|
||||
case TEXT_DECORATION_LINE.UNDERLINE:
|
||||
return 'underline';
|
||||
case TEXT_DECORATION_LINE.BLINK:
|
||||
return 'blink';
|
||||
}
|
||||
return 'UNKNOWN';
|
||||
}
|
||||
|
||||
textDecorationStyle(textDecorationStyle: TextDecorationStyle): string {
|
||||
switch (textDecorationStyle) {
|
||||
case TEXT_DECORATION_STYLE.SOLID:
|
||||
return 'solid';
|
||||
case TEXT_DECORATION_STYLE.DOTTED:
|
||||
return 'dotted';
|
||||
case TEXT_DECORATION_STYLE.DOUBLE:
|
||||
return 'double';
|
||||
case TEXT_DECORATION_STYLE.DASHED:
|
||||
return 'dashed';
|
||||
case TEXT_DECORATION_STYLE.WAVY:
|
||||
return 'WAVY';
|
||||
}
|
||||
return 'UNKNOWN';
|
||||
}
|
||||
|
||||
setOpacity(opacity: number) {
|
||||
this.writeLine(`Opacity: ${opacity}`);
|
||||
}
|
||||
|
||||
transform(offsetX: number, offsetY: number, matrix: Matrix, callback: () => void) {
|
||||
this.writeLine(
|
||||
`Transform: (${Math.round(offsetX)}, ${Math.round(offsetY)}) [${matrix
|
||||
.map(v => Math.round(v * 100) / 100)
|
||||
.join(', ')}]`
|
||||
);
|
||||
this.indent += 2;
|
||||
callback();
|
||||
this.indent -= 2;
|
||||
}
|
||||
|
||||
writeLine(text: string) {
|
||||
this.lines.push(`${new Array(this.indent + 1).join(' ')}${text}`);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = RefTestRenderer;
|
@ -1,242 +0,0 @@
|
||||
/*
|
||||
html2canvas @VERSION@ <http://html2canvas.hertzen.com>
|
||||
Copyright (c) 2011 Niklas von Hertzen. All rights reserved.
|
||||
http://www.twitter.com/niklasvh
|
||||
|
||||
Released under MIT License
|
||||
*/
|
||||
|
||||
|
||||
_html2canvas.Renderer.Canvas = function( options ) {
|
||||
|
||||
options = options || {};
|
||||
|
||||
var doc = document,
|
||||
canvas = options.canvas || doc.createElement('canvas'),
|
||||
usingFlashcanvas = false,
|
||||
_createCalled = false,
|
||||
canvasReadyToDraw = false,
|
||||
methods,
|
||||
flashMaxSize = 2880; // flash bitmap limited to 2880x2880px // http://stackoverflow.com/questions/2033792/argumenterror-error-2015-invalid-bitmapdata
|
||||
|
||||
|
||||
if (canvas.getContext){
|
||||
h2clog("html2canvas: Renderer: using canvas renderer");
|
||||
canvasReadyToDraw = true;
|
||||
} else if ( options.flashcanvas !== undefined ){
|
||||
usingFlashcanvas = true;
|
||||
h2clog("html2canvas: Renderer: canvas not available, using flashcanvas");
|
||||
var script = doc.createElement("script");
|
||||
script.src = options.flashcanvas;
|
||||
|
||||
script.onload = (function(script, func){
|
||||
var intervalFunc;
|
||||
|
||||
if (script.onload === undefined) {
|
||||
// IE lack of support for script onload
|
||||
|
||||
if( script.onreadystatechange !== undefined ) {
|
||||
|
||||
intervalFunc = function() {
|
||||
if (script.readyState !== "loaded" && script.readyState !== "complete") {
|
||||
window.setTimeout( intervalFunc, 250 );
|
||||
|
||||
} else {
|
||||
// it is loaded
|
||||
func();
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
window.setTimeout( intervalFunc, 250 );
|
||||
|
||||
} else {
|
||||
h2clog("html2canvas: Renderer: Can't track when flashcanvas is loaded");
|
||||
}
|
||||
|
||||
} else {
|
||||
return func;
|
||||
}
|
||||
|
||||
})(script, function(){
|
||||
|
||||
if (typeof window.FlashCanvas !== "undefined") {
|
||||
h2clog("html2canvas: Renderer: Flashcanvas initialized");
|
||||
window.FlashCanvas.initElement( canvas );
|
||||
|
||||
canvasReadyToDraw = true;
|
||||
if ( _createCalled !== false ) {
|
||||
methods._create.apply( null, _createCalled );
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
doc.body.appendChild( script );
|
||||
|
||||
}
|
||||
|
||||
methods = {
|
||||
_create: function( zStack, options, doc, queue, _html2canvas ) {
|
||||
|
||||
if ( !canvasReadyToDraw ) {
|
||||
_createCalled = arguments;
|
||||
return canvas;
|
||||
}
|
||||
|
||||
var ctx = canvas.getContext("2d"),
|
||||
storageContext,
|
||||
i,
|
||||
queueLen,
|
||||
a,
|
||||
newCanvas,
|
||||
bounds,
|
||||
testCanvas = document.createElement("canvas"),
|
||||
hasCTX = ( testCanvas.getContext !== undefined ),
|
||||
storageLen,
|
||||
renderItem,
|
||||
testctx = ( hasCTX ) ? testCanvas.getContext("2d") : {},
|
||||
safeImages = [],
|
||||
fstyle;
|
||||
|
||||
canvas.width = canvas.style.width = (!usingFlashcanvas) ? options.width || zStack.ctx.width : Math.min(flashMaxSize, (options.width || zStack.ctx.width) );
|
||||
canvas.height = canvas.style.height = (!usingFlashcanvas) ? options.height || zStack.ctx.height : Math.min(flashMaxSize, (options.height || zStack.ctx.height) );
|
||||
|
||||
fstyle = ctx.fillStyle;
|
||||
ctx.fillStyle = zStack.backgroundColor;
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.fillStyle = fstyle;
|
||||
|
||||
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 || {};
|
||||
|
||||
//this.canvasRenderContext(storageContext,parentctx);
|
||||
|
||||
// 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){
|
||||
|
||||
for (a = 0, storageLen = storageContext.ctx.storage.length; a < storageLen; a+=1){
|
||||
|
||||
renderItem = storageContext.ctx.storage[a];
|
||||
|
||||
|
||||
switch(renderItem.type){
|
||||
case "variable":
|
||||
ctx[renderItem.name] = renderItem['arguments'];
|
||||
break;
|
||||
case "function":
|
||||
if (renderItem.name === "fillRect") {
|
||||
|
||||
if (!usingFlashcanvas || renderItem['arguments'][0] + renderItem['arguments'][2] < flashMaxSize && renderItem['arguments'][1] + renderItem['arguments'][3] < flashMaxSize) {
|
||||
ctx.fillRect.apply( ctx, renderItem['arguments'] );
|
||||
}
|
||||
} else if (renderItem.name === "drawShape") {
|
||||
|
||||
( function( args ) {
|
||||
|
||||
var i, len = args.length;
|
||||
ctx.beginPath();
|
||||
for ( i = 0; i < len; i++ ) {
|
||||
ctx[ args[ i ].name ].apply( ctx, args[ i ]['arguments'] );
|
||||
}
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
})( renderItem['arguments'] );
|
||||
|
||||
} else if (renderItem.name === "fillText") {
|
||||
if (!usingFlashcanvas || renderItem['arguments'][1] < flashMaxSize && renderItem['arguments'][2] < flashMaxSize) {
|
||||
ctx.fillText.apply( ctx, renderItem['arguments'] );
|
||||
}
|
||||
} else if (renderItem.name === "drawImage") {
|
||||
|
||||
if (renderItem['arguments'][8] > 0 && renderItem['arguments'][7]){
|
||||
if ( hasCTX && options.taintTest ) {
|
||||
if ( safeImages.indexOf( renderItem['arguments'][ 0 ].src ) === -1 ) {
|
||||
testctx.drawImage( renderItem['arguments'][ 0 ], 0, 0 );
|
||||
try {
|
||||
testctx.getImageData( 0, 0, 1, 1 );
|
||||
} catch(e) {
|
||||
testCanvas = doc.createElement("canvas");
|
||||
testctx = testCanvas.getContext("2d");
|
||||
continue;
|
||||
}
|
||||
|
||||
safeImages.push( renderItem['arguments'][ 0 ].src );
|
||||
|
||||
}
|
||||
}
|
||||
ctx.drawImage.apply( ctx, renderItem['arguments'] );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
break;
|
||||
default:
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
if (storageContext.clip){
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
h2clog("html2canvas: Renderer: Canvas renderer done - returning canvas obj");
|
||||
|
||||
queueLen = options.elements.length;
|
||||
|
||||
if (queueLen === 1) {
|
||||
if (typeof options.elements[ 0 ] === "object" && options.elements[ 0 ].nodeName !== "BODY" && usingFlashcanvas === false) {
|
||||
// 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;
|
||||
ctx = newCanvas.getContext("2d");
|
||||
|
||||
ctx.drawImage( canvas, bounds.left, bounds.top, bounds.width, bounds.height, 0, 0, bounds.width, bounds.height );
|
||||
canvas = null;
|
||||
return newCanvas;
|
||||
}
|
||||
} /*else {
|
||||
// TODO clip and resize multiple elements
|
||||
|
||||
for ( i = 0; i < queueLen; i+=1 ) {
|
||||
if (options.elements[ i ] instanceof Element) {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
|
||||
|
||||
return canvas;
|
||||
}
|
||||
};
|
||||
|
||||
return methods;
|
||||
|
||||
};
|
@ -1,206 +0,0 @@
|
||||
/*
|
||||
html2canvas @VERSION@ <http://html2canvas.hertzen.com>
|
||||
Copyright (c) 2011 Niklas von Hertzen. All rights reserved.
|
||||
http://www.twitter.com/niklasvh
|
||||
|
||||
Released under MIT License
|
||||
*/
|
||||
|
||||
|
||||
// WARNING THIS file is outdated, and hasn't been tested in quite a while
|
||||
|
||||
_html2canvas.Renderer.SVG = function( options ) {
|
||||
|
||||
options = options || {};
|
||||
|
||||
var doc = document,
|
||||
svgNS = "http://www.w3.org/2000/svg",
|
||||
svg = doc.createElementNS(svgNS, "svg"),
|
||||
xlinkNS = "http://www.w3.org/1999/xlink",
|
||||
defs = doc.createElementNS(svgNS, "defs"),
|
||||
i,
|
||||
a,
|
||||
queueLen,
|
||||
storageLen,
|
||||
storageContext,
|
||||
renderItem,
|
||||
el,
|
||||
settings = {},
|
||||
text,
|
||||
fontStyle,
|
||||
clipId = 0,
|
||||
methods;
|
||||
|
||||
|
||||
methods = {
|
||||
_create: function( zStack, options, doc, queue, _html2canvas ) {
|
||||
svg.setAttribute("version", "1.1");
|
||||
svg.setAttribute("baseProfile", "full");
|
||||
|
||||
svg.setAttribute("viewBox", "0 0 " + Math.max(zStack.ctx.width, options.width) + " " + Math.max(zStack.ctx.height, options.height));
|
||||
svg.setAttribute("width", Math.max(zStack.ctx.width, options.width) + "px");
|
||||
svg.setAttribute("height", Math.max(zStack.ctx.height, options.height) + "px");
|
||||
svg.setAttribute("preserveAspectRatio", "none");
|
||||
svg.appendChild(defs);
|
||||
|
||||
|
||||
|
||||
for (i = 0, queueLen = queue.length; i < queueLen; i+=1){
|
||||
|
||||
storageContext = queue.splice(0, 1)[0];
|
||||
storageContext.canvasPosition = storageContext.canvasPosition || {};
|
||||
|
||||
//this.canvasRenderContext(storageContext,parentctx);
|
||||
|
||||
|
||||
/*
|
||||
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){
|
||||
|
||||
for (a = 0, storageLen = storageContext.ctx.storage.length; a < storageLen; a+=1){
|
||||
|
||||
renderItem = storageContext.ctx.storage[a];
|
||||
|
||||
|
||||
|
||||
switch(renderItem.type){
|
||||
case "variable":
|
||||
settings[renderItem.name] = renderItem['arguments'];
|
||||
break;
|
||||
case "function":
|
||||
if (renderItem.name === "fillRect") {
|
||||
|
||||
el = doc.createElementNS(svgNS, "rect");
|
||||
el.setAttribute("x", renderItem['arguments'][0]);
|
||||
el.setAttribute("y", renderItem['arguments'][1]);
|
||||
el.setAttribute("width", renderItem['arguments'][2]);
|
||||
el.setAttribute("height", renderItem['arguments'][3]);
|
||||
el.setAttribute("fill", settings.fillStyle);
|
||||
svg.appendChild(el);
|
||||
|
||||
} else if(renderItem.name === "fillText") {
|
||||
el = doc.createElementNS(svgNS, "text");
|
||||
|
||||
fontStyle = settings.font.split(" ");
|
||||
|
||||
el.style.fontVariant = fontStyle.splice(0, 1)[0];
|
||||
el.style.fontWeight = fontStyle.splice(0, 1)[0];
|
||||
el.style.fontStyle = fontStyle.splice(0, 1)[0];
|
||||
el.style.fontSize = fontStyle.splice(0, 1)[0];
|
||||
|
||||
el.setAttribute("x", renderItem['arguments'][1]);
|
||||
el.setAttribute("y", renderItem['arguments'][2] - (parseInt(el.style.fontSize, 10) + 3));
|
||||
|
||||
el.setAttribute("fill", settings.fillStyle);
|
||||
|
||||
|
||||
|
||||
|
||||
// TODO get proper baseline
|
||||
el.style.dominantBaseline = "text-before-edge";
|
||||
el.style.fontFamily = fontStyle.join(" ");
|
||||
|
||||
text = doc.createTextNode(renderItem['arguments'][0]);
|
||||
el.appendChild(text);
|
||||
|
||||
|
||||
svg.appendChild(el);
|
||||
|
||||
|
||||
|
||||
} else if(renderItem.name === "drawImage") {
|
||||
|
||||
if (renderItem['arguments'][8] > 0 && renderItem['arguments'][7]){
|
||||
|
||||
// TODO check whether even any clipping is necessary for this particular image
|
||||
el = doc.createElementNS(svgNS, "clipPath");
|
||||
el.setAttribute("id", "clipId" + clipId);
|
||||
|
||||
text = doc.createElementNS(svgNS, "rect");
|
||||
text.setAttribute("x", renderItem['arguments'][5] );
|
||||
text.setAttribute("y", renderItem['arguments'][6]);
|
||||
|
||||
text.setAttribute("width", renderItem['arguments'][3]);
|
||||
text.setAttribute("height", renderItem['arguments'][4]);
|
||||
el.appendChild(text);
|
||||
defs.appendChild(el);
|
||||
|
||||
el = doc.createElementNS(svgNS, "image");
|
||||
el.setAttributeNS(xlinkNS, "xlink:href", renderItem['arguments'][0].src);
|
||||
el.setAttribute("width", renderItem['arguments'][7]);
|
||||
el.setAttribute("height", renderItem['arguments'][8]);
|
||||
el.setAttribute("x", renderItem['arguments'][5]);
|
||||
el.setAttribute("y", renderItem['arguments'][6]);
|
||||
el.setAttribute("clip-path", "url(#clipId" + clipId + ")");
|
||||
// el.setAttribute("xlink:href", );
|
||||
|
||||
|
||||
el.setAttribute("preserveAspectRatio", "none");
|
||||
|
||||
svg.appendChild(el);
|
||||
|
||||
|
||||
clipId += 1;
|
||||
/*
|
||||
ctx.drawImage(
|
||||
renderItem['arguments'][0],
|
||||
renderItem['arguments'][1],
|
||||
renderItem['arguments'][2],
|
||||
renderItem['arguments'][3],
|
||||
renderItem['arguments'][4],
|
||||
renderItem['arguments'][5],
|
||||
renderItem['arguments'][6],
|
||||
renderItem['arguments'][7],
|
||||
renderItem['arguments'][8]
|
||||
);
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
break;
|
||||
default:
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
/*
|
||||
if (storageContext.clip){
|
||||
ctx.restore();
|
||||
}
|
||||
*/
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
h2clog("html2canvas: Renderer: SVG Renderer done - returning SVG DOM obj");
|
||||
|
||||
return svg;
|
||||
}
|
||||
};
|
||||
|
||||
return methods;
|
||||
|
||||
|
||||
};
|
16
tests/assets/iframe/frame1.html
Normal file
16
tests/assets/iframe/frame1.html
Normal file
@ -0,0 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head lang="en">
|
||||
<meta charset="UTF-8">
|
||||
<title>frame 1</title>
|
||||
<style>
|
||||
body {
|
||||
background: green;
|
||||
color: red;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
this is the content of frame1
|
||||
</body>
|
||||
</html>
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user