Compare commits

...

195 Commits

Author SHA1 Message Date
c4ba6f795c Update CHANGELOG 2017-12-12 22:55:32 +08:00
0b9f34a5bf Fix formatting 2017-12-12 22:39:27 +08:00
757c32f6c4 Update unsupported features 2017-12-12 22:32:20 +08:00
09bab18b48 Fix flow error 2017-12-12 22:32:12 +08:00
7e53b195ea Add no-response bot config 2017-12-12 22:28:27 +08:00
ab966ff311 Fix formatting 2017-12-12 22:23:48 +08:00
7bb4a6f08f Fix compiled code using symbols 2017-12-12 22:18:15 +08:00
f50da9718f Fix NaN color stop in IE 2017-12-12 22:08:46 +08:00
3965a0fd40 Fix backgroundColor option documentation (Fix #1164) 2017-12-12 21:23:53 +08:00
77d258f1d8 Fix rendering with multiple fonts defined (Fix #796) 2017-12-12 21:08:19 +08:00
261702a693 Merge branch 'vnmc-feature/HTC-0009_RadialGradients' 2017-12-12 20:58:07 +08:00
cacb9f64e4 Radial gradient support 2017-12-12 20:57:48 +08:00
8ef3861a5c added support for radial gradients 2017-12-12 20:16:04 +08:00
0b74e69611 Update website demo 2017-12-12 19:43:59 +08:00
c272b2e122 Remove reftest results 2017-12-11 21:38:16 +08:00
2d132b85c6 Add gzipped package size to website (Fix #992) 2017-12-11 21:20:14 +08:00
50608e9cd4 Fix external SVG loading with proxies (#802) 2017-12-11 20:51:39 +08:00
d87fef11a4 Fix logging option (#1302) 2017-12-11 20:23:43 +08:00
250208dc99 Add support for rendering webgl canvas content (#646) 2017-12-11 20:17:20 +08:00
2237e8e230 Update wip website 2017-12-10 22:28:34 +08:00
d3c640088c Merge branch 'feature/HTC-0008_LinearGradients' of git://github.com/vnmc/html2canvas into vnmc-feature/HTC-0008_LinearGradients 2017-12-10 16:46:31 +08:00
8fd616aed2 Update wip website 2017-12-10 16:43:34 +08:00
d1e870de88 added support for gradient background size and fixed linear gradient angle when vendor prefix is used 2017-12-09 23:07:27 +01:00
9bc0fb0bd1 Fix __DEV__ value for minified build 2017-12-09 18:00:45 +08:00
d83bc0247a Disable foreignObjectRendering by default (#1295) 2017-12-09 17:51:28 +08:00
13e80cc635 Begin implementing new website 2017-12-09 17:47:25 +08:00
b239937e00 Refactor Font.js 2017-12-09 17:46:32 +08:00
e8a4d775e8 Update docs 2017-12-09 17:46:07 +08:00
a6a3c1bd0f Fix tests and refactor background calculations out from Renderer 2017-12-09 17:45:58 +08:00
850338a76a added support for background-origin: content-box, fixed background-origin related background sizes 2017-12-09 00:12:29 +01:00
63377d47a4 Begin adding documentation 2017-12-07 23:50:30 +08:00
1d1c74a74e Add Github issue templates 2017-12-07 17:30:13 +08:00
ef5c59e26d Fix scroll positions for CanvasRenderer (Fix #1259) 2017-12-07 16:36:09 +08:00
8b653f89bc Prevent generated iframe from getting re-rendered 2017-12-07 16:16:22 +08:00
9db8580b97 Fix data-html2canvas-ignore attribute (Fix #1253) 2017-12-07 16:12:39 +08:00
4a09264103 Update CHANGELOG 2017-12-07 15:47:53 +08:00
b8178e92b4 Add deprecation warning for onrendered (Fix #1290) 2017-12-07 15:47:42 +08:00
166cbba7c2 Fix decimal letter spacing values (#1293) 2017-12-07 15:20:24 +08:00
2c8c604f9a Update safari tests to use v10.1 2017-12-04 19:35:34 +08:00
a967475826 Disable beta firefox tests temporarily 2017-12-03 22:37:56 +08:00
158435761f Fix package version for npm build 2017-12-03 18:22:46 +08:00
0e8a924ea2 Fix formatting 2017-12-03 17:30:52 +08:00
0b4f922405 Add Github release to travis build 2017-12-03 17:24:31 +08:00
7e0b2b5201 Add npm and minified builds 2017-12-03 17:07:10 +08:00
9445b0b598 Inline fonts for ForeignObjectRenderer 2017-10-18 20:34:17 +08:00
f16d581f04 Add foreignObjectRendering option 2017-09-28 19:46:00 +08:00
53dd885279 Implementing cropping and dimension options for rendering (Fix #1230) 2017-09-27 22:14:50 +08:00
ae47d901a1 Add no console.log lint rule 2017-09-25 23:01:59 +08:00
929b9de6e0 Implement proxied cross-origin iframe rendering 2017-09-25 22:53:09 +08:00
57dc7137b2 Fix wrong context for cloned element instanceof check 2017-09-24 15:26:37 +08:00
90a8422938 Implement Iframe rendering 2017-09-11 22:36:23 +08:00
aa47a3a3a6 Add support for loading cross origin images using proxy 2017-09-04 23:36:19 +08:00
c2b7ed9c42 Update link for releases in README (Fix #1203) 2017-09-04 20:36:54 +08:00
0f9f8ff494 Add dev server script 2017-09-03 21:25:06 +08:00
1a53643457 Improve logging 2017-09-03 21:24:17 +08:00
906a66eec7 Use crossOrigin images when useCORS option set 2017-09-03 21:24:06 +08:00
c013e49192 Only use foreignObject rendering if browser is capable of rendering images 2017-09-03 18:20:12 +08:00
c28263ddc2 Update README with build instruction (#1196) 2017-08-30 22:23:26 +08:00
6d9639d0af Use prefixed transform values 2017-08-30 22:20:55 +08:00
badbf52c1c Reduce test output size 2017-08-30 22:20:42 +08:00
23b6f29ecf Capture screenshots while running karma tests 2017-08-30 21:31:51 +08:00
a41ba8852f Set default config as empty object (Fix #1195) 2017-08-30 20:59:18 +08:00
b75fd70042 Add screenshot collecting server 2017-08-28 21:27:39 +08:00
5dbb197a82 Remove redundant style nodes from clone 2017-08-18 22:22:24 +08:00
bd463d9343 Correctly apply image timeout for loading images 2017-08-18 21:53:42 +08:00
c093c95881 Correctly resolve unsupported svg image testing 2017-08-18 21:34:05 +08:00
f79ae2b73a Don't copy styles for regular computed rendering 2017-08-18 20:52:29 +08:00
4f96abfb7b Handle inline svg correctly with foreignObject 2017-08-17 23:32:03 +08:00
26d8d8ea5b Remove reftests for now 2017-08-17 23:30:00 +08:00
a73dbf8067 Implement foreignObject renderer 2017-08-17 23:14:44 +08:00
26cdc0441b Fix formatting 2017-08-16 19:54:48 +08:00
fa4a4a4db5 Add saucelabs browsers 2017-08-16 19:50:14 +08:00
d77301a353 Fix base64 images for ios 10.3 2017-08-16 19:50:05 +08:00
8999c76181 Fix iOS 10.3 base64 image tainting canvas (Fix #1151) 2017-08-13 23:27:03 +08:00
fd1447a6e7 Add sauceconnect launcher 2017-08-13 23:24:39 +08:00
ea080e0f5d Fix recursion for safari 10 on pseudoelements 2017-08-13 21:39:57 +08:00
a101b52685 Render SVG nodes correctly 2017-08-13 17:48:37 +08:00
ae46010476 Merge pull request #1186 from niklasvh/travis-karma
Travis karma tests
2017-08-13 14:41:27 +08:00
05e5d932f0 Add IE and Edge browsers to saucelabs tests 2017-08-13 14:27:30 +08:00
068480f606 Add saucelabs to karma runner 2017-08-13 14:09:43 +08:00
c9fb5d5026 Add Chrome reftests to Travis 2017-08-13 14:02:48 +08:00
f3d6d2fdf4 Don't resolve images immediately if they appear complete 2017-08-13 14:02:35 +08:00
37a9249a4a Don't render SVG nodes if it taints canvas 2017-08-13 13:11:03 +08:00
c765e2042f Don't render 0 sized images 2017-08-13 12:16:48 +08:00
d327327166 Fix pseudoelement image content not being loaded in time 2017-08-11 23:21:28 +08:00
96cde64c6e Normalize more tests 2017-08-11 22:22:39 +08:00
31a9f913ed Allow tests to be ignored by specific browsers 2017-08-11 22:22:10 +08:00
af09280c38 Draw checkboxes as vector path 2017-08-11 22:21:23 +08:00
a1b8cbc2fb Remove profiler 2017-08-11 20:41:05 +08:00
18761b7352 Fix textShadow parsing 2017-08-11 20:40:49 +08:00
ad487f4585 Set overflow hidden for input elements 2017-08-11 20:40:37 +08:00
fb58d1f0b6 Fix incorrect value 2017-08-11 20:40:08 +08:00
42a87b8354 Normalize reftests 2017-08-10 23:26:22 +08:00
97b0a1f21d Fix reftest precision 2017-08-10 23:26:01 +08:00
5bd06895e9 Begin implementing webkit-gradient parsing 2017-08-10 23:25:50 +08:00
eb380f023f Fix zIndex value 2017-08-10 23:24:38 +08:00
1c318ab607 Add ignored reftests 2017-08-10 23:24:26 +08:00
8f575a446d Don't toggle canvas on right-mouse click 2017-08-10 21:59:26 +08:00
5969c95481 Set line number for text renders 2017-08-10 21:59:08 +08:00
82cfcf8704 Round reftest repeat values 2017-08-10 21:58:56 +08:00
c287f51cb6 Fix incorrect render order in Firefox for position: static an z-index value 2017-08-10 21:58:36 +08:00
edebe082f3 Remove animations from reftests 2017-08-09 12:05:16 +08:00
77393074ba Use tree order when z-index is the same 2017-08-09 11:52:42 +08:00
ed92d2354c Fix build 2017-08-09 11:10:40 +08:00
58d1bef3b6 Beging implementing reftests 2017-08-09 00:52:56 +08:00
93f08c7547 Implement RefTestRenderer 2017-08-07 00:26:09 +08:00
a2895691ba Extract render target logic out of renderer to be target agnostic 2017-08-06 20:21:35 +08:00
f7f445c71e Add license info to builds (Fix #1126) 2017-08-06 18:14:27 +08:00
8da77eb689 Add options to define window dimensions 2017-08-06 17:53:38 +08:00
965f850e68 Assign cssText string to same named property on target 2017-08-06 17:50:01 +08:00
216c290c4b Check availability of console before using it (Fix IE9) 2017-08-06 17:37:34 +08:00
68900c3087 Copy CSS properties individual with IE 2017-08-06 17:37:10 +08:00
018ed765ad Update CHANGELOG with changes for v1.0.0-alpha1 2017-08-06 16:05:36 +08:00
f0fdeac703 Fix formatting 2017-08-06 15:52:56 +08:00
6baa847092 Handle undefined values for textShadow/transform (IE9) 2017-08-06 15:44:30 +08:00
10ec079762 Remove direct console call (breaks IE9 when console is not open) 2017-08-06 15:43:53 +08:00
6554d4c8c8 Implement textShadow rendering (Fix #499 and #908) 2017-08-05 23:34:12 +08:00
30a2578f38 Fix pseudo-element css text assignment for Edge 2017-08-05 21:59:48 +08:00
82a7349e43 Use first background repeat value for multiple backgrounds if only 1 available (Edge bug) 2017-08-05 21:41:37 +08:00
0224592a96 Don't parse TEXTAREA child nodes (Edge bug) 2017-08-05 21:40:22 +08:00
12672839f1 Use correct JS context to find cloned element in Edge 2017-08-05 21:40:03 +08:00
9bdb871307 Implement linear-gradient rendering 2017-08-05 21:13:53 +08:00
56b3b6df27 Implement input/textarea/select element rendering 2017-08-05 00:00:17 +08:00
adb1f50f00 Support percentage border-radius values (Fix #1154) 2017-08-04 20:41:36 +08:00
e380e2c873 Use correct JS context to enable use of instanceof 2017-08-04 19:27:35 +08:00
3977ebeadd Log errors in __DEV__ mode (Fix #905) 2017-08-04 00:13:20 +08:00
9a7075252b Fix ImageLoader flow types to reflect possible error'd images 2017-08-04 00:00:02 +08:00
96fbe954e9 Correctly strip quotes from pseudoelement url() 2017-08-03 23:54:44 +08:00
b8450f4d4a Allow image loads to fail without crashing render 2017-08-03 23:54:23 +08:00
b3db735415 Render :before and :after pseudoelements 2017-08-03 23:46:29 +08:00
959b75a441 Update travis build 2017-08-03 22:03:05 +08:00
f6a5153d99 Implement support for multiple text-transforms with independent colors 2017-08-03 21:47:35 +08:00
ad1119a76c Apply border radius correctly on image elements 2017-08-03 21:05:17 +08:00
fe97851988 Implement HTMLCanvasElement rendering 2017-08-03 20:57:55 +08:00
f2b8c16c2c Implement visibility css prop 2017-08-03 20:28:39 +08:00
f278ba4f22 Begin implementing overflow clipping 2017-08-02 21:35:54 +08:00
52a815a13f Fix background-clip and background-origin rendering 2017-08-02 20:29:45 +08:00
213f35f61c Keep scroll position when toggling canvas view 2017-08-02 20:15:39 +08:00
7cc2b856cb Use correct canvas size for full document render 2017-08-01 23:41:12 +08:00
aafb0cfb9c Calculate correct bounds for text/elements under nested transforms 2017-08-01 23:27:12 +08:00
c5135f4839 Assign default options values 2017-08-01 22:51:59 +08:00
478155af64 Clone document before parsing it 2017-08-01 20:54:18 +08:00
7a3bad2fcb Add missing Flow tags 2017-08-01 18:51:59 +08:00
4f48bc9b0c Define browser build target as umd 2017-08-01 18:32:37 +08:00
6f7a3145fe Remove dist folder from git 2017-08-01 18:26:11 +08:00
ba089b4771 Render multiple backgrounds in correct order 2017-08-01 18:25:32 +08:00
9f8bae4b09 Correctly parse multi background-repeat values 2017-08-01 18:25:20 +08:00
f89ba365bd Fix README typos (#779 and #930) 2017-08-01 18:14:37 +08:00
50579f399e Update README and Fix #1140 2017-08-01 00:31:41 +08:00
8a6fb5f733 Library rewrite 2017-08-01 00:25:58 +08:00
83e9b85e1e 0.5.0-beta4 2016-01-23 22:21:21 +02:00
3c7c3ddf60 Update changelog 2016-01-23 22:20:02 +02:00
2ef53b37cc Remove safari browser tests 2016-01-23 22:07:42 +02:00
3b49cba21c Fix rendering of content when window is scrolled 2016-01-23 22:05:43 +02:00
a4aa0c6444 Derequire browserify bundles 2016-01-23 20:53:20 +02:00
4ebe9c5fcc Don't require logger to be exposed to window object 2016-01-23 20:41:53 +02:00
e17bbacd17 0.5.0-beta3 2015-12-06 13:48:42 -05:00
47a7240d6b Re-adding dist/ to version control for now 2015-12-06 13:46:23 -05:00
6539f9d9c3 Fix webdriver tests for node 4.0 2015-12-06 13:32:35 -05:00
3cb0911de3 Update dependencies 2015-12-06 12:01:18 -05:00
144c9a903e Merge pull request #724 from usmonster/adds-editorconfig
Adds .editorconfig
2015-11-11 20:40:21 +02:00
57dd9b5461 Adds .editorconfig
Makes contributing consistently-formatted code a bit easier
2015-11-11 13:21:29 -05:00
6d168f46be Merge pull request #708 from usmonster/fix-firefox-gradients
Linear gradients now parse color names
2015-11-09 19:14:46 +02:00
318ca48157 Linear gradients now parse color names
Also:
- Cleans up color stop and linear gradient regular expressions.
- Handles percentage-based linear gradient positions (fixes Firefox).

Fixes niklasvh/html2canvas#469.
2015-10-25 09:44:14 -04:00
bebb353b3f Merge pull request #703 from niklasvh/travis-fix
Lower travis nodejs version back to 0.10
2015-10-20 23:37:35 +03:00
eb5ac1122c Lower travis nodejs version back to 0.10 2015-10-20 23:16:49 +03:00
ae97dd9a3d Remove npm bower dependency 2015-10-19 23:13:05 +03:00
11fdc501b1 Fix package dependency versions 2015-10-19 02:57:40 +03:00
4df19968b5 0.5.0-beta2 2015-10-19 01:49:34 +03:00
9ab7f8cdb1 Update travis npm deployment 2015-10-19 01:44:08 +03:00
5c5531fd47 Update node version for travis 2015-10-19 01:34:38 +03:00
e88ac871a3 Remove Promise polyfill 2015-10-19 01:25:03 +03:00
2a2ad9bb65 0.5.0-beta1 2015-10-19 01:00:58 +03:00
81e60975cc Update build/deploy workflow 2015-10-19 01:00:04 +03:00
a0669300c4 Update grunt-mocha-phantomjs to 2.0.0 2015-10-18 23:58:10 +03:00
ba9758cf14 Reformat mocha text shadow test names 2015-10-18 23:57:55 +03:00
aa05241ff8 Fix gradient tests 2015-10-18 23:57:41 +03:00
5b4a6c26ee Add another border-radius test 2015-10-18 23:15:51 +03:00
364a8aac1c Merge remote-tracking branch 'origin/pr/645' 2015-10-18 23:15:12 +03:00
46078acf71 Fix #688 fatal exception on unmatched color stop 2015-10-18 16:51:53 +03:00
4b37909f09 Fix support for requirejs/amd 2015-08-30 02:27:38 +03:00
90f9eeba83 Fix #599 2015-06-10 16:13:17 +09:00
98ee30643a Fix iOS scrolling issue on clone 2015-03-30 00:58:50 +03:00
a49c3a2320 Correctly handle named colors in gradients 2015-03-29 23:20:17 +03:00
4b80102e77 Merge pull request #545 from cornedor/ie-transforms
Transform matrix for IE
2015-03-08 01:03:22 +02:00
9201cf7e95 Accept matrix3d and convert it 2015-03-05 15:52:23 +01:00
c2baf42145 Fix fail on transparent colors in linear gradients 2015-03-01 17:43:18 +02:00
d9a9615ed7 Add acid2 test case 2015-03-01 17:12:09 +02:00
585a96a918 Preserve scrolling positions of nodes on clone (#511) 2015-02-28 16:51:52 +02:00
9a0d43d60b Bump v0.5.0-alpha2 2015-02-03 22:11:20 +02:00
3671de81f9 Fix module name casing 2015-02-03 21:39:57 +02:00
f3b6df267e Switch build to use browserify (#502) 2015-02-03 21:34:05 +02:00
60619dca72 Fix #517 2015-01-26 22:55:10 +02:00
ed299b3db1 Remove dead code 2015-01-20 19:51:52 +02:00
c7e484af89 Add npm bower postpublish script 2015-01-19 23:52:50 +02:00
244 changed files with 35052 additions and 38845 deletions

4
.babelrc Normal file
View File

@ -0,0 +1,4 @@
{
"plugins": ["transform-object-rest-spread"],
"presets": ["es2015", "flow"]
}

17
.editorconfig Normal file
View 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
View 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
View File

@ -0,0 +1,7 @@
[ignore]
.*/www/.*
[include]
[libs]
./flow-typed
[options]
[lints]

19
.github/ISSUE_TEMPLATE.md vendored Normal file
View 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
View 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
View 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.

6
.gitignore vendored
View File

@ -1,3 +1,5 @@
/dist
/build
/nbproject/
image.jpg
/.project
@ -9,3 +11,7 @@ node_modules/
*.iml
.idea/
.DS_Store
npm-debug.log
debug.log
tests/reftests.js
*.log

6
.gitmodules vendored
View File

@ -1,6 +0,0 @@
[submodule "src/fabric"]
path = src/fabric
url = https://github.com/kangax/fabric.js.git
[submodule "src/punycode"]
path = src/punycode
url = https://github.com/bestiejs/punycode.js.git

View File

@ -1,20 +0,0 @@
{
"curly": true,
"eqeqeq": true,
"immed": true,
"latedef": false,
"newcap": true,
"noarg": true,
"sub": true,
"undef": true,
"boss": true,
"eqnull": true,
"browser": true,
"node": true,
"indent": 4,
"globals": {
"jQuery": true
},
"predef": ["NodeParser", "NodeContainer", "Color", "StackingContext", "TextContainer", "PseudoElementContainer", "ImageLoader", "CanvasRenderer", "Renderer", "Support", "bind", "Promise", "getBounds", "offsetBounds", "XHR",
"ImageContainer", "ProxyImageContainer", "DummyImageContainer", "Font", "FontMetrics", "GradientContainer", "LinearGradientContainer", "WebkitGradientContainer", "SVGContainer", "SVGNodeContainer", "FrameContainer", "html2canvas", "log", "smallImage", "parseBackgrounds", "loadUrlDocument", "decode64", "Proxy", "ProxyURL"]
}

18
.npmignore Normal file
View 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

View File

@ -1,21 +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:
sauce_connect: true
chrome: stable
firefox: latest
dist: trusty
sudo: false
before_script:
- npm install -g grunt-cli
- npm install -g uglify-js
- export DISPLAY=:99.0
- sh -e /etc/init.d/xvfb start
notifications:
webhooks:
urls:
- https://webhooks.gitter.im/e/2b007d4f86de89588804
- 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-test
fqdn: html2canvas.hertzen.com
github_token:
secure: "PowO/Jat660k3gHcjgI6DlJz15RM7pLUu11UPsLCtYJ8ZwodppE6Keg0DfVkSFSIZttZor+UssDwP/WOEqfZNLqmXbcj3Gec4xolohet/GOe0KJKKuF/HgggbcxumopxMX6sMVePlMBpkLpHh7tgEAEHBWTlzC1c1a7Xa48fZ7k="
on:
branch: master
repo: niklasvh/html2canvas

View File

@ -1,6 +1,62 @@
### Changelog ###
v0.5.0-alpha - 19.1.2015
#### 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.
@ -11,14 +67,14 @@ v0.5.0-alpha - 19.1.2015
* 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
#### 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
#### 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
@ -28,7 +84,7 @@ v0.4.0 - 30.1.2013
* Support for placeholder rendering
* Reformatted all tests to small units to test specific features
v0.3.4 - 26.6.2012
#### v0.3.4 - 26.6.2012 ####
* Removed (last?) jQuery dependencies (<a href="https://github.com/niklasvh/html2canvas/commit/343b86705fe163766fcf735eb0217130e4bd5b17">niklasvh</a>)
* SVG-powered rendering (<a href="https://github.com/niklasvh/html2canvas/commit/67d3e0d0f59a5a654caf71a2e3be6494ff146c75">niklasvh</a>)
@ -36,7 +92,7 @@ v0.3.4 - 26.6.2012
* Split renderers to their own objects (<a href="https://github.com/niklasvh/html2canvas/commit/94f2f799a457cd29a21cc56ef8c06f1697866739">niklasvh</a>)
* Simplified API, cleaned up code (<a href="https://github.com/niklasvh/html2canvas/commit/c7d526c9eaa6a4abf4754d205fe1dee360c7660e">niklasvh</a>)
v0.3.3 - 2.3.2012
#### v0.3.3 - 2.3.2012 ####
* SVG taint fix, and additional taint testing options for rendering (<a href="https://github.com/niklasvh/html2canvas/commit/2dc8b9385e656696cb019d615bdfa1d98b17d5d4">niklasvh</a>)
* Added support for CORS images and option to create canvas as tainted (<a href="https://github.com/niklasvh/html2canvas/commit/3ad49efa0032cde25c6ed32a39e35d1505d3b2ef">niklasvh</a>)
@ -44,7 +100,7 @@ v0.3.3 - 2.3.2012
* Added integrated support for Flashcanvas (<a href="https://github.com/niklasvh/html2canvas/commit/e9257191519f67d74fd5e364d8dee3c0963ba5fc">niklasvh</a>)
* Fixed a variety of legacy IE bugs (<a href="https://github.com/niklasvh/html2canvas/commit/b65357c55d0701017bafcd357bc654b54d458f8f">niklasvh</a>)
v0.3.2 - 20.2.2012
#### v0.3.2 - 20.2.2012 ####
* Added changelog!
* Added bookmarklet (<a href="https://github.com/niklasvh/html2canvas/commit/b320dd306e1a2d32a3bc5a71b6ebf6d8c060cde5">cobexer</a>)

View File

@ -1,221 +0,0 @@
/*global module:false*/
var _ = require('lodash'), path = require('path');
var proxy = require('html2canvas-proxy');
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, exports, global, define, undefined){\n\n',
post: '\n}).call({}, typeof(window) !== "undefined" ? window : undefined, typeof(document) !== "undefined" ? document : undefined);'
};
var browsers = {
chrome: {
browserName: "chrome",
platform: "Windows 7",
version: "39"
},
firefox: {
browserName: "firefox",
version: "15",
platform: "Windows 7"
},
ie9: {
browserName: "internet explorer",
version: "9",
platform: "Windows 7"
},
ie10: {
browserName: "internet explorer",
version: "10",
platform: "Windows 8"
},
ie11: {
browserName: "internet explorer",
version: "11",
platform: "Windows 8.1"
},
safari6: {
browserName: "safari",
version: "6",
platform: "OS X 10.8"
},
safari7:{
browserName: "safari",
platform: "OS X 10.9",
version: "7"
},
chromeOSX:{
browserName: "chrome",
platform: "OS X 10.8",
version: "39"
}
};
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
concat: {
dist: {
src: [
'src/promise.js', 'src/fallback.js', 'src/punycode/punycode.js', 'src/core.js', 'src/*.js', 'src/renderers/*.js'
],
nonull: true,
dest: 'dist/<%= pkg.name %>.js',
options:{
banner: meta.banner + meta.pre,
footer: meta.post
}
},
svg: {
src: [
'src/fabric/dist/fabric.js'
],
dest: 'dist/<%= pkg.name %>.svg.js',
options:{
banner: meta.banner + '\n(function(window, document, exports, undefined){\n\n',
footer: '\n}).call({}, window, document, html2canvas);'
}
}
},
connect: {
server: {
options: {
port: 8080,
base: './',
keepalive: true
}
},
altServer: {
options: {
port: 8083,
base: './'
}
},
cors: {
options: {
port: 8081,
base: './',
middleware: function(connect, options) {
return [
function(req, res, next) {
if (req.url !== '/tests/assets/image2.jpg') {
next();
return;
}
res.setHeader("Access-Control-Allow-Origin", "*");
res.end(require("fs").readFileSync('tests/assets/image2.jpg'));
}
];
}
}
},
proxy: {
options: {
port: 8082,
middleware: function(connect, options) {
return [
function(req, res, next) {
res.jsonp = function(content) {
res.end(req.query.callback + "(" + JSON.stringify(content) + ")");
};
next();
},
proxy()
];
}
}
},
ci: {
options: {
port: 8080,
base: './'
}
}
},
execute: {
fabric: {
options: {
args: ['modules=' + ['text','serialization',
'parser', 'gradient', 'pattern', 'shadow', 'freedrawing',
'image_filters', 'serialization'].join(","), 'no-es5-compat', 'dest=' + path.resolve(__dirname, 'src/fabric/dist/') + '/']
},
src: ['src/fabric/build.js']
}
},
uglify: {
dist: {
src: ['<%= concat.dist.dest %>'],
dest: 'dist/<%= pkg.name %>.min.js'
},
svg: {
src: ['<%= concat.svg.dest %>'],
dest: 'dist/<%= pkg.name %>.svg.min.js'
},
options: {
banner: meta.banner
}
},
watch: {
files: ['src/**/*', '!src/fabric/**/*'],
tasks: ['jshint', 'build']
},
jshint: {
all: ['src/*.js', 'src/renderers/*.js', '!src/promise.js'],
options: grunt.file.readJSON('./.jshintrc')
},
mochacli: {
options: {
reporter: 'spec'
},
all: ['tests/node/*.js']
},
mocha_phantomjs: {
all: ['tests/mocha/**/*.html']
},
mocha_webdriver: browsers,
webdriver: browsers
});
grunt.registerTask('webdriver', 'Browser render tests', function(browser, test) {
var selenium = require("./tests/selenium.js");
var done = this.async();
var browsers = (browser) ? [grunt.config.get(this.name + "." + browser)] : _.values(grunt.config.get(this.name));
selenium.tests(browsers, test).catch(function() {
done(false);
}).finally(function() {
console.log("Done");
done();
});
});
grunt.registerTask('mocha_webdriver', 'Browser mocha tests', function(browser, test) {
var selenium = require("./tests/mocha/selenium.js");
var done = this.async();
var browsers = (browser) ? [grunt.config.get(this.name + "." + browser)] : _.values(grunt.config.get(this.name));
selenium.tests(browsers, test).catch(function() {
done(false);
}).finally(function() {
done();
});
});
grunt.loadNpmTasks('grunt-mocha-phantomjs');
grunt.loadNpmTasks('grunt-contrib-watch');
grunt.loadNpmTasks('grunt-contrib-concat');
grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.loadNpmTasks('grunt-contrib-jshint');
grunt.loadNpmTasks('grunt-contrib-connect');
grunt.loadNpmTasks('grunt-execute');
grunt.loadNpmTasks('grunt-mocha-cli');
grunt.registerTask('server', ['connect:cors', 'connect:proxy', 'connect:altServer', 'connect:server']);
grunt.registerTask('build', ['execute', 'concat', 'uglify']);
grunt.registerTask('default', ['jshint', 'build', 'mochacli', 'connect:altServer', 'mocha_phantomjs']);
grunt.registerTask('travis', ['jshint', 'build', 'connect:altServer', 'connect:ci', 'connect:proxy', 'connect:cors', 'mocha_phantomjs', 'webdriver']);
};

View File

@ -1,7 +1,7 @@
html2canvas
===========
[Homepage](http://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/)
[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/)
[![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/niklasvh/html2canvas?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) [![Build Status](https://travis-ci.org/niklasvh/html2canvas.png)](https://travis-ci.org/niklasvh/html2canvas)
@ -10,17 +10,17 @@ html2canvas
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?###
### 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 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###
### Browser compatibility ###
The script should work fine on the following browsers:
The library should work fine on the following browsers (with `Promise` polyfill):
* Firefox 3.5+
* Google Chrome
@ -32,6 +32,10 @@ As each CSS property needs to be manually built to be supported, there are a num
### 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:
@ -45,41 +49,37 @@ The function returns a [Promise](https://developer.mozilla.org/en-US/docs/Web/Ja
### Building ###
The library uses [grunt](http://gruntjs.com/) for building. Alternatively, you can download the latest build from [here](https://github.com/niklasvh/html2canvas/blob/master/dist/html2canvas.js).
You can download ready builds [here](https://github.com/niklasvh/html2canvas/releases).
Clone git repository with submodules:
Clone git repository:
$ git clone --recursive git://github.com/niklasvh/html2canvas.git
$ git clone git://github.com/niklasvh/html2canvas.git
Install Grunt and uglifyjs:
Install dependencies:
$ npm install -g grunt-cli uglify-js
$ npm install
Run the full build process (including lint, qunit and webdriver tests):
Build browser bundle
$ grunt
Skip lint and tests and simply build from source:
$ grunt build
$ 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 commiting changes, these should generally not go decrease from the baseline values.
The other set of tests run Firefox, Chrome and Internet Explorer with [webdriver](https://github.com/niklasvh/webdriver.js). The selenium standalone server (runs on Java) is required for these tests and can be downloaded from [here](http://code.google.com/p/selenium/downloads/list). They capture an actual screenshot from the test pages and compare the image to the screenshot created by html2canvas and calculate the percentage differences. These tests generally aren't expected to provide 100% matches, but while committing changes, these should generally not go decrease from the baseline values.
Start by downloading the dependencies:
$ npm install
Run qunit tests:
Run tests:
$ grunt test
$ npm 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).
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 ###

View File

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

3375
dist/html2canvas.js vendored

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

17702
dist/html2canvas.svg.js vendored

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

31
docs/configuration.md Normal file
View File

@ -0,0 +1,31 @@
---
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.
| 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

42
docs/documentation.md Normal file
View 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
View 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()).

80
docs/features.md Normal file
View File

@ -0,0 +1,80 @@
---
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 (**Does not support `attr()`**)
- color
- display
- flex
- float
- font
- font-family
- font-size
- font-style
- font-variant
- font-weight
- height
- left
- letter-spacing
- margin
- max-height
- max-width
- min-height
- min-width
- opacity
- overflow
- padding
- position
- right
- text-align
- text-decoration
- text-decoration-color
- text-decoration-line
- text-decoration-style (**Only supports `solid`**)
- text-shadow
- text-transform
- top
- transform (**Limited support**)
- visibility
- white-space
- width
- word-spacing
- z-index
## Unsupported CSS properties
These CSS properties are **NOT** currently supported
- [background-blend-mode](https://github.com/niklasvh/html2canvas/issues/966)
- [border-image](https://github.com/niklasvh/html2canvas/issues/1287)
- [box-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)
- [list-style](https://github.com/niklasvh/html2canvas/issues/177)
- [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
View 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
View 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)

11
flow-typed/myLibDef.js vendored Normal file
View 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
View 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
View 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();
});

View File

@ -1 +0,0 @@
module.exports = require('./dist/html2canvas').html2canvas;

7326
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -2,17 +2,16 @@
"title": "html2canvas",
"name": "html2canvas",
"description": "Screenshots with JavaScript",
"main": "main.js",
"version": "0.5.0-alpha1",
"main": "dist/npm/index.js",
"version": "1.0.0-alpha.4",
"author": {
"name": "Niklas von Hertzen",
"email": "niklasvh@gmail.com",
"url": "http://hertzen.com"
"url": "https://hertzen.com"
},
"engines": {
"node": ">=0.10.0"
"node": ">=4.0.0"
},
"dependencies": {},
"repository": {
"type": "git",
"url": "git@github.com:niklasvh/html2canvas.git"
@ -21,30 +20,63 @@
"url": "https://github.com/niklasvh/html2canvas/issues"
},
"devDependencies": {
"base64-arraybuffer": ">= 0.1.0",
"bluebird": "^2.7.1",
"grunt": "^0.4.5",
"grunt-contrib-concat": "^0.5.0",
"grunt-contrib-connect": "^0.8.0",
"grunt-contrib-jshint": "^0.10.0",
"grunt-contrib-uglify": "^0.6.0",
"grunt-contrib-watch": "^0.6.1",
"grunt-execute": "^0.2.2",
"grunt-mocha-cli": "^1.12.0",
"grunt-mocha-phantomjs": "^0.6.0",
"html2canvas-proxy": "0.0.5",
"humanize-duration": "^2.0.1",
"lodash": "^2.4.1",
"png-js": ">= 0.1.1",
"wd": "^0.2.21"
"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"
}
}

View 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`);
}
);

View 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
View 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
View 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
View 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)
);
}
};

592
src/Clone.js Normal file
View File

@ -0,0 +1,592 @@
/* @flow */
'use strict';
import type {Bounds} from './Bounds';
import type {Options} from './index';
import type Logger from './Logger';
import {parseBounds} from './Bounds';
import {Proxy} from './Proxy';
import ResourceLoader from './ResourceLoader';
import {copyCSSStyles} from './Util';
import {parseBackgroundImage} from './parsing/background';
import CanvasRenderer from './renderer/CanvasRenderer';
const IGNORE_ATTRIBUTE = 'data-html2canvas-ignore';
export class DocumentCloner {
scrolledElements: Array<[HTMLElement, number, number]>;
referenceElement: HTMLElement;
clonedReferenceElement: HTMLElement;
documentElement: HTMLElement;
resourceLoader: ResourceLoader;
logger: Logger;
options: Options;
inlineImages: boolean;
copyStyles: boolean;
renderer: (element: HTMLElement, options: Options, logger: Logger) => Promise<*>;
constructor(
element: HTMLElement,
options: Options,
logger: Logger,
copyInline: boolean,
renderer: (element: HTMLElement, options: Options, logger: Logger) => Promise<*>
) {
this.referenceElement = element;
this.scrolledElements = [];
this.copyStyles = copyInline;
this.inlineImages = copyInline;
this.logger = logger;
this.options = options;
this.renderer = renderer;
this.resourceLoader = new ResourceLoader(options, logger, window);
// $FlowFixMe
this.documentElement = this.cloneNode(element.ownerDocument.documentElement);
}
inlineAllImages(node: ?HTMLElement) {
if (this.inlineImages && node) {
const style = node.style;
Promise.all(
parseBackgroundImage(style.backgroundImage).map(backgroundImage => {
if (backgroundImage.method === 'url') {
return this.resourceLoader
.inlineImage(backgroundImage.args[0])
.then(
img =>
img && typeof img.src === 'string'
? `url("${img.src}")`
: 'none'
)
.catch(e => {
if (__DEV__) {
this.logger.log(`Unable to load image`, e);
}
});
}
return Promise.resolve(
`${backgroundImage.prefix}${backgroundImage.method}(${backgroundImage.args.join(
','
)})`
);
})
).then(backgroundImages => {
if (backgroundImages.length > 1) {
// TODO Multiple backgrounds somehow broken in Chrome
style.backgroundColor = '';
}
style.backgroundImage = backgroundImages.join(',');
});
if (node instanceof HTMLImageElement) {
this.resourceLoader
.inlineImage(node.src)
.then(img => {
if (img && node instanceof HTMLImageElement && node.parentNode) {
const parentNode = node.parentNode;
const clonedChild = copyCSSStyles(node.style, img.cloneNode(false));
parentNode.replaceChild(clonedChild, node);
}
})
.catch(e => {
if (__DEV__) {
this.logger.log(`Unable to load image`, e);
}
});
}
}
}
inlineFonts(document: Document): Promise<void> {
return Promise.all(
Array.from(document.styleSheets).map(sheet => {
if (sheet.href) {
return fetch(sheet.href)
.then(res => res.text())
.then(text => createStyleSheetFontsFromText(text, sheet.href))
.catch(e => {
if (__DEV__) {
this.logger.log(`Unable to load stylesheet`, e);
}
return [];
});
}
return getSheetFonts(sheet, document);
})
)
.then(fonts => fonts.reduce((acc, font) => acc.concat(font), []))
.then(fonts =>
Promise.all(
fonts.map(font =>
fetch(font.formats[0].src)
.then(response => response.blob())
.then(
blob =>
new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onerror = reject;
reader.onload = () => {
// $FlowFixMe
const result: string = reader.result;
resolve(result);
};
reader.readAsDataURL(blob);
})
)
.then(dataUri => {
font.fontFace.setProperty('src', `url("${dataUri}")`);
return `@font-face {${font.fontFace.cssText} `;
})
)
)
)
.then(fontCss => {
const style = document.createElement('style');
style.textContent = fontCss.join('\n');
this.documentElement.appendChild(style);
});
}
createElementClone(node: Node) {
if (this.copyStyles && node instanceof HTMLCanvasElement) {
const img = node.ownerDocument.createElement('img');
try {
img.src = node.toDataURL();
return img;
} catch (e) {
if (__DEV__) {
this.logger.log(`Unable to clone canvas contents, canvas is tainted`);
}
}
}
if (node instanceof HTMLIFrameElement) {
const tempIframe = node.cloneNode(false);
const iframeKey = generateIframeKey();
tempIframe.setAttribute('data-html2canvas-internal-iframe-key', iframeKey);
const {width, height} = parseBounds(node, 0, 0);
this.resourceLoader.cache[iframeKey] = getIframeDocumentElement(node, this.options)
.then(documentElement => {
return this.renderer(
documentElement,
{
async: this.options.async,
allowTaint: this.options.allowTaint,
backgroundColor: '#ffffff',
canvas: null,
imageTimeout: this.options.imageTimeout,
logging: this.options.logging,
proxy: this.options.proxy,
removeContainer: this.options.removeContainer,
scale: this.options.scale,
foreignObjectRendering: this.options.foreignObjectRendering,
target: new CanvasRenderer(),
width,
height,
x: 0,
y: 0,
windowWidth: documentElement.ownerDocument.defaultView.innerWidth,
windowHeight: documentElement.ownerDocument.defaultView.innerHeight,
scrollX: documentElement.ownerDocument.defaultView.pageXOffset,
scrollY: documentElement.ownerDocument.defaultView.pageYOffset
},
this.logger.child(iframeKey)
);
})
.then(
canvas =>
new Promise((resolve, reject) => {
const iframeCanvas = document.createElement('img');
iframeCanvas.onload = () => resolve(canvas);
iframeCanvas.onerror = reject;
iframeCanvas.src = canvas.toDataURL();
if (tempIframe.parentNode) {
tempIframe.parentNode.replaceChild(
copyCSSStyles(
node.ownerDocument.defaultView.getComputedStyle(node),
iframeCanvas
),
tempIframe
);
}
})
);
return tempIframe;
}
return node.cloneNode(false);
}
cloneNode(node: Node): Node {
const clone =
node.nodeType === Node.TEXT_NODE
? document.createTextNode(node.nodeValue)
: this.createElementClone(node);
const window = node.ownerDocument.defaultView;
if (this.referenceElement === node && clone instanceof window.HTMLElement) {
this.clonedReferenceElement = clone;
}
if (clone instanceof window.HTMLBodyElement) {
createPseudoHideStyles(clone);
}
for (let child = node.firstChild; child; child = child.nextSibling) {
if (
child.nodeType !== Node.ELEMENT_NODE ||
// $FlowFixMe
(child.nodeName !== 'SCRIPT' && !child.hasAttribute(IGNORE_ATTRIBUTE))
) {
if (!this.copyStyles || child.nodeName !== 'STYLE') {
clone.appendChild(this.cloneNode(child));
}
}
}
if (node instanceof window.HTMLElement && clone instanceof window.HTMLElement) {
this.inlineAllImages(inlinePseudoElement(node, clone, PSEUDO_BEFORE));
this.inlineAllImages(inlinePseudoElement(node, clone, PSEUDO_AFTER));
if (this.copyStyles && !(node instanceof HTMLIFrameElement)) {
copyCSSStyles(node.ownerDocument.defaultView.getComputedStyle(node), clone);
}
this.inlineAllImages(clone);
if (node.scrollTop !== 0 || node.scrollLeft !== 0) {
this.scrolledElements.push([clone, node.scrollLeft, node.scrollTop]);
}
switch (node.nodeName) {
case 'CANVAS':
if (!this.copyStyles) {
cloneCanvasContents(node, clone);
}
break;
case 'TEXTAREA':
case 'SELECT':
clone.value = node.value;
break;
}
}
return clone;
}
}
type Font = {
src: string,
format: string
};
type FontFamily = {
formats: Array<Font>,
fontFace: CSSStyleDeclaration
};
const getSheetFonts = (sheet: StyleSheet, document: Document): Array<FontFamily> => {
// $FlowFixMe
return (sheet.cssRules ? Array.from(sheet.cssRules) : [])
.filter(rule => rule.type === CSSRule.FONT_FACE_RULE)
.map(rule => {
const src = parseBackgroundImage(rule.style.getPropertyValue('src'));
const formats = [];
for (let i = 0; i < src.length; i++) {
if (src[i].method === 'url' && src[i + 1] && src[i + 1].method === 'format') {
const a = document.createElement('a');
a.href = src[i].args[0];
if (document.body) {
document.body.appendChild(a);
}
const font = {
src: a.href,
format: src[i + 1].args[0]
};
formats.push(font);
}
}
return {
// TODO select correct format for browser),
formats: formats.filter(font => /^woff/i.test(font.format)),
fontFace: rule.style
};
})
.filter(font => font.formats.length);
};
const createStyleSheetFontsFromText = (text: string, baseHref: string): Array<FontFamily> => {
const doc = document.implementation.createHTMLDocument('');
const base = document.createElement('base');
// $FlowFixMe
base.href = baseHref;
const style = document.createElement('style');
style.textContent = text;
if (doc.head) {
doc.head.appendChild(base);
}
if (doc.body) {
doc.body.appendChild(style);
}
return style.sheet ? getSheetFonts(style.sheet, doc) : [];
};
const restoreOwnerScroll = (ownerDocument: Document, x: number, y: number) => {
if (
ownerDocument.defaultView &&
(x !== ownerDocument.defaultView.pageXOffset || y !== ownerDocument.defaultView.pageYOffset)
) {
ownerDocument.defaultView.scrollTo(x, y);
}
};
const cloneCanvasContents = (canvas: HTMLCanvasElement, clonedCanvas: HTMLCanvasElement) => {
try {
if (clonedCanvas) {
clonedCanvas.width = canvas.width;
clonedCanvas.height = canvas.height;
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,
pseudoElt: ':before' | ':after'
): ?HTMLElement => {
const style = node.ownerDocument.defaultView.getComputedStyle(node, pseudoElt);
if (
!style ||
!style.content ||
style.content === 'none' ||
style.content === '-moz-alt-content' ||
style.display === 'none'
) {
return;
}
const content = stripQuotes(style.content);
const image = content.match(URL_REGEXP);
const anonymousReplacedElement = clone.ownerDocument.createElement(
image ? 'img' : 'html2canvaspseudoelement'
);
if (image) {
// $FlowFixMe
anonymousReplacedElement.src = stripQuotes(image[1]);
} else {
anonymousReplacedElement.textContent = content;
}
copyCSSStyles(style, anonymousReplacedElement);
anonymousReplacedElement.className = `${PSEUDO_HIDE_ELEMENT_CLASS_BEFORE} ${PSEUDO_HIDE_ELEMENT_CLASS_AFTER}`;
clone.className +=
pseudoElt === PSEUDO_BEFORE
? ` ${PSEUDO_HIDE_ELEMENT_CLASS_BEFORE}`
: ` ${PSEUDO_HIDE_ELEMENT_CLASS_AFTER}`;
if (pseudoElt === PSEUDO_BEFORE) {
clone.insertBefore(anonymousReplacedElement, clone.firstChild);
} else {
clone.appendChild(anonymousReplacedElement);
}
return anonymousReplacedElement;
};
const stripQuotes = (content: string): string => {
const first = content.substr(0, 1);
return first === content.substr(content.length - 1) && first.match(/['"]/)
? content.substr(1, content.length - 2)
: content;
};
const URL_REGEXP = /^url\((.+)\)$/i;
const PSEUDO_BEFORE = ':before';
const PSEUDO_AFTER = ':after';
const PSEUDO_HIDE_ELEMENT_CLASS_BEFORE = '___html2canvas___pseudoelement_before';
const PSEUDO_HIDE_ELEMENT_CLASS_AFTER = '___html2canvas___pseudoelement_after';
const PSEUDO_HIDE_ELEMENT_STYLE = `{
content: "" !important;
display: none !important;
}`;
const createPseudoHideStyles = (body: HTMLElement) => {
createStyles(
body,
`.${PSEUDO_HIDE_ELEMENT_CLASS_BEFORE}${PSEUDO_BEFORE}${PSEUDO_HIDE_ELEMENT_STYLE}
.${PSEUDO_HIDE_ELEMENT_CLASS_AFTER}${PSEUDO_AFTER}${PSEUDO_HIDE_ELEMENT_STYLE}`
);
};
const createStyles = (body: HTMLElement, styles) => {
const style = body.ownerDocument.createElement('style');
style.innerHTML = styles;
body.appendChild(style);
};
const initNode = ([element, x, y]: [HTMLElement, number, number]) => {
element.scrollLeft = x;
element.scrollTop = y;
};
const generateIframeKey = (): string =>
Math.ceil(Date.now() + Math.random() * 10000000).toString(16);
const DATA_URI_REGEXP = /^data:text\/(.+);(base64)?,(.*)$/i;
const getIframeDocumentElement = (
node: HTMLIFrameElement,
options: Options
): Promise<HTMLElement> => {
try {
return Promise.resolve(node.contentWindow.document.documentElement);
} catch (e) {
return options.proxy
? Proxy(node.src, options)
.then(html => {
const match = html.match(DATA_URI_REGEXP);
if (!match) {
return Promise.reject();
}
return match[2] === 'base64'
? window.atob(decodeURIComponent(match[3]))
: decodeURIComponent(match[3]);
})
.then(html =>
createIframeContainer(
node.ownerDocument,
parseBounds(node, 0, 0)
).then(cloneIframeContainer => {
const cloneWindow = cloneIframeContainer.contentWindow;
const documentClone = cloneWindow.document;
documentClone.open();
documentClone.write(html);
const iframeLoad = iframeLoader(cloneIframeContainer).then(
() => documentClone.documentElement
);
documentClone.close();
return iframeLoad;
})
)
: Promise.reject();
}
};
const createIframeContainer = (
ownerDocument: Document,
bounds: Bounds
): Promise<HTMLIFrameElement> => {
const cloneIframeContainer = ownerDocument.createElement('iframe');
cloneIframeContainer.className = 'html2canvas-container';
cloneIframeContainer.style.visibility = 'hidden';
cloneIframeContainer.style.position = 'fixed';
cloneIframeContainer.style.left = '-10000px';
cloneIframeContainer.style.top = '0px';
cloneIframeContainer.style.border = '0';
cloneIframeContainer.width = bounds.width.toString();
cloneIframeContainer.height = bounds.height.toString();
cloneIframeContainer.scrolling = 'no'; // ios won't scroll without it
cloneIframeContainer.setAttribute(IGNORE_ATTRIBUTE, 'true');
if (!ownerDocument.body) {
return Promise.reject(
__DEV__ ? `Body element not found in Document that is getting rendered` : ''
);
}
ownerDocument.body.appendChild(cloneIframeContainer);
return Promise.resolve(cloneIframeContainer);
};
const iframeLoader = (cloneIframeContainer: HTMLIFrameElement): Promise<HTMLIFrameElement> => {
const cloneWindow = cloneIframeContainer.contentWindow;
const documentClone = cloneWindow.document;
return new Promise((resolve, reject) => {
cloneWindow.onload = cloneIframeContainer.onload = documentClone.onreadystatechange = () => {
const interval = setInterval(() => {
if (
documentClone.body.childNodes.length > 0 &&
documentClone.readyState === 'complete'
) {
clearInterval(interval);
resolve(cloneIframeContainer);
}
}, 50);
};
});
};
export const cloneWindow = (
ownerDocument: Document,
bounds: Bounds,
referenceElement: HTMLElement,
options: Options,
logger: Logger,
renderer: (element: HTMLElement, options: Options, logger: Logger) => Promise<*>
): Promise<[HTMLIFrameElement, HTMLElement, ResourceLoader]> => {
const cloner = new DocumentCloner(referenceElement, options, logger, false, renderer);
const scrollX = ownerDocument.defaultView.pageXOffset;
const scrollY = ownerDocument.defaultView.pageYOffset;
return createIframeContainer(ownerDocument, bounds).then(cloneIframeContainer => {
const cloneWindow = cloneIframeContainer.contentWindow;
const documentClone = cloneWindow.document;
/* Chrome doesn't detect relative background-images assigned in inline <style> sheets when fetched through getComputedStyle
if window url is about:blank, we can assign the url to current by writing onto the document
*/
const iframeLoad = iframeLoader(cloneIframeContainer).then(() => {
cloner.scrolledElements.forEach(initNode);
cloneWindow.scrollTo(bounds.left, bounds.top);
if (
/(iPad|iPhone|iPod)/g.test(navigator.userAgent) &&
(cloneWindow.scrollY !== bounds.top || cloneWindow.scrollX !== bounds.left)
) {
documentClone.documentElement.style.top = -bounds.top + 'px';
documentClone.documentElement.style.left = -bounds.left + 'px';
documentClone.documentElement.style.position = 'absolute';
}
return cloner.clonedReferenceElement instanceof cloneWindow.HTMLElement ||
cloner.clonedReferenceElement instanceof ownerDocument.defaultView.HTMLElement ||
cloner.clonedReferenceElement instanceof HTMLElement
? Promise.resolve([
cloneIframeContainer,
cloner.clonedReferenceElement,
cloner.resourceLoader
])
: Promise.reject(
__DEV__
? `Error finding the ${referenceElement.nodeName} in the cloned document`
: ''
);
});
documentClone.open();
documentClone.write('<!DOCTYPE html><html></html>');
// Chrome scrolls the parent document for some reason after the write to the cloned window???
restoreOwnerScroll(referenceElement.ownerDocument, scrollX, scrollY);
documentClone.replaceChild(
documentClone.adoptNode(cloner.documentElement),
documentClone.documentElement
);
documentClone.close();
return iframeLoad;
});
};

251
src/Color.js Normal file
View 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]);

179
src/Feature.js Normal file
View File

@ -0,0 +1,179 @@
/* @flow */
'use strict';
import {createForeignObjectSVG, loadSerializedSVG} from './renderer/ForeignObjectRenderer';
const testRangeBounds = document => {
const TEST_HEIGHT = 123;
if (document.createRange) {
const range = document.createRange();
if (range.getBoundingClientRect) {
const testElement = document.createElement('boundtest');
testElement.style.height = `${TEST_HEIGHT}px`;
testElement.style.display = 'block';
document.body.appendChild(testElement);
range.selectNode(testElement);
const rangeBounds = range.getBoundingClientRect();
const rangeHeight = Math.round(rangeBounds.height);
document.body.removeChild(testElement);
if (rangeHeight === TEST_HEIGHT) {
return true;
}
}
}
return false;
};
// iOS 10.3 taints canvas with base64 images unless crossOrigin = 'anonymous'
const testBase64 = (document: Document, src: string): Promise<boolean> => {
const img = new Image();
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
return new Promise(resolve => {
// Single pixel base64 image renders fine on iOS 10.3???
img.src = src;
const onload = () => {
try {
ctx.drawImage(img, 0, 0);
canvas.toDataURL();
} catch (e) {
return resolve(false);
}
return resolve(true);
};
img.onload = onload;
img.onerror = () => resolve(false);
if (img.complete === true) {
setTimeout(() => {
onload();
}, 500);
}
});
};
const testCORS = () => typeof new Image().crossOrigin !== 'undefined';
const testResponseType = () => typeof new XMLHttpRequest().responseType === 'string';
const testSVG = document => {
const img = new Image();
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
img.src = `data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg'></svg>`;
try {
ctx.drawImage(img, 0, 0);
canvas.toDataURL();
} catch (e) {
return false;
}
return true;
};
const isGreenPixel = data => data[0] === 0 && data[1] === 255 && data[2] === 0 && data[3] === 255;
const testForeignObject = document => {
const canvas = document.createElement('canvas');
const size = 100;
canvas.width = size;
canvas.height = size;
const ctx = canvas.getContext('2d');
ctx.fillStyle = 'rgb(0, 255, 0)';
ctx.fillRect(0, 0, size, size);
const img = new Image();
const greenImageSrc = canvas.toDataURL();
img.src = greenImageSrc;
const svg = createForeignObjectSVG(size, size, 0, 0, img);
ctx.fillStyle = 'red';
ctx.fillRect(0, 0, size, size);
return loadSerializedSVG(svg)
.then(img => {
ctx.drawImage(img, 0, 0);
const data = ctx.getImageData(0, 0, size, size).data;
ctx.fillStyle = 'red';
ctx.fillRect(0, 0, size, size);
const node = document.createElement('div');
node.style.backgroundImage = `url(${greenImageSrc})`;
node.style.height = `${size}px`;
// Firefox 55 does not render inline <img /> tags
return isGreenPixel(data)
? loadSerializedSVG(createForeignObjectSVG(size, size, 0, 0, node))
: Promise.reject(false);
})
.then(img => {
ctx.drawImage(img, 0, 0);
// Edge does not render background-images
return isGreenPixel(ctx.getImageData(0, 0, size, size).data);
})
.catch(e => false);
};
const FEATURES = {
// $FlowFixMe - get/set properties not yet supported
get SUPPORT_RANGE_BOUNDS() {
'use strict';
const value = testRangeBounds(document);
Object.defineProperty(FEATURES, 'SUPPORT_RANGE_BOUNDS', {value});
return value;
},
// $FlowFixMe - get/set properties not yet supported
get SUPPORT_SVG_DRAWING() {
'use strict';
const value = testSVG(document);
Object.defineProperty(FEATURES, 'SUPPORT_SVG_DRAWING', {value});
return value;
},
// $FlowFixMe - get/set properties not yet supported
get SUPPORT_BASE64_DRAWING() {
'use strict';
return (src: string) => {
const value = testBase64(document, src);
Object.defineProperty(FEATURES, 'SUPPORT_BASE64_DRAWING', {value: () => value});
return value;
};
},
// $FlowFixMe - get/set properties not yet supported
get SUPPORT_FOREIGNOBJECT_DRAWING() {
'use strict';
const value =
typeof Array.from === 'function' && typeof window.fetch === 'function'
? testForeignObject(document)
: Promise.resolve(false);
Object.defineProperty(FEATURES, 'SUPPORT_FOREIGNOBJECT_DRAWING', {value});
return value;
},
// $FlowFixMe - get/set properties not yet supported
get SUPPORT_CORS_IMAGES() {
'use strict';
const value = testCORS();
Object.defineProperty(FEATURES, 'SUPPORT_CORS_IMAGES', {value});
return value;
},
// $FlowFixMe - get/set properties not yet supported
get SUPPORT_RESPONSE_TYPE() {
'use strict';
const value = testResponseType();
Object.defineProperty(FEATURES, 'SUPPORT_RESPONSE_TYPE', {value});
return value;
},
// $FlowFixMe - get/set properties not yet supported
get SUPPORT_CORS_XHR() {
'use strict';
const value = 'withCredentials' in new XMLHttpRequest();
Object.defineProperty(FEATURES, 'SUPPORT_CORS_XHR', {value});
return value;
}
};
export default FEATURES;

73
src/Font.js Normal file
View File

@ -0,0 +1,73 @@
/* @flow */
'use strict';
import type {Font} from './parsing/font';
const SAMPLE_TEXT = 'Hidden Text';
import {SMALL_IMAGE} from './Util';
export class FontMetrics {
_data: {};
_document: Document;
constructor(document: Document) {
this._data = {};
this._document = document;
}
_parseMetrics(font: Font) {
const container = this._document.createElement('div');
const img = this._document.createElement('img');
const span = this._document.createElement('span');
const body = this._document.body;
if (!body) {
throw new Error(__DEV__ ? 'No document found for font metrics' : '');
}
container.style.visibility = 'hidden';
container.style.fontFamily = font.fontFamily;
container.style.fontSize = font.fontSize;
container.style.margin = '0';
container.style.padding = '0';
body.appendChild(container);
img.src = SMALL_IMAGE;
img.width = 1;
img.height = 1;
img.style.margin = '0';
img.style.padding = '0';
img.style.verticalAlign = 'baseline';
span.style.fontFamily = font.fontFamily;
span.style.fontSize = font.fontSize;
span.style.margin = '0';
span.style.padding = '0';
span.appendChild(this._document.createTextNode(SAMPLE_TEXT));
container.appendChild(span);
container.appendChild(img);
const baseline = img.offsetTop - span.offsetTop + 2;
container.removeChild(span);
container.appendChild(this._document.createTextNode(SAMPLE_TEXT));
container.style.lineHeight = 'normal';
img.style.verticalAlign = 'super';
const middle = img.offsetTop - container.offsetTop + 2;
body.removeChild(container);
return {baseline, middle};
}
getMetrics(font: Font) {
const key = `${font.fontFamily} ${font.fontSize}`;
if (this._data[key] === undefined) {
this._data[key] = this._parseMetrics(font);
}
return this._data[key];
}
}

558
src/Gradient.js Normal file
View 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 gradients 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 gradients 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
View 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
View 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');
}
};

48
src/Logger.js Normal file
View 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))
);
}
}
}

254
src/NodeContainer.js Normal file
View File

@ -0,0 +1,254 @@
/* @flow */
'use strict';
import type {Background} from './parsing/background';
import type {Border} from './parsing/border';
import type {BorderRadius} from './parsing/borderRadius';
import type {DisplayBit} from './parsing/display';
import type {Float} from './parsing/float';
import type {Font} from './parsing/font';
import type {Overflow} from './parsing/overflow';
import type {Padding} from './parsing/padding';
import type {Position} from './parsing/position';
import type {TextShadow} from './parsing/textShadow';
import type {TextTransform} from './parsing/textTransform';
import type {TextDecoration} from './parsing/textDecoration';
import type {Transform} from './parsing/transform';
import type {Visibility} from './parsing/visibility';
import type {zIndex} from './parsing/zIndex';
import type {Bounds, BoundCurves} from './Bounds';
import type ResourceLoader, {ImageElement} from './ResourceLoader';
import type {Path} from './drawing/Path';
import type TextContainer from './TextContainer';
import Color from './Color';
import {contains} from './Util';
import {parseBackground} from './parsing/background';
import {parseBorder} from './parsing/border';
import {parseBorderRadius} from './parsing/borderRadius';
import {parseDisplay, DISPLAY} from './parsing/display';
import {parseCSSFloat, FLOAT} from './parsing/float';
import {parseFont} from './parsing/font';
import {parseLetterSpacing} from './parsing/letterSpacing';
import {parseOverflow, OVERFLOW} from './parsing/overflow';
import {parsePadding} from './parsing/padding';
import {parsePosition, POSITION} from './parsing/position';
import {parseTextDecoration} from './parsing/textDecoration';
import {parseTextShadow} from './parsing/textShadow';
import {parseTextTransform} from './parsing/textTransform';
import {parseTransform} from './parsing/transform';
import {parseVisibility, VISIBILITY} from './parsing/visibility';
import {parseZIndex} from './parsing/zIndex';
import {parseBounds, parseBoundCurves, calculatePaddingBoxPath} from './Bounds';
import {
INPUT_BACKGROUND,
INPUT_BORDERS,
INPUT_COLOR,
getInputBorderRadius,
reformatInputBounds
} from './Input';
type StyleDeclaration = {
background: Background,
border: Array<Border>,
borderRadius: Array<BorderRadius>,
color: Color,
display: DisplayBit,
float: Float,
font: Font,
letterSpacing: number,
opacity: number,
overflow: Overflow,
padding: Padding,
position: Position,
textDecoration: TextDecoration | null,
textShadow: Array<TextShadow> | null,
textTransform: TextTransform,
transform: Transform,
visibility: Visibility,
zIndex: zIndex
};
const INPUT_TAGS = ['INPUT', 'TEXTAREA', 'SELECT'];
export default class NodeContainer {
name: ?string;
parent: ?NodeContainer;
style: StyleDeclaration;
childNodes: Array<TextContainer | Path>;
bounds: Bounds;
curvedBounds: BoundCurves;
image: ?string;
index: number;
constructor(
node: HTMLElement | SVGSVGElement,
parent: ?NodeContainer,
resourceLoader: ResourceLoader,
index: number
) {
this.parent = parent;
this.index = index;
this.childNodes = [];
const defaultView = node.ownerDocument.defaultView;
const scrollX = defaultView.pageXOffset;
const scrollY = defaultView.pageYOffset;
const style = defaultView.getComputedStyle(node, null);
const display = parseDisplay(style.display);
const IS_INPUT = node.type === 'radio' || node.type === 'checkbox';
const position = parsePosition(style.position);
this.style = {
background: IS_INPUT ? INPUT_BACKGROUND : parseBackground(style, resourceLoader),
border: IS_INPUT ? INPUT_BORDERS : parseBorder(style),
borderRadius:
(node instanceof defaultView.HTMLInputElement ||
node instanceof HTMLInputElement) &&
IS_INPUT
? getInputBorderRadius(node)
: parseBorderRadius(style),
color: IS_INPUT ? INPUT_COLOR : new Color(style.color),
display: display,
float: parseCSSFloat(style.float),
font: parseFont(style),
letterSpacing: parseLetterSpacing(style.letterSpacing),
opacity: parseFloat(style.opacity),
overflow:
INPUT_TAGS.indexOf(node.tagName) === -1
? parseOverflow(style.overflow)
: OVERFLOW.HIDDEN,
padding: parsePadding(style),
position: position,
textDecoration: parseTextDecoration(style),
textShadow: parseTextShadow(style.textShadow),
textTransform: parseTextTransform(style.textTransform),
transform: parseTransform(style),
visibility: parseVisibility(style.visibility),
zIndex: parseZIndex(position !== POSITION.STATIC ? style.zIndex : 'auto')
};
if (this.isTransformed()) {
// getBoundingClientRect provides values post-transform, we want them without the transformation
node.style.transform = 'matrix(1,0,0,1,0,0)';
}
// TODO move bound retrieval for all nodes to a later stage?
if (node.tagName === 'IMG') {
node.addEventListener('load', () => {
this.bounds = parseBounds(node, scrollX, scrollY);
this.curvedBounds = parseBoundCurves(
this.bounds,
this.style.border,
this.style.borderRadius
);
});
}
this.image = getImage(node, resourceLoader);
this.bounds = IS_INPUT
? reformatInputBounds(parseBounds(node, scrollX, scrollY))
: parseBounds(node, scrollX, scrollY);
this.curvedBounds = parseBoundCurves(
this.bounds,
this.style.border,
this.style.borderRadius
);
if (__DEV__) {
this.name = `${node.tagName.toLowerCase()}${node.id
? `#${node.id}`
: ''}${node.className
.toString()
.split(' ')
.map(s => (s.length ? `.${s}` : ''))
.join('')}`;
}
}
getClipPaths(): Array<Path> {
const parentClips = this.parent ? this.parent.getClipPaths() : [];
const isClipped =
this.style.overflow === OVERFLOW.HIDDEN || this.style.overflow === OVERFLOW.SCROLL;
return isClipped
? parentClips.concat([calculatePaddingBoxPath(this.curvedBounds)])
: parentClips;
}
isInFlow(): boolean {
return this.isRootElement() && !this.isFloating() && !this.isAbsolutelyPositioned();
}
isVisible(): boolean {
return (
!contains(this.style.display, DISPLAY.NONE) &&
this.style.opacity > 0 &&
this.style.visibility === VISIBILITY.VISIBLE
);
}
isAbsolutelyPositioned(): boolean {
return this.style.position !== POSITION.STATIC && this.style.position !== POSITION.RELATIVE;
}
isPositioned(): boolean {
return this.style.position !== POSITION.STATIC;
}
isFloating(): boolean {
return this.style.float !== FLOAT.NONE;
}
isRootElement(): boolean {
return this.parent === null;
}
isTransformed(): boolean {
return this.style.transform !== null;
}
isPositionedWithZIndex(): boolean {
return this.isPositioned() && !this.style.zIndex.auto;
}
isInlineLevel(): boolean {
return (
contains(this.style.display, DISPLAY.INLINE) ||
contains(this.style.display, DISPLAY.INLINE_BLOCK) ||
contains(this.style.display, DISPLAY.INLINE_FLEX) ||
contains(this.style.display, DISPLAY.INLINE_GRID) ||
contains(this.style.display, DISPLAY.INLINE_LIST_ITEM) ||
contains(this.style.display, DISPLAY.INLINE_TABLE)
);
}
isInlineBlockOrInlineTable(): boolean {
return (
contains(this.style.display, DISPLAY.INLINE_BLOCK) ||
contains(this.style.display, DISPLAY.INLINE_TABLE)
);
}
}
const getImage = (node: HTMLElement | SVGSVGElement, resourceLoader: ResourceLoader): ?string => {
if (
node instanceof node.ownerDocument.defaultView.SVGSVGElement ||
node instanceof SVGSVGElement
) {
const s = new XMLSerializer();
return resourceLoader.loadImage(
`data:image/svg+xml,${encodeURIComponent(s.serializeToString(node))}`
);
}
switch (node.tagName) {
case 'IMG':
// $FlowFixMe
const img: HTMLImageElement = node;
return resourceLoader.loadImage(img.currentSrc || img.src);
case 'CANVAS':
// $FlowFixMe
const canvas: HTMLCanvasElement = node;
return resourceLoader.loadCanvas(canvas);
case 'IFRAME':
const iframeKey = node.getAttribute('data-html2canvas-internal-iframe-key');
if (iframeKey) {
return iframeKey;
}
break;
}
return null;
};

158
src/NodeParser.js Normal file
View File

@ -0,0 +1,158 @@
/* @flow */
'use strict';
import type ResourceLoader, {ImageElement} from './ResourceLoader';
import type Logger from './Logger';
import StackingContext from './StackingContext';
import NodeContainer from './NodeContainer';
import TextContainer from './TextContainer';
import {inlineInputElement, inlineTextAreaElement, inlineSelectElement} from './Input';
export const NodeParser = (
node: HTMLElement,
resourceLoader: ResourceLoader,
logger: Logger
): StackingContext => {
if (__DEV__) {
logger.log(`Starting node parsing`);
}
let index = 0;
const container = new NodeContainer(node, null, resourceLoader, index++);
const stack = new StackingContext(container, null, true);
parseNodeTree(node, container, stack, resourceLoader, index);
if (__DEV__) {
logger.log(`Finished parsing node tree`);
}
return stack;
};
const IGNORED_NODE_NAMES = ['SCRIPT', 'HEAD', 'TITLE', 'OBJECT', 'BR', 'OPTION'];
const parseNodeTree = (
node: HTMLElement,
parent: NodeContainer,
stack: StackingContext,
resourceLoader: ResourceLoader,
index: number
): void => {
if (__DEV__ && index > 50000) {
throw new Error(`Recursion error while parsing node tree`);
}
for (let childNode = node.firstChild, nextNode; childNode; childNode = nextNode) {
nextNode = childNode.nextSibling;
const defaultView = childNode.ownerDocument.defaultView;
if (
childNode instanceof defaultView.Text ||
childNode instanceof Text ||
(defaultView.parent && childNode instanceof defaultView.parent.Text)
) {
if (childNode.data.trim().length > 0) {
parent.childNodes.push(TextContainer.fromTextNode(childNode, parent));
}
} else if (
childNode instanceof defaultView.HTMLElement ||
childNode instanceof HTMLElement ||
(defaultView.parent && childNode instanceof defaultView.parent.HTMLElement)
) {
if (IGNORED_NODE_NAMES.indexOf(childNode.nodeName) === -1) {
const container = new NodeContainer(childNode, parent, resourceLoader, index++);
if (container.isVisible()) {
if (childNode.tagName === 'INPUT') {
// $FlowFixMe
inlineInputElement(childNode, container);
} else if (childNode.tagName === 'TEXTAREA') {
// $FlowFixMe
inlineTextAreaElement(childNode, container);
} else if (childNode.tagName === 'SELECT') {
// $FlowFixMe
inlineSelectElement(childNode, container);
}
const SHOULD_TRAVERSE_CHILDREN = childNode.tagName !== 'TEXTAREA';
const treatAsRealStackingContext = createsRealStackingContext(
container,
childNode
);
if (treatAsRealStackingContext || createsStackingContext(container)) {
// for treatAsRealStackingContext:false, any positioned descendants and descendants
// which actually create a new stacking context should be considered part of the parent stacking context
const parentStack =
treatAsRealStackingContext || container.isPositioned()
? stack.getRealParentStackingContext()
: stack;
const childStack = new StackingContext(
container,
parentStack,
treatAsRealStackingContext
);
parentStack.contexts.push(childStack);
if (SHOULD_TRAVERSE_CHILDREN) {
parseNodeTree(childNode, container, childStack, resourceLoader, index);
}
} else {
stack.children.push(container);
if (SHOULD_TRAVERSE_CHILDREN) {
parseNodeTree(childNode, container, stack, resourceLoader, index);
}
}
}
}
} else if (
childNode instanceof defaultView.SVGSVGElement ||
childNode instanceof SVGSVGElement ||
(defaultView.parent && childNode instanceof defaultView.parent.SVGSVGElement)
) {
const container = new NodeContainer(childNode, parent, resourceLoader, index++);
const treatAsRealStackingContext = createsRealStackingContext(container, childNode);
if (treatAsRealStackingContext || createsStackingContext(container)) {
// for treatAsRealStackingContext:false, any positioned descendants and descendants
// which actually create a new stacking context should be considered part of the parent stacking context
const parentStack =
treatAsRealStackingContext || container.isPositioned()
? stack.getRealParentStackingContext()
: stack;
const childStack = new StackingContext(
container,
parentStack,
treatAsRealStackingContext
);
parentStack.contexts.push(childStack);
} else {
stack.children.push(container);
}
}
}
};
const createsRealStackingContext = (
container: NodeContainer,
node: HTMLElement | SVGSVGElement
): boolean => {
return (
container.isRootElement() ||
container.isPositionedWithZIndex() ||
container.style.opacity < 1 ||
container.isTransformed() ||
isBodyWithTransparentRoot(container, node)
);
};
const createsStackingContext = (container: NodeContainer): boolean => {
return container.isPositioned() || container.isFloating();
};
const isBodyWithTransparentRoot = (
container: NodeContainer,
node: HTMLElement | SVGSVGElement
): boolean => {
return (
node.nodeName === 'BODY' &&
container.parent instanceof NodeContainer &&
container.parent.style.background.backgroundColor.isTransparent()
);
};

62
src/Proxy.js Normal file
View 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();
});
};

445
src/Renderer.js Normal file
View File

@ -0,0 +1,445 @@
/* @flow */
'use strict';
import type Color from './Color';
import type {Path} from './drawing/Path';
import type Size from './drawing/Size';
import type Logger from './Logger';
import type {BackgroundImage} from './parsing/background';
import type {Border, BorderSide} from './parsing/border';
import type {Font} from './parsing/font';
import type {TextDecoration} from './parsing/textDecoration';
import type {TextShadow} from './parsing/textShadow';
import type {Matrix} from './parsing/transform';
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';
import {Bounds, parsePathForBorder, calculateContentBox, calculatePaddingBoxPath} from './Bounds';
import {FontMetrics} from './Font';
import {parseGradient, GRADIENT_TYPE} from './Gradient';
import TextContainer from './TextContainer';
import {
calculateBackgroungPositioningArea,
calculateBackgroungPaintingArea,
calculateBackgroundPosition,
calculateBackgroundRepeatPath,
calculateBackgroundSize,
calculateGradientBackgroundSize
} from './parsing/background';
import {BORDER_STYLE} from './parsing/border';
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
View 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
View File

@ -0,0 +1,37 @@
/* @flow */
'use strict';
import NodeContainer from './NodeContainer';
import {POSITION} from './parsing/position';
export default class StackingContext {
container: NodeContainer;
parent: ?StackingContext;
contexts: Array<StackingContext>;
children: Array<NodeContainer>;
treatAsRealStackingContext: boolean;
constructor(
container: NodeContainer,
parent: ?StackingContext,
treatAsRealStackingContext: boolean
) {
this.container = container;
this.parent = parent;
this.contexts = [];
this.children = [];
this.treatAsRealStackingContext = treatAsRealStackingContext;
}
getOpacity(): number {
return this.parent
? this.container.style.opacity * this.parent.getOpacity()
: this.container.style.opacity;
}
getRealParentStackingContext(): StackingContext {
return !this.parent || this.treatAsRealStackingContext
? this
: this.parent.getRealParentStackingContext();
}
}

129
src/TextBounds.js Normal file
View 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
View 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;
}

21
src/Util.js Normal file
View File

@ -0,0 +1,21 @@
/* @flow */
'use strict';
export const contains = (bit: number, value: number): boolean => (bit & value) !== 0;
export const distance = (a: number, b: number): number => Math.sqrt(a * a + b * b);
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));
}
}
return target;
};
export const SMALL_IMAGE =
'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';

141
src/Window.js Normal file
View File

@ -0,0 +1,141 @@
/* @flow */
'use strict';
import type {Options} from './index';
import Logger from './Logger';
import {NodeParser} from './NodeParser';
import Renderer from './Renderer';
import ForeignObjectRenderer from './renderer/ForeignObjectRenderer';
import Feature from './Feature';
import {Bounds} from './Bounds';
import {cloneWindow, DocumentCloner} from './Clone';
import {FontMetrics} from './Font';
import Color, {TRANSPARENT} from './Color';
export const renderElement = (
element: HTMLElement,
options: Options,
logger: Logger
): Promise<*> => {
const ownerDocument = element.ownerDocument;
const windowBounds = new Bounds(
options.scrollX,
options.scrollY,
options.windowWidth,
options.windowHeight
);
// http://www.w3.org/TR/css3-background/#special-backgrounds
const documentBackgroundColor = ownerDocument.documentElement
? new Color(getComputedStyle(ownerDocument.documentElement).backgroundColor)
: TRANSPARENT;
const bodyBackgroundColor = ownerDocument.body
? new Color(getComputedStyle(ownerDocument.body).backgroundColor)
: TRANSPARENT;
const backgroundColor =
element === ownerDocument.documentElement
? documentBackgroundColor.isTransparent()
? bodyBackgroundColor.isTransparent()
? options.backgroundColor ? new Color(options.backgroundColor) : null
: bodyBackgroundColor
: documentBackgroundColor
: options.backgroundColor ? new Color(options.backgroundColor) : null;
return (options.foreignObjectRendering
? // $FlowFixMe
Feature.SUPPORT_FOREIGNOBJECT_DRAWING
: Promise.resolve(false)).then(
supportForeignObject =>
supportForeignObject
? (cloner => {
if (__DEV__) {
logger.log(`Document cloned, using foreignObject rendering`);
}
return cloner
.inlineFonts(ownerDocument)
.then(() => cloner.resourceLoader.ready())
.then(() => {
const renderer = new ForeignObjectRenderer(cloner.documentElement);
return renderer.render({
backgroundColor,
logger,
scale: options.scale,
x: options.x,
y: options.y,
width: options.width,
height: options.height,
windowWidth: options.windowWidth,
windowHeight: options.windowHeight,
scrollX: options.scrollX,
scrollY: options.scrollY
});
});
})(new DocumentCloner(element, options, logger, true, renderElement))
: cloneWindow(
ownerDocument,
windowBounds,
element,
options,
logger,
renderElement
).then(([container, clonedElement, resourceLoader]) => {
if (__DEV__) {
logger.log(`Document cloned, using computed rendering`);
}
const stack = NodeParser(clonedElement, resourceLoader, logger);
const clonedDocument = clonedElement.ownerDocument;
if (backgroundColor === stack.container.style.background.backgroundColor) {
stack.container.style.background.backgroundColor = TRANSPARENT;
}
return resourceLoader.ready().then(imageStore => {
if (options.removeContainer === true) {
if (container.parentNode) {
container.parentNode.removeChild(container);
} else if (__DEV__) {
logger.log(
`Cannot detach cloned iframe as it is not in the DOM anymore`
);
}
}
const fontMetrics = new FontMetrics(clonedDocument);
if (__DEV__) {
logger.log(`Starting renderer`);
}
const renderOptions = {
backgroundColor,
fontMetrics,
imageStore,
logger,
scale: options.scale,
x: options.x,
y: options.y,
width: options.width,
height: options.height
};
if (Array.isArray(options.target)) {
return Promise.all(
options.target.map(target => {
const renderer = new Renderer(target, renderOptions);
return renderer.render(stack);
})
);
} else {
const renderer = new Renderer(options.target, renderOptions);
return renderer.render(stack);
}
});
})
);
};

View File

@ -1,270 +0,0 @@
// http://dev.w3.org/csswg/css-color/
function Color(value) {
this.r = 0;
this.g = 0;
this.b = 0;
this.a = null;
var result = this.fromArray(value) ||
this.namedColor(value) ||
this.rgb(value) ||
this.rgba(value) ||
this.hex6(value) ||
this.hex3(value);
}
Color.prototype.darken = function(amount) {
var a = 1 - amount;
return new Color([
Math.round(this.r * a),
Math.round(this.g * a),
Math.round(this.b * a),
this.a
]);
};
Color.prototype.isTransparent = function() {
return this.a === 0;
};
Color.prototype.isBlack = function() {
return this.r === 0 && this.g === 0 && this.b === 0;
};
Color.prototype.fromArray = function(array) {
if (Array.isArray(array)) {
this.r = Math.min(array[0], 255);
this.g = Math.min(array[1], 255);
this.b = Math.min(array[2], 255);
if (array.length > 3) {
this.a = array[3];
}
}
return (Array.isArray(array));
};
var _hex3 = /^#([a-f0-9]{3})$/i;
Color.prototype.hex3 = function(value) {
var match = null;
if ((match = value.match(_hex3)) !== null) {
this.r = parseInt(match[1][0] + match[1][0], 16);
this.g = parseInt(match[1][1] + match[1][1], 16);
this.b = parseInt(match[1][2] + match[1][2], 16);
}
return match !== null;
};
var _hex6 = /^#([a-f0-9]{6})$/i;
Color.prototype.hex6 = function(value) {
var match = null;
if ((match = value.match(_hex6)) !== null) {
this.r = parseInt(match[1].substring(0, 2), 16);
this.g = parseInt(match[1].substring(2, 4), 16);
this.b = parseInt(match[1].substring(4, 6), 16);
}
return match !== null;
};
var _rgb = /^rgb\((\d{1,3}) *, *(\d{1,3}) *, *(\d{1,3})\)$/;
Color.prototype.rgb = function(value) {
var match = null;
if ((match = value.match(_rgb)) !== null) {
this.r = Number(match[1]);
this.g = Number(match[2]);
this.b = Number(match[3]);
}
return match !== null;
};
var _rgba = /^rgba\((\d{1,3}) *, *(\d{1,3}) *, *(\d{1,3}) *, *(\d+\.?\d*)\)$/;
Color.prototype.rgba = function(value) {
var match = null;
if ((match = value.match(_rgba)) !== null) {
this.r = Number(match[1]);
this.g = Number(match[2]);
this.b = Number(match[3]);
this.a = Number(match[4]);
}
return match !== null;
};
Color.prototype.toString = function() {
return this.a !== null && this.a !== 1 ?
"rgba(" + [this.r, this.g, this.b, this.a].join(",") + ")" :
"rgb(" + [this.r, this.g, this.b].join(",") + ")";
};
Color.prototype.namedColor = function(value) {
var color = colors[value.toLowerCase()];
if (color) {
this.r = color[0];
this.g = color[1];
this.b = color[2];
} else if (value.toLowerCase() === "transparent") {
this.r = this.g = this.b = this.a = 0;
return true;
}
return !!color;
};
Color.prototype.isColor = true;
// JSON.stringify([].slice.call($$('.named-color-table tr'), 1).map(function(row) { return [row.childNodes[3].textContent, row.childNodes[5].textContent.trim().split(",").map(Number)] }).reduce(function(data, row) {data[row[0]] = row[1]; return data}, {}))
var colors = {
"aliceblue": [240, 248, 255],
"antiquewhite": [250, 235, 215],
"aqua": [0, 255, 255],
"aquamarine": [127, 255, 212],
"azure": [240, 255, 255],
"beige": [245, 245, 220],
"bisque": [255, 228, 196],
"black": [0, 0, 0],
"blanchedalmond": [255, 235, 205],
"blue": [0, 0, 255],
"blueviolet": [138, 43, 226],
"brown": [165, 42, 42],
"burlywood": [222, 184, 135],
"cadetblue": [95, 158, 160],
"chartreuse": [127, 255, 0],
"chocolate": [210, 105, 30],
"coral": [255, 127, 80],
"cornflowerblue": [100, 149, 237],
"cornsilk": [255, 248, 220],
"crimson": [220, 20, 60],
"cyan": [0, 255, 255],
"darkblue": [0, 0, 139],
"darkcyan": [0, 139, 139],
"darkgoldenrod": [184, 134, 11],
"darkgray": [169, 169, 169],
"darkgreen": [0, 100, 0],
"darkgrey": [169, 169, 169],
"darkkhaki": [189, 183, 107],
"darkmagenta": [139, 0, 139],
"darkolivegreen": [85, 107, 47],
"darkorange": [255, 140, 0],
"darkorchid": [153, 50, 204],
"darkred": [139, 0, 0],
"darksalmon": [233, 150, 122],
"darkseagreen": [143, 188, 143],
"darkslateblue": [72, 61, 139],
"darkslategray": [47, 79, 79],
"darkslategrey": [47, 79, 79],
"darkturquoise": [0, 206, 209],
"darkviolet": [148, 0, 211],
"deeppink": [255, 20, 147],
"deepskyblue": [0, 191, 255],
"dimgray": [105, 105, 105],
"dimgrey": [105, 105, 105],
"dodgerblue": [30, 144, 255],
"firebrick": [178, 34, 34],
"floralwhite": [255, 250, 240],
"forestgreen": [34, 139, 34],
"fuchsia": [255, 0, 255],
"gainsboro": [220, 220, 220],
"ghostwhite": [248, 248, 255],
"gold": [255, 215, 0],
"goldenrod": [218, 165, 32],
"gray": [128, 128, 128],
"green": [0, 128, 0],
"greenyellow": [173, 255, 47],
"grey": [128, 128, 128],
"honeydew": [240, 255, 240],
"hotpink": [255, 105, 180],
"indianred": [205, 92, 92],
"indigo": [75, 0, 130],
"ivory": [255, 255, 240],
"khaki": [240, 230, 140],
"lavender": [230, 230, 250],
"lavenderblush": [255, 240, 245],
"lawngreen": [124, 252, 0],
"lemonchiffon": [255, 250, 205],
"lightblue": [173, 216, 230],
"lightcoral": [240, 128, 128],
"lightcyan": [224, 255, 255],
"lightgoldenrodyellow": [250, 250, 210],
"lightgray": [211, 211, 211],
"lightgreen": [144, 238, 144],
"lightgrey": [211, 211, 211],
"lightpink": [255, 182, 193],
"lightsalmon": [255, 160, 122],
"lightseagreen": [32, 178, 170],
"lightskyblue": [135, 206, 250],
"lightslategray": [119, 136, 153],
"lightslategrey": [119, 136, 153],
"lightsteelblue": [176, 196, 222],
"lightyellow": [255, 255, 224],
"lime": [0, 255, 0],
"limegreen": [50, 205, 50],
"linen": [250, 240, 230],
"magenta": [255, 0, 255],
"maroon": [128, 0, 0],
"mediumaquamarine": [102, 205, 170],
"mediumblue": [0, 0, 205],
"mediumorchid": [186, 85, 211],
"mediumpurple": [147, 112, 219],
"mediumseagreen": [60, 179, 113],
"mediumslateblue": [123, 104, 238],
"mediumspringgreen": [0, 250, 154],
"mediumturquoise": [72, 209, 204],
"mediumvioletred": [199, 21, 133],
"midnightblue": [25, 25, 112],
"mintcream": [245, 255, 250],
"mistyrose": [255, 228, 225],
"moccasin": [255, 228, 181],
"navajowhite": [255, 222, 173],
"navy": [0, 0, 128],
"oldlace": [253, 245, 230],
"olive": [128, 128, 0],
"olivedrab": [107, 142, 35],
"orange": [255, 165, 0],
"orangered": [255, 69, 0],
"orchid": [218, 112, 214],
"palegoldenrod": [238, 232, 170],
"palegreen": [152, 251, 152],
"paleturquoise": [175, 238, 238],
"palevioletred": [219, 112, 147],
"papayawhip": [255, 239, 213],
"peachpuff": [255, 218, 185],
"peru": [205, 133, 63],
"pink": [255, 192, 203],
"plum": [221, 160, 221],
"powderblue": [176, 224, 230],
"purple": [128, 0, 128],
"rebeccapurple": [102, 51, 153],
"red": [255, 0, 0],
"rosybrown": [188, 143, 143],
"royalblue": [65, 105, 225],
"saddlebrown": [139, 69, 19],
"salmon": [250, 128, 114],
"sandybrown": [244, 164, 96],
"seagreen": [46, 139, 87],
"seashell": [255, 245, 238],
"sienna": [160, 82, 45],
"silver": [192, 192, 192],
"skyblue": [135, 206, 235],
"slateblue": [106, 90, 205],
"slategray": [112, 128, 144],
"slategrey": [112, 128, 144],
"snow": [255, 250, 250],
"springgreen": [0, 255, 127],
"steelblue": [70, 130, 180],
"tan": [210, 180, 140],
"teal": [0, 128, 128],
"thistle": [216, 191, 216],
"tomato": [255, 99, 71],
"turquoise": [64, 224, 208],
"violet": [238, 130, 238],
"wheat": [245, 222, 179],
"white": [255, 255, 255],
"whitesmoke": [245, 245, 245],
"yellow": [255, 255, 0],
"yellowgreen": [154, 205, 50]
};

View File

@ -1,288 +0,0 @@
var html2canvasNodeAttribute = "data-html2canvas-node";
var html2canvasCanvasCloneAttribute = "data-html2canvas-canvas-clone";
var html2canvasCanvasCloneIndex = 0;
var html2canvasCloneIndex = 0;
window.html2canvas = function(nodeList, options) {
var index = html2canvasCloneIndex++;
options = options || {};
if (options.logging) {
window.html2canvas.logging = true;
window.html2canvas.start = Date.now();
}
options.async = typeof(options.async) === "undefined" ? true : options.async;
options.allowTaint = typeof(options.allowTaint) === "undefined" ? false : options.allowTaint;
options.removeContainer = typeof(options.removeContainer) === "undefined" ? true : options.removeContainer;
options.javascriptEnabled = typeof(options.javascriptEnabled) === "undefined" ? false : options.javascriptEnabled;
options.imageTimeout = typeof(options.imageTimeout) === "undefined" ? 10000 : options.imageTimeout;
options.renderer = typeof(options.renderer) === "function" ? options.renderer : CanvasRenderer;
options.strict = !!options.strict;
if (typeof(nodeList) === "string") {
if (typeof(options.proxy) !== "string") {
return Promise.reject("Proxy must be used when rendering url");
}
var width = options.width != null ? options.width : window.innerWidth;
var height = options.height != null ? options.height : window.innerHeight;
return loadUrlDocument(absoluteUrl(nodeList), options.proxy, document, width, height, options).then(function(container) {
return renderWindow(container.contentWindow.document.documentElement, container, options, width, height);
});
}
var node = ((nodeList === undefined) ? [document.documentElement] : ((nodeList.length) ? nodeList : [nodeList]))[0];
node.setAttribute(html2canvasNodeAttribute + index, index);
return renderDocument(node.ownerDocument, options, node.ownerDocument.defaultView.innerWidth, node.ownerDocument.defaultView.innerHeight, index).then(function(canvas) {
if (typeof(options.onrendered) === "function") {
log("options.onrendered is deprecated, html2canvas returns a Promise containing the canvas");
options.onrendered(canvas);
}
return canvas;
});
};
window.html2canvas.punycode = this.punycode;
window.html2canvas.proxy = {};
function renderDocument(document, options, windowWidth, windowHeight, html2canvasIndex) {
return createWindowClone(document, document, windowWidth, windowHeight, options, document.defaultView.pageXOffset, document.defaultView.pageYOffset).then(function(container) {
log("Document cloned");
var attributeName = html2canvasNodeAttribute + html2canvasIndex;
var selector = "[" + attributeName + "='" + html2canvasIndex + "']";
document.querySelector(selector).removeAttribute(attributeName);
var clonedWindow = container.contentWindow;
var node = clonedWindow.document.querySelector(selector);
var oncloneHandler = (typeof(options.onclone) === "function") ? Promise.resolve(options.onclone(clonedWindow.document)) : Promise.resolve(true);
return oncloneHandler.then(function() {
return renderWindow(node, container, options, windowWidth, windowHeight);
});
});
}
function renderWindow(node, container, options, windowWidth, windowHeight) {
var clonedWindow = container.contentWindow;
var support = new Support(clonedWindow.document);
var imageLoader = new ImageLoader(options, support);
var bounds = getBounds(node);
var width = options.type === "view" ? windowWidth : documentWidth(clonedWindow.document);
var height = options.type === "view" ? windowHeight : documentHeight(clonedWindow.document);
var renderer = new options.renderer(width, height, imageLoader, options, document);
var parser = new NodeParser(node, renderer, support, imageLoader, options);
return parser.ready.then(function() {
log("Finished rendering");
var canvas;
if (options.type === "view") {
canvas = crop(renderer.canvas, {width: renderer.canvas.width, height: renderer.canvas.height, top: 0, left: 0, x: 0, y: 0});
} else if (node === clonedWindow.document.body || node === clonedWindow.document.documentElement || options.canvas != null) {
canvas = renderer.canvas;
} else {
canvas = crop(renderer.canvas, {width: options.width != null ? options.width : bounds.width, height: options.height != null ? options.height : bounds.height, top: bounds.top, left: bounds.left, x: clonedWindow.pageXOffset, y: clonedWindow.pageYOffset});
}
cleanupContainer(container, options);
return canvas;
});
}
function cleanupContainer(container, options) {
if (options.removeContainer) {
container.parentNode.removeChild(container);
log("Cleaned up container");
}
}
function crop(canvas, bounds) {
var croppedCanvas = document.createElement("canvas");
var x1 = Math.min(canvas.width - 1, Math.max(0, bounds.left));
var x2 = Math.min(canvas.width, Math.max(1, bounds.left + bounds.width));
var y1 = Math.min(canvas.height - 1, Math.max(0, bounds.top));
var y2 = Math.min(canvas.height, Math.max(1, bounds.top + bounds.height));
croppedCanvas.width = bounds.width;
croppedCanvas.height = bounds.height;
log("Cropping canvas at:", "left:", bounds.left, "top:", bounds.top, "width:", (x2-x1), "height:", (y2-y1));
log("Resulting crop with width", bounds.width, "and height", bounds.height, " with x", x1, "and y", y1);
croppedCanvas.getContext("2d").drawImage(canvas, x1, y1, x2-x1, y2-y1, bounds.x, bounds.y, x2-x1, y2-y1);
return croppedCanvas;
}
function documentWidth (doc) {
return Math.max(
Math.max(doc.body.scrollWidth, doc.documentElement.scrollWidth),
Math.max(doc.body.offsetWidth, doc.documentElement.offsetWidth),
Math.max(doc.body.clientWidth, doc.documentElement.clientWidth)
);
}
function documentHeight (doc) {
return Math.max(
Math.max(doc.body.scrollHeight, doc.documentElement.scrollHeight),
Math.max(doc.body.offsetHeight, doc.documentElement.offsetHeight),
Math.max(doc.body.clientHeight, doc.documentElement.clientHeight)
);
}
function smallImage() {
return "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
}
function isIE9() {
return document.documentMode && document.documentMode <= 9;
}
// https://github.com/niklasvh/html2canvas/issues/503
function cloneNodeIE9(node, javascriptEnabled) {
var clone = node.nodeType === 3 ? document.createTextNode(node.nodeValue) : node.cloneNode(false);
var child = node.firstChild;
while(child) {
if (javascriptEnabled === true || child.nodeType !== 1 || child.nodeName !== 'SCRIPT') {
clone.appendChild(cloneNodeIE9(child, javascriptEnabled));
}
child = child.nextSibling;
}
return clone;
}
function createWindowClone(ownerDocument, containerDocument, width, height, options, x ,y) {
labelCanvasElements(ownerDocument);
var documentElement = isIE9() ? cloneNodeIE9(ownerDocument.documentElement, options.javascriptEnabled) : ownerDocument.documentElement.cloneNode(true);
var container = containerDocument.createElement("iframe");
container.className = "html2canvas-container";
container.style.visibility = "hidden";
container.style.position = "fixed";
container.style.left = "-10000px";
container.style.top = "0px";
container.style.border = "0";
container.width = width;
container.height = height;
container.scrolling = "no"; // ios won't scroll without it
containerDocument.body.appendChild(container);
return new Promise(function(resolve) {
var documentClone = container.contentWindow.document;
cloneNodeValues(ownerDocument.documentElement, documentElement, "textarea");
cloneNodeValues(ownerDocument.documentElement, documentElement, "select");
/* 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
*/
container.contentWindow.onload = container.onload = function() {
var interval = setInterval(function() {
if (documentClone.body.childNodes.length > 0) {
cloneCanvasContents(ownerDocument, documentClone);
clearInterval(interval);
if (options.type === "view") {
container.contentWindow.scrollTo(x, y);
}
resolve(container);
}
}, 50);
};
documentClone.open();
documentClone.write("<!DOCTYPE html><html></html>");
// Chrome scrolls the parent document for some reason after the write to the cloned window???
restoreOwnerScroll(ownerDocument, x, y);
documentClone.replaceChild(options.javascriptEnabled === true ? documentClone.adoptNode(documentElement) : removeScriptNodes(documentClone.adoptNode(documentElement)), documentClone.documentElement);
documentClone.close();
});
}
function cloneNodeValues(document, clone, nodeName) {
var originalNodes = document.getElementsByTagName(nodeName);
var clonedNodes = clone.getElementsByTagName(nodeName);
var count = originalNodes.length;
for (var i = 0; i < count; i++) {
clonedNodes[i].value = originalNodes[i].value;
}
}
function restoreOwnerScroll(ownerDocument, x, y) {
if (ownerDocument.defaultView && (x !== ownerDocument.defaultView.pageXOffset || y !== ownerDocument.defaultView.pageYOffset)) {
ownerDocument.defaultView.scrollTo(x, y);
}
}
function loadUrlDocument(src, proxy, document, width, height, options) {
return new Proxy(src, proxy, window.document).then(documentFromHTML(src)).then(function(doc) {
return createWindowClone(doc, document, width, height, options, 0, 0);
});
}
function documentFromHTML(src) {
return function(html) {
var parser = new DOMParser(), doc;
try {
doc = parser.parseFromString(html, "text/html");
} catch(e) {
log("DOMParser not supported, falling back to createHTMLDocument");
doc = document.implementation.createHTMLDocument("");
try {
doc.open();
doc.write(html);
doc.close();
} catch(ee) {
log("createHTMLDocument write not supported, falling back to document.body.innerHTML");
doc.body.innerHTML = html; // ie9 doesnt support writing to documentElement
}
}
var b = doc.querySelector("base");
if (!b || !b.href.host) {
var base = doc.createElement("base");
base.href = src;
doc.head.insertBefore(base, doc.head.firstChild);
}
return doc;
};
}
function labelCanvasElements(ownerDocument) {
[].slice.call(ownerDocument.querySelectorAll("canvas"), 0).forEach(function(canvas) {
canvas.setAttribute(html2canvasCanvasCloneAttribute, "canvas-" + html2canvasCanvasCloneIndex++);
});
}
function cloneCanvasContents(ownerDocument, documentClone) {
[].slice.call(ownerDocument.querySelectorAll("[" + html2canvasCanvasCloneAttribute + "]"), 0).forEach(function(canvas) {
try {
var clonedCanvas = documentClone.querySelector('[' + html2canvasCanvasCloneAttribute + '="' + canvas.getAttribute(html2canvasCanvasCloneAttribute) + '"]');
if (clonedCanvas) {
clonedCanvas.width = canvas.width;
clonedCanvas.height = canvas.height;
clonedCanvas.getContext("2d").putImageData(canvas.getContext("2d").getImageData(0, 0, canvas.width, canvas.height), 0, 0);
}
} catch(e) {
log("Unable to copy canvas content from", canvas, e);
}
canvas.removeAttribute(html2canvasCanvasCloneAttribute);
});
}
function removeScriptNodes(parent) {
[].slice.call(parent.childNodes, 0).filter(isElementNode).forEach(function(node) {
if (node.tagName === "SCRIPT") {
parent.removeChild(node);
} else {
removeScriptNodes(node);
}
});
return parent;
}
function isElementNode(node) {
return node.nodeType === Node.ELEMENT_NODE;
}
function absoluteUrl(url) {
var link = document.createElement("a");
link.href = url;
link.href = link.href;
return link;
}

View 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
View 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
View 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
View 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
View 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`);
}
}
}
}

View File

@ -1,17 +0,0 @@
function DummyImageContainer(src) {
this.src = src;
log("DummyImageContainer for", src);
if (!this.promise || !this.image) {
log("Initiating DummyImageContainer");
DummyImageContainer.prototype.image = new Image();
var image = this.image;
DummyImageContainer.prototype.promise = new Promise(function(resolve, reject) {
image.onload = resolve;
image.onerror = reject;
image.src = smallImage();
if (image.complete === true) {
resolve(image);
}
});
}
}

Submodule src/fabric deleted from 791c74a82e

View File

@ -1,6 +0,0 @@
if (typeof(document) === "undefined" || typeof(Object.create) !== "function" || typeof(document.createElement("canvas").getContext) !== "function") {
(window || module.exports).html2canvas = function() {
return Promise.reject("No canvas support");
};
return;
}

View File

@ -1,48 +0,0 @@
function Font(family, size) {
var container = document.createElement('div'),
img = document.createElement('img'),
span = document.createElement('span'),
sampleText = 'Hidden Text',
baseline,
middle;
container.style.visibility = "hidden";
container.style.fontFamily = family;
container.style.fontSize = size;
container.style.margin = 0;
container.style.padding = 0;
document.body.appendChild(container);
img.src = smallImage();
img.width = 1;
img.height = 1;
img.style.margin = 0;
img.style.padding = 0;
img.style.verticalAlign = "baseline";
span.style.fontFamily = family;
span.style.fontSize = size;
span.style.margin = 0;
span.style.padding = 0;
span.appendChild(document.createTextNode(sampleText));
container.appendChild(span);
container.appendChild(img);
baseline = (img.offsetTop - span.offsetTop) + 1;
container.removeChild(span);
container.appendChild(document.createTextNode(sampleText));
container.style.lineHeight = "normal";
img.style.verticalAlign = "super";
middle = (img.offsetTop-container.offsetTop) + 1;
document.body.removeChild(container);
this.baseline = baseline;
this.lineWidth = 1;
this.middle = middle;
}

View File

@ -1,10 +0,0 @@
function FontMetrics() {
this.data = {};
}
FontMetrics.prototype.getMetrics = function(family, size) {
if (this.data[family + "-" + size] === undefined) {
this.data[family + "-" + size] = new Font(family, size);
}
return this.data[family + "-" + size];
};

View File

@ -1,24 +0,0 @@
function FrameContainer(container, sameOrigin, options) {
this.image = null;
this.src = container;
var self = this;
var bounds = getBounds(container);
this.promise = (!sameOrigin ? this.proxyLoad(options.proxy, bounds, options) : new Promise(function(resolve) {
if (container.contentWindow.document.URL === "about:blank" || container.contentWindow.document.documentElement == null) {
container.contentWindow.onload = container.onload = function() {
resolve(container);
};
} else {
resolve(container);
}
})).then(function(container) {
return html2canvas(container.contentWindow.document.documentElement, {type: 'view', width: container.width, height: container.height, proxy: options.proxy, javascriptEnabled: options.javascriptEnabled, removeContainer: options.removeContainer, allowTaint: options.allowTaint, imageTimeout: options.imageTimeout / 2});
}).then(function(canvas) {
return self.image = canvas;
});
}
FrameContainer.prototype.proxyLoad = function(proxy, bounds, options) {
var container = this.src;
return loadUrlDocument(container.src, proxy, container.ownerDocument, bounds.width, bounds.height, options);
};

View File

@ -1,15 +0,0 @@
function GradientContainer(imageData) {
this.src = imageData.value;
this.colorStops = [];
this.type = null;
this.x0 = 0.5;
this.y0 = 0.5;
this.x1 = 0.5;
this.y1 = 0.5;
this.promise = Promise.resolve(true);
}
GradientContainer.prototype.TYPES = {
LINEAR: 1,
RADIAL: 2
};

View File

@ -1,17 +0,0 @@
function ImageContainer(src, cors) {
this.src = src;
this.image = new Image();
var self = this;
this.tainted = null;
this.promise = new Promise(function(resolve, reject) {
self.image.onload = resolve;
self.image.onerror = reject;
if (cors) {
self.image.crossOrigin = "anonymous";
}
self.image.src = src;
if (self.image.complete === true) {
resolve(self.image);
}
});
}

View File

@ -1,144 +0,0 @@
function ImageLoader(options, support) {
this.link = null;
this.options = options;
this.support = support;
this.origin = this.getOrigin(window.location.href);
}
ImageLoader.prototype.findImages = function(nodes) {
var images = [];
nodes.reduce(function(imageNodes, container) {
switch(container.node.nodeName) {
case "IMG":
return imageNodes.concat([{
args: [container.node.src],
method: "url"
}]);
case "svg":
case "IFRAME":
return imageNodes.concat([{
args: [container.node],
method: container.node.nodeName
}]);
}
return imageNodes;
}, []).forEach(this.addImage(images, this.loadImage), this);
return images;
};
ImageLoader.prototype.findBackgroundImage = function(images, container) {
container.parseBackgroundImages().filter(this.hasImageBackground).forEach(this.addImage(images, this.loadImage), this);
return images;
};
ImageLoader.prototype.addImage = function(images, callback) {
return function(newImage) {
newImage.args.forEach(function(image) {
if (!this.imageExists(images, image)) {
images.splice(0, 0, callback.call(this, newImage));
log('Added image #' + (images.length), typeof(image) === "string" ? image.substring(0, 100) : image);
}
}, this);
};
};
ImageLoader.prototype.hasImageBackground = function(imageData) {
return imageData.method !== "none";
};
ImageLoader.prototype.loadImage = function(imageData) {
if (imageData.method === "url") {
var src = imageData.args[0];
if (this.isSVG(src) && !this.support.svg && !this.options.allowTaint) {
return new SVGContainer(src);
} else if (src.match(/data:image\/.*;base64,/i)) {
return new ImageContainer(src.replace(/url\(['"]{0,}|['"]{0,}\)$/ig, ''), false);
} else if (this.isSameOrigin(src) || this.options.allowTaint === true || this.isSVG(src)) {
return new ImageContainer(src, false);
} else if (this.support.cors && !this.options.allowTaint && this.options.useCORS) {
return new ImageContainer(src, true);
} else if (this.options.proxy) {
return new ProxyImageContainer(src, this.options.proxy);
} else {
return new DummyImageContainer(src);
}
} else if (imageData.method === "linear-gradient") {
return new LinearGradientContainer(imageData);
} else if (imageData.method === "gradient") {
return new WebkitGradientContainer(imageData);
} else if (imageData.method === "svg") {
return new SVGNodeContainer(imageData.args[0], this.support.svg);
} else if (imageData.method === "IFRAME") {
return new FrameContainer(imageData.args[0], this.isSameOrigin(imageData.args[0].src), this.options);
} else {
return new DummyImageContainer(imageData);
}
};
ImageLoader.prototype.isSVG = function(src) {
return src.substring(src.length - 3).toLowerCase() === "svg" || SVGContainer.prototype.isInline(src);
};
ImageLoader.prototype.imageExists = function(images, src) {
return images.some(function(image) {
return image.src === src;
});
};
ImageLoader.prototype.isSameOrigin = function(url) {
return (this.getOrigin(url) === this.origin);
};
ImageLoader.prototype.getOrigin = function(url) {
var link = this.link || (this.link = document.createElement("a"));
link.href = url;
link.href = link.href; // IE9, LOL! - http://jsfiddle.net/niklasvh/2e48b/
return link.protocol + link.hostname + link.port;
};
ImageLoader.prototype.getPromise = function(container) {
return this.timeout(container, this.options.imageTimeout)['catch'](function() {
var dummy = new DummyImageContainer(container.src);
return dummy.promise.then(function(image) {
container.image = image;
});
});
};
ImageLoader.prototype.get = function(src) {
var found = null;
return this.images.some(function(img) {
return (found = img).src === src;
}) ? found : null;
};
ImageLoader.prototype.fetch = function(nodes) {
this.images = nodes.reduce(bind(this.findBackgroundImage, this), this.findImages(nodes));
this.images.forEach(function(image, index) {
image.promise.then(function() {
log("Succesfully loaded image #"+ (index+1), image);
}, function(e) {
log("Failed loading image #"+ (index+1), image, e);
});
});
this.ready = Promise.all(this.images.map(this.getPromise, this));
log("Finished searching images");
return this;
};
ImageLoader.prototype.timeout = function(container, timeout) {
var timer;
var promise = Promise.race([container.promise, new Promise(function(res, reject) {
timer = setTimeout(function() {
log("Timed out loading image", container);
reject(container);
}, timeout);
})]).then(function(container) {
clearTimeout(timer);
return container;
});
promise['catch'](function() {
clearTimeout(timer);
});
return promise;
};

98
src/index.js Normal file
View File

@ -0,0 +1,98 @@
/* @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<*>,
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),
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;

View File

@ -1,73 +0,0 @@
function LinearGradientContainer(imageData) {
GradientContainer.apply(this, arguments);
this.type = this.TYPES.LINEAR;
var hasDirection = imageData.args[0].match(this.stepRegExp) === null;
if (hasDirection) {
imageData.args[0].split(" ").reverse().forEach(function(position) {
switch(position) {
case "left":
this.x0 = 0;
this.x1 = 1;
break;
case "top":
this.y0 = 0;
this.y1 = 1;
break;
case "right":
this.x0 = 1;
this.x1 = 0;
break;
case "bottom":
this.y0 = 1;
this.y1 = 0;
break;
case "to":
var y0 = this.y0;
var x0 = this.x0;
this.y0 = this.y1;
this.x0 = this.x1;
this.x1 = x0;
this.y1 = y0;
break;
}
}, this);
} else {
this.y0 = 0;
this.y1 = 1;
}
this.colorStops = imageData.args.slice(hasDirection ? 1 : 0).map(function(colorStop) {
var colorStopMatch = colorStop.match(this.stepRegExp);
return {
color: new Color(colorStopMatch[1]),
stop: colorStopMatch[3] === "%" ? colorStopMatch[2] / 100 : null
};
}, this);
if (this.colorStops[0].stop === null) {
this.colorStops[0].stop = 0;
}
if (this.colorStops[this.colorStops.length - 1].stop === null) {
this.colorStops[this.colorStops.length - 1].stop = 1;
}
this.colorStops.forEach(function(colorStop, index) {
if (colorStop.stop === null) {
this.colorStops.slice(index).some(function(find, count) {
if (find.stop !== null) {
colorStop.stop = ((find.stop - this.colorStops[index - 1].stop) / (count + 1)) + this.colorStops[index - 1].stop;
return true;
} else {
return false;
}
}, this);
}
}, this);
}
LinearGradientContainer.prototype = Object.create(GradientContainer.prototype);
LinearGradientContainer.prototype.stepRegExp = /((?:rgb|rgba)\(\d{1,3},\s\d{1,3},\s\d{1,3}(?:,\s[0-9\.]+)?\))\s*(\d{1,3})?(%|px)?/;

View File

@ -1,5 +0,0 @@
function log() {
if (window.html2canvas.logging && window.console && window.console.log) {
Function.prototype.bind.call(window.console.log, (window.console)).apply(window.console, [(Date.now() - window.html2canvas.start) + "ms", "html2canvas:"].concat([].slice.call(arguments, 0)));
}
}

View File

@ -1,408 +0,0 @@
function NodeContainer(node, parent) {
this.node = node;
this.parent = parent;
this.stack = null;
this.bounds = null;
this.borders = null;
this.clip = [];
this.backgroundClip = [];
this.offsetBounds = null;
this.visible = null;
this.computedStyles = null;
this.colors = {};
this.styles = {};
this.backgroundImages = null;
this.transformData = null;
this.transformMatrix = null;
this.isPseudoElement = false;
this.opacity = null;
}
NodeContainer.prototype.cloneTo = function(stack) {
stack.visible = this.visible;
stack.borders = this.borders;
stack.bounds = this.bounds;
stack.clip = this.clip;
stack.backgroundClip = this.backgroundClip;
stack.computedStyles = this.computedStyles;
stack.styles = this.styles;
stack.backgroundImages = this.backgroundImages;
stack.opacity = this.opacity;
};
NodeContainer.prototype.getOpacity = function() {
return this.opacity === null ? (this.opacity = this.cssFloat('opacity')) : this.opacity;
};
NodeContainer.prototype.assignStack = function(stack) {
this.stack = stack;
stack.children.push(this);
};
NodeContainer.prototype.isElementVisible = function() {
return this.node.nodeType === Node.TEXT_NODE ? this.parent.visible : (
this.css('display') !== "none" &&
this.css('visibility') !== "hidden" &&
!this.node.hasAttribute("data-html2canvas-ignore") &&
(this.node.nodeName !== "INPUT" || this.node.getAttribute("type") !== "hidden")
);
};
NodeContainer.prototype.css = function(attribute) {
if (!this.computedStyles) {
this.computedStyles = this.isPseudoElement ? this.parent.computedStyle(this.before ? ":before" : ":after") : this.computedStyle(null);
}
return this.styles[attribute] || (this.styles[attribute] = this.computedStyles[attribute]);
};
NodeContainer.prototype.prefixedCss = function(attribute) {
var prefixes = ["webkit", "moz", "ms", "o"];
var value = this.css(attribute);
if (value === undefined) {
prefixes.some(function(prefix) {
value = this.css(prefix + attribute.substr(0, 1).toUpperCase() + attribute.substr(1));
return value !== undefined;
}, this);
}
return value === undefined ? null : value;
};
NodeContainer.prototype.computedStyle = function(type) {
return this.node.ownerDocument.defaultView.getComputedStyle(this.node, type);
};
NodeContainer.prototype.cssInt = function(attribute) {
var value = parseInt(this.css(attribute), 10);
return (isNaN(value)) ? 0 : value; // borders in old IE are throwing 'medium' for demo.html
};
NodeContainer.prototype.color = function(attribute) {
return this.colors[attribute] || (this.colors[attribute] = new Color(this.css(attribute)));
};
NodeContainer.prototype.cssFloat = function(attribute) {
var value = parseFloat(this.css(attribute));
return (isNaN(value)) ? 0 : value;
};
NodeContainer.prototype.fontWeight = function() {
var weight = this.css("fontWeight");
switch(parseInt(weight, 10)){
case 401:
weight = "bold";
break;
case 400:
weight = "normal";
break;
}
return weight;
};
NodeContainer.prototype.parseClip = function() {
var matches = this.css('clip').match(this.CLIP);
if (matches) {
return {
top: parseInt(matches[1], 10),
right: parseInt(matches[2], 10),
bottom: parseInt(matches[3], 10),
left: parseInt(matches[4], 10)
};
}
return null;
};
NodeContainer.prototype.parseBackgroundImages = function() {
return this.backgroundImages || (this.backgroundImages = parseBackgrounds(this.css("backgroundImage")));
};
NodeContainer.prototype.cssList = function(property, index) {
var value = (this.css(property) || '').split(',');
value = value[index || 0] || value[0] || 'auto';
value = value.trim().split(' ');
if (value.length === 1) {
value = [value[0], value[0]];
}
return value;
};
NodeContainer.prototype.parseBackgroundSize = function(bounds, image, index) {
var size = this.cssList("backgroundSize", index);
var width, height;
if (isPercentage(size[0])) {
width = bounds.width * parseFloat(size[0]) / 100;
} else if (/contain|cover/.test(size[0])) {
var targetRatio = bounds.width / bounds.height, currentRatio = image.width / image.height;
return (targetRatio < currentRatio ^ size[0] === 'contain') ? {width: bounds.height * currentRatio, height: bounds.height} : {width: bounds.width, height: bounds.width / currentRatio};
} else {
width = parseInt(size[0], 10);
}
if (size[0] === 'auto' && size[1] === 'auto') {
height = image.height;
} else if (size[1] === 'auto') {
height = width / image.width * image.height;
} else if (isPercentage(size[1])) {
height = bounds.height * parseFloat(size[1]) / 100;
} else {
height = parseInt(size[1], 10);
}
if (size[0] === 'auto') {
width = height / image.height * image.width;
}
return {width: width, height: height};
};
NodeContainer.prototype.parseBackgroundPosition = function(bounds, image, index, backgroundSize) {
var position = this.cssList('backgroundPosition', index);
var left, top;
if (isPercentage(position[0])){
left = (bounds.width - (backgroundSize || image).width) * (parseFloat(position[0]) / 100);
} else {
left = parseInt(position[0], 10);
}
if (position[1] === 'auto') {
top = left / image.width * image.height;
} else if (isPercentage(position[1])){
top = (bounds.height - (backgroundSize || image).height) * parseFloat(position[1]) / 100;
} else {
top = parseInt(position[1], 10);
}
if (position[0] === 'auto') {
left = top / image.height * image.width;
}
return {left: left, top: top};
};
NodeContainer.prototype.parseBackgroundRepeat = function(index) {
return this.cssList("backgroundRepeat", index)[0];
};
NodeContainer.prototype.parseTextShadows = function() {
var textShadow = this.css("textShadow");
var results = [];
if (textShadow && textShadow !== 'none') {
var shadows = textShadow.match(this.TEXT_SHADOW_PROPERTY);
for (var i = 0; shadows && (i < shadows.length); i++) {
var s = shadows[i].match(this.TEXT_SHADOW_VALUES);
results.push({
color: new Color(s[0]),
offsetX: s[1] ? parseFloat(s[1].replace('px', '')) : 0,
offsetY: s[2] ? parseFloat(s[2].replace('px', '')) : 0,
blur: s[3] ? s[3].replace('px', '') : 0
});
}
}
return results;
};
NodeContainer.prototype.parseTransform = function() {
if (!this.transformData) {
if (this.hasTransform()) {
var offset = this.parseBounds();
var origin = this.prefixedCss("transformOrigin").split(" ").map(removePx).map(asFloat);
origin[0] += offset.left;
origin[1] += offset.top;
this.transformData = {
origin: origin,
matrix: this.parseTransformMatrix()
};
} else {
this.transformData = {
origin: [0, 0],
matrix: [1, 0, 0, 1, 0, 0]
};
}
}
return this.transformData;
};
NodeContainer.prototype.parseTransformMatrix = function() {
if (!this.transformMatrix) {
var transform = this.prefixedCss("transform");
var matrix = transform ? parseMatrix(transform.match(this.MATRIX_PROPERTY)) : null;
this.transformMatrix = matrix ? matrix : [1, 0, 0, 1, 0, 0];
}
return this.transformMatrix;
};
NodeContainer.prototype.parseBounds = function() {
return this.bounds || (this.bounds = this.hasTransform() ? offsetBounds(this.node) : getBounds(this.node));
};
NodeContainer.prototype.hasTransform = function() {
return this.parseTransformMatrix().join(",") !== "1,0,0,1,0,0" || (this.parent && this.parent.hasTransform());
};
NodeContainer.prototype.getValue = function() {
var value = this.node.value || "";
if (this.node.tagName === "SELECT") {
value = selectionValue(this.node);
} else if (this.node.type === "password") {
value = Array(value.length + 1).join('\u2022'); // jshint ignore:line
}
return value.length === 0 ? (this.node.placeholder || "") : value;
};
NodeContainer.prototype.MATRIX_PROPERTY = /(matrix)\((.+)\)/;
NodeContainer.prototype.TEXT_SHADOW_PROPERTY = /((rgba|rgb)\([^\)]+\)(\s-?\d+px){0,})/g;
NodeContainer.prototype.TEXT_SHADOW_VALUES = /(-?\d+px)|(#.+)|(rgb\(.+\))|(rgba\(.+\))/g;
NodeContainer.prototype.CLIP = /^rect\((\d+)px,? (\d+)px,? (\d+)px,? (\d+)px\)$/;
function selectionValue(node) {
var option = node.options[node.selectedIndex || 0];
return option ? (option.text || "") : "";
}
function parseMatrix(match) {
if (match && match[1] === "matrix") {
return match[2].split(",").map(function(s) {
return parseFloat(s.trim());
});
}
}
function isPercentage(value) {
return value.toString().indexOf("%") !== -1;
}
function parseBackgrounds(backgroundImage) {
var whitespace = ' \r\n\t',
method, definition, prefix, prefix_i, block, results = [],
mode = 0, numParen = 0, quote, args;
var appendResult = function() {
if(method) {
if (definition.substr(0, 1) === '"') {
definition = definition.substr(1, definition.length - 2);
}
if (definition) {
args.push(definition);
}
if (method.substr(0, 1) === '-' && (prefix_i = method.indexOf('-', 1 ) + 1) > 0) {
prefix = method.substr(0, prefix_i);
method = method.substr(prefix_i);
}
results.push({
prefix: prefix,
method: method.toLowerCase(),
value: block,
args: args,
image: null
});
}
args = [];
method = prefix = definition = block = '';
};
args = [];
method = prefix = definition = block = '';
backgroundImage.split("").forEach(function(c) {
if (mode === 0 && whitespace.indexOf(c) > -1) {
return;
}
switch(c) {
case '"':
if(!quote) {
quote = c;
} else if(quote === c) {
quote = null;
}
break;
case '(':
if(quote) {
break;
} else if(mode === 0) {
mode = 1;
block += c;
return;
} else {
numParen++;
}
break;
case ')':
if (quote) {
break;
} else if(mode === 1) {
if(numParen === 0) {
mode = 0;
block += c;
appendResult();
return;
} else {
numParen--;
}
}
break;
case ',':
if (quote) {
break;
} else if(mode === 0) {
appendResult();
return;
} else if (mode === 1) {
if (numParen === 0 && !method.match(/^url$/i)) {
args.push(definition);
definition = '';
block += c;
return;
}
}
break;
}
block += c;
if (mode === 0) {
method += c;
} else {
definition += c;
}
});
appendResult();
return results;
}
function removePx(str) {
return str.replace("px", "");
}
function asFloat(str) {
return parseFloat(str);
}
function getBounds(node) {
if (node.getBoundingClientRect) {
var clientRect = node.getBoundingClientRect();
var width = node.offsetWidth == null ? clientRect.width : node.offsetWidth;
return {
top: clientRect.top,
bottom: clientRect.bottom || (clientRect.top + clientRect.height),
right: clientRect.left + width,
left: clientRect.left,
width: width,
height: node.offsetHeight == null ? clientRect.height : node.offsetHeight
};
}
return {};
}
function offsetBounds(node) {
var parent = node.offsetParent ? offsetBounds(node.offsetParent) : {top: 0, left: 0};
return {
top: node.offsetTop + parent.top,
bottom: node.offsetTop + node.offsetHeight + parent.top,
right: node.offsetLeft + parent.left + node.offsetWidth,
left: node.offsetLeft + parent.left,
width: node.offsetWidth,
height: node.offsetHeight
};
}

View File

@ -1,859 +0,0 @@
function NodeParser(element, renderer, support, imageLoader, options) {
log("Starting NodeParser");
this.renderer = renderer;
this.options = options;
this.range = null;
this.support = support;
this.renderQueue = [];
this.stack = new StackingContext(true, 1, element.ownerDocument, null);
var parent = new NodeContainer(element, null);
if (options.background) {
renderer.rectangle(0, 0, renderer.width, renderer.height, new Color(options.background));
}
if (element === element.ownerDocument.documentElement) {
// http://www.w3.org/TR/css3-background/#special-backgrounds
var canvasBackground = new NodeContainer(parent.color('backgroundColor').isTransparent() ? element.ownerDocument.body : element.ownerDocument.documentElement, null);
renderer.rectangle(0, 0, renderer.width, renderer.height, canvasBackground.color('backgroundColor'));
}
parent.visibile = parent.isElementVisible();
this.createPseudoHideStyles(element.ownerDocument);
this.disableAnimations(element.ownerDocument);
this.nodes = flatten([parent].concat(this.getChildren(parent)).filter(function(container) {
return container.visible = container.isElementVisible();
}).map(this.getPseudoElements, this));
this.fontMetrics = new FontMetrics();
log("Fetched nodes, total:", this.nodes.length);
log("Calculate overflow clips");
this.calculateOverflowClips();
log("Start fetching images");
this.images = imageLoader.fetch(this.nodes.filter(isElement));
this.ready = this.images.ready.then(bind(function() {
log("Images loaded, starting parsing");
log("Creating stacking contexts");
this.createStackingContexts();
log("Sorting stacking contexts");
this.sortStackingContexts(this.stack);
this.parse(this.stack);
log("Render queue created with " + this.renderQueue.length + " items");
return new Promise(bind(function(resolve) {
if (!options.async) {
this.renderQueue.forEach(this.paint, this);
resolve();
} else if (typeof(options.async) === "function") {
options.async.call(this, this.renderQueue, resolve);
} else if (this.renderQueue.length > 0){
this.renderIndex = 0;
this.asyncRenderer(this.renderQueue, resolve);
} else {
resolve();
}
}, this));
}, this));
}
NodeParser.prototype.calculateOverflowClips = function() {
this.nodes.forEach(function(container) {
if (isElement(container)) {
if (isPseudoElement(container)) {
container.appendToDOM();
}
container.borders = this.parseBorders(container);
var clip = (container.css('overflow') === "hidden") ? [container.borders.clip] : [];
var cssClip = container.parseClip();
if (cssClip && ["absolute", "fixed"].indexOf(container.css('position')) !== -1) {
clip.push([["rect",
container.bounds.left + cssClip.left,
container.bounds.top + cssClip.top,
cssClip.right - cssClip.left,
cssClip.bottom - cssClip.top
]]);
}
container.clip = hasParentClip(container) ? container.parent.clip.concat(clip) : clip;
container.backgroundClip = (container.css('overflow') !== "hidden") ? container.clip.concat([container.borders.clip]) : container.clip;
if (isPseudoElement(container)) {
container.cleanDOM();
}
} else if (isTextNode(container)) {
container.clip = hasParentClip(container) ? container.parent.clip : [];
}
if (!isPseudoElement(container)) {
container.bounds = null;
}
}, this);
};
function hasParentClip(container) {
return container.parent && container.parent.clip.length;
}
NodeParser.prototype.asyncRenderer = function(queue, resolve, asyncTimer) {
asyncTimer = asyncTimer || Date.now();
this.paint(queue[this.renderIndex++]);
if (queue.length === this.renderIndex) {
resolve();
} else if (asyncTimer + 20 > Date.now()) {
this.asyncRenderer(queue, resolve, asyncTimer);
} else {
setTimeout(bind(function() {
this.asyncRenderer(queue, resolve);
}, this), 0);
}
};
NodeParser.prototype.createPseudoHideStyles = function(document) {
this.createStyles(document, '.' + PseudoElementContainer.prototype.PSEUDO_HIDE_ELEMENT_CLASS_BEFORE + ':before { content: "" !important; display: none !important; }' +
'.' + PseudoElementContainer.prototype.PSEUDO_HIDE_ELEMENT_CLASS_AFTER + ':after { content: "" !important; display: none !important; }');
};
NodeParser.prototype.disableAnimations = function(document) {
this.createStyles(document, '* { -webkit-animation: none !important; -moz-animation: none !important; -o-animation: none !important; animation: none !important; ' +
'-webkit-transition: none !important; -moz-transition: none !important; -o-transition: none !important; transition: none !important;}');
};
NodeParser.prototype.createStyles = function(document, styles) {
var hidePseudoElements = document.createElement('style');
hidePseudoElements.innerHTML = styles;
document.body.appendChild(hidePseudoElements);
};
NodeParser.prototype.getPseudoElements = function(container) {
var nodes = [[container]];
if (container.node.nodeType === Node.ELEMENT_NODE) {
var before = this.getPseudoElement(container, ":before");
var after = this.getPseudoElement(container, ":after");
if (before) {
nodes.push(before);
}
if (after) {
nodes.push(after);
}
}
return flatten(nodes);
};
function toCamelCase(str) {
return str.replace(/(\-[a-z])/g, function(match){
return match.toUpperCase().replace('-','');
});
}
NodeParser.prototype.getPseudoElement = function(container, type) {
var style = container.computedStyle(type);
if(!style || !style.content || style.content === "none" || style.content === "-moz-alt-content" || style.display === "none") {
return null;
}
var content = stripQuotes(style.content);
var isImage = content.substr(0, 3) === 'url';
var pseudoNode = document.createElement(isImage ? 'img' : 'html2canvaspseudoelement');
var pseudoContainer = new PseudoElementContainer(pseudoNode, container, type);
for (var i = style.length-1; i >= 0; i--) {
var property = toCamelCase(style.item(i));
pseudoNode.style[property] = style[property];
}
pseudoNode.className = PseudoElementContainer.prototype.PSEUDO_HIDE_ELEMENT_CLASS_BEFORE + " " + PseudoElementContainer.prototype.PSEUDO_HIDE_ELEMENT_CLASS_AFTER;
if (isImage) {
pseudoNode.src = parseBackgrounds(content)[0].args[0];
return [pseudoContainer];
} else {
var text = document.createTextNode(content);
pseudoNode.appendChild(text);
return [pseudoContainer, new TextContainer(text, pseudoContainer)];
}
};
NodeParser.prototype.getChildren = function(parentContainer) {
return flatten([].filter.call(parentContainer.node.childNodes, renderableNode).map(function(node) {
var container = [node.nodeType === Node.TEXT_NODE ? new TextContainer(node, parentContainer) : new NodeContainer(node, parentContainer)].filter(nonIgnoredElement);
return node.nodeType === Node.ELEMENT_NODE && container.length && node.tagName !== "TEXTAREA" ? (container[0].isElementVisible() ? container.concat(this.getChildren(container[0])) : []) : container;
}, this));
};
NodeParser.prototype.newStackingContext = function(container, hasOwnStacking) {
var stack = new StackingContext(hasOwnStacking, container.getOpacity(), container.node, container.parent);
container.cloneTo(stack);
var parentStack = hasOwnStacking ? stack.getParentStack(this) : stack.parent.stack;
parentStack.contexts.push(stack);
container.stack = stack;
};
NodeParser.prototype.createStackingContexts = function() {
this.nodes.forEach(function(container) {
if (isElement(container) && (this.isRootElement(container) || hasOpacity(container) || isPositionedForStacking(container) || this.isBodyWithTransparentRoot(container) || container.hasTransform())) {
this.newStackingContext(container, true);
} else if (isElement(container) && ((isPositioned(container) && zIndex0(container)) || isInlineBlock(container) || isFloating(container))) {
this.newStackingContext(container, false);
} else {
container.assignStack(container.parent.stack);
}
}, this);
};
NodeParser.prototype.isBodyWithTransparentRoot = function(container) {
return container.node.nodeName === "BODY" && container.parent.color('backgroundColor').isTransparent();
};
NodeParser.prototype.isRootElement = function(container) {
return container.parent === null;
};
NodeParser.prototype.sortStackingContexts = function(stack) {
stack.contexts.sort(zIndexSort(stack.contexts.slice(0)));
stack.contexts.forEach(this.sortStackingContexts, this);
};
NodeParser.prototype.parseTextBounds = function(container) {
return function(text, index, textList) {
if (container.parent.css("textDecoration").substr(0, 4) !== "none" || text.trim().length !== 0) {
if (this.support.rangeBounds && !container.parent.hasTransform()) {
var offset = textList.slice(0, index).join("").length;
return this.getRangeBounds(container.node, offset, text.length);
} else if (container.node && typeof(container.node.data) === "string") {
var replacementNode = container.node.splitText(text.length);
var bounds = this.getWrapperBounds(container.node, container.parent.hasTransform());
container.node = replacementNode;
return bounds;
}
} else if(!this.support.rangeBounds || container.parent.hasTransform()){
container.node = container.node.splitText(text.length);
}
return {};
};
};
NodeParser.prototype.getWrapperBounds = function(node, transform) {
var wrapper = node.ownerDocument.createElement('html2canvaswrapper');
var parent = node.parentNode,
backupText = node.cloneNode(true);
wrapper.appendChild(node.cloneNode(true));
parent.replaceChild(wrapper, node);
var bounds = transform ? offsetBounds(wrapper) : getBounds(wrapper);
parent.replaceChild(backupText, wrapper);
return bounds;
};
NodeParser.prototype.getRangeBounds = function(node, offset, length) {
var range = this.range || (this.range = node.ownerDocument.createRange());
range.setStart(node, offset);
range.setEnd(node, offset + length);
return range.getBoundingClientRect();
};
function ClearTransform() {}
NodeParser.prototype.parse = function(stack) {
// http://www.w3.org/TR/CSS21/visuren.html#z-index
var negativeZindex = stack.contexts.filter(negativeZIndex); // 2. the child stacking contexts with negative stack levels (most negative first).
var descendantElements = stack.children.filter(isElement);
var descendantNonFloats = descendantElements.filter(not(isFloating));
var nonInlineNonPositionedDescendants = descendantNonFloats.filter(not(isPositioned)).filter(not(inlineLevel)); // 3 the in-flow, non-inline-level, non-positioned descendants.
var nonPositionedFloats = descendantElements.filter(not(isPositioned)).filter(isFloating); // 4. the non-positioned floats.
var inFlow = descendantNonFloats.filter(not(isPositioned)).filter(inlineLevel); // 5. the in-flow, inline-level, non-positioned descendants, including inline tables and inline blocks.
var stackLevel0 = stack.contexts.concat(descendantNonFloats.filter(isPositioned)).filter(zIndex0); // 6. the child stacking contexts with stack level 0 and the positioned descendants with stack level 0.
var text = stack.children.filter(isTextNode).filter(hasText);
var positiveZindex = stack.contexts.filter(positiveZIndex); // 7. the child stacking contexts with positive stack levels (least positive first).
negativeZindex.concat(nonInlineNonPositionedDescendants).concat(nonPositionedFloats)
.concat(inFlow).concat(stackLevel0).concat(text).concat(positiveZindex).forEach(function(container) {
this.renderQueue.push(container);
if (isStackingContext(container)) {
this.parse(container);
this.renderQueue.push(new ClearTransform());
}
}, this);
};
NodeParser.prototype.paint = function(container) {
try {
if (container instanceof ClearTransform) {
this.renderer.ctx.restore();
} else if (isTextNode(container)) {
if (isPseudoElement(container.parent)) {
container.parent.appendToDOM();
}
this.paintText(container);
if (isPseudoElement(container.parent)) {
container.parent.cleanDOM();
}
} else {
this.paintNode(container);
}
} catch(e) {
log(e);
if (this.options.strict) {
throw e;
}
}
};
NodeParser.prototype.paintNode = function(container) {
if (isStackingContext(container)) {
this.renderer.setOpacity(container.opacity);
this.renderer.ctx.save();
if (container.hasTransform()) {
this.renderer.setTransform(container.parseTransform());
}
}
if (container.node.nodeName === "INPUT" && container.node.type === "checkbox") {
this.paintCheckbox(container);
} else if (container.node.nodeName === "INPUT" && container.node.type === "radio") {
this.paintRadio(container);
} else {
this.paintElement(container);
}
};
NodeParser.prototype.paintElement = function(container) {
var bounds = container.parseBounds();
this.renderer.clip(container.backgroundClip, function() {
this.renderer.renderBackground(container, bounds, container.borders.borders.map(getWidth));
}, this);
this.renderer.clip(container.clip, function() {
this.renderer.renderBorders(container.borders.borders);
}, this);
this.renderer.clip(container.backgroundClip, function() {
switch (container.node.nodeName) {
case "svg":
case "IFRAME":
var imgContainer = this.images.get(container.node);
if (imgContainer) {
this.renderer.renderImage(container, bounds, container.borders, imgContainer);
} else {
log("Error loading <" + container.node.nodeName + ">", container.node);
}
break;
case "IMG":
var imageContainer = this.images.get(container.node.src);
if (imageContainer) {
this.renderer.renderImage(container, bounds, container.borders, imageContainer);
} else {
log("Error loading <img>", container.node.src);
}
break;
case "CANVAS":
this.renderer.renderImage(container, bounds, container.borders, {image: container.node});
break;
case "SELECT":
case "INPUT":
case "TEXTAREA":
this.paintFormValue(container);
break;
}
}, this);
};
NodeParser.prototype.paintCheckbox = function(container) {
var b = container.parseBounds();
var size = Math.min(b.width, b.height);
var bounds = {width: size - 1, height: size - 1, top: b.top, left: b.left};
var r = [3, 3];
var radius = [r, r, r, r];
var borders = [1,1,1,1].map(function(w) {
return {color: new Color('#A5A5A5'), width: w};
});
var borderPoints = calculateCurvePoints(bounds, radius, borders);
this.renderer.clip(container.backgroundClip, function() {
this.renderer.rectangle(bounds.left + 1, bounds.top + 1, bounds.width - 2, bounds.height - 2, new Color("#DEDEDE"));
this.renderer.renderBorders(calculateBorders(borders, bounds, borderPoints, radius));
if (container.node.checked) {
this.renderer.font(new Color('#424242'), 'normal', 'normal', 'bold', (size - 3) + "px", 'arial');
this.renderer.text("\u2714", bounds.left + size / 6, bounds.top + size - 1);
}
}, this);
};
NodeParser.prototype.paintRadio = function(container) {
var bounds = container.parseBounds();
var size = Math.min(bounds.width, bounds.height) - 2;
this.renderer.clip(container.backgroundClip, function() {
this.renderer.circleStroke(bounds.left + 1, bounds.top + 1, size, new Color('#DEDEDE'), 1, new Color('#A5A5A5'));
if (container.node.checked) {
this.renderer.circle(Math.ceil(bounds.left + size / 4) + 1, Math.ceil(bounds.top + size / 4) + 1, Math.floor(size / 2), new Color('#424242'));
}
}, this);
};
NodeParser.prototype.paintFormValue = function(container) {
var value = container.getValue();
if (value.length > 0) {
var document = container.node.ownerDocument;
var wrapper = document.createElement('html2canvaswrapper');
var properties = ['lineHeight', 'textAlign', 'fontFamily', 'fontWeight', 'fontSize', 'color',
'paddingLeft', 'paddingTop', 'paddingRight', 'paddingBottom',
'width', 'height', 'borderLeftStyle', 'borderTopStyle', 'borderLeftWidth', 'borderTopWidth',
'boxSizing', 'whiteSpace', 'wordWrap'];
properties.forEach(function(property) {
try {
wrapper.style[property] = container.css(property);
} catch(e) {
// Older IE has issues with "border"
log("html2canvas: Parse: Exception caught in renderFormValue: " + e.message);
}
});
var bounds = container.parseBounds();
wrapper.style.position = "fixed";
wrapper.style.left = bounds.left + "px";
wrapper.style.top = bounds.top + "px";
wrapper.textContent = value;
document.body.appendChild(wrapper);
this.paintText(new TextContainer(wrapper.firstChild, container));
document.body.removeChild(wrapper);
}
};
NodeParser.prototype.paintText = function(container) {
container.applyTextTransform();
var characters = window.html2canvas.punycode.ucs2.decode(container.node.data);
var textList = (!this.options.letterRendering || noLetterSpacing(container)) && !hasUnicode(container.node.data) ? getWords(characters) : characters.map(function(character) {
return window.html2canvas.punycode.ucs2.encode([character]);
});
var weight = container.parent.fontWeight();
var size = container.parent.css('fontSize');
var family = container.parent.css('fontFamily');
var shadows = container.parent.parseTextShadows();
this.renderer.font(container.parent.color('color'), container.parent.css('fontStyle'), container.parent.css('fontVariant'), weight, size, family);
if (shadows.length) {
// TODO: support multiple text shadows
this.renderer.fontShadow(shadows[0].color, shadows[0].offsetX, shadows[0].offsetY, shadows[0].blur);
} else {
this.renderer.clearShadow();
}
this.renderer.clip(container.parent.clip, function() {
textList.map(this.parseTextBounds(container), this).forEach(function(bounds, index) {
if (bounds) {
this.renderer.text(textList[index], bounds.left, bounds.bottom);
this.renderTextDecoration(container.parent, bounds, this.fontMetrics.getMetrics(family, size));
}
}, this);
}, this);
};
NodeParser.prototype.renderTextDecoration = function(container, bounds, metrics) {
switch(container.css("textDecoration").split(" ")[0]) {
case "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
this.renderer.rectangle(bounds.left, Math.round(bounds.top + metrics.baseline + metrics.lineWidth), bounds.width, 1, container.color("color"));
break;
case "overline":
this.renderer.rectangle(bounds.left, Math.round(bounds.top), bounds.width, 1, container.color("color"));
break;
case "line-through":
// TODO try and find exact position for line-through
this.renderer.rectangle(bounds.left, Math.ceil(bounds.top + metrics.middle + metrics.lineWidth), bounds.width, 1, container.color("color"));
break;
}
};
var borderColorTransforms = {
inset: [
["darken", 0.60],
["darken", 0.10],
["darken", 0.10],
["darken", 0.60]
]
};
NodeParser.prototype.parseBorders = function(container) {
var nodeBounds = container.parseBounds();
var radius = getBorderRadiusData(container);
var borders = ["Top", "Right", "Bottom", "Left"].map(function(side, index) {
var style = container.css('border' + side + 'Style');
var color = container.color('border' + side + 'Color');
if (style === "inset" && color.isBlack()) {
color = new Color([255, 255, 255, color.a]); // this is wrong, but
}
var colorTransform = borderColorTransforms[style] ? borderColorTransforms[style][index] : null;
return {
width: container.cssInt('border' + side + 'Width'),
color: colorTransform ? color[colorTransform[0]](colorTransform[1]) : color,
args: null
};
});
var borderPoints = calculateCurvePoints(nodeBounds, radius, borders);
return {
clip: this.parseBackgroundClip(container, borderPoints, borders, radius, nodeBounds),
borders: calculateBorders(borders, nodeBounds, borderPoints, radius)
};
};
function calculateBorders(borders, nodeBounds, borderPoints, radius) {
return borders.map(function(border, borderSide) {
if (border.width > 0) {
var bx = nodeBounds.left;
var by = nodeBounds.top;
var bw = nodeBounds.width;
var bh = nodeBounds.height - (borders[2].width);
switch(borderSide) {
case 0:
// top border
bh = borders[0].width;
border.args = drawSide({
c1: [bx, by],
c2: [bx + bw, by],
c3: [bx + bw - borders[1].width, by + bh],
c4: [bx + borders[3].width, by + bh]
}, radius[0], radius[1],
borderPoints.topLeftOuter, borderPoints.topLeftInner, borderPoints.topRightOuter, borderPoints.topRightInner);
break;
case 1:
// right border
bx = nodeBounds.left + nodeBounds.width - (borders[1].width);
bw = borders[1].width;
border.args = drawSide({
c1: [bx + bw, by],
c2: [bx + bw, by + bh + borders[2].width],
c3: [bx, by + bh],
c4: [bx, by + borders[0].width]
}, radius[1], radius[2],
borderPoints.topRightOuter, borderPoints.topRightInner, borderPoints.bottomRightOuter, borderPoints.bottomRightInner);
break;
case 2:
// bottom border
by = (by + nodeBounds.height) - (borders[2].width);
bh = borders[2].width;
border.args = drawSide({
c1: [bx + bw, by + bh],
c2: [bx, by + bh],
c3: [bx + borders[3].width, by],
c4: [bx + bw - borders[3].width, by]
}, radius[2], radius[3],
borderPoints.bottomRightOuter, borderPoints.bottomRightInner, borderPoints.bottomLeftOuter, borderPoints.bottomLeftInner);
break;
case 3:
// left border
bw = borders[3].width;
border.args = drawSide({
c1: [bx, by + bh + borders[2].width],
c2: [bx, by],
c3: [bx + bw, by + borders[0].width],
c4: [bx + bw, by + bh]
}, radius[3], radius[0],
borderPoints.bottomLeftOuter, borderPoints.bottomLeftInner, borderPoints.topLeftOuter, borderPoints.topLeftInner);
break;
}
}
return border;
});
}
NodeParser.prototype.parseBackgroundClip = function(container, borderPoints, borders, radius, bounds) {
var backgroundClip = container.css('backgroundClip'),
borderArgs = [];
switch(backgroundClip) {
case "content-box":
case "padding-box":
parseCorner(borderArgs, radius[0], radius[1], borderPoints.topLeftInner, borderPoints.topRightInner, bounds.left + borders[3].width, bounds.top + borders[0].width);
parseCorner(borderArgs, radius[1], radius[2], borderPoints.topRightInner, borderPoints.bottomRightInner, bounds.left + bounds.width - borders[1].width, bounds.top + borders[0].width);
parseCorner(borderArgs, radius[2], radius[3], borderPoints.bottomRightInner, borderPoints.bottomLeftInner, bounds.left + bounds.width - borders[1].width, bounds.top + bounds.height - borders[2].width);
parseCorner(borderArgs, radius[3], radius[0], borderPoints.bottomLeftInner, borderPoints.topLeftInner, bounds.left + borders[3].width, bounds.top + bounds.height - borders[2].width);
break;
default:
parseCorner(borderArgs, radius[0], radius[1], borderPoints.topLeftOuter, borderPoints.topRightOuter, bounds.left, bounds.top);
parseCorner(borderArgs, radius[1], radius[2], borderPoints.topRightOuter, borderPoints.bottomRightOuter, bounds.left + bounds.width, bounds.top);
parseCorner(borderArgs, radius[2], radius[3], borderPoints.bottomRightOuter, borderPoints.bottomLeftOuter, bounds.left + bounds.width, bounds.top + bounds.height);
parseCorner(borderArgs, radius[3], radius[0], borderPoints.bottomLeftOuter, borderPoints.topLeftOuter, bounds.left, bounds.top + bounds.height);
break;
}
return borderArgs;
};
function getCurvePoints(x, y, r1, r2) {
var kappa = 4 * ((Math.sqrt(2) - 1) / 3);
var ox = (r1) * kappa, // control point offset horizontal
oy = (r2) * kappa, // control point offset vertical
xm = x + r1, // x-middle
ym = y + r2; // y-middle
return {
topLeft: bezierCurve({x: x, y: ym}, {x: x, y: ym - oy}, {x: xm - ox, y: y}, {x: xm, y: y}),
topRight: bezierCurve({x: x, y: y}, {x: x + ox,y: y}, {x: xm, y: ym - oy}, {x: xm, y: ym}),
bottomRight: bezierCurve({x: xm, y: y}, {x: xm, y: y + oy}, {x: x + ox, y: ym}, {x: x, y: ym}),
bottomLeft: bezierCurve({x: xm, y: ym}, {x: xm - ox, y: ym}, {x: x, y: y + oy}, {x: x, y:y})
};
}
function calculateCurvePoints(bounds, borderRadius, borders) {
var x = bounds.left,
y = bounds.top,
width = bounds.width,
height = bounds.height,
tlh = borderRadius[0][0],
tlv = borderRadius[0][1],
trh = borderRadius[1][0],
trv = borderRadius[1][1],
brh = borderRadius[2][0],
brv = borderRadius[2][1],
blh = borderRadius[3][0],
blv = borderRadius[3][1];
var topWidth = width - trh,
rightHeight = height - brv,
bottomWidth = width - brh,
leftHeight = height - blv;
return {
topLeftOuter: getCurvePoints(x, y, tlh, tlv).topLeft.subdivide(0.5),
topLeftInner: getCurvePoints(x + borders[3].width, y + borders[0].width, Math.max(0, tlh - borders[3].width), Math.max(0, tlv - borders[0].width)).topLeft.subdivide(0.5),
topRightOuter: getCurvePoints(x + topWidth, y, trh, trv).topRight.subdivide(0.5),
topRightInner: getCurvePoints(x + Math.min(topWidth, width + borders[3].width), y + borders[0].width, (topWidth > width + borders[3].width) ? 0 :trh - borders[3].width, trv - borders[0].width).topRight.subdivide(0.5),
bottomRightOuter: getCurvePoints(x + bottomWidth, y + rightHeight, brh, brv).bottomRight.subdivide(0.5),
bottomRightInner: getCurvePoints(x + Math.min(bottomWidth, width - borders[3].width), y + Math.min(rightHeight, height + borders[0].width), Math.max(0, brh - borders[1].width), brv - borders[2].width).bottomRight.subdivide(0.5),
bottomLeftOuter: getCurvePoints(x, y + leftHeight, blh, blv).bottomLeft.subdivide(0.5),
bottomLeftInner: getCurvePoints(x + borders[3].width, y + leftHeight, Math.max(0, blh - borders[3].width), blv - borders[2].width).bottomLeft.subdivide(0.5)
};
}
function bezierCurve(start, startControl, endControl, end) {
var lerp = function (a, b, t) {
return {
x: a.x + (b.x - a.x) * t,
y: a.y + (b.y - a.y) * t
};
};
return {
start: start,
startControl: startControl,
endControl: endControl,
end: end,
subdivide: function(t) {
var ab = lerp(start, startControl, t),
bc = lerp(startControl, endControl, t),
cd = lerp(endControl, end, t),
abbc = lerp(ab, bc, t),
bccd = lerp(bc, cd, t),
dest = lerp(abbc, bccd, t);
return [bezierCurve(start, ab, abbc, dest), bezierCurve(dest, bccd, cd, end)];
},
curveTo: function(borderArgs) {
borderArgs.push(["bezierCurve", startControl.x, startControl.y, endControl.x, endControl.y, end.x, end.y]);
},
curveToReversed: function(borderArgs) {
borderArgs.push(["bezierCurve", endControl.x, endControl.y, startControl.x, startControl.y, start.x, start.y]);
}
};
}
function drawSide(borderData, radius1, radius2, outer1, inner1, outer2, inner2) {
var borderArgs = [];
if (radius1[0] > 0 || radius1[1] > 0) {
borderArgs.push(["line", outer1[1].start.x, outer1[1].start.y]);
outer1[1].curveTo(borderArgs);
} else {
borderArgs.push([ "line", borderData.c1[0], borderData.c1[1]]);
}
if (radius2[0] > 0 || radius2[1] > 0) {
borderArgs.push(["line", outer2[0].start.x, outer2[0].start.y]);
outer2[0].curveTo(borderArgs);
borderArgs.push(["line", inner2[0].end.x, inner2[0].end.y]);
inner2[0].curveToReversed(borderArgs);
} else {
borderArgs.push(["line", borderData.c2[0], borderData.c2[1]]);
borderArgs.push(["line", borderData.c3[0], borderData.c3[1]]);
}
if (radius1[0] > 0 || radius1[1] > 0) {
borderArgs.push(["line", inner1[1].end.x, inner1[1].end.y]);
inner1[1].curveToReversed(borderArgs);
} else {
borderArgs.push(["line", borderData.c4[0], borderData.c4[1]]);
}
return borderArgs;
}
function parseCorner(borderArgs, radius1, radius2, corner1, corner2, x, y) {
if (radius1[0] > 0 || radius1[1] > 0) {
borderArgs.push(["line", corner1[0].start.x, corner1[0].start.y]);
corner1[0].curveTo(borderArgs);
corner1[1].curveTo(borderArgs);
} else {
borderArgs.push(["line", x, y]);
}
if (radius2[0] > 0 || radius2[1] > 0) {
borderArgs.push(["line", corner2[0].start.x, corner2[0].start.y]);
}
}
function negativeZIndex(container) {
return container.cssInt("zIndex") < 0;
}
function positiveZIndex(container) {
return container.cssInt("zIndex") > 0;
}
function zIndex0(container) {
return container.cssInt("zIndex") === 0;
}
function inlineLevel(container) {
return ["inline", "inline-block", "inline-table"].indexOf(container.css("display")) !== -1;
}
function isStackingContext(container) {
return (container instanceof StackingContext);
}
function hasText(container) {
return container.node.data.trim().length > 0;
}
function noLetterSpacing(container) {
return (/^(normal|none|0px)$/.test(container.parent.css("letterSpacing")));
}
function getBorderRadiusData(container) {
return ["TopLeft", "TopRight", "BottomRight", "BottomLeft"].map(function(side) {
var value = container.css('border' + side + 'Radius');
var arr = value.split(" ");
if (arr.length <= 1) {
arr[1] = arr[0];
}
return arr.map(asInt);
});
}
function renderableNode(node) {
return (node.nodeType === Node.TEXT_NODE || node.nodeType === Node.ELEMENT_NODE);
}
function isPositionedForStacking(container) {
var position = container.css("position");
var zIndex = (["absolute", "relative", "fixed"].indexOf(position) !== -1) ? container.css("zIndex") : "auto";
return zIndex !== "auto";
}
function isPositioned(container) {
return container.css("position") !== "static";
}
function isFloating(container) {
return container.css("float") !== "none";
}
function isInlineBlock(container) {
return ["inline-block", "inline-table"].indexOf(container.css("display")) !== -1;
}
function not(callback) {
var context = this;
return function() {
return !callback.apply(context, arguments);
};
}
function isElement(container) {
return container.node.nodeType === Node.ELEMENT_NODE;
}
function isPseudoElement(container) {
return container.isPseudoElement === true;
}
function isTextNode(container) {
return container.node.nodeType === Node.TEXT_NODE;
}
function zIndexSort(contexts) {
return function(a, b) {
return (a.cssInt("zIndex") + (contexts.indexOf(a) / contexts.length)) - (b.cssInt("zIndex") + (contexts.indexOf(b) / contexts.length));
};
}
function hasOpacity(container) {
return container.getOpacity() < 1;
}
function bind(callback, context) {
return function() {
return callback.apply(context, arguments);
};
}
function asInt(value) {
return parseInt(value, 10);
}
function getWidth(border) {
return border.width;
}
function nonIgnoredElement(nodeContainer) {
return (nodeContainer.node.nodeType !== Node.ELEMENT_NODE || ["SCRIPT", "HEAD", "TITLE", "OBJECT", "BR", "OPTION"].indexOf(nodeContainer.node.nodeName) === -1);
}
function flatten(arrays) {
return [].concat.apply([], arrays);
}
function stripQuotes(content) {
var first = content.substr(0, 1);
return (first === content.substr(content.length - 1) && first.match(/'|"/)) ? content.substr(1, content.length - 2) : content;
}
function getWords(characters) {
var words = [], i = 0, onWordBoundary = false, word;
while(characters.length) {
if (isWordBoundary(characters[i]) === onWordBoundary) {
word = characters.splice(0, i);
if (word.length) {
words.push(window.html2canvas.punycode.ucs2.encode(word));
}
onWordBoundary =! onWordBoundary;
i = 0;
} else {
i++;
}
if (i >= characters.length) {
word = characters.splice(0, i);
if (word.length) {
words.push(window.html2canvas.punycode.ucs2.encode(word));
}
}
}
return words;
}
function isWordBoundary(characterCode) {
return [
32, // <space>
13, // \r
10, // \n
9, // \t
45 // -
].indexOf(characterCode) !== -1;
}
function hasUnicode(string) {
return (/[^\u0000-\u00ff]/).test(string);
}

475
src/parsing/background.js Normal file
View 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
View 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
};
});
};

View 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
View 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
View 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
View 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
};
};

View File

@ -0,0 +1,10 @@
/* @flow */
'use strict';
export const parseLetterSpacing = (letterSpacing: string): number => {
if (letterSpacing === 'normal') {
return 0;
}
const value = parseFloat(letterSpacing);
return isNaN(value) ? 0 : value;
};

25
src/parsing/overflow.js Normal file
View 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
View 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
View 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;
};

View 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
View 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;
};

View 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
View 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
View 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
View 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)
};
};

View File

@ -1,22 +0,0 @@
/*!
* @overview es6-promise - a tiny implementation of Promises/A+.
* @copyright Copyright (c) 2014 Yehuda Katz, Tom Dale, Stefan Penner and contributors (Conversion to ES6 API by Jake Archibald)
* @license Licensed under MIT license
* See https://raw.githubusercontent.com/jakearchibald/es6-promise/master/LICENSE
* @version 2.0.1
*/
(function(){function r(a,b){n[l]=a;n[l+1]=b;l+=2;2===l&&A()}function s(a){return"function"===typeof a}function F(){return function(){process.nextTick(t)}}function G(){var a=0,b=new B(t),c=document.createTextNode("");b.observe(c,{characterData:!0});return function(){c.data=a=++a%2}}function H(){var a=new MessageChannel;a.port1.onmessage=t;return function(){a.port2.postMessage(0)}}function I(){return function(){setTimeout(t,1)}}function t(){for(var a=0;a<l;a+=2)(0,n[a])(n[a+1]),n[a]=void 0,n[a+1]=void 0;
l=0}function p(){}function J(a,b,c,d){try{a.call(b,c,d)}catch(e){return e}}function K(a,b,c){r(function(a){var e=!1,f=J(c,b,function(c){e||(e=!0,b!==c?q(a,c):m(a,c))},function(b){e||(e=!0,g(a,b))});!e&&f&&(e=!0,g(a,f))},a)}function L(a,b){1===b.a?m(a,b.b):2===a.a?g(a,b.b):u(b,void 0,function(b){q(a,b)},function(b){g(a,b)})}function q(a,b){if(a===b)g(a,new TypeError("You cannot resolve a promise with itself"));else if("function"===typeof b||"object"===typeof b&&null!==b)if(b.constructor===a.constructor)L(a,
b);else{var c;try{c=b.then}catch(d){v.error=d,c=v}c===v?g(a,v.error):void 0===c?m(a,b):s(c)?K(a,b,c):m(a,b)}else m(a,b)}function M(a){a.f&&a.f(a.b);x(a)}function m(a,b){void 0===a.a&&(a.b=b,a.a=1,0!==a.e.length&&r(x,a))}function g(a,b){void 0===a.a&&(a.a=2,a.b=b,r(M,a))}function u(a,b,c,d){var e=a.e,f=e.length;a.f=null;e[f]=b;e[f+1]=c;e[f+2]=d;0===f&&a.a&&r(x,a)}function x(a){var b=a.e,c=a.a;if(0!==b.length){for(var d,e,f=a.b,g=0;g<b.length;g+=3)d=b[g],e=b[g+c],d?C(c,d,e,f):e(f);a.e.length=0}}function D(){this.error=
null}function C(a,b,c,d){var e=s(c),f,k,h,l;if(e){try{f=c(d)}catch(n){y.error=n,f=y}f===y?(l=!0,k=f.error,f=null):h=!0;if(b===f){g(b,new TypeError("A promises callback cannot return that same promise."));return}}else f=d,h=!0;void 0===b.a&&(e&&h?q(b,f):l?g(b,k):1===a?m(b,f):2===a&&g(b,f))}function N(a,b){try{b(function(b){q(a,b)},function(b){g(a,b)})}catch(c){g(a,c)}}function k(a,b,c,d){this.n=a;this.c=new a(p,d);this.i=c;this.o(b)?(this.m=b,this.d=this.length=b.length,this.l(),0===this.length?m(this.c,
this.b):(this.length=this.length||0,this.k(),0===this.d&&m(this.c,this.b))):g(this.c,this.p())}function h(a){O++;this.b=this.a=void 0;this.e=[];if(p!==a){if(!s(a))throw new TypeError("You must pass a resolver function as the first argument to the promise constructor");if(!(this instanceof h))throw new TypeError("Failed to construct 'Promise': Please use the 'new' operator, this object constructor cannot be called as a function.");N(this,a)}}var E=Array.isArray?Array.isArray:function(a){return"[object Array]"===
Object.prototype.toString.call(a)},l=0,w="undefined"!==typeof window?window:{},B=w.MutationObserver||w.WebKitMutationObserver,w="undefined"!==typeof Uint8ClampedArray&&"undefined"!==typeof importScripts&&"undefined"!==typeof MessageChannel,n=Array(1E3),A;A="undefined"!==typeof process&&"[object process]"==={}.toString.call(process)?F():B?G():w?H():I();var v=new D,y=new D;k.prototype.o=function(a){return E(a)};k.prototype.p=function(){return Error("Array Methods must be provided an Array")};k.prototype.l=
function(){this.b=Array(this.length)};k.prototype.k=function(){for(var a=this.length,b=this.c,c=this.m,d=0;void 0===b.a&&d<a;d++)this.j(c[d],d)};k.prototype.j=function(a,b){var c=this.n;"object"===typeof a&&null!==a?a.constructor===c&&void 0!==a.a?(a.f=null,this.g(a.a,b,a.b)):this.q(c.resolve(a),b):(this.d--,this.b[b]=this.h(a))};k.prototype.g=function(a,b,c){var d=this.c;void 0===d.a&&(this.d--,this.i&&2===a?g(d,c):this.b[b]=this.h(c));0===this.d&&m(d,this.b)};k.prototype.h=function(a){return a};
k.prototype.q=function(a,b){var c=this;u(a,void 0,function(a){c.g(1,b,a)},function(a){c.g(2,b,a)})};var O=0;h.all=function(a,b){return(new k(this,a,!0,b)).c};h.race=function(a,b){function c(a){q(e,a)}function d(a){g(e,a)}var e=new this(p,b);if(!E(a))return (g(e,new TypeError("You must pass an array to race.")), e);for(var f=a.length,h=0;void 0===e.a&&h<f;h++)u(this.resolve(a[h]),void 0,c,d);return e};h.resolve=function(a,b){if(a&&"object"===typeof a&&a.constructor===this)return a;var c=new this(p,b);
q(c,a);return c};h.reject=function(a,b){var c=new this(p,b);g(c,a);return c};h.prototype={constructor:h,then:function(a,b){var c=this.a;if(1===c&&!a||2===c&&!b)return this;var d=new this.constructor(p),e=this.b;if(c){var f=arguments[c-1];r(function(){C(c,d,f,e)})}else u(this,d,a,b);return d},"catch":function(a){return this.then(null,a)}};var z={Promise:h,polyfill:function(){var a;a="undefined"!==typeof global?global:"undefined"!==typeof window&&window.document?window:self;"Promise"in a&&"resolve"in
a.Promise&&"reject"in a.Promise&&"all"in a.Promise&&"race"in a.Promise&&function(){var b;new a.Promise(function(a){b=a});return s(b)}()||(a.Promise=h)}};"function"===typeof define&&define.amd?define(function(){return z}):"undefined"!==typeof module&&module.exports?module.exports=z:"undefined"!==typeof this&&(this.ES6Promise=z);}).call(window);
if (window) {
window.ES6Promise.polyfill();
}

View File

@ -1,51 +0,0 @@
function Proxy(src, proxyUrl, document) {
if (!proxyUrl) {
return Promise.reject("No proxy configured");
}
var callback = createCallback(supportsCORS);
var url = createProxyUrl(proxyUrl, src, callback);
return supportsCORS ? XHR(url) : (jsonp(document, url, callback).then(function(response) {
return decode64(response.content);
}));
}
var proxyCount = 0;
var supportsCORS = ('withCredentials' in new XMLHttpRequest());
var supportsCORSImage = ('crossOrigin' in new Image());
function ProxyURL(src, proxyUrl, document) {
var callback = createCallback(supportsCORSImage);
var url = createProxyUrl(proxyUrl, src, callback);
return (supportsCORSImage ? Promise.resolve(url) : jsonp(document, url, callback).then(function(response) {
return "data:" + response.type + ";base64," + response.content;
}));
}
function jsonp(document, url, callback) {
return new Promise(function(resolve, reject) {
var s = document.createElement("script");
var cleanup = function() {
delete window.html2canvas.proxy[callback];
document.body.removeChild(s);
};
window.html2canvas.proxy[callback] = function(response) {
cleanup();
resolve(response);
};
s.src = url;
s.onerror = function(e) {
cleanup();
reject(e);
};
document.body.appendChild(s);
});
}
function createCallback(useCORS) {
return !useCORS ? "html2canvas_" + Date.now() + "_" + (++proxyCount) + "_" + Math.round(Math.random() * 100000) : "";
}
function createProxyUrl(proxyUrl, src, callback) {
return proxyUrl + "?url=" + encodeURIComponent(src) + (callback.length ? "&callback=html2canvas.proxy." + callback : "");
}

View File

@ -1,18 +0,0 @@
function ProxyImageContainer(src, proxy) {
var script = document.createElement("script");
var link = document.createElement("a");
link.href = src;
src = link.href;
this.src = src;
this.image = new Image();
var self = this;
this.promise = new Promise(function(resolve, reject) {
self.image.crossOrigin = "Anonymous";
self.image.onload = resolve;
self.image.onerror = reject;
new ProxyURL(src, proxy, document).then(function(url) {
self.image.src = url;
})['catch'](reject);
});
}

View File

@ -1,34 +0,0 @@
function PseudoElementContainer(node, parent, type) {
NodeContainer.call(this, node, parent);
this.isPseudoElement = true;
this.before = type === ":before";
}
PseudoElementContainer.prototype.cloneTo = function(stack) {
PseudoElementContainer.prototype.cloneTo.call(this, stack);
stack.isPseudoElement = true;
stack.before = this.before;
};
PseudoElementContainer.prototype = Object.create(NodeContainer.prototype);
PseudoElementContainer.prototype.appendToDOM = function() {
if (this.before) {
this.parent.node.insertBefore(this.node, this.parent.node.firstChild);
} else {
this.parent.node.appendChild(this.node);
}
this.parent.node.className += " " + this.getHideClass();
};
PseudoElementContainer.prototype.cleanDOM = function() {
this.node.parentNode.removeChild(this.node);
this.parent.node.className = this.parent.node.className.replace(this.getHideClass(), "");
};
PseudoElementContainer.prototype.getHideClass = function() {
return this["PSEUDO_HIDE_ELEMENT_CLASS_" + (this.before ? "BEFORE" : "AFTER")];
};
PseudoElementContainer.prototype.PSEUDO_HIDE_ELEMENT_CLASS_BEFORE = "___html2canvas___pseudoelement_before";
PseudoElementContainer.prototype.PSEUDO_HIDE_ELEMENT_CLASS_AFTER = "___html2canvas___pseudoelement_after";

Submodule src/punycode deleted from 5c04e6fe95

View File

@ -1,104 +0,0 @@
function Renderer(width, height, images, options, document) {
this.width = width;
this.height = height;
this.images = images;
this.options = options;
this.document = document;
}
Renderer.prototype.renderImage = function(container, bounds, borderData, imageContainer) {
var paddingLeft = container.cssInt('paddingLeft'),
paddingTop = container.cssInt('paddingTop'),
paddingRight = container.cssInt('paddingRight'),
paddingBottom = container.cssInt('paddingBottom'),
borders = borderData.borders;
var width = bounds.width - (borders[1].width + borders[3].width + paddingLeft + paddingRight);
var height = bounds.height - (borders[0].width + borders[2].width + paddingTop + paddingBottom);
this.drawImage(
imageContainer,
0,
0,
imageContainer.image.width || width,
imageContainer.image.height || height,
bounds.left + paddingLeft + borders[3].width,
bounds.top + paddingTop + borders[0].width,
width,
height
);
};
Renderer.prototype.renderBackground = function(container, bounds, borderData) {
if (bounds.height > 0 && bounds.width > 0) {
this.renderBackgroundColor(container, bounds);
this.renderBackgroundImage(container, bounds, borderData);
}
};
Renderer.prototype.renderBackgroundColor = function(container, bounds) {
var color = container.color("backgroundColor");
if (!color.isTransparent()) {
this.rectangle(bounds.left, bounds.top, bounds.width, bounds.height, color);
}
};
Renderer.prototype.renderBorders = function(borders) {
borders.forEach(this.renderBorder, this);
};
Renderer.prototype.renderBorder = function(data) {
if (!data.color.isTransparent() && data.args !== null) {
this.drawShape(data.args, data.color);
}
};
Renderer.prototype.renderBackgroundImage = function(container, bounds, borderData) {
var backgroundImages = container.parseBackgroundImages();
backgroundImages.reverse().forEach(function(backgroundImage, index, arr) {
switch(backgroundImage.method) {
case "url":
var image = this.images.get(backgroundImage.args[0]);
if (image) {
this.renderBackgroundRepeating(container, bounds, image, arr.length - (index+1), borderData);
} else {
log("Error loading background-image", backgroundImage.args[0]);
}
break;
case "linear-gradient":
case "gradient":
var gradientImage = this.images.get(backgroundImage.value);
if (gradientImage) {
this.renderBackgroundGradient(gradientImage, bounds, borderData);
} else {
log("Error loading background-image", backgroundImage.args[0]);
}
break;
case "none":
break;
default:
log("Unknown background-image type", backgroundImage.args[0]);
}
}, this);
};
Renderer.prototype.renderBackgroundRepeating = function(container, bounds, imageContainer, index, borderData) {
var size = container.parseBackgroundSize(bounds, imageContainer.image, index);
var position = container.parseBackgroundPosition(bounds, imageContainer.image, index, size);
var repeat = container.parseBackgroundRepeat(index);
switch (repeat) {
case "repeat-x":
case "repeat no-repeat":
this.backgroundRepeatShape(imageContainer, position, size, bounds, bounds.left + borderData[3], bounds.top + position.top + borderData[0], 99999, size.height, borderData);
break;
case "repeat-y":
case "no-repeat repeat":
this.backgroundRepeatShape(imageContainer, position, size, bounds, bounds.left + position.left + borderData[3], bounds.top + borderData[0], size.width, 99999, borderData);
break;
case "no-repeat":
this.backgroundRepeatShape(imageContainer, position, size, bounds, bounds.left + position.left + borderData[3], bounds.top + position.top + borderData[0], size.width, size.height, borderData);
break;
default:
this.renderBackgroundRepeat(imageContainer, position, size, {top: bounds.top, left: bounds.left}, borderData[3], borderData[0]);
break;
}
};

View 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 + 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();
}
}

View 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)
)}`;
});
};

Some files were not shown because too many files have changed in this diff Show More