mirror of
https://github.com/niklasvh/html2canvas.git
synced 2023-08-10 21:13:10 +03:00
Compare commits
412 Commits
0.4.1
...
v1.0.0-alp
Author | SHA1 | Date | |
---|---|---|---|
c4e670addf | |||
0aeb54ca2e | |||
eec84fa39e | |||
22f58d5d1c | |||
9046e0d554 | |||
afa5d7cb8e | |||
3881e3cf96 | |||
0aa973ab0d | |||
baaf9b0701 | |||
02de2ee829 | |||
a570f5df74 | |||
38749bc4b6 | |||
e1d6b4c76f | |||
31f2c22477 | |||
6d0cd2d226 | |||
7335984ab7 | |||
78c3c7fc71 | |||
4551976246 | |||
9e04772b42 | |||
54c4002df7 | |||
91641a3746 | |||
c4ba6f795c | |||
0b9f34a5bf | |||
757c32f6c4 | |||
09bab18b48 | |||
7e53b195ea | |||
ab966ff311 | |||
7bb4a6f08f | |||
f50da9718f | |||
3965a0fd40 | |||
77d258f1d8 | |||
261702a693 | |||
cacb9f64e4 | |||
8ef3861a5c | |||
0b74e69611 | |||
c272b2e122 | |||
2d132b85c6 | |||
50608e9cd4 | |||
d87fef11a4 | |||
250208dc99 | |||
2237e8e230 | |||
d3c640088c | |||
8fd616aed2 | |||
d1e870de88 | |||
9bc0fb0bd1 | |||
d83bc0247a | |||
13e80cc635 | |||
b239937e00 | |||
e8a4d775e8 | |||
a6a3c1bd0f | |||
850338a76a | |||
63377d47a4 | |||
1d1c74a74e | |||
ef5c59e26d | |||
8b653f89bc | |||
9db8580b97 | |||
4a09264103 | |||
b8178e92b4 | |||
166cbba7c2 | |||
0c8d38d9c0 | |||
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 |
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 #
|
13
.github/no-response.yml
vendored
Normal file
13
.github/no-response.yml
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
# Configuration for probot-no-response - https://github.com/probot/no-response
|
||||
|
||||
# Number of days of inactivity before an Issue is closed for lack of response
|
||||
daysUntilClose: 14
|
||||
# Label requiring a response
|
||||
responseRequiredLabel: Needs More Information
|
||||
# Comment to post when closing an Issue for lack of response. Set to `false` to disable
|
||||
closeComment: >
|
||||
This issue has been automatically closed because there has been no response
|
||||
to our request for more information from the original author. With only the
|
||||
information that is currently in the issue, we don't have enough information
|
||||
to take action. Please reach out if you have or find the answers we need so
|
||||
that we can investigate further.
|
15
.gitignore
vendored
15
.gitignore
vendored
@ -1,20 +1,17 @@
|
||||
/dist
|
||||
/build
|
||||
/nbproject/
|
||||
/images/
|
||||
/tests/templates/
|
||||
/tests/cache/
|
||||
/tests/flashcanvas.html
|
||||
/lib/
|
||||
/bin/
|
||||
image.jpg
|
||||
/.project
|
||||
/.settings/
|
||||
/tests/certificate.pem
|
||||
node_modules/
|
||||
.envrc
|
||||
server.js
|
||||
*.sublime-workspace
|
||||
chromedriver.log
|
||||
*.baseline
|
||||
*.iml
|
||||
.idea/
|
||||
.DS_Store
|
||||
npm-debug.log
|
||||
debug.log
|
||||
tests/reftests.js
|
||||
*.log
|
||||
|
18
.npmignore
Normal file
18
.npmignore
Normal file
@ -0,0 +1,18 @@
|
||||
build/
|
||||
docs/
|
||||
examples/
|
||||
scripts/
|
||||
src/
|
||||
tests/
|
||||
www/
|
||||
.github/
|
||||
*.iml
|
||||
.babelrc
|
||||
.idea/
|
||||
.editorconfig
|
||||
.npmignore
|
||||
.eslintrc
|
||||
.travis.yml
|
||||
karma.js
|
||||
karma.conf.js
|
||||
webpack.config.js
|
59
.travis.yml
59
.travis.yml
@ -1,12 +1,57 @@
|
||||
---
|
||||
language: node_js
|
||||
node_js:
|
||||
- '0.10'
|
||||
- '7'
|
||||
env:
|
||||
global:
|
||||
- secure: "eW41gIqOizwO4pTgWnAAbW75AP7F+CK9qfSed/fSh4sJ9HWMIY1YRIaY8gjr+6jV/f7XVHcXuym6ZxgINYSkVKbF1JKxBJNLOXtSgNbVHSic58pYFvUjwxIBI9aPig9uux1+DbnpWqXFDTcACJSevQZE0xwmjdrSkDLgB0G34v8="
|
||||
- secure: "Y2Av+Gd3z9uQEB36GwdOOuGka0hx0/HeitASEo59z934O8RxnmN9eNTXS7dDT3XtKtwxIyLTOEpS7qlRdWahH28hr/dS4xJj6ao58C+1xMcDs6NAPGmDxUlcJWpcGEsnjmXjQCc3fBioSTdpIBrK/gdvgpNh77UKG74Sk7Z+YGk="
|
||||
- secure: "YI+YbTOGf2x4fPMKW+KhJiZWswoXT6xOKGwLfsQsVwmFX1LerJouil5D5iYOQuL4FE3pNaoJSNakIsokJQuGKJMmnPc8rdhMZuBJBk6MRghurE2Xe9qBHfuUBPlfD61nARESm4WDcyMwM0QVYaOKeY6aIpZ91qbUbyc60EEx3C4="
|
||||
- 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:
|
||||
- npm install -g grunt-cli
|
||||
- curl https://gist.github.com/niklasvh/6150144/raw/sauce_connect_setup.sh | bash
|
||||
- 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
|
||||
- cd www && npm install && npm run build && cd ..
|
||||
- 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
|
||||
- provider: pages
|
||||
skip_cleanup: true
|
||||
local_dir: www/public
|
||||
target_branch: gh-pages
|
||||
fqdn: html2canvas.hertzen.com
|
||||
github_token:
|
||||
secure: "PowO/Jat660k3gHcjgI6DlJz15RM7pLUu11UPsLCtYJ8ZwodppE6Keg0DfVkSFSIZttZor+UssDwP/WOEqfZNLqmXbcj3Gec4xolohet/GOe0KJKKuF/HgggbcxumopxMX6sMVePlMBpkLpHh7tgEAEHBWTlzC1c1a7Xa48fZ7k="
|
||||
on:
|
||||
branch: master
|
||||
repo: niklasvh/html2canvas
|
||||
|
119
CHANGELOG.md
Normal file
119
CHANGELOG.md
Normal file
@ -0,0 +1,119 @@
|
||||
### Changelog ###
|
||||
|
||||
#### v1.0.0-alpha6 - 28.12.2017 ####
|
||||
* Fix list-style: none (#1340)
|
||||
* Extend supported values for pseudo element content
|
||||
|
||||
#### v1.0.0-alpha5 - 21.12.2017 ####
|
||||
* Fix underline positioning
|
||||
* Fix canvas rendering on Chrome
|
||||
* Fix overflow: auto
|
||||
* Added support for rendering list-style
|
||||
|
||||
#### v1.0.0-alpha4 - 12.12.2017 ####
|
||||
* Fix rendering with multiple fonts defined (Fix #796)
|
||||
* Add support for radial-gradients
|
||||
* Fix logging option (#1302)
|
||||
* Add support for rendering webgl canvas content (#646)
|
||||
* Fix external SVG loading with proxies (#802)
|
||||
|
||||
#### v1.0.0-alpha3 - 9.12.2017 ####
|
||||
* 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>)
|
99
Gruntfile.js
99
Gruntfile.js
@ -1,99 +0,0 @@
|
||||
/*global module:false*/
|
||||
module.exports = function(grunt) {
|
||||
|
||||
var meta = {
|
||||
banner: '/*\n <%= pkg.title || pkg.name %> <%= pkg.version %>' +
|
||||
'<%= pkg.homepage ? " <" + pkg.homepage + ">" : "" %>' + '\n' +
|
||||
' Copyright (c) <%= grunt.template.today("yyyy") %> <%= pkg.author.name %>' +
|
||||
'\n\n Released under <%= _.pluck(pkg.licenses, "type").join(", ") %> License\n*/\n',
|
||||
pre: '\n(function(window, document, undefined){\n\n',
|
||||
post: '\n})(window,document);'
|
||||
};
|
||||
|
||||
// Project configuration.
|
||||
grunt.initConfig({
|
||||
|
||||
pkg: grunt.file.readJSON('package.json'),
|
||||
|
||||
qunit: {
|
||||
files: ['tests/qunit/index.html']
|
||||
},
|
||||
concat: {
|
||||
dist: {
|
||||
src: [
|
||||
'src/Core.js',
|
||||
'src/Font.js',
|
||||
'src/Generate.js',
|
||||
'src/Queue.js',
|
||||
'src/Parse.js',
|
||||
'src/Preload.js',
|
||||
'src/Renderer.js',
|
||||
'src/Support.js',
|
||||
'src/Util.js',
|
||||
'src/renderers/Canvas.js'
|
||||
],
|
||||
dest: 'build/<%= pkg.name %>.js'
|
||||
},
|
||||
options:{
|
||||
banner: meta.banner + meta.pre,
|
||||
footer: meta.post
|
||||
}
|
||||
},
|
||||
uglify: {
|
||||
dist: {
|
||||
src: ['<%= concat.dist.dest %>'],
|
||||
dest: 'build/<%= pkg.name %>.min.js'
|
||||
},
|
||||
options: {
|
||||
banner: meta.banner
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
files: 'src/*',
|
||||
tasks: ['build', 'jshint']
|
||||
},
|
||||
jshint: {
|
||||
all: ['<%= concat.dist.dest %>'],
|
||||
options: {
|
||||
curly: true,
|
||||
eqeqeq: true,
|
||||
immed: true,
|
||||
latedef: true,
|
||||
newcap: true,
|
||||
noarg: true,
|
||||
sub: true,
|
||||
undef: true,
|
||||
boss: true,
|
||||
eqnull: true,
|
||||
browser: true,
|
||||
globals: {
|
||||
jQuery: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
grunt.registerTask('webdriver', 'Browser render tests', function(arg1) {
|
||||
var selenium = require("./tests/selenium.js");
|
||||
var done = this.async();
|
||||
|
||||
if (arguments.length) {
|
||||
selenium[arg1].apply(null, arguments);
|
||||
} else {
|
||||
selenium.tests();
|
||||
}
|
||||
});
|
||||
|
||||
// Load tasks
|
||||
grunt.loadNpmTasks('grunt-contrib-watch');
|
||||
grunt.loadNpmTasks('grunt-contrib-concat');
|
||||
grunt.loadNpmTasks('grunt-contrib-uglify');
|
||||
grunt.loadNpmTasks('grunt-contrib-jshint');
|
||||
grunt.loadNpmTasks('grunt-contrib-qunit');
|
||||
|
||||
// Default task.
|
||||
grunt.registerTask('build', ['concat', 'uglify']);
|
||||
grunt.registerTask('default', ['concat', 'jshint', 'qunit', 'uglify']);
|
||||
grunt.registerTask('travis', ['concat', 'jshint', 'qunit', 'uglify', 'webdriver']);
|
||||
|
||||
};
|
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.
|
10
bower.json
10
bower.json
@ -1,10 +0,0 @@
|
||||
{
|
||||
"name": "html2canvas",
|
||||
"version": "0.4.1",
|
||||
"description": "Screenshots with JavaScript",
|
||||
"main": "build/html2canvas.js",
|
||||
"ignore": [
|
||||
"tests",
|
||||
".travis.yml"
|
||||
]
|
||||
}
|
2868
build/html2canvas.js
2868
build/html2canvas.js
File diff suppressed because it is too large
Load Diff
8
build/html2canvas.min.js
vendored
8
build/html2canvas.min.js
vendored
File diff suppressed because one or more lines are too long
34
docs/configuration.md
Normal file
34
docs/configuration.md
Normal file
@ -0,0 +1,34 @@
|
||||
---
|
||||
title: "Configuration"
|
||||
description: "Explore the different configuration options available for html2canvas"
|
||||
previousUrl: "/getting-started"
|
||||
previousTitle: "Getting Started"
|
||||
nextUrl: "/features"
|
||||
nextTitle: "Features"
|
||||
---
|
||||
|
||||
These are all of the available configuration options.
|
||||
|
||||
| Name | Default | Description |
|
||||
| ------------- | :------: | ----------- |
|
||||
| async | `true` | Whether to parse and render the element asynchronously
|
||||
| allowTaint | `false` | Whether to allow cross-origin images to taint the canvas
|
||||
| backgroundColor | `#ffffff` | Canvas background color, if none is specified in DOM. Set `null` for transparent
|
||||
| canvas | `null` | Existing `canvas` element to use as a base for drawing on
|
||||
| foreignObjectRendering | `false` | Whether to use ForeignObject rendering if the browser supports it
|
||||
| imageTimeout | `15000` | Timeout for loading an image (in milliseconds). Set to `0` to disable timeout.
|
||||
| logging | `true` | Enable logging for debug purposes
|
||||
| proxy | `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 | `true` | Whether to cleanup the cloned DOM elements html2canvas creates temporarily
|
||||
| scale | `window.devicePixelRatio` | The scale to use for rendering. Defaults to the browsers device pixel ratio.
|
||||
| useCORS | `false` | Whether to attempt to load images from a server using CORS
|
||||
| width | `Element` width | The width of the `canvas`
|
||||
| height | `Element` height | The height of the `canvas`
|
||||
| x | `Element` x-offset | Crop canvas x-coordinate
|
||||
| y | `Element` y-offset| Crop canvas y-coordinate
|
||||
| scrollX | `Element` scrollX | The x-scroll position to used when rendering element, (for example if the Element uses `position: fixed`)
|
||||
| scrollY | `Element` scrollY | The y-scroll position to used when rendering element, (for example if the Element uses `position: fixed`)
|
||||
| windowWidth | `Window.innerWidth` | Window width to use when rendering `Element`, which may affect things like Media queries
|
||||
| windowHeight | `Window.innerHeight` | Window height to use when rendering `Element`, which may affect things like Media queries
|
||||
|
||||
If you wish to exclude certain `Element`s from getting rendered, you can add a `data-html2canvas-ignore` attribute to those elements and html2canvas will exclude them from the rendering.
|
42
docs/documentation.md
Normal file
42
docs/documentation.md
Normal file
@ -0,0 +1,42 @@
|
||||
---
|
||||
title: "About"
|
||||
description: "Learn about html2canvas, how it works and some of its limitations"
|
||||
nextUrl: "/getting-started"
|
||||
nextTitle: "Getting Started"
|
||||
---
|
||||
|
||||
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
|
||||
[supported 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+
|
42
docs/faq.md
Normal file
42
docs/faq.md
Normal file
@ -0,0 +1,42 @@
|
||||
---
|
||||
title: "FAQ"
|
||||
description: "Explore Frequently Asked Questions regarding html2canvas"
|
||||
---
|
||||
|
||||
## Why aren't my images rendered?
|
||||
html2canvas does not get around content policy restrictions set by your browser. Drawing images that reside outside of
|
||||
the [origin](https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy) of the current page [taint the
|
||||
canvas](https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_enabled_image#What_is_a_tainted_canvas) that they are drawn upon. If the canvas gets tainted, it cannot be read anymore. As such, html2canvas implements
|
||||
methods to check whether an image would taint the canvas before applying it. If you have set the `allowTaint`
|
||||
[option](/configuration) to `false`, it will not draw the image.
|
||||
|
||||
If you wish to load images that reside outside of your pages origin, you can use a [proxy](/proxy) to load the images.
|
||||
|
||||
## Why is the produced canvas empty or cuts off half way through?
|
||||
Make sure that `canvas` element doesn't hit [browser limitations](https://stackoverflow.com/questions/6081483/maximum-size-of-a-canvas-element) for the `canvas` size.
|
||||
The limitations vary by browser, operating system and system hardware.
|
||||
|
||||
### Chrome
|
||||
> Maximum height/width: 32,767 pixels
|
||||
> Maximum area: 268,435,456 pixels (e.g., 16,384 x 16,384)
|
||||
|
||||
### Firefox
|
||||
> Maximum height/width: 32,767 pixels
|
||||
> Maximum area: 472,907,776 pixels (e.g., 22,528 x 20,992)
|
||||
|
||||
### Internet Explorer
|
||||
> Maximum height/width: 8,192 pixels
|
||||
> Maximum area: N/A
|
||||
|
||||
### iOS
|
||||
> The maximum size for a canvas element is 3 megapixels for devices with less than 256 MB RAM and 5 megapixels for devices with greater or equal than 256 MB RAM
|
||||
|
||||
## Why doesn't CSS property X render correctly or only partially?
|
||||
As each CSS property needs to be manually coded to render correctly, html2canvas will *never* have full CSS support.
|
||||
The library tries to support the most [commonly used CSS properties](/features) to the extent that it can. If some CSS property
|
||||
is missing or incomplete and you feel that it should be part of the library, create test cases for it and a new issue for it.
|
||||
|
||||
## How do I get html2canvas to work in a browser extension?
|
||||
You shouldn't use html2canvas in a browser extension. Most browsers have native support for capturing screenshots from
|
||||
tabs within extensions. Relevant information for [Chrome](https://developer.chrome.com/extensions/tabs#method-captureVisibleTab) and
|
||||
[Firefox](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D#drawWindow()).
|
83
docs/features.md
Normal file
83
docs/features.md
Normal file
@ -0,0 +1,83 @@
|
||||
---
|
||||
title: "Features"
|
||||
description: "Discover the different features supported by html2canvas"
|
||||
---
|
||||
|
||||
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()
|
||||
- radial-gradient()
|
||||
- background-origin
|
||||
- background-position
|
||||
- background-size
|
||||
- border
|
||||
- border-color
|
||||
- border-radius
|
||||
- border-style (**Only supports `solid`**)
|
||||
- border-width
|
||||
- bottom
|
||||
- box-sizing
|
||||
- content
|
||||
- color
|
||||
- display
|
||||
- flex
|
||||
- float
|
||||
- font
|
||||
- font-family
|
||||
- font-size
|
||||
- font-style
|
||||
- font-variant
|
||||
- font-weight
|
||||
- height
|
||||
- left
|
||||
- letter-spacing
|
||||
- list-style
|
||||
- list-style-image
|
||||
- list-style-position
|
||||
- list-style-type
|
||||
- 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-decoration-break](https://github.com/niklasvh/html2canvas/issues/552)
|
||||
- [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)
|
||||
- [mix-blend-mode](https://github.com/niklasvh/html2canvas/issues/580)
|
||||
- [object-fit](https://github.com/niklasvh/html2canvas/issues/1064)
|
||||
- [repeating-linear-gradient()](https://github.com/niklasvh/html2canvas/issues/1162)
|
||||
- word-break
|
||||
- [word-wrap](https://github.com/niklasvh/html2canvas/issues/664)
|
||||
- [writing-mode](https://github.com/niklasvh/html2canvas/issues/1258)
|
||||
- [zoom](https://github.com/niklasvh/html2canvas/issues/732)
|
||||
|
30
docs/getting-started.md
Normal file
30
docs/getting-started.md
Normal file
@ -0,0 +1,30 @@
|
||||
---
|
||||
title: "Getting Started"
|
||||
description: "Learn how to start using html2canvas"
|
||||
previousUrl: "/documentation"
|
||||
previousTitle: "About"
|
||||
nextUrl: "/configuration"
|
||||
nextTitle: "Configuration"
|
||||
---
|
||||
|
||||
## 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);
|
||||
});
|
||||
```
|
12
docs/proxy.md
Normal file
12
docs/proxy.md
Normal file
@ -0,0 +1,12 @@
|
||||
---
|
||||
title: "Proxy"
|
||||
description: "Browse different proxies available for supporting CORS content"
|
||||
---
|
||||
|
||||
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)
|
@ -171,12 +171,10 @@
|
||||
<p style="color: black; font-size: 1em; line-height: 1.3em; clear: both">
|
||||
This is a nonsensical document, but syntactically valid HTML 4.0. All 100% conformant CSS1 agents should be able to render the document elements above this paragraph <b>indistinguishably</b> (to the pixel) from this reference rendering, (except font rasterization and form widgets). All discrepancies should be traceable to CSS1 implementation shortcomings. Once you have finished evaluating this test, you can return to the <A HREF="sec5526c.htm" style="text-decoration:none">parent page</A>.
|
||||
</p>
|
||||
<script type="text/javascript" src="../build/html2canvas.js"></script>
|
||||
<script type="text/javascript" src="../dist/html2canvas.js"></script>
|
||||
<script type="text/javascript">
|
||||
html2canvas(document.body, {
|
||||
onrendered: function(canvas) {
|
||||
document.body.appendChild(canvas);
|
||||
}
|
||||
html2canvas(document.body).then(function(canvas) {
|
||||
document.body.appendChild(canvas);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
@ -53,12 +53,10 @@
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<script type="text/javascript" src="../build/html2canvas.js"></script>
|
||||
<script type="text/javascript" src="../dist/html2canvas.js"></script>
|
||||
<script type="text/javascript">
|
||||
html2canvas(document.body, {
|
||||
onrendered: function(canvas) {
|
||||
document.body.appendChild(canvas);
|
||||
}
|
||||
html2canvas(document.body).then(function(canvas) {
|
||||
document.body.appendChild(canvas);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
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>
|
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 {}
|
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();
|
||||
});
|
||||
|
||||
|
7326
package-lock.json
generated
Normal file
7326
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
84
package.json
84
package.json
@ -2,16 +2,16 @@
|
||||
"title": "html2canvas",
|
||||
"name": "html2canvas",
|
||||
"description": "Screenshots with JavaScript",
|
||||
"version": "0.4.1",
|
||||
"main": "dist/npm/index.js",
|
||||
"version": "1.0.0-alpha.6",
|
||||
"author": {
|
||||
"name": "Niklas von Hertzen",
|
||||
"email": "niklasvh@gmail.com",
|
||||
"url": "http://hertzen.com"
|
||||
"url": "https://hertzen.com"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8.0"
|
||||
"node": ">=4.0.0"
|
||||
},
|
||||
"dependencies": {},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git@github.com:niklasvh/html2canvas.git"
|
||||
@ -20,27 +20,63 @@
|
||||
"url": "https://github.com/niklasvh/html2canvas/issues"
|
||||
},
|
||||
"devDependencies": {
|
||||
"grunt": ">=0.4.0",
|
||||
"grunt-contrib-concat": "*",
|
||||
"grunt-contrib-uglify": "*",
|
||||
"grunt-contrib-jshint": "*",
|
||||
"grunt-contrib-qunit": "*",
|
||||
"grunt-contrib-watch": "~0.5.1",
|
||||
"googleapis": "~0.4.3",
|
||||
"jwt-sign": "~0.1.0",
|
||||
"base64-arraybuffer": ">= 0.1.0",
|
||||
"png-js": ">= 0.1.1",
|
||||
"sync-webdriver": ">=0.1.1",
|
||||
"express": "~3.2.3",
|
||||
"baconjs": "~0.3.15"
|
||||
"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": {
|
||||
"test": "grunt travis --verbose"
|
||||
"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": "http://html2canvas.hertzen.com",
|
||||
"licenses": [
|
||||
{
|
||||
"type": "MIT"
|
||||
}
|
||||
]
|
||||
"homepage": "https://html2canvas.hertzen.com",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"punycode": "2.1.0"
|
||||
}
|
||||
}
|
||||
|
122
readme.md
122
readme.md
@ -1,122 +0,0 @@
|
||||
html2canvas
|
||||
===========
|
||||
|
||||
### Current build status ###
|
||||
[](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 **clients browser**. However, as it is heavily dependent on the browser, this library is *not suitable* to be used in nodejs.
|
||||
It doesn't magically circumvent any browser content policy restrictions either, so rendering cross-origin content will require a [proxy](https://github.com/niklasvh/html2canvas/wiki/Proxies) to get the content to the [same origin](http://en.wikipedia.org/wiki/Same_origin_policy).
|
||||
|
||||
The script is still in a **very experimental state**, so I don't recommend using it in a production environment nor start building applications with it yet, as there will be still major changes made.
|
||||
|
||||
###Browser compatibility###
|
||||
|
||||
The script should work fine on the following browsers:
|
||||
|
||||
* 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 ###
|
||||
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 );
|
||||
*/
|
||||
}
|
||||
});
|
||||
|
||||
### Building ###
|
||||
|
||||
The library uses [grunt](http://gruntjs.com/) for building. Alternatively, you can download the latest build from [here](http://html2canvas.hertzen.com/build/html2canvas.js).
|
||||
|
||||
Run the full build process (including lint, qunit and webdriver tests):
|
||||
|
||||
$ grunt
|
||||
|
||||
Skip lint and tests and simply build from source:
|
||||
|
||||
$ grunt build
|
||||
|
||||
### Running tests ###
|
||||
|
||||
The library has two sets of tests. The first set is a number of qunit tests that check that different values parsed by browsers are correctly converted in html2canvas. To run these tests with grunt you'll need [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 commiting changes, these should generally not go decrease from the baseline values.
|
||||
|
||||
Start by downloading the dependencies:
|
||||
|
||||
$ npm install
|
||||
|
||||
Run qunit tests:
|
||||
|
||||
$ grunt test
|
||||
|
||||
### Examples ###
|
||||
|
||||
For more information and examples, please visit the [homepage](http://html2canvas.hertzen.com) or try the [test console](http://html2canvas.hertzen.com/screenshots.html).
|
||||
|
||||
### Contributing ###
|
||||
|
||||
If you wish to contribute to the project, please send the pull requests to the develop branch. Before submitting any changes, try and test that the changes work with all the support browsers. If some CSS property isn't supported or is incomplete, please create appropriate tests for it as well before submitting any code changes.
|
||||
|
||||
### Changelog ###
|
||||
|
||||
v0.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>)
|
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)
|
||||
);
|
||||
}
|
||||
};
|
628
src/Clone.js
Normal file
628
src/Clone.js
Normal file
@ -0,0 +1,628 @@
|
||||
/* @flow */
|
||||
'use strict';
|
||||
import type {Bounds} from './Bounds';
|
||||
import type {Options} from './index';
|
||||
import type {PseudoContentData, PseudoContentItem} from './PseudoNodeContent';
|
||||
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';
|
||||
import {
|
||||
parseCounterReset,
|
||||
popCounters,
|
||||
resolvePseudoContent,
|
||||
PSEUDO_CONTENT_ITEM_TYPE
|
||||
} from './PseudoNodeContent';
|
||||
|
||||
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<*>;
|
||||
pseudoContentData: PseudoContentData;
|
||||
|
||||
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);
|
||||
this.pseudoContentData = {
|
||||
counters: {},
|
||||
quoteDepth: 0
|
||||
};
|
||||
// $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,
|
||||
logging: this.options.logging,
|
||||
proxy: this.options.proxy,
|
||||
removeContainer: this.options.removeContainer,
|
||||
scale: this.options.scale,
|
||||
foreignObjectRendering: this.options.foreignObjectRendering,
|
||||
useCORS: this.options.useCORS,
|
||||
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;
|
||||
const style = node instanceof window.HTMLElement ? window.getComputedStyle(node) : null;
|
||||
const styleBefore =
|
||||
node instanceof window.HTMLElement ? window.getComputedStyle(node, ':before') : null;
|
||||
const styleAfter =
|
||||
node instanceof window.HTMLElement ? window.getComputedStyle(node, ':after') : null;
|
||||
|
||||
if (this.referenceElement === node && clone instanceof window.HTMLElement) {
|
||||
this.clonedReferenceElement = clone;
|
||||
}
|
||||
|
||||
if (clone instanceof window.HTMLBodyElement) {
|
||||
createPseudoHideStyles(clone);
|
||||
}
|
||||
|
||||
const counters = parseCounterReset(style, this.pseudoContentData);
|
||||
const contentBefore = resolvePseudoContent(node, styleBefore, this.pseudoContentData);
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const contentAfter = resolvePseudoContent(node, styleAfter, this.pseudoContentData);
|
||||
popCounters(counters, this.pseudoContentData);
|
||||
|
||||
if (node instanceof window.HTMLElement && clone instanceof window.HTMLElement) {
|
||||
if (styleBefore) {
|
||||
this.inlineAllImages(
|
||||
inlinePseudoElement(node, clone, styleBefore, contentBefore, PSEUDO_BEFORE)
|
||||
);
|
||||
}
|
||||
if (styleAfter) {
|
||||
this.inlineAllImages(
|
||||
inlinePseudoElement(node, clone, styleAfter, contentAfter, PSEUDO_AFTER)
|
||||
);
|
||||
}
|
||||
if (style && this.copyStyles && !(node instanceof HTMLIFrameElement)) {
|
||||
copyCSSStyles(style, 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;
|
||||
const ctx = canvas.getContext('2d');
|
||||
const clonedCtx = clonedCanvas.getContext('2d');
|
||||
if (ctx) {
|
||||
clonedCtx.putImageData(ctx.getImageData(0, 0, canvas.width, canvas.height), 0, 0);
|
||||
} else {
|
||||
clonedCtx.drawImage(canvas, 0, 0);
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
};
|
||||
|
||||
const inlinePseudoElement = (
|
||||
node: HTMLElement,
|
||||
clone: HTMLElement,
|
||||
style: CSSStyleDeclaration,
|
||||
contentItems: ?Array<PseudoContentItem>,
|
||||
pseudoElt: ':before' | ':after'
|
||||
): ?HTMLElement => {
|
||||
if (
|
||||
!style ||
|
||||
!style.content ||
|
||||
style.content === 'none' ||
|
||||
style.content === '-moz-alt-content' ||
|
||||
style.display === 'none'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const anonymousReplacedElement = clone.ownerDocument.createElement('html2canvaspseudoelement');
|
||||
copyCSSStyles(style, anonymousReplacedElement);
|
||||
|
||||
if (contentItems) {
|
||||
const len = contentItems.length;
|
||||
for (var i = 0; i < len; i++) {
|
||||
const item = contentItems[i];
|
||||
switch (item.type) {
|
||||
case PSEUDO_CONTENT_ITEM_TYPE.IMAGE:
|
||||
const img = clone.ownerDocument.createElement('img');
|
||||
img.src = parseBackgroundImage(`url(${item.value})`)[0].args[0];
|
||||
img.style.opacity = '1';
|
||||
anonymousReplacedElement.appendChild(img);
|
||||
break;
|
||||
case PSEUDO_CONTENT_ITEM_TYPE.TEXT:
|
||||
anonymousReplacedElement.appendChild(
|
||||
clone.ownerDocument.createTextNode(item.value)
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 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]);
|
410
src/Core.js
410
src/Core.js
@ -1,410 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
var _html2canvas = {},
|
||||
previousElement,
|
||||
computedCSS,
|
||||
html2canvas;
|
||||
|
||||
_html2canvas.Util = {};
|
||||
|
||||
_html2canvas.Util.log = function(a) {
|
||||
if (_html2canvas.logging && window.console && window.console.log) {
|
||||
window.console.log(a);
|
||||
}
|
||||
};
|
||||
|
||||
_html2canvas.Util.trimText = (function(isNative){
|
||||
return function(input) {
|
||||
return isNative ? isNative.apply(input) : ((input || '') + '').replace( /^\s+|\s+$/g , '' );
|
||||
};
|
||||
})(String.prototype.trim);
|
||||
|
||||
_html2canvas.Util.asFloat = function(v) {
|
||||
return parseFloat(v);
|
||||
};
|
||||
|
||||
(function() {
|
||||
// TODO: support all possible length values
|
||||
var TEXT_SHADOW_PROPERTY = /((rgba|rgb)\([^\)]+\)(\s-?\d+px){0,})/g;
|
||||
var TEXT_SHADOW_VALUES = /(-?\d+px)|(#.+)|(rgb\(.+\))|(rgba\(.+\))/g;
|
||||
_html2canvas.Util.parseTextShadows = function (value) {
|
||||
if (!value || value === 'none') {
|
||||
return [];
|
||||
}
|
||||
|
||||
// find multiple shadow declarations
|
||||
var shadows = value.match(TEXT_SHADOW_PROPERTY),
|
||||
results = [];
|
||||
for (var i = 0; shadows && (i < shadows.length); i++) {
|
||||
var s = shadows[i].match(TEXT_SHADOW_VALUES);
|
||||
results.push({
|
||||
color: s[0],
|
||||
offsetX: s[1] ? s[1].replace('px', '') : 0,
|
||||
offsetY: s[2] ? s[2].replace('px', '') : 0,
|
||||
blur: s[3] ? s[3].replace('px', '') : 0
|
||||
});
|
||||
}
|
||||
return results;
|
||||
};
|
||||
})();
|
||||
|
||||
|
||||
_html2canvas.Util.parseBackgroundImage = function (value) {
|
||||
var whitespace = ' \r\n\t',
|
||||
method, definition, prefix, prefix_i, block, results = [],
|
||||
c, mode = 0, numParen = 0, quote, args;
|
||||
|
||||
var appendResult = function(){
|
||||
if(method) {
|
||||
if(definition.substr( 0, 1 ) === '"') {
|
||||
definition = definition.substr( 1, definition.length - 2 );
|
||||
}
|
||||
if(definition) {
|
||||
args.push(definition);
|
||||
}
|
||||
if(method.substr( 0, 1 ) === '-' &&
|
||||
(prefix_i = method.indexOf( '-', 1 ) + 1) > 0) {
|
||||
prefix = method.substr( 0, prefix_i);
|
||||
method = method.substr( prefix_i );
|
||||
}
|
||||
results.push({
|
||||
prefix: prefix,
|
||||
method: method.toLowerCase(),
|
||||
value: block,
|
||||
args: args
|
||||
});
|
||||
}
|
||||
args = []; //for some odd reason, setting .length = 0 didn't work in safari
|
||||
method =
|
||||
prefix =
|
||||
definition =
|
||||
block = '';
|
||||
};
|
||||
|
||||
appendResult();
|
||||
for(var i = 0, ii = value.length; i<ii; i++) {
|
||||
c = value[i];
|
||||
if(mode === 0 && whitespace.indexOf( c ) > -1){
|
||||
continue;
|
||||
}
|
||||
switch(c) {
|
||||
case '"':
|
||||
if(!quote) {
|
||||
quote = c;
|
||||
}
|
||||
else if(quote === c) {
|
||||
quote = null;
|
||||
}
|
||||
break;
|
||||
|
||||
case '(':
|
||||
if(quote) { break; }
|
||||
else if(mode === 0) {
|
||||
mode = 1;
|
||||
block += c;
|
||||
continue;
|
||||
} else {
|
||||
numParen++;
|
||||
}
|
||||
break;
|
||||
|
||||
case ')':
|
||||
if(quote) { break; }
|
||||
else if(mode === 1) {
|
||||
if(numParen === 0) {
|
||||
mode = 0;
|
||||
block += c;
|
||||
appendResult();
|
||||
continue;
|
||||
} else {
|
||||
numParen--;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case ',':
|
||||
if(quote) { break; }
|
||||
else if(mode === 0) {
|
||||
appendResult();
|
||||
continue;
|
||||
}
|
||||
else if (mode === 1) {
|
||||
if(numParen === 0 && !method.match(/^url$/i)) {
|
||||
args.push(definition);
|
||||
definition = '';
|
||||
block += c;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
block += c;
|
||||
if(mode === 0) { method += c; }
|
||||
else { definition += c; }
|
||||
}
|
||||
appendResult();
|
||||
|
||||
return results;
|
||||
};
|
||||
|
||||
_html2canvas.Util.Bounds = function (element) {
|
||||
var clientRect, bounds = {};
|
||||
|
||||
if (element.getBoundingClientRect){
|
||||
clientRect = element.getBoundingClientRect();
|
||||
|
||||
// TODO add scroll position to bounds, so no scrolling of window necessary
|
||||
bounds.top = clientRect.top;
|
||||
bounds.bottom = clientRect.bottom || (clientRect.top + clientRect.height);
|
||||
bounds.left = clientRect.left;
|
||||
|
||||
bounds.width = element.offsetWidth;
|
||||
bounds.height = element.offsetHeight;
|
||||
}
|
||||
|
||||
return bounds;
|
||||
};
|
||||
|
||||
// TODO ideally, we'd want everything to go through this function instead of Util.Bounds,
|
||||
// but would require further work to calculate the correct positions for elements with offsetParents
|
||||
_html2canvas.Util.OffsetBounds = function (element) {
|
||||
var parent = element.offsetParent ? _html2canvas.Util.OffsetBounds(element.offsetParent) : {top: 0, left: 0};
|
||||
|
||||
return {
|
||||
top: element.offsetTop + parent.top,
|
||||
bottom: element.offsetTop + element.offsetHeight + parent.top,
|
||||
left: element.offsetLeft + parent.left,
|
||||
width: element.offsetWidth,
|
||||
height: element.offsetHeight
|
||||
};
|
||||
};
|
||||
|
||||
function toPX(element, attribute, value ) {
|
||||
var rsLeft = element.runtimeStyle && element.runtimeStyle[attribute],
|
||||
left,
|
||||
style = element.style;
|
||||
|
||||
// Check if we are not dealing with pixels, (Opera has issues with this)
|
||||
// Ported from jQuery css.js
|
||||
// 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( value ) && /^-?\d/.test(value) ) {
|
||||
// Remember the original values
|
||||
left = style.left;
|
||||
|
||||
// Put in the new values to get a computed value out
|
||||
if (rsLeft) {
|
||||
element.runtimeStyle.left = element.currentStyle.left;
|
||||
}
|
||||
style.left = attribute === "fontSize" ? "1em" : (value || 0);
|
||||
value = style.pixelLeft + "px";
|
||||
|
||||
// Revert the changed values
|
||||
style.left = left;
|
||||
if (rsLeft) {
|
||||
element.runtimeStyle.left = rsLeft;
|
||||
}
|
||||
}
|
||||
|
||||
if (!/^(thin|medium|thick)$/i.test(value)) {
|
||||
return Math.round(parseFloat(value)) + "px";
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
function asInt(val) {
|
||||
return parseInt(val, 10);
|
||||
}
|
||||
|
||||
function parseBackgroundSizePosition(value, element, attribute, index) {
|
||||
value = (value || '').split(',');
|
||||
value = value[index || 0] || value[0] || 'auto';
|
||||
value = _html2canvas.Util.trimText(value).split(' ');
|
||||
|
||||
if(attribute === 'backgroundSize' && (!value[0] || value[0].match(/cover|contain|auto/))) {
|
||||
//these values will be handled in the parent function
|
||||
} else {
|
||||
value[0] = (value[0].indexOf( "%" ) === -1) ? toPX(element, attribute + "X", value[0]) : value[0];
|
||||
if(value[1] === undefined) {
|
||||
if(attribute === 'backgroundSize') {
|
||||
value[1] = 'auto';
|
||||
return value;
|
||||
} else {
|
||||
// IE 9 doesn't return double digit always
|
||||
value[1] = value[0];
|
||||
}
|
||||
}
|
||||
value[1] = (value[1].indexOf("%") === -1) ? toPX(element, attribute + "Y", value[1]) : value[1];
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
_html2canvas.Util.getCSS = function (element, attribute, index) {
|
||||
if (previousElement !== element) {
|
||||
computedCSS = document.defaultView.getComputedStyle(element, null);
|
||||
}
|
||||
|
||||
var value = computedCSS[attribute];
|
||||
|
||||
if (/^background(Size|Position)$/.test(attribute)) {
|
||||
return parseBackgroundSizePosition(value, element, attribute, index);
|
||||
} else if (/border(Top|Bottom)(Left|Right)Radius/.test(attribute)) {
|
||||
var arr = value.split(" ");
|
||||
if (arr.length <= 1) {
|
||||
arr[1] = arr[0];
|
||||
}
|
||||
return arr.map(asInt);
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
_html2canvas.Util.resizeBounds = function( current_width, current_height, target_width, target_height, stretch_mode ){
|
||||
var target_ratio = target_width / target_height,
|
||||
current_ratio = current_width / current_height,
|
||||
output_width, output_height;
|
||||
|
||||
if(!stretch_mode || stretch_mode === 'auto') {
|
||||
output_width = target_width;
|
||||
output_height = target_height;
|
||||
} else if(target_ratio < current_ratio ^ stretch_mode === 'contain') {
|
||||
output_height = target_height;
|
||||
output_width = target_height * current_ratio;
|
||||
} else {
|
||||
output_width = target_width;
|
||||
output_height = target_width / current_ratio;
|
||||
}
|
||||
|
||||
return {
|
||||
width: output_width,
|
||||
height: output_height
|
||||
};
|
||||
};
|
||||
|
||||
function backgroundBoundsFactory( prop, el, bounds, image, imageIndex, backgroundSize ) {
|
||||
var bgposition = _html2canvas.Util.getCSS( el, prop, imageIndex ) ,
|
||||
topPos,
|
||||
left,
|
||||
percentage,
|
||||
val;
|
||||
|
||||
if (bgposition.length === 1){
|
||||
val = bgposition[0];
|
||||
|
||||
bgposition = [];
|
||||
|
||||
bgposition[0] = val;
|
||||
bgposition[1] = val;
|
||||
}
|
||||
|
||||
if (bgposition[0].toString().indexOf("%") !== -1){
|
||||
percentage = (parseFloat(bgposition[0])/100);
|
||||
left = bounds.width * percentage;
|
||||
if(prop !== 'backgroundSize') {
|
||||
left -= (backgroundSize || image).width*percentage;
|
||||
}
|
||||
} else {
|
||||
if(prop === 'backgroundSize') {
|
||||
if(bgposition[0] === 'auto') {
|
||||
left = image.width;
|
||||
} else {
|
||||
if (/contain|cover/.test(bgposition[0])) {
|
||||
var resized = _html2canvas.Util.resizeBounds(image.width, image.height, bounds.width, bounds.height, bgposition[0]);
|
||||
left = resized.width;
|
||||
topPos = resized.height;
|
||||
} else {
|
||||
left = parseInt(bgposition[0], 10);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
left = parseInt( bgposition[0], 10);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if(bgposition[1] === 'auto') {
|
||||
topPos = left / image.width * image.height;
|
||||
} else if (bgposition[1].toString().indexOf("%") !== -1){
|
||||
percentage = (parseFloat(bgposition[1])/100);
|
||||
topPos = bounds.height * percentage;
|
||||
if(prop !== 'backgroundSize') {
|
||||
topPos -= (backgroundSize || image).height * percentage;
|
||||
}
|
||||
|
||||
} else {
|
||||
topPos = parseInt(bgposition[1],10);
|
||||
}
|
||||
|
||||
return [left, topPos];
|
||||
}
|
||||
|
||||
_html2canvas.Util.BackgroundPosition = function( el, bounds, image, imageIndex, backgroundSize ) {
|
||||
var result = backgroundBoundsFactory( 'backgroundPosition', el, bounds, image, imageIndex, backgroundSize );
|
||||
return { left: result[0], top: result[1] };
|
||||
};
|
||||
|
||||
_html2canvas.Util.BackgroundSize = function( el, bounds, image, imageIndex ) {
|
||||
var result = backgroundBoundsFactory( 'backgroundSize', el, bounds, image, imageIndex );
|
||||
return { width: result[0], height: result[1] };
|
||||
};
|
||||
|
||||
_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) {
|
||||
_html2canvas.Util.log("html2canvas.Util.Children failed with exception: " + ex.message);
|
||||
children = [];
|
||||
}
|
||||
return children;
|
||||
};
|
||||
|
||||
_html2canvas.Util.isTransparent = function(backgroundColor) {
|
||||
return (backgroundColor === "transparent" || backgroundColor === "rgba(0, 0, 0, 0)");
|
||||
};
|
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;
|
105
src/Font.js
105
src/Font.js
@ -1,64 +1,73 @@
|
||||
_html2canvas.Util.Font = (function () {
|
||||
/* @flow */
|
||||
'use strict';
|
||||
|
||||
var fontData = {};
|
||||
import type {Font} from './parsing/font';
|
||||
|
||||
return function(font, fontSize, doc) {
|
||||
if (fontData[font + "-" + fontSize] !== undefined) {
|
||||
return fontData[font + "-" + fontSize];
|
||||
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');
|
||||
|
||||
var container = doc.createElement('div'),
|
||||
img = doc.createElement('img'),
|
||||
span = doc.createElement('span'),
|
||||
sampleText = 'Hidden Text',
|
||||
baseline,
|
||||
middle,
|
||||
metricsObj;
|
||||
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;
|
||||
container.style.fontSize = fontSize;
|
||||
container.style.margin = 0;
|
||||
container.style.padding = 0;
|
||||
container.style.visibility = 'hidden';
|
||||
container.style.fontFamily = font.fontFamily;
|
||||
container.style.fontSize = font.fontSize;
|
||||
container.style.margin = '0';
|
||||
container.style.padding = '0';
|
||||
|
||||
doc.body.appendChild(container);
|
||||
body.appendChild(container);
|
||||
|
||||
// http://probablyprogramming.com/2009/03/15/the-tiniest-gif-ever (handtinywhite.gif)
|
||||
img.src = "data:image/gif;base64,R0lGODlhAQABAIABAP///wAAACwAAAAAAQABAAACAkQBADs=";
|
||||
img.width = 1;
|
||||
img.height = 1;
|
||||
img.src = SMALL_IMAGE;
|
||||
img.width = 1;
|
||||
img.height = 1;
|
||||
|
||||
img.style.margin = 0;
|
||||
img.style.padding = 0;
|
||||
img.style.verticalAlign = "baseline";
|
||||
img.style.margin = '0';
|
||||
img.style.padding = '0';
|
||||
img.style.verticalAlign = 'baseline';
|
||||
|
||||
span.style.fontFamily = font;
|
||||
span.style.fontSize = fontSize;
|
||||
span.style.margin = 0;
|
||||
span.style.padding = 0;
|
||||
span.style.fontFamily = font.fontFamily;
|
||||
span.style.fontSize = font.fontSize;
|
||||
span.style.margin = '0';
|
||||
span.style.padding = '0';
|
||||
|
||||
span.appendChild(doc.createTextNode(sampleText));
|
||||
container.appendChild(span);
|
||||
container.appendChild(img);
|
||||
baseline = (img.offsetTop - span.offsetTop) + 1;
|
||||
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(doc.createTextNode(sampleText));
|
||||
container.removeChild(span);
|
||||
container.appendChild(this._document.createTextNode(SAMPLE_TEXT));
|
||||
|
||||
container.style.lineHeight = "normal";
|
||||
img.style.verticalAlign = "super";
|
||||
container.style.lineHeight = 'normal';
|
||||
img.style.verticalAlign = 'super';
|
||||
|
||||
middle = (img.offsetTop-container.offsetTop) + 1;
|
||||
metricsObj = {
|
||||
baseline: baseline,
|
||||
lineWidth: 1,
|
||||
middle: middle
|
||||
};
|
||||
const middle = img.offsetTop - container.offsetTop + 2;
|
||||
|
||||
fontData[font + "-" + fontSize] = metricsObj;
|
||||
body.removeChild(container);
|
||||
|
||||
doc.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 metricsObj;
|
||||
};
|
||||
})();
|
||||
return this._data[key];
|
||||
}
|
||||
}
|
||||
|
424
src/Generate.js
424
src/Generate.js
@ -1,424 +0,0 @@
|
||||
(function(){
|
||||
var Util = _html2canvas.Util,
|
||||
Generate = {};
|
||||
|
||||
_html2canvas.Generate = 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)
|
||||
*/
|
||||
Generate.parseGradient = function(css, bounds) {
|
||||
var gradient, i, len = reGradients.length, m1, stop, m2, m2Len, step, m3, tl,tr,br,bl;
|
||||
|
||||
for(i = 0; i < len; i+=1){
|
||||
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 :(
|
||||
tl = Math.sqrt(Math.pow(gradient.cx, 2) + Math.pow(gradient.cy, 2));
|
||||
tr = Math.sqrt(Math.pow(gradient.cx, 2) + Math.pow(gradient.y1 - gradient.cy, 2));
|
||||
br = Math.sqrt(Math.pow(gradient.x1 - gradient.cx, 2) + Math.pow(gradient.y1 - gradient.cy, 2));
|
||||
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':
|
||||
tl = Math.sqrt(Math.pow(gradient.cx, 2) + Math.pow(gradient.cy, 2));
|
||||
tr = Math.sqrt(Math.pow(gradient.cx, 2) + Math.pow(gradient.y1 - gradient.cy, 2));
|
||||
br = Math.sqrt(Math.pow(gradient.x1 - gradient.cx, 2) + Math.pow(gradient.y1 - gradient.cy, 2));
|
||||
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;
|
||||
};
|
||||
|
||||
function addScrollStops(grad) {
|
||||
return function(colorStop) {
|
||||
try {
|
||||
grad.addColorStop(colorStop.stop, colorStop.color);
|
||||
}
|
||||
catch(e) {
|
||||
Util.log(['failed to add color stop: ', e, '; tried to add: ', colorStop]);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Generate.Gradient = function(src, bounds) {
|
||||
if(bounds.width === 0 || bounds.height === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
var canvas = document.createElement('canvas'),
|
||||
ctx = canvas.getContext('2d'),
|
||||
gradient, grad;
|
||||
|
||||
canvas.width = bounds.width;
|
||||
canvas.height = bounds.height;
|
||||
|
||||
// TODO: add support for multi defined background gradients
|
||||
gradient = _html2canvas.Generate.parseGradient(src, bounds);
|
||||
|
||||
if(gradient) {
|
||||
switch(gradient.type) {
|
||||
case 'linear':
|
||||
grad = ctx.createLinearGradient(gradient.x0, gradient.y0, gradient.x1, gradient.y1);
|
||||
gradient.colorStops.forEach(addScrollStops(grad));
|
||||
ctx.fillStyle = grad;
|
||||
ctx.fillRect(0, 0, bounds.width, bounds.height);
|
||||
break;
|
||||
|
||||
case 'circle':
|
||||
grad = ctx.createRadialGradient(gradient.cx, gradient.cy, 0, gradient.cx, gradient.cy, gradient.rx);
|
||||
gradient.colorStops.forEach(addScrollStops(grad));
|
||||
ctx.fillStyle = grad;
|
||||
ctx.fillRect(0, 0, bounds.width, bounds.height);
|
||||
break;
|
||||
|
||||
case 'ellipse':
|
||||
var canvasRadial = document.createElement('canvas'),
|
||||
ctxRadial = canvasRadial.getContext('2d'),
|
||||
ri = Math.max(gradient.rx, gradient.ry),
|
||||
di = ri * 2;
|
||||
|
||||
canvasRadial.width = canvasRadial.height = di;
|
||||
|
||||
grad = ctxRadial.createRadialGradient(gradient.rx, gradient.ry, 0, gradient.rx, gradient.ry, ri);
|
||||
gradient.colorStops.forEach(addScrollStops(grad));
|
||||
|
||||
ctxRadial.fillStyle = grad;
|
||||
ctxRadial.fillRect(0, 0, di, di);
|
||||
|
||||
ctx.fillStyle = gradient.colorStops[gradient.colorStops.length - 1].color;
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.drawImage(canvasRadial, gradient.cx - gradient.rx, gradient.cy - gradient.ry, 2 * gradient.rx, 2 * gradient.ry);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return canvas;
|
||||
};
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
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;
|
||||
};
|
||||
})();
|
558
src/Gradient.js
Normal file
558
src/Gradient.js
Normal file
@ -0,0 +1,558 @@
|
||||
/* @flow */
|
||||
'use strict';
|
||||
|
||||
import type {BackgroundSource} from './parsing/background';
|
||||
import type {Bounds} from './Bounds';
|
||||
import NodeContainer from './NodeContainer';
|
||||
import {parseAngle} from './Angle';
|
||||
import Color from './Color';
|
||||
import Length, {LENGTH_TYPE, calculateLengthFromValueWithUnit} from './Length';
|
||||
import {distance} from './Util';
|
||||
|
||||
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_COLORSTOP = /^(from|to|color-stop)\((?:([\d.]+)(%)?,\s*)?(.+?)\)$/i;
|
||||
const RADIAL_SHAPE_DEFINITION = /^\s*(circle|ellipse)?\s*((?:([\d.]+)(px|r?em|%)\s*(?:([\d.]+)(px|r?em|%))?)|closest-side|closest-corner|farthest-side|farthest-corner)?\s*(?:at\s*(?:(left|center|right)|([\d.]+)(px|r?em|%))\s+(?:(top|center|bottom)|([\d.]+)(px|r?em|%)))?(?:\s|$)/i;
|
||||
|
||||
export type Point = {
|
||||
x: number,
|
||||
y: number
|
||||
};
|
||||
|
||||
export type Direction = {
|
||||
x0: number,
|
||||
x1: number,
|
||||
y0: number,
|
||||
y1: number
|
||||
};
|
||||
|
||||
export type ColorStop = {
|
||||
color: Color,
|
||||
stop: number
|
||||
};
|
||||
|
||||
export interface Gradient {
|
||||
type: GradientType,
|
||||
colorStops: Array<ColorStop>
|
||||
}
|
||||
|
||||
export const GRADIENT_TYPE = {
|
||||
LINEAR_GRADIENT: 0,
|
||||
RADIAL_GRADIENT: 1
|
||||
};
|
||||
|
||||
export type GradientType = $Values<typeof GRADIENT_TYPE>;
|
||||
|
||||
export const RADIAL_GRADIENT_SHAPE = {
|
||||
CIRCLE: 0,
|
||||
ELLIPSE: 1
|
||||
};
|
||||
|
||||
export type RadialGradientShapeType = $Values<typeof RADIAL_GRADIENT_SHAPE>;
|
||||
|
||||
const LENGTH_FOR_POSITION = {
|
||||
left: new Length('0%'),
|
||||
top: new Length('0%'),
|
||||
center: new Length('50%'),
|
||||
right: new Length('100%'),
|
||||
bottom: new Length('100%')
|
||||
};
|
||||
|
||||
export class LinearGradient implements Gradient {
|
||||
type: GradientType;
|
||||
colorStops: Array<ColorStop>;
|
||||
direction: Direction;
|
||||
|
||||
constructor(colorStops: Array<ColorStop>, direction: Direction) {
|
||||
this.type = GRADIENT_TYPE.LINEAR_GRADIENT;
|
||||
this.colorStops = colorStops;
|
||||
this.direction = direction;
|
||||
}
|
||||
}
|
||||
|
||||
export class RadialGradient implements Gradient {
|
||||
type: GradientType;
|
||||
colorStops: Array<ColorStop>;
|
||||
shape: RadialGradientShapeType;
|
||||
center: Point;
|
||||
radius: Point;
|
||||
|
||||
constructor(
|
||||
colorStops: Array<ColorStop>,
|
||||
shape: RadialGradientShapeType,
|
||||
center: Point,
|
||||
radius: Point
|
||||
) {
|
||||
this.type = GRADIENT_TYPE.RADIAL_GRADIENT;
|
||||
this.colorStops = colorStops;
|
||||
this.shape = shape;
|
||||
this.center = center;
|
||||
this.radius = radius;
|
||||
}
|
||||
}
|
||||
|
||||
export const parseGradient = (
|
||||
container: NodeContainer,
|
||||
{args, method, prefix}: BackgroundSource,
|
||||
bounds: Bounds
|
||||
): ?Gradient => {
|
||||
if (method === 'linear-gradient') {
|
||||
return parseLinearGradient(args, bounds, !!prefix);
|
||||
} else if (method === 'gradient' && args[0] === 'linear') {
|
||||
// TODO handle correct angle
|
||||
return parseLinearGradient(
|
||||
['to bottom'].concat(transformObsoleteColorStops(args.slice(3))),
|
||||
bounds,
|
||||
!!prefix
|
||||
);
|
||||
} else if (method === 'radial-gradient') {
|
||||
return parseRadialGradient(
|
||||
container,
|
||||
prefix === '-webkit-' ? transformWebkitRadialGradientArgs(args) : args,
|
||||
bounds
|
||||
);
|
||||
} else if (method === 'gradient' && args[0] === 'radial') {
|
||||
return parseRadialGradient(
|
||||
container,
|
||||
transformObsoleteColorStops(transformWebkitRadialGradientArgs(args.slice(1))),
|
||||
bounds
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const parseColorStops = (args: Array<string>, firstColorStopIndex: number, lineLength: number) => {
|
||||
const colorStops = [];
|
||||
|
||||
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});
|
||||
}
|
||||
|
||||
const absoluteValuedColorStops = colorStops.map(({color, stop}) => {
|
||||
const absoluteStop =
|
||||
lineLength === 0 ? 0 : stop ? stop.getAbsoluteValue(lineLength) / lineLength : null;
|
||||
|
||||
return {
|
||||
color,
|
||||
// $FlowFixMe
|
||||
stop: absoluteStop
|
||||
};
|
||||
});
|
||||
|
||||
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 absoluteValuedColorStops;
|
||||
};
|
||||
|
||||
const parseLinearGradient = (
|
||||
args: Array<string>,
|
||||
bounds: Bounds,
|
||||
hasPrefix: boolean
|
||||
): LinearGradient => {
|
||||
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(
|
||||
// if there is a prefix, the 0° angle points due East (instead of North per W3C)
|
||||
hasPrefix ? angle - Math.PI * 0.5 : angle,
|
||||
bounds
|
||||
)
|
||||
: HAS_SIDE_OR_CORNER
|
||||
? parseSideOrCorner(args[0], bounds)
|
||||
: parsePercentageAngle(args[0], bounds)
|
||||
: calculateGradientDirection(Math.PI, bounds);
|
||||
const firstColorStopIndex = HAS_DIRECTION ? 1 : 0;
|
||||
|
||||
// TODO: Fix some inaccuracy with color stops with px values
|
||||
const lineLength = Math.min(
|
||||
distance(
|
||||
Math.abs(direction.x0) + Math.abs(direction.x1),
|
||||
Math.abs(direction.y0) + Math.abs(direction.y1)
|
||||
),
|
||||
bounds.width * 2,
|
||||
bounds.height * 2
|
||||
);
|
||||
|
||||
return new LinearGradient(parseColorStops(args, firstColorStopIndex, lineLength), direction);
|
||||
};
|
||||
|
||||
const parseRadialGradient = (
|
||||
container: NodeContainer,
|
||||
args: Array<string>,
|
||||
bounds: Bounds
|
||||
): RadialGradient => {
|
||||
const m = args[0].match(RADIAL_SHAPE_DEFINITION);
|
||||
const shape =
|
||||
m &&
|
||||
(m[1] === 'circle' || // explicit shape specification
|
||||
(m[3] !== undefined && m[5] === undefined)) // only one radius coordinate
|
||||
? RADIAL_GRADIENT_SHAPE.CIRCLE
|
||||
: RADIAL_GRADIENT_SHAPE.ELLIPSE;
|
||||
const radius = {};
|
||||
const center = {};
|
||||
|
||||
if (m) {
|
||||
// Radius
|
||||
if (m[3] !== undefined) {
|
||||
radius.x = calculateLengthFromValueWithUnit(container, m[3], m[4]).getAbsoluteValue(
|
||||
bounds.width
|
||||
);
|
||||
}
|
||||
|
||||
if (m[5] !== undefined) {
|
||||
radius.y = calculateLengthFromValueWithUnit(container, m[5], m[6]).getAbsoluteValue(
|
||||
bounds.height
|
||||
);
|
||||
}
|
||||
|
||||
// Position
|
||||
if (m[7]) {
|
||||
center.x = LENGTH_FOR_POSITION[m[7].toLowerCase()];
|
||||
} else if (m[8] !== undefined) {
|
||||
center.x = calculateLengthFromValueWithUnit(container, m[8], m[9]);
|
||||
}
|
||||
|
||||
if (m[10]) {
|
||||
center.y = LENGTH_FOR_POSITION[m[10].toLowerCase()];
|
||||
} else if (m[11] !== undefined) {
|
||||
center.y = calculateLengthFromValueWithUnit(container, m[11], m[12]);
|
||||
}
|
||||
}
|
||||
|
||||
const gradientCenter = {
|
||||
x: center.x === undefined ? bounds.width / 2 : center.x.getAbsoluteValue(bounds.width),
|
||||
y: center.y === undefined ? bounds.height / 2 : center.y.getAbsoluteValue(bounds.height)
|
||||
};
|
||||
const gradientRadius = calculateRadius(
|
||||
(m && m[2]) || 'farthest-corner',
|
||||
shape,
|
||||
gradientCenter,
|
||||
radius,
|
||||
bounds
|
||||
);
|
||||
|
||||
return new RadialGradient(
|
||||
parseColorStops(args, m ? 1 : 0, Math.min(gradientRadius.x, gradientRadius.y)),
|
||||
shape,
|
||||
gradientCenter,
|
||||
gradientRadius
|
||||
);
|
||||
};
|
||||
|
||||
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 / (distance(bounds.width, bounds.height) / 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);
|
||||
};
|
||||
|
||||
const findCorner = (bounds: Bounds, x: number, y: number, closest: boolean): Point => {
|
||||
var corners = [
|
||||
{x: 0, y: 0},
|
||||
{x: 0, y: bounds.height},
|
||||
{x: bounds.width, y: 0},
|
||||
{x: bounds.width, y: bounds.height}
|
||||
];
|
||||
|
||||
// $FlowFixMe
|
||||
return corners.reduce(
|
||||
(stat, corner) => {
|
||||
const d = distance(x - corner.x, y - corner.y);
|
||||
if (closest ? d < stat.optimumDistance : d > stat.optimumDistance) {
|
||||
return {
|
||||
optimumCorner: corner,
|
||||
optimumDistance: d
|
||||
};
|
||||
}
|
||||
|
||||
return stat;
|
||||
},
|
||||
{
|
||||
optimumDistance: closest ? Infinity : -Infinity,
|
||||
optimumCorner: null
|
||||
}
|
||||
).optimumCorner;
|
||||
};
|
||||
|
||||
const calculateRadius = (
|
||||
extent: string,
|
||||
shape: RadialGradientShapeType,
|
||||
center: Point,
|
||||
radius: Point,
|
||||
bounds: Bounds
|
||||
): Point => {
|
||||
const x = center.x;
|
||||
const y = center.y;
|
||||
let rx = 0;
|
||||
let ry = 0;
|
||||
|
||||
switch (extent) {
|
||||
case 'closest-side':
|
||||
// The ending shape is sized so that that it exactly meets the side of the gradient box closest to the gradient’s center.
|
||||
// If the shape is an ellipse, it exactly meets the closest side in each dimension.
|
||||
if (shape === RADIAL_GRADIENT_SHAPE.CIRCLE) {
|
||||
rx = ry = Math.min(
|
||||
Math.abs(x),
|
||||
Math.abs(x - bounds.width),
|
||||
Math.abs(y),
|
||||
Math.abs(y - bounds.height)
|
||||
);
|
||||
} else if (shape === RADIAL_GRADIENT_SHAPE.ELLIPSE) {
|
||||
rx = Math.min(Math.abs(x), Math.abs(x - bounds.width));
|
||||
ry = Math.min(Math.abs(y), Math.abs(y - bounds.height));
|
||||
}
|
||||
break;
|
||||
|
||||
case 'closest-corner':
|
||||
// The ending shape is sized so that that it passes through the corner of the gradient box closest to the gradient’s center.
|
||||
// If the shape is an ellipse, the ending shape is given the same aspect-ratio it would have if closest-side were specified.
|
||||
if (shape === RADIAL_GRADIENT_SHAPE.CIRCLE) {
|
||||
rx = ry = Math.min(
|
||||
distance(x, y),
|
||||
distance(x, y - bounds.height),
|
||||
distance(x - bounds.width, y),
|
||||
distance(x - bounds.width, y - bounds.height)
|
||||
);
|
||||
} else if (shape === RADIAL_GRADIENT_SHAPE.ELLIPSE) {
|
||||
// Compute the ratio ry/rx (which is to be the same as for "closest-side")
|
||||
const c =
|
||||
Math.min(Math.abs(y), Math.abs(y - bounds.height)) /
|
||||
Math.min(Math.abs(x), Math.abs(x - bounds.width));
|
||||
const corner = findCorner(bounds, x, y, true);
|
||||
rx = distance(corner.x - x, (corner.y - y) / c);
|
||||
ry = c * rx;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'farthest-side':
|
||||
// Same as closest-side, except the ending shape is sized based on the farthest side(s)
|
||||
if (shape === RADIAL_GRADIENT_SHAPE.CIRCLE) {
|
||||
rx = ry = Math.max(
|
||||
Math.abs(x),
|
||||
Math.abs(x - bounds.width),
|
||||
Math.abs(y),
|
||||
Math.abs(y - bounds.height)
|
||||
);
|
||||
} else if (shape === RADIAL_GRADIENT_SHAPE.ELLIPSE) {
|
||||
rx = Math.max(Math.abs(x), Math.abs(x - bounds.width));
|
||||
ry = Math.max(Math.abs(y), Math.abs(y - bounds.height));
|
||||
}
|
||||
break;
|
||||
|
||||
case 'farthest-corner':
|
||||
// Same as closest-corner, except the ending shape is sized based on the farthest corner.
|
||||
// If the shape is an ellipse, the ending shape is given the same aspect ratio it would have if farthest-side were specified.
|
||||
if (shape === RADIAL_GRADIENT_SHAPE.CIRCLE) {
|
||||
rx = ry = Math.max(
|
||||
distance(x, y),
|
||||
distance(x, y - bounds.height),
|
||||
distance(x - bounds.width, y),
|
||||
distance(x - bounds.width, y - bounds.height)
|
||||
);
|
||||
} else if (shape === RADIAL_GRADIENT_SHAPE.ELLIPSE) {
|
||||
// Compute the ratio ry/rx (which is to be the same as for "farthest-side")
|
||||
const c =
|
||||
Math.max(Math.abs(y), Math.abs(y - bounds.height)) /
|
||||
Math.max(Math.abs(x), Math.abs(x - bounds.width));
|
||||
const corner = findCorner(bounds, x, y, false);
|
||||
rx = distance(corner.x - x, (corner.y - y) / c);
|
||||
ry = c * rx;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
// pixel or percentage values
|
||||
rx = radius.x || 0;
|
||||
ry = radius.y !== undefined ? radius.y : rx;
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
x: rx,
|
||||
y: ry
|
||||
};
|
||||
};
|
||||
|
||||
export const transformWebkitRadialGradientArgs = (args: Array<string>): Array<string> => {
|
||||
let shape = '';
|
||||
let radius = '';
|
||||
let extent = '';
|
||||
let position = '';
|
||||
let idx = 0;
|
||||
|
||||
const POSITION = /^(left|center|right|\d+(?:px|r?em|%)?)(?:\s+(top|center|bottom|\d+(?:px|r?em|%)?))?$/i;
|
||||
const SHAPE_AND_EXTENT = /^(circle|ellipse)?\s*(closest-side|closest-corner|farthest-side|farthest-corner|contain|cover)?$/i;
|
||||
const RADIUS = /^\d+(px|r?em|%)?(?:\s+\d+(px|r?em|%)?)?$/i;
|
||||
|
||||
const matchStartPosition = args[idx].match(POSITION);
|
||||
if (matchStartPosition) {
|
||||
idx++;
|
||||
}
|
||||
|
||||
const matchShapeExtent = args[idx].match(SHAPE_AND_EXTENT);
|
||||
if (matchShapeExtent) {
|
||||
shape = matchShapeExtent[1] || '';
|
||||
extent = matchShapeExtent[2] || '';
|
||||
if (extent === 'contain') {
|
||||
extent = 'closest-side';
|
||||
} else if (extent === 'cover') {
|
||||
extent = 'farthest-corner';
|
||||
}
|
||||
idx++;
|
||||
}
|
||||
|
||||
const matchStartRadius = args[idx].match(RADIUS);
|
||||
if (matchStartRadius) {
|
||||
idx++;
|
||||
}
|
||||
|
||||
const matchEndPosition = args[idx].match(POSITION);
|
||||
if (matchEndPosition) {
|
||||
idx++;
|
||||
}
|
||||
|
||||
const matchEndRadius = args[idx].match(RADIUS);
|
||||
if (matchEndRadius) {
|
||||
idx++;
|
||||
}
|
||||
|
||||
const matchPosition = matchEndPosition || matchStartPosition;
|
||||
if (matchPosition && matchPosition[1]) {
|
||||
position = matchPosition[1] + (/^\d+$/.test(matchPosition[1]) ? 'px' : '');
|
||||
if (matchPosition[2]) {
|
||||
position += ' ' + matchPosition[2] + (/^\d+$/.test(matchPosition[2]) ? 'px' : '');
|
||||
}
|
||||
}
|
||||
|
||||
const matchRadius = matchEndRadius || matchStartRadius;
|
||||
if (matchRadius) {
|
||||
radius = matchRadius[0];
|
||||
if (!matchRadius[1]) {
|
||||
radius += 'px';
|
||||
}
|
||||
}
|
||||
|
||||
if (position && !shape && !radius && !extent) {
|
||||
radius = position;
|
||||
position = '';
|
||||
}
|
||||
|
||||
if (position) {
|
||||
position = `at ${position}`;
|
||||
}
|
||||
|
||||
return [[shape, extent, radius, position].filter(s => !!s).join(' ')].concat(args.slice(idx));
|
||||
};
|
||||
|
||||
const transformObsoleteColorStops = (args: Array<string>): Array<string> => {
|
||||
return (
|
||||
args
|
||||
.map(color => color.match(FROM_TO_COLORSTOP))
|
||||
// $FlowFixMe
|
||||
.map((v: Array<string>, index: number) => {
|
||||
if (!v) {
|
||||
return args[index];
|
||||
}
|
||||
|
||||
switch (v[1]) {
|
||||
case 'from':
|
||||
return `${v[4]} 0%`;
|
||||
case 'to':
|
||||
return `${v[4]} 100%`;
|
||||
case 'color-stop':
|
||||
if (v[3] === '%') {
|
||||
return `${v[4]} ${v[2]}`;
|
||||
}
|
||||
return `${v[4]} ${parseFloat(v[2]) * 100}%`;
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
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;
|
||||
};
|
68
src/Length.js
Normal file
68
src/Length.js
Normal file
@ -0,0 +1,68 @@
|
||||
/* @flow */
|
||||
'use strict';
|
||||
|
||||
import NodeContainer from './NodeContainer';
|
||||
|
||||
const LENGTH_WITH_UNIT = /([\d.]+)(px|r?em|%)/i;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
const getRootFontSize = (container: NodeContainer): number => {
|
||||
const parent = container.parent;
|
||||
return parent ? getRootFontSize(parent) : parseFloat(container.style.font.fontSize);
|
||||
};
|
||||
|
||||
export const calculateLengthFromValueWithUnit = (
|
||||
container: NodeContainer,
|
||||
value: string,
|
||||
unit: string
|
||||
): Length => {
|
||||
switch (unit) {
|
||||
case 'px':
|
||||
case '%':
|
||||
return new Length(value + unit);
|
||||
case 'em':
|
||||
case 'rem':
|
||||
const length = new Length(value);
|
||||
length.value *=
|
||||
unit === 'em'
|
||||
? parseFloat(container.style.font.fontSize)
|
||||
: getRootFontSize(container);
|
||||
return length;
|
||||
default:
|
||||
// TODO: handle correctly if unknown unit is used
|
||||
return new Length('0');
|
||||
}
|
||||
};
|
711
src/ListItem.js
Normal file
711
src/ListItem.js
Normal file
@ -0,0 +1,711 @@
|
||||
/* @flow */
|
||||
'use strict';
|
||||
|
||||
import type ResourceLoader from './ResourceLoader';
|
||||
import type {ListStyleType} from './parsing/listStyle';
|
||||
|
||||
import {copyCSSStyles, contains} from './Util';
|
||||
import NodeContainer from './NodeContainer';
|
||||
import TextContainer from './TextContainer';
|
||||
import {LIST_STYLE_POSITION, LIST_STYLE_TYPE} from './parsing/listStyle';
|
||||
import {fromCodePoint} from './Unicode';
|
||||
|
||||
// Margin between the enumeration and the list item content
|
||||
const MARGIN_RIGHT = 7;
|
||||
|
||||
const ancestorTypes = ['OL', 'UL', 'MENU'];
|
||||
|
||||
export const getListOwner = (container: NodeContainer): ?NodeContainer => {
|
||||
let parent = container.parent;
|
||||
if (!parent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
do {
|
||||
let isAncestor = ancestorTypes.indexOf(parent.tagName) !== -1;
|
||||
if (isAncestor) {
|
||||
return parent;
|
||||
}
|
||||
parent = parent.parent;
|
||||
} while (parent);
|
||||
|
||||
return container.parent;
|
||||
};
|
||||
|
||||
export const inlineListItemElement = (
|
||||
node: HTMLElement,
|
||||
container: NodeContainer,
|
||||
resourceLoader: ResourceLoader
|
||||
): void => {
|
||||
const listStyle = container.style.listStyle;
|
||||
|
||||
if (!listStyle) {
|
||||
return;
|
||||
}
|
||||
|
||||
const style = node.ownerDocument.defaultView.getComputedStyle(node, null);
|
||||
const wrapper = node.ownerDocument.createElement('html2canvaswrapper');
|
||||
copyCSSStyles(style, wrapper);
|
||||
|
||||
wrapper.style.position = 'absolute';
|
||||
wrapper.style.bottom = 'auto';
|
||||
wrapper.style.display = 'block';
|
||||
wrapper.style.letterSpacing = 'normal';
|
||||
|
||||
switch (listStyle.listStylePosition) {
|
||||
case LIST_STYLE_POSITION.OUTSIDE:
|
||||
wrapper.style.left = 'auto';
|
||||
wrapper.style.right = `${node.ownerDocument.defaultView.innerWidth -
|
||||
container.bounds.left -
|
||||
container.style.margin[1].getAbsoluteValue(container.bounds.width) +
|
||||
MARGIN_RIGHT}px`;
|
||||
wrapper.style.textAlign = 'right';
|
||||
break;
|
||||
case LIST_STYLE_POSITION.INSIDE:
|
||||
wrapper.style.left = `${container.bounds.left -
|
||||
container.style.margin[3].getAbsoluteValue(container.bounds.width)}px`;
|
||||
wrapper.style.right = 'auto';
|
||||
wrapper.style.textAlign = 'left';
|
||||
break;
|
||||
}
|
||||
|
||||
let text;
|
||||
const MARGIN_TOP = container.style.margin[0].getAbsoluteValue(container.bounds.width);
|
||||
const styleImage = listStyle.listStyleImage;
|
||||
if (styleImage) {
|
||||
if (styleImage.method === 'url') {
|
||||
const image = node.ownerDocument.createElement('img');
|
||||
image.src = styleImage.args[0];
|
||||
wrapper.style.top = `${container.bounds.top - MARGIN_TOP}px`;
|
||||
wrapper.style.width = 'auto';
|
||||
wrapper.style.height = 'auto';
|
||||
wrapper.appendChild(image);
|
||||
} else {
|
||||
const size = parseFloat(container.style.font.fontSize) * 0.5;
|
||||
wrapper.style.top = `${container.bounds.top -
|
||||
MARGIN_TOP +
|
||||
container.bounds.height -
|
||||
1.5 * size}px`;
|
||||
wrapper.style.width = `${size}px`;
|
||||
wrapper.style.height = `${size}px`;
|
||||
wrapper.style.backgroundImage = style.listStyleImage;
|
||||
}
|
||||
} else if (typeof container.listIndex === 'number') {
|
||||
text = node.ownerDocument.createTextNode(
|
||||
createCounterText(container.listIndex, listStyle.listStyleType, true)
|
||||
);
|
||||
wrapper.appendChild(text);
|
||||
wrapper.style.top = `${container.bounds.top - MARGIN_TOP}px`;
|
||||
}
|
||||
|
||||
// $FlowFixMe
|
||||
const body: HTMLBodyElement = node.ownerDocument.body;
|
||||
body.appendChild(wrapper);
|
||||
|
||||
if (text) {
|
||||
container.childNodes.push(TextContainer.fromTextNode(text, container));
|
||||
body.removeChild(wrapper);
|
||||
} else {
|
||||
// $FlowFixMe
|
||||
container.childNodes.push(new NodeContainer(wrapper, container, resourceLoader, 0));
|
||||
}
|
||||
};
|
||||
|
||||
const ROMAN_UPPER = {
|
||||
integers: [1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1],
|
||||
values: ['M', 'CM', 'D', 'CD', 'C', 'XC', 'L', 'XL', 'X', 'IX', 'V', 'IV', 'I']
|
||||
};
|
||||
|
||||
const ARMENIAN = {
|
||||
integers: [
|
||||
9000,
|
||||
8000,
|
||||
7000,
|
||||
6000,
|
||||
5000,
|
||||
4000,
|
||||
3000,
|
||||
2000,
|
||||
1000,
|
||||
900,
|
||||
800,
|
||||
700,
|
||||
600,
|
||||
500,
|
||||
400,
|
||||
300,
|
||||
200,
|
||||
100,
|
||||
90,
|
||||
80,
|
||||
70,
|
||||
60,
|
||||
50,
|
||||
40,
|
||||
30,
|
||||
20,
|
||||
10,
|
||||
9,
|
||||
8,
|
||||
7,
|
||||
6,
|
||||
5,
|
||||
4,
|
||||
3,
|
||||
2,
|
||||
1
|
||||
],
|
||||
values: [
|
||||
'Ք',
|
||||
'Փ',
|
||||
'Ւ',
|
||||
'Ց',
|
||||
'Ր',
|
||||
'Տ',
|
||||
'Վ',
|
||||
'Ս',
|
||||
'Ռ',
|
||||
'Ջ',
|
||||
'Պ',
|
||||
'Չ',
|
||||
'Ո',
|
||||
'Շ',
|
||||
'Ն',
|
||||
'Յ',
|
||||
'Մ',
|
||||
'Ճ',
|
||||
'Ղ',
|
||||
'Ձ',
|
||||
'Հ',
|
||||
'Կ',
|
||||
'Ծ',
|
||||
'Խ',
|
||||
'Լ',
|
||||
'Ի',
|
||||
'Ժ',
|
||||
'Թ',
|
||||
'Ը',
|
||||
'Է',
|
||||
'Զ',
|
||||
'Ե',
|
||||
'Դ',
|
||||
'Գ',
|
||||
'Բ',
|
||||
'Ա'
|
||||
]
|
||||
};
|
||||
|
||||
const HEBREW = {
|
||||
integers: [
|
||||
10000,
|
||||
9000,
|
||||
8000,
|
||||
7000,
|
||||
6000,
|
||||
5000,
|
||||
4000,
|
||||
3000,
|
||||
2000,
|
||||
1000,
|
||||
400,
|
||||
300,
|
||||
200,
|
||||
100,
|
||||
90,
|
||||
80,
|
||||
70,
|
||||
60,
|
||||
50,
|
||||
40,
|
||||
30,
|
||||
20,
|
||||
19,
|
||||
18,
|
||||
17,
|
||||
16,
|
||||
15,
|
||||
10,
|
||||
9,
|
||||
8,
|
||||
7,
|
||||
6,
|
||||
5,
|
||||
4,
|
||||
3,
|
||||
2,
|
||||
1
|
||||
],
|
||||
values: [
|
||||
'י׳',
|
||||
'ט׳',
|
||||
'ח׳',
|
||||
'ז׳',
|
||||
'ו׳',
|
||||
'ה׳',
|
||||
'ד׳',
|
||||
'ג׳',
|
||||
'ב׳',
|
||||
'א׳',
|
||||
'ת',
|
||||
'ש',
|
||||
'ר',
|
||||
'ק',
|
||||
'צ',
|
||||
'פ',
|
||||
'ע',
|
||||
'ס',
|
||||
'נ',
|
||||
'מ',
|
||||
'ל',
|
||||
'כ',
|
||||
'יט',
|
||||
'יח',
|
||||
'יז',
|
||||
'טז',
|
||||
'טו',
|
||||
'י',
|
||||
'ט',
|
||||
'ח',
|
||||
'ז',
|
||||
'ו',
|
||||
'ה',
|
||||
'ד',
|
||||
'ג',
|
||||
'ב',
|
||||
'א'
|
||||
]
|
||||
};
|
||||
|
||||
const GEORGIAN = {
|
||||
integers: [
|
||||
10000,
|
||||
9000,
|
||||
8000,
|
||||
7000,
|
||||
6000,
|
||||
5000,
|
||||
4000,
|
||||
3000,
|
||||
2000,
|
||||
1000,
|
||||
900,
|
||||
800,
|
||||
700,
|
||||
600,
|
||||
500,
|
||||
400,
|
||||
300,
|
||||
200,
|
||||
100,
|
||||
90,
|
||||
80,
|
||||
70,
|
||||
60,
|
||||
50,
|
||||
40,
|
||||
30,
|
||||
20,
|
||||
10,
|
||||
9,
|
||||
8,
|
||||
7,
|
||||
6,
|
||||
5,
|
||||
4,
|
||||
3,
|
||||
2,
|
||||
1
|
||||
],
|
||||
values: [
|
||||
'ჵ',
|
||||
'ჰ',
|
||||
'ჯ',
|
||||
'ჴ',
|
||||
'ხ',
|
||||
'ჭ',
|
||||
'წ',
|
||||
'ძ',
|
||||
'ც',
|
||||
'ჩ',
|
||||
'შ',
|
||||
'ყ',
|
||||
'ღ',
|
||||
'ქ',
|
||||
'ფ',
|
||||
'ჳ',
|
||||
'ტ',
|
||||
'ს',
|
||||
'რ',
|
||||
'ჟ',
|
||||
'პ',
|
||||
'ო',
|
||||
'ჲ',
|
||||
'ნ',
|
||||
'მ',
|
||||
'ლ',
|
||||
'კ',
|
||||
'ი',
|
||||
'თ',
|
||||
'ჱ',
|
||||
'ზ',
|
||||
'ვ',
|
||||
'ე',
|
||||
'დ',
|
||||
'გ',
|
||||
'ბ',
|
||||
'ა'
|
||||
]
|
||||
};
|
||||
|
||||
const createAdditiveCounter = (
|
||||
value: number,
|
||||
min: number,
|
||||
max: number,
|
||||
symbols,
|
||||
fallback: ListStyleType,
|
||||
suffix: string
|
||||
) => {
|
||||
if (value < min || value > max) {
|
||||
return createCounterText(value, fallback, suffix.length > 0);
|
||||
}
|
||||
|
||||
return (
|
||||
symbols.integers.reduce((string, integer, index) => {
|
||||
while (value >= integer) {
|
||||
value -= integer;
|
||||
string += symbols.values[index];
|
||||
}
|
||||
return string;
|
||||
}, '') + suffix
|
||||
);
|
||||
};
|
||||
|
||||
const createCounterStyleWithSymbolResolver = (
|
||||
value: number,
|
||||
codePointRangeLength: number,
|
||||
isNumeric: boolean,
|
||||
resolver
|
||||
): string => {
|
||||
let string = '';
|
||||
|
||||
do {
|
||||
if (!isNumeric) {
|
||||
value--;
|
||||
}
|
||||
string = resolver(value) + string;
|
||||
value /= codePointRangeLength;
|
||||
} while (value * codePointRangeLength >= codePointRangeLength);
|
||||
|
||||
return string;
|
||||
};
|
||||
|
||||
const createCounterStyleFromRange = (
|
||||
value: number,
|
||||
codePointRangeStart: number,
|
||||
codePointRangeEnd: number,
|
||||
isNumeric: boolean,
|
||||
suffix: string
|
||||
): string => {
|
||||
const codePointRangeLength = codePointRangeEnd - codePointRangeStart + 1;
|
||||
|
||||
return (
|
||||
(value < 0 ? '-' : '') +
|
||||
(createCounterStyleWithSymbolResolver(
|
||||
Math.abs(value),
|
||||
codePointRangeLength,
|
||||
isNumeric,
|
||||
codePoint =>
|
||||
fromCodePoint(Math.floor(codePoint % codePointRangeLength) + codePointRangeStart)
|
||||
) +
|
||||
suffix)
|
||||
);
|
||||
};
|
||||
|
||||
const createCounterStyleFromSymbols = (
|
||||
value: number,
|
||||
symbols: string,
|
||||
suffix: string = '. '
|
||||
): string => {
|
||||
const codePointRangeLength = symbols.length;
|
||||
return (
|
||||
createCounterStyleWithSymbolResolver(
|
||||
Math.abs(value),
|
||||
codePointRangeLength,
|
||||
false,
|
||||
codePoint => symbols[Math.floor(codePoint % codePointRangeLength)]
|
||||
) + suffix
|
||||
);
|
||||
};
|
||||
|
||||
const CJK_ZEROS = 1 << 0;
|
||||
const CJK_TEN_COEFFICIENTS = 1 << 1;
|
||||
const CJK_TEN_HIGH_COEFFICIENTS = 1 << 2;
|
||||
const CJK_HUNDRED_COEFFICIENTS = 1 << 3;
|
||||
|
||||
const createCJKCounter = (
|
||||
value: number,
|
||||
numbers: string,
|
||||
multipliers: string,
|
||||
negativeSign: string,
|
||||
suffix: string,
|
||||
flags: number
|
||||
): string => {
|
||||
if (value < -9999 || value > 9999) {
|
||||
return createCounterText(value, LIST_STYLE_TYPE.CJK_DECIMAL, suffix.length > 0);
|
||||
}
|
||||
let tmp = Math.abs(value);
|
||||
let string = suffix;
|
||||
|
||||
if (tmp === 0) {
|
||||
return numbers[0] + string;
|
||||
}
|
||||
|
||||
for (let digit = 0; tmp > 0 && digit <= 4; digit++) {
|
||||
let coefficient = tmp % 10;
|
||||
|
||||
if (coefficient === 0 && contains(flags, CJK_ZEROS) && string !== '') {
|
||||
string = numbers[coefficient] + string;
|
||||
} else if (
|
||||
coefficient > 1 ||
|
||||
(coefficient === 1 && digit === 0) ||
|
||||
(coefficient === 1 && digit === 1 && contains(flags, CJK_TEN_COEFFICIENTS)) ||
|
||||
(coefficient === 1 &&
|
||||
digit === 1 &&
|
||||
contains(flags, CJK_TEN_HIGH_COEFFICIENTS) &&
|
||||
value > 100) ||
|
||||
(coefficient === 1 && digit > 1 && contains(flags, CJK_HUNDRED_COEFFICIENTS))
|
||||
) {
|
||||
string = numbers[coefficient] + (digit > 0 ? multipliers[digit - 1] : '') + string;
|
||||
} else if (coefficient === 1 && digit > 0) {
|
||||
string = multipliers[digit - 1] + string;
|
||||
}
|
||||
tmp = Math.floor(tmp / 10);
|
||||
}
|
||||
|
||||
return (value < 0 ? negativeSign : '') + string;
|
||||
};
|
||||
|
||||
const CHINESE_INFORMAL_MULTIPLIERS = '十百千萬';
|
||||
const CHINESE_FORMAL_MULTIPLIERS = '拾佰仟萬';
|
||||
const JAPANESE_NEGATIVE = 'マイナス';
|
||||
const KOREAN_NEGATIVE = '마이너스 ';
|
||||
|
||||
export const createCounterText = (
|
||||
value: number,
|
||||
type: ListStyleType,
|
||||
appendSuffix: boolean
|
||||
): string => {
|
||||
const defaultSuffix = appendSuffix ? '. ' : '';
|
||||
const cjkSuffix = appendSuffix ? '、' : '';
|
||||
const koreanSuffix = appendSuffix ? ', ' : '';
|
||||
switch (type) {
|
||||
case LIST_STYLE_TYPE.DISC:
|
||||
return '•';
|
||||
case LIST_STYLE_TYPE.CIRCLE:
|
||||
return '◦';
|
||||
case LIST_STYLE_TYPE.SQUARE:
|
||||
return '◾';
|
||||
case LIST_STYLE_TYPE.DECIMAL_LEADING_ZERO:
|
||||
const string = createCounterStyleFromRange(value, 48, 57, true, defaultSuffix);
|
||||
return string.length < 4 ? `0${string}` : string;
|
||||
case LIST_STYLE_TYPE.CJK_DECIMAL:
|
||||
return createCounterStyleFromSymbols(value, '〇一二三四五六七八九', cjkSuffix);
|
||||
case LIST_STYLE_TYPE.LOWER_ROMAN:
|
||||
return createAdditiveCounter(
|
||||
value,
|
||||
1,
|
||||
3999,
|
||||
ROMAN_UPPER,
|
||||
LIST_STYLE_TYPE.DECIMAL,
|
||||
defaultSuffix
|
||||
).toLowerCase();
|
||||
case LIST_STYLE_TYPE.UPPER_ROMAN:
|
||||
return createAdditiveCounter(
|
||||
value,
|
||||
1,
|
||||
3999,
|
||||
ROMAN_UPPER,
|
||||
LIST_STYLE_TYPE.DECIMAL,
|
||||
defaultSuffix
|
||||
);
|
||||
case LIST_STYLE_TYPE.LOWER_GREEK:
|
||||
return createCounterStyleFromRange(value, 945, 969, false, defaultSuffix);
|
||||
case LIST_STYLE_TYPE.LOWER_ALPHA:
|
||||
return createCounterStyleFromRange(value, 97, 122, false, defaultSuffix);
|
||||
case LIST_STYLE_TYPE.UPPER_ALPHA:
|
||||
return createCounterStyleFromRange(value, 65, 90, false, defaultSuffix);
|
||||
case LIST_STYLE_TYPE.ARABIC_INDIC:
|
||||
return createCounterStyleFromRange(value, 1632, 1641, true, defaultSuffix);
|
||||
case LIST_STYLE_TYPE.ARMENIAN:
|
||||
case LIST_STYLE_TYPE.UPPER_ARMENIAN:
|
||||
return createAdditiveCounter(
|
||||
value,
|
||||
1,
|
||||
9999,
|
||||
ARMENIAN,
|
||||
LIST_STYLE_TYPE.DECIMAL,
|
||||
defaultSuffix
|
||||
);
|
||||
case LIST_STYLE_TYPE.LOWER_ARMENIAN:
|
||||
return createAdditiveCounter(
|
||||
value,
|
||||
1,
|
||||
9999,
|
||||
ARMENIAN,
|
||||
LIST_STYLE_TYPE.DECIMAL,
|
||||
defaultSuffix
|
||||
).toLowerCase();
|
||||
case LIST_STYLE_TYPE.BENGALI:
|
||||
return createCounterStyleFromRange(value, 2534, 2543, true, defaultSuffix);
|
||||
case LIST_STYLE_TYPE.CAMBODIAN:
|
||||
case LIST_STYLE_TYPE.KHMER:
|
||||
return createCounterStyleFromRange(value, 6112, 6121, true, defaultSuffix);
|
||||
case LIST_STYLE_TYPE.CJK_EARTHLY_BRANCH:
|
||||
return createCounterStyleFromSymbols(value, '子丑寅卯辰巳午未申酉戌亥', cjkSuffix);
|
||||
case LIST_STYLE_TYPE.CJK_HEAVENLY_STEM:
|
||||
return createCounterStyleFromSymbols(value, '甲乙丙丁戊己庚辛壬癸', cjkSuffix);
|
||||
case LIST_STYLE_TYPE.CJK_IDEOGRAPHIC:
|
||||
case LIST_STYLE_TYPE.TRAD_CHINESE_INFORMAL:
|
||||
return createCJKCounter(
|
||||
value,
|
||||
'零一二三四五六七八九',
|
||||
CHINESE_INFORMAL_MULTIPLIERS,
|
||||
'負',
|
||||
cjkSuffix,
|
||||
CJK_TEN_COEFFICIENTS | CJK_TEN_HIGH_COEFFICIENTS | CJK_HUNDRED_COEFFICIENTS
|
||||
);
|
||||
case LIST_STYLE_TYPE.TRAD_CHINESE_FORMAL:
|
||||
return createCJKCounter(
|
||||
value,
|
||||
'零壹貳參肆伍陸柒捌玖',
|
||||
CHINESE_FORMAL_MULTIPLIERS,
|
||||
'負',
|
||||
cjkSuffix,
|
||||
CJK_ZEROS |
|
||||
CJK_TEN_COEFFICIENTS |
|
||||
CJK_TEN_HIGH_COEFFICIENTS |
|
||||
CJK_HUNDRED_COEFFICIENTS
|
||||
);
|
||||
case LIST_STYLE_TYPE.SIMP_CHINESE_INFORMAL:
|
||||
return createCJKCounter(
|
||||
value,
|
||||
'零一二三四五六七八九',
|
||||
CHINESE_INFORMAL_MULTIPLIERS,
|
||||
'负',
|
||||
cjkSuffix,
|
||||
CJK_TEN_COEFFICIENTS | CJK_TEN_HIGH_COEFFICIENTS | CJK_HUNDRED_COEFFICIENTS
|
||||
);
|
||||
case LIST_STYLE_TYPE.SIMP_CHINESE_FORMAL:
|
||||
return createCJKCounter(
|
||||
value,
|
||||
'零壹贰叁肆伍陆柒捌玖',
|
||||
CHINESE_FORMAL_MULTIPLIERS,
|
||||
'负',
|
||||
cjkSuffix,
|
||||
CJK_ZEROS |
|
||||
CJK_TEN_COEFFICIENTS |
|
||||
CJK_TEN_HIGH_COEFFICIENTS |
|
||||
CJK_HUNDRED_COEFFICIENTS
|
||||
);
|
||||
case LIST_STYLE_TYPE.JAPANESE_INFORMAL:
|
||||
return createCJKCounter(value, '〇一二三四五六七八九', '十百千万', JAPANESE_NEGATIVE, cjkSuffix, 0);
|
||||
case LIST_STYLE_TYPE.JAPANESE_FORMAL:
|
||||
return createCJKCounter(
|
||||
value,
|
||||
'零壱弐参四伍六七八九',
|
||||
'拾百千万',
|
||||
JAPANESE_NEGATIVE,
|
||||
cjkSuffix,
|
||||
CJK_ZEROS | CJK_TEN_COEFFICIENTS | CJK_TEN_HIGH_COEFFICIENTS
|
||||
);
|
||||
case LIST_STYLE_TYPE.KOREAN_HANGUL_FORMAL:
|
||||
return createCJKCounter(
|
||||
value,
|
||||
'영일이삼사오육칠팔구',
|
||||
'십백천만',
|
||||
KOREAN_NEGATIVE,
|
||||
koreanSuffix,
|
||||
CJK_ZEROS | CJK_TEN_COEFFICIENTS | CJK_TEN_HIGH_COEFFICIENTS
|
||||
);
|
||||
case LIST_STYLE_TYPE.KOREAN_HANJA_INFORMAL:
|
||||
return createCJKCounter(value, '零一二三四五六七八九', '十百千萬', KOREAN_NEGATIVE, koreanSuffix, 0);
|
||||
case LIST_STYLE_TYPE.KOREAN_HANJA_FORMAL:
|
||||
return createCJKCounter(
|
||||
value,
|
||||
'零壹貳參四五六七八九',
|
||||
'拾百千',
|
||||
KOREAN_NEGATIVE,
|
||||
koreanSuffix,
|
||||
CJK_ZEROS | CJK_TEN_COEFFICIENTS | CJK_TEN_HIGH_COEFFICIENTS
|
||||
);
|
||||
case LIST_STYLE_TYPE.DEVANAGARI:
|
||||
return createCounterStyleFromRange(value, 0x966, 0x96f, true, defaultSuffix);
|
||||
case LIST_STYLE_TYPE.GEORGIAN:
|
||||
return createAdditiveCounter(
|
||||
value,
|
||||
1,
|
||||
19999,
|
||||
GEORGIAN,
|
||||
LIST_STYLE_TYPE.DECIMAL,
|
||||
defaultSuffix
|
||||
);
|
||||
case LIST_STYLE_TYPE.GUJARATI:
|
||||
return createCounterStyleFromRange(value, 0xae6, 0xaef, true, defaultSuffix);
|
||||
case LIST_STYLE_TYPE.GURMUKHI:
|
||||
return createCounterStyleFromRange(value, 0xa66, 0xa6f, true, defaultSuffix);
|
||||
case LIST_STYLE_TYPE.HEBREW:
|
||||
return createAdditiveCounter(
|
||||
value,
|
||||
1,
|
||||
10999,
|
||||
HEBREW,
|
||||
LIST_STYLE_TYPE.DECIMAL,
|
||||
defaultSuffix
|
||||
);
|
||||
case LIST_STYLE_TYPE.HIRAGANA:
|
||||
return createCounterStyleFromSymbols(
|
||||
value,
|
||||
'あいうえおかきくけこさしすせそたちつてとなにぬねのはひふへほまみむめもやゆよらりるれろわゐゑをん'
|
||||
);
|
||||
case LIST_STYLE_TYPE.HIRAGANA_IROHA:
|
||||
return createCounterStyleFromSymbols(
|
||||
value,
|
||||
'いろはにほへとちりぬるをわかよたれそつねならむうゐのおくやまけふこえてあさきゆめみしゑひもせす'
|
||||
);
|
||||
case LIST_STYLE_TYPE.KANNADA:
|
||||
return createCounterStyleFromRange(value, 0xce6, 0xcef, true, defaultSuffix);
|
||||
case LIST_STYLE_TYPE.KATAKANA:
|
||||
return createCounterStyleFromSymbols(
|
||||
value,
|
||||
'アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヰヱヲン',
|
||||
cjkSuffix
|
||||
);
|
||||
case LIST_STYLE_TYPE.KATAKANA_IROHA:
|
||||
return createCounterStyleFromSymbols(
|
||||
value,
|
||||
'イロハニホヘトチリヌルヲワカヨタレソツネナラムウヰノオクヤマケフコエテアサキユメミシヱヒモセス',
|
||||
cjkSuffix
|
||||
);
|
||||
case LIST_STYLE_TYPE.LAO:
|
||||
return createCounterStyleFromRange(value, 0xed0, 0xed9, true, defaultSuffix);
|
||||
case LIST_STYLE_TYPE.MONGOLIAN:
|
||||
return createCounterStyleFromRange(value, 0x1810, 0x1819, true, defaultSuffix);
|
||||
case LIST_STYLE_TYPE.MYANMAR:
|
||||
return createCounterStyleFromRange(value, 0x1040, 0x1049, true, defaultSuffix);
|
||||
case LIST_STYLE_TYPE.ORIYA:
|
||||
return createCounterStyleFromRange(value, 0xb66, 0xb6f, true, defaultSuffix);
|
||||
case LIST_STYLE_TYPE.PERSIAN:
|
||||
return createCounterStyleFromRange(value, 0x6f0, 0x6f9, true, defaultSuffix);
|
||||
case LIST_STYLE_TYPE.TAMIL:
|
||||
return createCounterStyleFromRange(value, 0xbe6, 0xbef, true, defaultSuffix);
|
||||
case LIST_STYLE_TYPE.TELUGU:
|
||||
return createCounterStyleFromRange(value, 0xc66, 0xc6f, true, defaultSuffix);
|
||||
case LIST_STYLE_TYPE.THAI:
|
||||
return createCounterStyleFromRange(value, 0xe50, 0xe59, true, defaultSuffix);
|
||||
case LIST_STYLE_TYPE.TIBETAN:
|
||||
return createCounterStyleFromRange(value, 0xf20, 0xf29, true, defaultSuffix);
|
||||
case LIST_STYLE_TYPE.DECIMAL:
|
||||
default:
|
||||
return createCounterStyleFromRange(value, 48, 57, true, defaultSuffix);
|
||||
}
|
||||
};
|
48
src/Logger.js
Normal file
48
src/Logger.js
Normal file
@ -0,0 +1,48 @@
|
||||
/* @flow */
|
||||
'use strict';
|
||||
|
||||
export default class Logger {
|
||||
enabled: boolean;
|
||||
start: number;
|
||||
id: ?string;
|
||||
|
||||
constructor(enabled: boolean, id: ?string, start: ?number) {
|
||||
this.enabled = enabled;
|
||||
this.start = start ? start : Date.now();
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
child(id: string) {
|
||||
return new Logger(this.enabled, id, this.start);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line flowtype/no-weak-types
|
||||
log(...args: any) {
|
||||
if (this.enabled && 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 (this.enabled && 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))
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
285
src/NodeContainer.js
Normal file
285
src/NodeContainer.js
Normal file
@ -0,0 +1,285 @@
|
||||
/* @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 {ListStyle} from './parsing/listStyle';
|
||||
import type {Margin} from './parsing/margin';
|
||||
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 {parseListStyle} from './parsing/listStyle';
|
||||
import {parseMargin} from './parsing/margin';
|
||||
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';
|
||||
import {getListOwner} from './ListItem';
|
||||
|
||||
type StyleDeclaration = {
|
||||
background: Background,
|
||||
border: Array<Border>,
|
||||
borderRadius: Array<BorderRadius>,
|
||||
color: Color,
|
||||
display: DisplayBit,
|
||||
float: Float,
|
||||
font: Font,
|
||||
letterSpacing: number,
|
||||
listStyle: ListStyle | null,
|
||||
margin: Margin,
|
||||
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>;
|
||||
listItems: Array<NodeContainer>;
|
||||
listIndex: ?number;
|
||||
listStart: ?number;
|
||||
bounds: Bounds;
|
||||
curvedBounds: BoundCurves;
|
||||
image: ?string;
|
||||
index: number;
|
||||
tagName: string;
|
||||
|
||||
constructor(
|
||||
node: HTMLElement | SVGSVGElement,
|
||||
parent: ?NodeContainer,
|
||||
resourceLoader: ResourceLoader,
|
||||
index: number
|
||||
) {
|
||||
this.parent = parent;
|
||||
this.tagName = node.tagName;
|
||||
this.index = index;
|
||||
this.childNodes = [];
|
||||
this.listItems = [];
|
||||
if (typeof node.start === 'number') {
|
||||
this.listStart = node.start;
|
||||
}
|
||||
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),
|
||||
listStyle: display === DISPLAY.LIST_ITEM ? parseListStyle(style) : null,
|
||||
margin: parseMargin(style),
|
||||
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)';
|
||||
}
|
||||
|
||||
if (display === DISPLAY.LIST_ITEM) {
|
||||
const listOwner = getListOwner(this);
|
||||
if (listOwner) {
|
||||
const listIndex = listOwner.listItems.length;
|
||||
listOwner.listItems.push(this);
|
||||
this.listIndex =
|
||||
node.hasAttribute('value') && typeof node.value === 'number'
|
||||
? node.value
|
||||
: listIndex === 0
|
||||
? typeof listOwner.listStart === 'number' ? listOwner.listStart : 1
|
||||
: listOwner.listItems[listIndex - 1].listIndex + 1;
|
||||
}
|
||||
}
|
||||
|
||||
// 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.VISIBLE;
|
||||
|
||||
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;
|
||||
};
|
165
src/NodeParser.js
Normal file
165
src/NodeParser.js
Normal file
@ -0,0 +1,165 @@
|
||||
/* @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';
|
||||
import {inlineListItemElement} from './ListItem';
|
||||
import {LIST_STYLE_TYPE} from './parsing/listStyle';
|
||||
|
||||
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);
|
||||
} else if (
|
||||
container.style.listStyle &&
|
||||
container.style.listStyle.listStyleType !== LIST_STYLE_TYPE.NONE
|
||||
) {
|
||||
inlineListItemElement(childNode, container, resourceLoader);
|
||||
}
|
||||
|
||||
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()
|
||||
);
|
||||
};
|
1143
src/Parse.js
1143
src/Parse.js
File diff suppressed because it is too large
Load Diff
317
src/Preload.js
317
src/Preload.js
@ -1,317 +0,0 @@
|
||||
_html2canvas.Preload = function( options ) {
|
||||
|
||||
var images = {
|
||||
numLoaded: 0, // also failed are counted here
|
||||
numFailed: 0,
|
||||
numTotal: 0,
|
||||
cleanupDone: false
|
||||
},
|
||||
pageOrigin,
|
||||
Util = _html2canvas.Util,
|
||||
methods,
|
||||
i,
|
||||
count = 0,
|
||||
element = options.elements[0] || document.body,
|
||||
doc = element.ownerDocument,
|
||||
domImages = element.getElementsByTagName('img'), // Fetch images of the present 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(){
|
||||
Util.log("html2canvas: start: images: " + images.numLoaded + " / " + images.numTotal + " (failed: " + images.numFailed + ")");
|
||||
if (!images.firstRun && images.numLoaded >= images.numTotal){
|
||||
Util.log("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 loadPseudoElement(element, type) {
|
||||
var style = window.getComputedStyle(element, type),
|
||||
content = style.content;
|
||||
if (content.substr(0, 3) === 'url') {
|
||||
methods.loadImage(_html2canvas.Util.parseBackgroundImage(content)[0].args[0]);
|
||||
}
|
||||
loadBackgroundImages(style.backgroundImage, element);
|
||||
}
|
||||
|
||||
function loadPseudoElementImages(element) {
|
||||
loadPseudoElement(element, ":before");
|
||||
loadPseudoElement(element, ":after");
|
||||
}
|
||||
|
||||
function loadGradientImage(backgroundImage, bounds) {
|
||||
var img = _html2canvas.Generate.Gradient(backgroundImage, bounds);
|
||||
|
||||
if (img !== undefined){
|
||||
images[backgroundImage] = {
|
||||
img: img,
|
||||
succeeded: true
|
||||
};
|
||||
images.numTotal++;
|
||||
images.numLoaded++;
|
||||
start();
|
||||
}
|
||||
}
|
||||
|
||||
function invalidBackgrounds(background_image) {
|
||||
return (background_image && background_image.method && background_image.args && background_image.args.length > 0 );
|
||||
}
|
||||
|
||||
function loadBackgroundImages(background_image, el) {
|
||||
var bounds;
|
||||
|
||||
_html2canvas.Util.parseBackgroundImage(background_image).filter(invalidBackgrounds).forEach(function(background_image) {
|
||||
if (background_image.method === 'url') {
|
||||
methods.loadImage(background_image.args[0]);
|
||||
} else if(background_image.method.match(/\-?gradient$/)) {
|
||||
if(bounds === undefined) {
|
||||
bounds = _html2canvas.Util.Bounds(el);
|
||||
}
|
||||
loadGradientImage(background_image.value, bounds);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getImages (el) {
|
||||
var elNodeType = false;
|
||||
|
||||
// Firefox fails with permission denied on pages with iframes
|
||||
try {
|
||||
Util.Children(el).forEach(getImages);
|
||||
}
|
||||
catch( e ) {}
|
||||
|
||||
try {
|
||||
elNodeType = el.nodeType;
|
||||
} catch (ex) {
|
||||
elNodeType = false;
|
||||
Util.log("html2canvas: failed to access some element's nodeType - Exception: " + ex.message);
|
||||
}
|
||||
|
||||
if (elNodeType === 1 || elNodeType === undefined) {
|
||||
loadPseudoElementImages(el);
|
||||
try {
|
||||
loadBackgroundImages(Util.getCSS(el, 'backgroundImage'), el);
|
||||
} catch(e) {
|
||||
Util.log("html2canvas: failed to get background-image - Exception: " + e.message);
|
||||
}
|
||||
loadBackgroundImages(el);
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
};
|
||||
}
|
||||
|
||||
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;
|
||||
} 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") {
|
||||
Util.log("html2canvas: Cleanup because: " + cause);
|
||||
} else {
|
||||
Util.log("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++;
|
||||
Util.log("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);
|
||||
}
|
||||
|
||||
Util.log('html2canvas: Preload starts: finding background-images');
|
||||
images.firstRun = true;
|
||||
|
||||
getImages(element);
|
||||
|
||||
Util.log('html2canvas: Preload: Finding images');
|
||||
// load <img> images
|
||||
for (i = 0; i < imgLen; i+=1){
|
||||
methods.loadImage( domImages[i].getAttribute( "src" ) );
|
||||
}
|
||||
|
||||
images.firstRun = false;
|
||||
Util.log('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();
|
||||
});
|
||||
};
|
341
src/PseudoNodeContent.js
Normal file
341
src/PseudoNodeContent.js
Normal file
@ -0,0 +1,341 @@
|
||||
/* @flow */
|
||||
'use strict';
|
||||
|
||||
import {createCounterText} from './ListItem';
|
||||
import {parseListStyleType} from './parsing/listStyle';
|
||||
|
||||
export const PSEUDO_CONTENT_ITEM_TYPE = {
|
||||
TEXT: 0,
|
||||
IMAGE: 1
|
||||
};
|
||||
|
||||
export const TOKEN_TYPE = {
|
||||
STRING: 0,
|
||||
ATTRIBUTE: 1,
|
||||
URL: 2,
|
||||
COUNTER: 3,
|
||||
COUNTERS: 4,
|
||||
OPENQUOTE: 5,
|
||||
CLOSEQUOTE: 6
|
||||
};
|
||||
|
||||
export type PseudoContentData = {
|
||||
counters: {[string]: Array<number>},
|
||||
quoteDepth: number
|
||||
};
|
||||
|
||||
export type PseudoContentItem = {
|
||||
type: $Values<typeof PSEUDO_CONTENT_ITEM_TYPE>,
|
||||
value: string
|
||||
};
|
||||
|
||||
export type Token = {
|
||||
type: $Values<typeof TOKEN_TYPE>,
|
||||
value?: string,
|
||||
name?: string,
|
||||
format?: string,
|
||||
glue?: string
|
||||
};
|
||||
|
||||
export const parseCounterReset = (
|
||||
style: ?CSSStyleDeclaration,
|
||||
data: PseudoContentData
|
||||
): Array<string> => {
|
||||
if (!style || !style.counterReset || style.counterReset === 'none') {
|
||||
return [];
|
||||
}
|
||||
|
||||
const counterNames: Array<string> = [];
|
||||
const counterResets = style.counterReset.split(/\s*,\s*/);
|
||||
const lenCounterResets = counterResets.length;
|
||||
|
||||
for (let i = 0; i < lenCounterResets; i++) {
|
||||
const [counterName, initialValue] = counterResets[i].split(/\s+/);
|
||||
counterNames.push(counterName);
|
||||
let counter = data.counters[counterName];
|
||||
if (!counter) {
|
||||
counter = data.counters[counterName] = [];
|
||||
}
|
||||
counter.push(parseInt(initialValue || 0, 10));
|
||||
}
|
||||
|
||||
return counterNames;
|
||||
};
|
||||
|
||||
export const popCounters = (counterNames: Array<string>, data: PseudoContentData): void => {
|
||||
const lenCounters = counterNames.length;
|
||||
for (let i = 0; i < lenCounters; i++) {
|
||||
data.counters[counterNames[i]].pop();
|
||||
}
|
||||
};
|
||||
|
||||
export const resolvePseudoContent = (
|
||||
node: Node,
|
||||
style: ?CSSStyleDeclaration,
|
||||
data: PseudoContentData
|
||||
): ?Array<PseudoContentItem> => {
|
||||
if (
|
||||
!style ||
|
||||
!style.content ||
|
||||
style.content === 'none' ||
|
||||
style.content === '-moz-alt-content' ||
|
||||
style.display === 'none'
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const tokens = parseContent(style.content);
|
||||
|
||||
const len = tokens.length;
|
||||
const contentItems: Array<PseudoContentItem> = [];
|
||||
let s = '';
|
||||
|
||||
// increment the counter (if there is a "counter-increment" declaration)
|
||||
const counterIncrement = style.counterIncrement;
|
||||
if (counterIncrement && counterIncrement !== 'none') {
|
||||
const [counterName, incrementValue] = counterIncrement.split(/\s+/);
|
||||
const counter = data.counters[counterName];
|
||||
if (counter) {
|
||||
counter[counter.length - 1] +=
|
||||
incrementValue === undefined ? 1 : parseInt(incrementValue, 10);
|
||||
}
|
||||
}
|
||||
|
||||
// build the content string
|
||||
for (let i = 0; i < len; i++) {
|
||||
const token = tokens[i];
|
||||
switch (token.type) {
|
||||
case TOKEN_TYPE.STRING:
|
||||
s += token.value || '';
|
||||
break;
|
||||
|
||||
case TOKEN_TYPE.ATTRIBUTE:
|
||||
if (node instanceof HTMLElement && token.value) {
|
||||
s += node.getAttribute(token.value) || '';
|
||||
}
|
||||
break;
|
||||
|
||||
case TOKEN_TYPE.COUNTER:
|
||||
const counter = data.counters[token.name || ''];
|
||||
if (counter) {
|
||||
s += formatCounterValue([counter[counter.length - 1]], '', token.format);
|
||||
}
|
||||
break;
|
||||
|
||||
case TOKEN_TYPE.COUNTERS:
|
||||
const counters = data.counters[token.name || ''];
|
||||
if (counters) {
|
||||
s += formatCounterValue(counters, token.glue, token.format);
|
||||
}
|
||||
break;
|
||||
|
||||
case TOKEN_TYPE.OPENQUOTE:
|
||||
s += getQuote(style, true, data.quoteDepth);
|
||||
data.quoteDepth++;
|
||||
break;
|
||||
|
||||
case TOKEN_TYPE.CLOSEQUOTE:
|
||||
data.quoteDepth--;
|
||||
s += getQuote(style, false, data.quoteDepth);
|
||||
break;
|
||||
|
||||
case TOKEN_TYPE.URL:
|
||||
if (s) {
|
||||
contentItems.push({type: PSEUDO_CONTENT_ITEM_TYPE.TEXT, value: s});
|
||||
s = '';
|
||||
}
|
||||
contentItems.push({type: PSEUDO_CONTENT_ITEM_TYPE.IMAGE, value: token.value || ''});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (s) {
|
||||
contentItems.push({type: PSEUDO_CONTENT_ITEM_TYPE.TEXT, value: s});
|
||||
}
|
||||
|
||||
return contentItems;
|
||||
};
|
||||
|
||||
export const parseContent = (content: string, cache?: {[string]: Array<Token>}): Array<Token> => {
|
||||
if (cache && cache[content]) {
|
||||
return cache[content];
|
||||
}
|
||||
|
||||
const tokens: Array<Token> = [];
|
||||
const len = content.length;
|
||||
|
||||
let isString = false;
|
||||
let isEscaped = false;
|
||||
let isFunction = false;
|
||||
let str = '';
|
||||
let functionName = '';
|
||||
let args = [];
|
||||
|
||||
for (let i = 0; i < len; i++) {
|
||||
const c = content.charAt(i);
|
||||
|
||||
switch (c) {
|
||||
case "'":
|
||||
case '"':
|
||||
if (isEscaped) {
|
||||
str += c;
|
||||
} else {
|
||||
isString = !isString;
|
||||
if (!isFunction && !isString) {
|
||||
tokens.push({type: TOKEN_TYPE.STRING, value: str});
|
||||
str = '';
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case '\\':
|
||||
if (isEscaped) {
|
||||
str += c;
|
||||
isEscaped = false;
|
||||
} else {
|
||||
isEscaped = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case '(':
|
||||
if (isString) {
|
||||
str += c;
|
||||
} else {
|
||||
isFunction = true;
|
||||
functionName = str;
|
||||
str = '';
|
||||
args = [];
|
||||
}
|
||||
break;
|
||||
|
||||
case ')':
|
||||
if (isString) {
|
||||
str += c;
|
||||
} else if (isFunction) {
|
||||
if (str) {
|
||||
args.push(str);
|
||||
}
|
||||
|
||||
switch (functionName) {
|
||||
case 'attr':
|
||||
if (args.length > 0) {
|
||||
tokens.push({type: TOKEN_TYPE.ATTRIBUTE, value: args[0]});
|
||||
}
|
||||
break;
|
||||
|
||||
case 'counter':
|
||||
if (args.length > 0) {
|
||||
const counter: Token = {
|
||||
type: TOKEN_TYPE.COUNTER,
|
||||
name: args[0]
|
||||
};
|
||||
if (args.length > 1) {
|
||||
counter.format = args[1];
|
||||
}
|
||||
tokens.push(counter);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'counters':
|
||||
if (args.length > 0) {
|
||||
const counters: Token = {
|
||||
type: TOKEN_TYPE.COUNTERS,
|
||||
name: args[0]
|
||||
};
|
||||
if (args.length > 1) {
|
||||
counters.glue = args[1];
|
||||
}
|
||||
if (args.length > 2) {
|
||||
counters.format = args[2];
|
||||
}
|
||||
tokens.push(counters);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'url':
|
||||
if (args.length > 0) {
|
||||
tokens.push({type: TOKEN_TYPE.URL, value: args[0]});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
isFunction = false;
|
||||
str = '';
|
||||
}
|
||||
break;
|
||||
|
||||
case ',':
|
||||
if (isString) {
|
||||
str += c;
|
||||
} else if (isFunction) {
|
||||
args.push(str);
|
||||
str = '';
|
||||
}
|
||||
break;
|
||||
|
||||
case ' ':
|
||||
case '\t':
|
||||
if (isString) {
|
||||
str += c;
|
||||
} else if (str) {
|
||||
addOtherToken(tokens, str);
|
||||
str = '';
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
str += c;
|
||||
}
|
||||
|
||||
if (c !== '\\') {
|
||||
isEscaped = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (str) {
|
||||
addOtherToken(tokens, str);
|
||||
}
|
||||
|
||||
if (cache) {
|
||||
cache[content] = tokens;
|
||||
}
|
||||
|
||||
return tokens;
|
||||
};
|
||||
|
||||
const addOtherToken = (tokens: Array<Token>, identifier: string): void => {
|
||||
switch (identifier) {
|
||||
case 'open-quote':
|
||||
tokens.push({type: TOKEN_TYPE.OPENQUOTE});
|
||||
break;
|
||||
case 'close-quote':
|
||||
tokens.push({type: TOKEN_TYPE.CLOSEQUOTE});
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const getQuote = (style: CSSStyleDeclaration, isOpening: boolean, quoteDepth: number): string => {
|
||||
const quotes = style.quotes ? style.quotes.split(/\s+/) : ["'\"'", "'\"'"];
|
||||
let idx = quoteDepth * 2;
|
||||
if (idx >= quotes.length) {
|
||||
idx = quotes.length - 2;
|
||||
}
|
||||
if (!isOpening) {
|
||||
++idx;
|
||||
}
|
||||
return quotes[idx].replace(/^["']|["']$/g, '');
|
||||
};
|
||||
|
||||
const formatCounterValue = (counter, glue: ?string, format: ?string): string => {
|
||||
const len = counter.length;
|
||||
let result = '';
|
||||
|
||||
for (let i = 0; i < len; i++) {
|
||||
if (i > 0) {
|
||||
result += glue || '';
|
||||
}
|
||||
result += createCounterText(counter[i], parseListStyleType(format || 'decimal'), false);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
123
src/Queue.js
123
src/Queue.js
@ -1,123 +0,0 @@
|
||||
function h2cRenderContext(width, height) {
|
||||
var storage = [];
|
||||
return {
|
||||
storage: storage,
|
||||
width: width,
|
||||
height: height,
|
||||
clip: function() {
|
||||
storage.push({
|
||||
type: "function",
|
||||
name: "clip",
|
||||
'arguments': arguments
|
||||
});
|
||||
},
|
||||
translate: function() {
|
||||
storage.push({
|
||||
type: "function",
|
||||
name: "translate",
|
||||
'arguments': arguments
|
||||
});
|
||||
},
|
||||
fill: function() {
|
||||
storage.push({
|
||||
type: "function",
|
||||
name: "fill",
|
||||
'arguments': arguments
|
||||
});
|
||||
},
|
||||
save: function() {
|
||||
storage.push({
|
||||
type: "function",
|
||||
name: "save",
|
||||
'arguments': arguments
|
||||
});
|
||||
},
|
||||
restore: function() {
|
||||
storage.push({
|
||||
type: "function",
|
||||
name: "restore",
|
||||
'arguments': arguments
|
||||
});
|
||||
},
|
||||
fillRect: function () {
|
||||
storage.push({
|
||||
type: "function",
|
||||
name: "fillRect",
|
||||
'arguments': arguments
|
||||
});
|
||||
},
|
||||
createPattern: function() {
|
||||
storage.push({
|
||||
type: "function",
|
||||
name: "createPattern",
|
||||
'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
|
||||
});
|
||||
},
|
||||
arcTo: function() {
|
||||
shape.push({
|
||||
name: "arcTo",
|
||||
'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
|
||||
});
|
||||
return value;
|
||||
}
|
||||
};
|
||||
}
|
532
src/Renderer.js
532
src/Renderer.js
@ -1,101 +1,445 @@
|
||||
_html2canvas.Renderer = function(parseQueue, options){
|
||||
/* @flow */
|
||||
'use strict';
|
||||
|
||||
// http://www.w3.org/TR/CSS21/zindex.html
|
||||
function createRenderQueue(parseQueue) {
|
||||
var queue = [],
|
||||
rootContext;
|
||||
import type Color from './Color';
|
||||
import type {Path} from './drawing/Path';
|
||||
import type Size from './drawing/Size';
|
||||
import type Logger from './Logger';
|
||||
|
||||
rootContext = (function buildStackingContext(rootNode) {
|
||||
var rootContext = {};
|
||||
function insert(context, node, specialParent) {
|
||||
var zi = (node.zIndex.zindex === 'auto') ? 0 : Number(node.zIndex.zindex),
|
||||
contextForChildren = context, // the stacking context for children
|
||||
isPositioned = node.zIndex.isPositioned,
|
||||
isFloated = node.zIndex.isFloated,
|
||||
stub = {node: node},
|
||||
childrenDest = specialParent; // where children without z-index should be pushed into
|
||||
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';
|
||||
|
||||
if (node.zIndex.ownStacking) {
|
||||
// '!' comes before numbers in sorted array
|
||||
contextForChildren = stub.context = { '!': [{node:node, children: []}]};
|
||||
childrenDest = undefined;
|
||||
} else if (isPositioned || isFloated) {
|
||||
childrenDest = stub.children = [];
|
||||
}
|
||||
import type {BoundCurves} from './Bounds';
|
||||
import type {LinearGradient, RadialGradient} from './Gradient';
|
||||
import type {ResourceStore, ImageElement} from './ResourceLoader';
|
||||
import type NodeContainer from './NodeContainer';
|
||||
import type StackingContext from './StackingContext';
|
||||
import type {TextBounds} from './TextBounds';
|
||||
|
||||
if (zi === 0 && specialParent) {
|
||||
specialParent.push(stub);
|
||||
} else {
|
||||
if (!context[zi]) { context[zi] = []; }
|
||||
context[zi].push(stub);
|
||||
}
|
||||
import {Bounds, parsePathForBorder, calculateContentBox, calculatePaddingBoxPath} from './Bounds';
|
||||
import {FontMetrics} from './Font';
|
||||
import {parseGradient, GRADIENT_TYPE} from './Gradient';
|
||||
import TextContainer from './TextContainer';
|
||||
|
||||
node.zIndex.children.forEach(function(childNode) {
|
||||
insert(contextForChildren, childNode, childrenDest);
|
||||
});
|
||||
}
|
||||
insert(rootContext, rootNode);
|
||||
return rootContext;
|
||||
})(parseQueue);
|
||||
import {
|
||||
calculateBackgroungPositioningArea,
|
||||
calculateBackgroungPaintingArea,
|
||||
calculateBackgroundPosition,
|
||||
calculateBackgroundRepeatPath,
|
||||
calculateBackgroundSize,
|
||||
calculateGradientBackgroundSize
|
||||
} from './parsing/background';
|
||||
import {BORDER_STYLE} from './parsing/border';
|
||||
|
||||
function sortZ(context) {
|
||||
Object.keys(context).sort().forEach(function(zi) {
|
||||
var nonPositioned = [],
|
||||
floated = [],
|
||||
positioned = [],
|
||||
list = [];
|
||||
|
||||
// positioned after static
|
||||
context[zi].forEach(function(v) {
|
||||
if (v.node.zIndex.isPositioned || v.node.zIndex.opacity < 1) {
|
||||
// http://www.w3.org/TR/css3-color/#transparency
|
||||
// non-positioned element with opactiy < 1 should be stacked as if it were a positioned element with ‘z-index: 0’ and ‘opacity: 1’.
|
||||
positioned.push(v);
|
||||
} else if (v.node.zIndex.isFloated) {
|
||||
floated.push(v);
|
||||
} else {
|
||||
nonPositioned.push(v);
|
||||
}
|
||||
});
|
||||
|
||||
(function walk(arr) {
|
||||
arr.forEach(function(v) {
|
||||
list.push(v);
|
||||
if (v.children) { walk(v.children); }
|
||||
});
|
||||
})(nonPositioned.concat(floated, positioned));
|
||||
|
||||
list.forEach(function(v) {
|
||||
if (v.context) {
|
||||
sortZ(v.context);
|
||||
} else {
|
||||
queue.push(v.node);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
sortZ(rootContext);
|
||||
|
||||
return queue;
|
||||
}
|
||||
|
||||
function getRenderer(rendererName) {
|
||||
var renderer;
|
||||
|
||||
if (typeof options.renderer === "string" && _html2canvas.Renderer[rendererName] !== undefined) {
|
||||
renderer = _html2canvas.Renderer[rendererName](options);
|
||||
} else if (typeof rendererName === "function") {
|
||||
renderer = rendererName(options);
|
||||
} else {
|
||||
throw new Error("Unknown renderer");
|
||||
}
|
||||
|
||||
if ( typeof renderer !== "function" ) {
|
||||
throw new Error("Invalid renderer defined");
|
||||
}
|
||||
return renderer;
|
||||
}
|
||||
|
||||
return getRenderer(options.renderer)(parseQueue, options, document, createRenderQueue(parseQueue.stack), _html2canvas);
|
||||
export type RenderOptions = {
|
||||
scale: number,
|
||||
backgroundColor: ?Color,
|
||||
imageStore: ResourceStore,
|
||||
fontMetrics: FontMetrics,
|
||||
logger: Logger,
|
||||
x: number,
|
||||
y: number,
|
||||
width: number,
|
||||
height: number
|
||||
};
|
||||
|
||||
export interface RenderTarget<Output> {
|
||||
clip(clipPaths: Array<Path>, callback: () => void): void,
|
||||
|
||||
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: LinearGradient): void,
|
||||
|
||||
renderRadialGradient(bounds: Bounds, gradient: RadialGradient): 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 if (/gradient/i.test(backgroundImage.source.method)) {
|
||||
this.renderBackgroundGradient(container, backgroundImage);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
renderBackgroundGradient(container: NodeContainer, background: BackgroundImage) {
|
||||
const backgroundPositioningArea = calculateBackgroungPositioningArea(
|
||||
container.style.background.backgroundOrigin,
|
||||
container.bounds,
|
||||
container.style.padding,
|
||||
container.style.border
|
||||
);
|
||||
const backgroundImageSize = calculateGradientBackgroundSize(
|
||||
background,
|
||||
backgroundPositioningArea
|
||||
);
|
||||
const position = calculateBackgroundPosition(
|
||||
background.position,
|
||||
backgroundImageSize,
|
||||
backgroundPositioningArea
|
||||
);
|
||||
const gradientBounds = new Bounds(
|
||||
Math.round(backgroundPositioningArea.left + position.x),
|
||||
Math.round(backgroundPositioningArea.top + position.y),
|
||||
backgroundImageSize.width,
|
||||
backgroundImageSize.height
|
||||
);
|
||||
|
||||
const gradient = parseGradient(container, background.source, gradientBounds);
|
||||
if (gradient) {
|
||||
switch (gradient.type) {
|
||||
case GRADIENT_TYPE.LINEAR_GRADIENT:
|
||||
// $FlowFixMe
|
||||
this.target.renderLinearGradient(gradientBounds, gradient);
|
||||
break;
|
||||
case GRADIENT_TYPE.RADIAL_GRADIENT:
|
||||
// $FlowFixMe
|
||||
this.target.renderRadialGradient(gradientBounds, gradient);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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(
|
||||
this.options.x,
|
||||
this.options.y,
|
||||
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;
|
||||
};
|
||||
|
241
src/ResourceLoader.js
Normal file
241
src/ResourceLoader.js
Normal file
@ -0,0 +1,241 @@
|
||||
/* @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) || FEATURES.SUPPORT_SVG_DRAWING) {
|
||||
if (this.options.allowTaint === true || isInlineImage(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();
|
||||
}
|
||||
}
|
@ -1,63 +0,0 @@
|
||||
_html2canvas.Util.Support = function (options, doc) {
|
||||
|
||||
function supportSVGRendering() {
|
||||
var img = new Image(),
|
||||
canvas = doc.createElement("canvas"),
|
||||
ctx = (canvas.getContext === undefined) ? false : canvas.getContext("2d");
|
||||
if (ctx === false) {
|
||||
return false;
|
||||
}
|
||||
canvas.width = canvas.height = 10;
|
||||
img.src = [
|
||||
"data:image/svg+xml,",
|
||||
"<svg xmlns='http://www.w3.org/2000/svg' width='10' height='10'>",
|
||||
"<foreignObject width='10' height='10'>",
|
||||
"<div xmlns='http://www.w3.org/1999/xhtml' style='width:10;height:10;'>",
|
||||
"sup",
|
||||
"</div>",
|
||||
"</foreignObject>",
|
||||
"</svg>"
|
||||
].join("");
|
||||
try {
|
||||
ctx.drawImage(img, 0, 0);
|
||||
canvas.toDataURL();
|
||||
} catch(e) {
|
||||
return false;
|
||||
}
|
||||
_html2canvas.Util.log('html2canvas: Parse: SVG powered rendering available');
|
||||
return true;
|
||||
}
|
||||
|
||||
// Test whether we can use ranges to measure bounding boxes
|
||||
// Opera doesn't provide valid bounds.height/bottom even though it supports the method.
|
||||
|
||||
function supportRangeBounds() {
|
||||
var r, testElement, rangeBounds, rangeHeight, support = false;
|
||||
|
||||
if (doc.createRange) {
|
||||
r = doc.createRange();
|
||||
if (r.getBoundingClientRect) {
|
||||
testElement = doc.createElement('boundtest');
|
||||
testElement.style.height = "123px";
|
||||
testElement.style.display = "block";
|
||||
doc.body.appendChild(testElement);
|
||||
|
||||
r.selectNode(testElement);
|
||||
rangeBounds = r.getBoundingClientRect();
|
||||
rangeHeight = rangeBounds.height;
|
||||
|
||||
if (rangeHeight === 123) {
|
||||
support = true;
|
||||
}
|
||||
doc.body.removeChild(testElement);
|
||||
}
|
||||
}
|
||||
|
||||
return support;
|
||||
}
|
||||
|
||||
return {
|
||||
rangeBounds: supportRangeBounds(),
|
||||
svgRendering: options.svgRendering && supportSVGRendering()
|
||||
};
|
||||
};
|
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;
|
||||
}
|
32
src/Unicode.js
Normal file
32
src/Unicode.js
Normal file
@ -0,0 +1,32 @@
|
||||
/* @flow */
|
||||
'use strict';
|
||||
|
||||
export const fromCodePoint = (...codePoints: Array<number>): string => {
|
||||
if (String.fromCodePoint) {
|
||||
return String.fromCodePoint(...codePoints);
|
||||
}
|
||||
|
||||
const length = codePoints.length;
|
||||
if (!length) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const codeUnits = [];
|
||||
|
||||
let index = -1;
|
||||
let result = '';
|
||||
while (++index < length) {
|
||||
let codePoint = codePoints[index];
|
||||
if (codePoint <= 0xffff) {
|
||||
codeUnits.push(codePoint);
|
||||
} else {
|
||||
codePoint -= 0x10000;
|
||||
codeUnits.push((codePoint >> 10) + 0xd800, codePoint % 0x400 + 0xdc00);
|
||||
}
|
||||
if (index + 1 === length || codeUnits.length > 0x4000) {
|
||||
result += String.fromCharCode(...codeUnits);
|
||||
codeUnits.length = 0;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
90
src/Util.js
90
src/Util.js
@ -1,81 +1,21 @@
|
||||
window.html2canvas = function(elements, opts) {
|
||||
elements = (elements.length) ? elements : [elements];
|
||||
var queue,
|
||||
canvas,
|
||||
options = {
|
||||
// general
|
||||
logging: false,
|
||||
elements: elements,
|
||||
background: "#fff",
|
||||
/* @flow */
|
||||
'use strict';
|
||||
|
||||
// preload options
|
||||
proxy: null,
|
||||
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
|
||||
export const contains = (bit: number, value: number): boolean => (bit & value) !== 0;
|
||||
|
||||
// parse options
|
||||
svgRendering: false, // use svg powered rendering where available (FF11+)
|
||||
ignoreElements: "IFRAME|OBJECT|PARAM",
|
||||
useOverflow: true,
|
||||
letterRendering: false,
|
||||
chinese: false,
|
||||
export const distance = (a: number, b: number): number => Math.sqrt(a * a + b * b);
|
||||
|
||||
// render options
|
||||
|
||||
width: null,
|
||||
height: null,
|
||||
taintTest: true, // do a taint test with all images before applying to canvas
|
||||
renderer: "Canvas"
|
||||
};
|
||||
|
||||
options = _html2canvas.Util.Extend(opts, options);
|
||||
|
||||
_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: _html2canvas.Util.log
|
||||
};
|
||||
return target;
|
||||
};
|
||||
|
||||
window.html2canvas.log = _html2canvas.Util.log; // for renderers
|
||||
window.html2canvas.Renderer = {
|
||||
Canvas: undefined // We are assuming this will be used
|
||||
};
|
||||
export const SMALL_IMAGE =
|
||||
'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
|
||||
|
142
src/Window.js
Normal file
142
src/Window.js
Normal file
@ -0,0 +1,142 @@
|
||||
/* @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 => {
|
||||
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);
|
||||
const canvas = renderer.render(stack);
|
||||
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`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return canvas;
|
||||
}
|
||||
});
|
||||
})
|
||||
);
|
||||
};
|
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`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
100
src/index.js
Normal file
100
src/index.js
Normal file
@ -0,0 +1,100 @@
|
||||
/* @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,
|
||||
logging: boolean,
|
||||
proxy: ?string,
|
||||
removeContainer: ?boolean,
|
||||
scale: number,
|
||||
target: RenderTarget<*>,
|
||||
useCORS: boolean,
|
||||
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(typeof config.logging === 'boolean' ? config.logging : true);
|
||||
|
||||
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,
|
||||
backgroundColor: '#ffffff',
|
||||
imageTimeout: 15000,
|
||||
logging: true,
|
||||
proxy: null,
|
||||
removeContainer: true,
|
||||
foreignObjectRendering: false,
|
||||
scale: defaultView.devicePixelRatio || 1,
|
||||
target: new CanvasRenderer(config.canvas),
|
||||
useCORS: false,
|
||||
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;
|
475
src/parsing/background.js
Normal file
475
src/parsing/background.js
Normal file
@ -0,0 +1,475 @@
|
||||
/* @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);
|
||||
};
|
||||
|
||||
export const calculateGradientBackgroundSize = (
|
||||
backgroundImage: BackgroundImage,
|
||||
bounds: Bounds
|
||||
): Size => {
|
||||
const size = backgroundImage.size;
|
||||
const width = size[0].value ? size[0].value.getAbsoluteValue(bounds.width) : bounds.width;
|
||||
const height = size[1].value
|
||||
? size[1].value.getAbsoluteValue(bounds.height)
|
||||
: size[0].value ? width : bounds.height;
|
||||
|
||||
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;
|
||||
};
|
209
src/parsing/listStyle.js
Normal file
209
src/parsing/listStyle.js
Normal file
@ -0,0 +1,209 @@
|
||||
/* @flow */
|
||||
'use strict';
|
||||
|
||||
import type {BackgroundSource} from './background';
|
||||
import {parseBackgroundImage} from './background';
|
||||
|
||||
export type ListStyle = {
|
||||
listStyleType: ListStyleType,
|
||||
listStyleImage: ?BackgroundSource,
|
||||
listStylePosition: ListStylePosition
|
||||
};
|
||||
|
||||
export const LIST_STYLE_POSITION = {
|
||||
INSIDE: 0,
|
||||
OUTSIDE: 1
|
||||
};
|
||||
|
||||
export const LIST_STYLE_TYPE = {
|
||||
NONE: -1,
|
||||
DISC: 0,
|
||||
CIRCLE: 1,
|
||||
SQUARE: 2,
|
||||
DECIMAL: 3,
|
||||
CJK_DECIMAL: 4,
|
||||
DECIMAL_LEADING_ZERO: 5,
|
||||
LOWER_ROMAN: 6,
|
||||
UPPER_ROMAN: 7,
|
||||
LOWER_GREEK: 8,
|
||||
LOWER_ALPHA: 9,
|
||||
UPPER_ALPHA: 10,
|
||||
ARABIC_INDIC: 11,
|
||||
ARMENIAN: 12,
|
||||
BENGALI: 13,
|
||||
CAMBODIAN: 14,
|
||||
CJK_EARTHLY_BRANCH: 15,
|
||||
CJK_HEAVENLY_STEM: 16,
|
||||
CJK_IDEOGRAPHIC: 17,
|
||||
DEVANAGARI: 18,
|
||||
ETHIOPIC_NUMERIC: 19,
|
||||
GEORGIAN: 20,
|
||||
GUJARATI: 21,
|
||||
GURMUKHI: 22,
|
||||
HEBREW: 22,
|
||||
HIRAGANA: 23,
|
||||
HIRAGANA_IROHA: 24,
|
||||
JAPANESE_FORMAL: 25,
|
||||
JAPANESE_INFORMAL: 26,
|
||||
KANNADA: 27,
|
||||
KATAKANA: 28,
|
||||
KATAKANA_IROHA: 29,
|
||||
KHMER: 30,
|
||||
KOREAN_HANGUL_FORMAL: 31,
|
||||
KOREAN_HANJA_FORMAL: 32,
|
||||
KOREAN_HANJA_INFORMAL: 33,
|
||||
LAO: 34,
|
||||
LOWER_ARMENIAN: 35,
|
||||
MALAYALAM: 36,
|
||||
MONGOLIAN: 37,
|
||||
MYANMAR: 38,
|
||||
ORIYA: 39,
|
||||
PERSIAN: 40,
|
||||
SIMP_CHINESE_FORMAL: 41,
|
||||
SIMP_CHINESE_INFORMAL: 42,
|
||||
TAMIL: 43,
|
||||
TELUGU: 44,
|
||||
THAI: 45,
|
||||
TIBETAN: 46,
|
||||
TRAD_CHINESE_FORMAL: 47,
|
||||
TRAD_CHINESE_INFORMAL: 48,
|
||||
UPPER_ARMENIAN: 49,
|
||||
DISCLOSURE_OPEN: 50,
|
||||
DISCLOSURE_CLOSED: 51
|
||||
};
|
||||
|
||||
export type ListStylePosition = $Values<typeof LIST_STYLE_POSITION>;
|
||||
export type ListStyleType = $Values<typeof LIST_STYLE_TYPE>;
|
||||
|
||||
export const parseListStyleType = (type: string): ListStyleType => {
|
||||
switch (type) {
|
||||
case 'disc':
|
||||
return LIST_STYLE_TYPE.DISC;
|
||||
case 'circle':
|
||||
return LIST_STYLE_TYPE.CIRCLE;
|
||||
case 'square':
|
||||
return LIST_STYLE_TYPE.SQUARE;
|
||||
case 'decimal':
|
||||
return LIST_STYLE_TYPE.DECIMAL;
|
||||
case 'cjk-decimal':
|
||||
return LIST_STYLE_TYPE.CJK_DECIMAL;
|
||||
case 'decimal-leading-zero':
|
||||
return LIST_STYLE_TYPE.DECIMAL_LEADING_ZERO;
|
||||
case 'lower-roman':
|
||||
return LIST_STYLE_TYPE.LOWER_ROMAN;
|
||||
case 'upper-roman':
|
||||
return LIST_STYLE_TYPE.UPPER_ROMAN;
|
||||
case 'lower-greek':
|
||||
return LIST_STYLE_TYPE.LOWER_GREEK;
|
||||
case 'lower-alpha':
|
||||
return LIST_STYLE_TYPE.LOWER_ALPHA;
|
||||
case 'upper-alpha':
|
||||
return LIST_STYLE_TYPE.UPPER_ALPHA;
|
||||
case 'arabic-indic':
|
||||
return LIST_STYLE_TYPE.ARABIC_INDIC;
|
||||
case 'armenian':
|
||||
return LIST_STYLE_TYPE.ARMENIAN;
|
||||
case 'bengali':
|
||||
return LIST_STYLE_TYPE.BENGALI;
|
||||
case 'cambodian':
|
||||
return LIST_STYLE_TYPE.CAMBODIAN;
|
||||
case 'cjk-earthly-branch':
|
||||
return LIST_STYLE_TYPE.CJK_EARTHLY_BRANCH;
|
||||
case 'cjk-heavenly-stem':
|
||||
return LIST_STYLE_TYPE.CJK_HEAVENLY_STEM;
|
||||
case 'cjk-ideographic':
|
||||
return LIST_STYLE_TYPE.CJK_IDEOGRAPHIC;
|
||||
case 'devanagari':
|
||||
return LIST_STYLE_TYPE.DEVANAGARI;
|
||||
case 'ethiopic-numeric':
|
||||
return LIST_STYLE_TYPE.ETHIOPIC_NUMERIC;
|
||||
case 'georgian':
|
||||
return LIST_STYLE_TYPE.GEORGIAN;
|
||||
case 'gujarati':
|
||||
return LIST_STYLE_TYPE.GUJARATI;
|
||||
case 'gurmukhi':
|
||||
return LIST_STYLE_TYPE.GURMUKHI;
|
||||
case 'hebrew':
|
||||
return LIST_STYLE_TYPE.HEBREW;
|
||||
case 'hiragana':
|
||||
return LIST_STYLE_TYPE.HIRAGANA;
|
||||
case 'hiragana-iroha':
|
||||
return LIST_STYLE_TYPE.HIRAGANA_IROHA;
|
||||
case 'japanese-formal':
|
||||
return LIST_STYLE_TYPE.JAPANESE_FORMAL;
|
||||
case 'japanese-informal':
|
||||
return LIST_STYLE_TYPE.JAPANESE_INFORMAL;
|
||||
case 'kannada':
|
||||
return LIST_STYLE_TYPE.KANNADA;
|
||||
case 'katakana':
|
||||
return LIST_STYLE_TYPE.KATAKANA;
|
||||
case 'katakana-iroha':
|
||||
return LIST_STYLE_TYPE.KATAKANA_IROHA;
|
||||
case 'khmer':
|
||||
return LIST_STYLE_TYPE.KHMER;
|
||||
case 'korean-hangul-formal':
|
||||
return LIST_STYLE_TYPE.KOREAN_HANGUL_FORMAL;
|
||||
case 'korean-hanja-formal':
|
||||
return LIST_STYLE_TYPE.KOREAN_HANJA_FORMAL;
|
||||
case 'korean-hanja-informal':
|
||||
return LIST_STYLE_TYPE.KOREAN_HANJA_INFORMAL;
|
||||
case 'lao':
|
||||
return LIST_STYLE_TYPE.LAO;
|
||||
case 'lower-armenian':
|
||||
return LIST_STYLE_TYPE.LOWER_ARMENIAN;
|
||||
case 'malayalam':
|
||||
return LIST_STYLE_TYPE.MALAYALAM;
|
||||
case 'mongolian':
|
||||
return LIST_STYLE_TYPE.MONGOLIAN;
|
||||
case 'myanmar':
|
||||
return LIST_STYLE_TYPE.MYANMAR;
|
||||
case 'oriya':
|
||||
return LIST_STYLE_TYPE.ORIYA;
|
||||
case 'persian':
|
||||
return LIST_STYLE_TYPE.PERSIAN;
|
||||
case 'simp-chinese-formal':
|
||||
return LIST_STYLE_TYPE.SIMP_CHINESE_FORMAL;
|
||||
case 'simp-chinese-informal':
|
||||
return LIST_STYLE_TYPE.SIMP_CHINESE_INFORMAL;
|
||||
case 'tamil':
|
||||
return LIST_STYLE_TYPE.TAMIL;
|
||||
case 'telugu':
|
||||
return LIST_STYLE_TYPE.TELUGU;
|
||||
case 'thai':
|
||||
return LIST_STYLE_TYPE.THAI;
|
||||
case 'tibetan':
|
||||
return LIST_STYLE_TYPE.TIBETAN;
|
||||
case 'trad-chinese-formal':
|
||||
return LIST_STYLE_TYPE.TRAD_CHINESE_FORMAL;
|
||||
case 'trad-chinese-informal':
|
||||
return LIST_STYLE_TYPE.TRAD_CHINESE_INFORMAL;
|
||||
case 'upper-armenian':
|
||||
return LIST_STYLE_TYPE.UPPER_ARMENIAN;
|
||||
case 'disclosure-open':
|
||||
return LIST_STYLE_TYPE.DISCLOSURE_OPEN;
|
||||
case 'disclosure-closed':
|
||||
return LIST_STYLE_TYPE.DISCLOSURE_CLOSED;
|
||||
case 'none':
|
||||
default:
|
||||
return LIST_STYLE_TYPE.NONE;
|
||||
}
|
||||
};
|
||||
|
||||
export const parseListStyle = (style: CSSStyleDeclaration): ListStyle => {
|
||||
const listStyleImage = parseBackgroundImage(style.getPropertyValue('list-style-image'));
|
||||
return {
|
||||
listStyleType: parseListStyleType(style.getPropertyValue('list-style-type')),
|
||||
listStyleImage: listStyleImage.length ? listStyleImage[0] : null,
|
||||
listStylePosition: parseListStylePosition(style.getPropertyValue('list-style-position'))
|
||||
};
|
||||
};
|
||||
|
||||
const parseListStylePosition = (position: string): ListStylePosition => {
|
||||
switch (position) {
|
||||
case 'inside':
|
||||
return LIST_STYLE_POSITION.INSIDE;
|
||||
case 'outside':
|
||||
default:
|
||||
return LIST_STYLE_POSITION.OUTSIDE;
|
||||
}
|
||||
};
|
11
src/parsing/margin.js
Normal file
11
src/parsing/margin.js
Normal file
@ -0,0 +1,11 @@
|
||||
/* @flow */
|
||||
'use strict';
|
||||
import Length from '../Length';
|
||||
|
||||
const SIDES = ['top', 'right', 'bottom', 'left'];
|
||||
|
||||
export type Margin = Array<Length>;
|
||||
|
||||
export const parseMargin = (style: CSSStyleDeclaration): Margin => {
|
||||
return SIDES.map(side => new Length(style.getPropertyValue(`margin-${side}`)));
|
||||
};
|
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)
|
||||
};
|
||||
};
|
312
src/renderer/CanvasRenderer.js
Normal file
312
src/renderer/CanvasRenderer.js
Normal file
@ -0,0 +1,312 @@
|
||||
/* @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 {LinearGradient, RadialGradient} from '../Gradient';
|
||||
import type {TextBounds} from '../TextBounds';
|
||||
|
||||
import {PATH} from '../drawing/Path';
|
||||
import {TEXT_DECORATION_LINE} from '../parsing/textDecoration';
|
||||
|
||||
const addColorStops = (
|
||||
gradient: LinearGradient | RadialGradient,
|
||||
canvasGradient: CanvasGradient
|
||||
): void => {
|
||||
const maxStop = Math.max.apply(null, gradient.colorStops.map(colorStop => colorStop.stop));
|
||||
const f = 1 / Math.max(1, maxStop);
|
||||
gradient.colorStops.forEach(colorStop => {
|
||||
canvasGradient.addColorStop(f * colorStop.stop, colorStop.color.toString());
|
||||
});
|
||||
};
|
||||
|
||||
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: LinearGradient) {
|
||||
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
|
||||
);
|
||||
|
||||
addColorStops(gradient, linearGradient);
|
||||
this.ctx.fillStyle = linearGradient;
|
||||
this.ctx.fillRect(bounds.left, bounds.top, bounds.width, bounds.height);
|
||||
}
|
||||
|
||||
renderRadialGradient(bounds: Bounds, gradient: RadialGradient) {
|
||||
const x = bounds.left + gradient.center.x;
|
||||
const y = bounds.top + gradient.center.y;
|
||||
|
||||
const radialGradient = this.ctx.createRadialGradient(x, y, 0, x, y, gradient.radius.x);
|
||||
if (!radialGradient) {
|
||||
return;
|
||||
}
|
||||
|
||||
addColorStops(gradient, radialGradient);
|
||||
this.ctx.fillStyle = radialGradient;
|
||||
|
||||
if (gradient.radius.x !== gradient.radius.y) {
|
||||
// transforms for elliptical radial gradient
|
||||
const midX = bounds.left + 0.5 * bounds.width;
|
||||
const midY = bounds.top + 0.5 * bounds.height;
|
||||
const f = gradient.radius.y / gradient.radius.x;
|
||||
const invF = 1 / f;
|
||||
|
||||
this.transform(midX, midY, [1, 0, 0, f, 0, 0], () =>
|
||||
this.ctx.fillRect(
|
||||
bounds.left,
|
||||
invF * (bounds.top - midY) + midY,
|
||||
bounds.width,
|
||||
bounds.height * invF
|
||||
)
|
||||
);
|
||||
} else {
|
||||
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(' ');
|
||||
|
||||
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 + text.bounds.height - 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)
|
||||
)}`;
|
||||
});
|
||||
};
|
272
src/renderer/RefTestRenderer.js
Normal file
272
src/renderer/RefTestRenderer.js
Normal file
@ -0,0 +1,272 @@
|
||||
/* @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 {LinearGradient, RadialGradient} 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: LinearGradient) {
|
||||
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(', ')})`
|
||||
);
|
||||
}
|
||||
|
||||
renderRadialGradient(bounds: Bounds, gradient: RadialGradient) {
|
||||
const stops = gradient.colorStops.map(
|
||||
stop => `${stop.color.toString()} ${Math.ceil(stop.stop * 100) / 100}`
|
||||
);
|
||||
|
||||
this.writeLine(
|
||||
`RadialGradient: ${this.formatBounds(bounds)} radial-gradient(${gradient.radius
|
||||
.x} ${gradient.radius.y} at ${gradient.center.x} ${gradient.center.y}, ${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,128 +0,0 @@
|
||||
_html2canvas.Renderer.Canvas = function(options) {
|
||||
options = options || {};
|
||||
|
||||
var doc = document,
|
||||
safeImages = [],
|
||||
testCanvas = document.createElement("canvas"),
|
||||
testctx = testCanvas.getContext("2d"),
|
||||
Util = _html2canvas.Util,
|
||||
canvas = options.canvas || doc.createElement('canvas');
|
||||
|
||||
function createShape(ctx, args) {
|
||||
ctx.beginPath();
|
||||
args.forEach(function(arg) {
|
||||
ctx[arg.name].apply(ctx, arg['arguments']);
|
||||
});
|
||||
ctx.closePath();
|
||||
}
|
||||
|
||||
function safeImage(item) {
|
||||
if (safeImages.indexOf(item['arguments'][0].src ) === -1) {
|
||||
testctx.drawImage(item['arguments'][0], 0, 0);
|
||||
try {
|
||||
testctx.getImageData(0, 0, 1, 1);
|
||||
} catch(e) {
|
||||
testCanvas = doc.createElement("canvas");
|
||||
testctx = testCanvas.getContext("2d");
|
||||
return false;
|
||||
}
|
||||
safeImages.push(item['arguments'][0].src);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function renderItem(ctx, item) {
|
||||
switch(item.type){
|
||||
case "variable":
|
||||
ctx[item.name] = item['arguments'];
|
||||
break;
|
||||
case "function":
|
||||
switch(item.name) {
|
||||
case "createPattern":
|
||||
if (item['arguments'][0].width > 0 && item['arguments'][0].height > 0) {
|
||||
try {
|
||||
ctx.fillStyle = ctx.createPattern(item['arguments'][0], "repeat");
|
||||
}
|
||||
catch(e) {
|
||||
Util.log("html2canvas: Renderer: Error creating pattern", e.message);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "drawShape":
|
||||
createShape(ctx, item['arguments']);
|
||||
break;
|
||||
case "drawImage":
|
||||
if (item['arguments'][8] > 0 && item['arguments'][7] > 0) {
|
||||
if (!options.taintTest || (options.taintTest && safeImage(item))) {
|
||||
ctx.drawImage.apply( ctx, item['arguments'] );
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
ctx[item.name].apply(ctx, item['arguments']);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return function(parsedData, options, document, queue, _html2canvas) {
|
||||
var ctx = canvas.getContext("2d"),
|
||||
newCanvas,
|
||||
bounds,
|
||||
fstyle,
|
||||
zStack = parsedData.stack;
|
||||
|
||||
canvas.width = canvas.style.width = options.width || zStack.ctx.width;
|
||||
canvas.height = canvas.style.height = options.height || zStack.ctx.height;
|
||||
|
||||
fstyle = ctx.fillStyle;
|
||||
ctx.fillStyle = (Util.isTransparent(zStack.backgroundColor) && options.background !== undefined) ? options.background : parsedData.backgroundColor;
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.fillStyle = fstyle;
|
||||
|
||||
queue.forEach(function(storageContext) {
|
||||
// set common settings for canvas
|
||||
ctx.textBaseline = "bottom";
|
||||
ctx.save();
|
||||
|
||||
if (storageContext.transform.matrix) {
|
||||
ctx.translate(storageContext.transform.origin[0], storageContext.transform.origin[1]);
|
||||
ctx.transform.apply(ctx, storageContext.transform.matrix);
|
||||
ctx.translate(-storageContext.transform.origin[0], -storageContext.transform.origin[1]);
|
||||
}
|
||||
|
||||
if (storageContext.clip){
|
||||
ctx.beginPath();
|
||||
ctx.rect(storageContext.clip.left, storageContext.clip.top, storageContext.clip.width, storageContext.clip.height);
|
||||
ctx.clip();
|
||||
}
|
||||
|
||||
if (storageContext.ctx.storage) {
|
||||
storageContext.ctx.storage.forEach(function(item) {
|
||||
renderItem(ctx, item);
|
||||
});
|
||||
}
|
||||
|
||||
ctx.restore();
|
||||
});
|
||||
|
||||
Util.log("html2canvas: Renderer: Canvas renderer done - returning canvas obj");
|
||||
|
||||
if (options.elements.length === 1) {
|
||||
if (typeof options.elements[0] === "object" && options.elements[0].nodeName !== "BODY") {
|
||||
// crop image to the bounds of selected (single) element
|
||||
bounds = _html2canvas.Util.Bounds(options.elements[0]);
|
||||
newCanvas = document.createElement('canvas');
|
||||
newCanvas.width = Math.ceil(bounds.width);
|
||||
newCanvas.height = Math.ceil(bounds.height);
|
||||
ctx = newCanvas.getContext("2d");
|
||||
|
||||
ctx.drawImage(canvas, bounds.left, bounds.top, bounds.width, bounds.height, 0, 0, bounds.width, bounds.height);
|
||||
canvas = null;
|
||||
return newCanvas;
|
||||
}
|
||||
}
|
||||
|
||||
return canvas;
|
||||
};
|
||||
};
|
@ -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();
|
||||
}
|
||||
*/
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
_html2canvas.Util.log("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>
|
8981
tests/assets/jquery-1.6.2.js
vendored
8981
tests/assets/jquery-1.6.2.js
vendored
File diff suppressed because it is too large
Load Diff
@ -1,84 +0,0 @@
|
||||
/*
|
||||
* jQuery helper plugin for examples and tests
|
||||
*/
|
||||
(function( $ ){
|
||||
$.fn.html2canvas = function(options) {
|
||||
if (options && options.profile && window.console && window.console.profile && window.location.search !== "?selenium2") {
|
||||
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.addClass("html2canvas")
|
||||
.css({
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: 0
|
||||
}).appendTo(document.body);
|
||||
|
||||
if (window.location.search !== "?selenium") {
|
||||
$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);
|
||||
} else {
|
||||
$canvas.css('display', 'none');
|
||||
}
|
||||
// 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 );
|
@ -1,58 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Form tests</title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<script type="text/javascript" src="../test.js"></script>
|
||||
<style>
|
||||
|
||||
</style>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<input type="text" value="textbox" />
|
||||
<input type="text" value="textbox" style="border:5px solid navy;" />
|
||||
<input type="text" value="textbox" style="border:5px solid navy;height:40px;" />
|
||||
|
||||
<input type="text" value="textbox" style="border:5px solid navy;height:40px;padding:10px;" />
|
||||
|
||||
<input type="text" value="textbox" style="padding:10px;" />
|
||||
<input type="text" value="textbox" style="padding:10px;text-align:right;" />
|
||||
<hr />
|
||||
<select>
|
||||
<option value="1">Value 1</option>
|
||||
<option value="2">Value 2</option>
|
||||
<option value="3">Value 3</option>
|
||||
</select>
|
||||
<select>
|
||||
|
||||
</select>
|
||||
<select>
|
||||
<option value="">2</option>
|
||||
</select>
|
||||
|
||||
|
||||
<select>
|
||||
<option value="1">Value 1</option>
|
||||
<option value="2" selected>Value 2 with something else</option>
|
||||
<option value="3">Value 3</option>
|
||||
</select>
|
||||
<hr />
|
||||
<input type="submit" value="Submit" />
|
||||
<input type="Button" value="Button" />
|
||||
<input type="Reset" value="Reset" />
|
||||
|
||||
|
||||
<input type="submit" value="Submit" style="width:200px;" />
|
||||
<input type="Button" value="Button" style="width:200px;height:50px;" />
|
||||
<input type="Reset" value="Reset" style="width:200px;height:50px;text-align:left;" />
|
||||
|
||||
<hr />
|
||||
|
||||
<textarea> </textarea>
|
||||
<textarea style="border-width:10px;"></textarea>
|
||||
|
||||
<textarea> text </textarea>
|
||||
<textarea style="border-width:10px;">text</textarea>
|
||||
</body>
|
||||
</html>
|
@ -1,10 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>iframe test</title>
|
||||
<script type="text/javascript" src="../test.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<iframe src="http://experiments.hertzen.com/csstree/" width="500"></iframe>
|
||||
</body>
|
||||
</html>
|
@ -1,27 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Image tests</title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
|
||||
<script type="text/javascript" src="../../test.js"></script>
|
||||
<script type="text/javascript">
|
||||
function setUp() {
|
||||
if ($('#testcanvas')[0].getContext) {
|
||||
var ctx = $('#testcanvas')[0].getContext('2d');
|
||||
|
||||
ctx.fillStyle = "rgb(200,0,0)";
|
||||
ctx.fillRect (10, 10, 55, 50);
|
||||
|
||||
ctx.fillStyle = "rgba(0, 0, 200, 0.5)";
|
||||
ctx.fillRect (30, 30, 55, 50);
|
||||
} else {
|
||||
$('#testcanvas').remove();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<canvas id="testcanvas" style="width:700px;height:300px;"></canvas>
|
||||
</body>
|
||||
</html>
|
@ -1,21 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>External content tests</title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<script type="text/javascript" src="../../test.js"></script>
|
||||
|
||||
<base href="http://www.google.com/" />
|
||||
</head>
|
||||
<body>
|
||||
<h1>External image</h1>
|
||||
<img src="http://www.google.com/logos/2011/gregormendel11-hp.jpg" style="border:5px solid black;" />
|
||||
|
||||
<h1>External image (using <base> href)</h1>
|
||||
<img src="/logos/2011/gregormendel11-res.jpg" />
|
||||
|
||||
<h1>External image (CORS)</h1>
|
||||
<img src="http://publishmydata.com/assets/home/blue_bg.png" />
|
||||
|
||||
</body>
|
||||
</html>
|
@ -1,52 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Overflow tests</title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<script type="text/javascript" src="../test.js"></script>
|
||||
<style>
|
||||
.small{
|
||||
font-size:14px;
|
||||
}
|
||||
|
||||
.medium{
|
||||
font-size:18px;
|
||||
}
|
||||
.large{
|
||||
font-size:24px;
|
||||
}
|
||||
div{
|
||||
background: #ccc;
|
||||
border:6px solid black;
|
||||
width:300px;
|
||||
height:200px;
|
||||
margin: 0 0 60px 0;
|
||||
}
|
||||
h1 {
|
||||
margin:0;
|
||||
}
|
||||
</style>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<h1>Overflow: visible</h1>
|
||||
<div> Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like <b>Aldus PageMaker</b> including versions of Lorem Ipsum.
|
||||
</div>
|
||||
|
||||
<h1>Overflow: hidden</h1>
|
||||
<div style="overflow:hidden;">
|
||||
Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s
|
||||
|
||||
with the release of <div style="border-width:10px 0 5px 0;background:green;">a</div>Letraset sheets containing Lorem Ipsum passages, <img src="image.jpg" /> and more recently with desktop publishing software like <b>Aldus PageMaker</b> including versions of Lorem Ipsum.
|
||||
|
||||
|
||||
<div style="overflow:visible;position:relative;"><u>position:relative within a overflow:hidden element</u><br /> <br />
|
||||
|
||||
Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like <b>Aldus PageMaker</b> including versions of Lorem Ipsum.
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user