Compare commits

..

1 Commits

Author SHA1 Message Date
45ad98e73c v0.30 2011-09-07 19:54:19 +03:00
271 changed files with 18898 additions and 45288 deletions

View File

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

View File

@ -1,17 +0,0 @@
# 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

View File

@ -1,23 +0,0 @@
{
"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
}]
}
}

View File

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

View File

@ -1,19 +0,0 @@
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:

View File

@ -1,37 +0,0 @@
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 #

View File

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

22
.gitignore vendored
View File

@ -1,17 +1,9 @@
/dist
/build
/nbproject/
/images/
/tests/templates/
/tests/cache/
/dist/
/build/tmp.js
index.html
image.jpg
/.project
/.settings/
node_modules/
.envrc
*.sublime-workspace
*.baseline
*.iml
.idea/
.DS_Store
npm-debug.log
debug.log
tests/reftests.js
*.log
screenshots_local.html

View File

@ -1,18 +0,0 @@
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,57 +0,0 @@
language: node_js
node_js:
- '7'
env:
global:
- secure: eW41gIqOizwO4pTgWnAAbW75AP7F+CK9qfSed/fSh4sJ9HWMIY1YRIaY8gjr+6jV/f7XVHcXuym6ZxgINYSkVKbF1JKxBJNLOXtSgNbVHSic58pYFvUjwxIBI9aPig9uux1+DbnpWqXFDTcACJSevQZE0xwmjdrSkDLgB0G34v8=
- secure: Y2Av+Gd3z9uQEB36GwdOOuGka0hx0/HeitASEo59z934O8RxnmN9eNTXS7dDT3XtKtwxIyLTOEpS7qlRdWahH28hr/dS4xJj6ao58C+1xMcDs6NAPGmDxUlcJWpcGEsnjmXjQCc3fBioSTdpIBrK/gdvgpNh77UKG74Sk7Z+YGk=
addons:
chrome: stable
firefox: latest
dist: trusty
sudo: true
before_script:
- export DISPLAY=:99.0
- sh -e /etc/init.d/xvfb start
notifications:
webhooks:
urls:
- https://webhooks.gitter.im/e/2b007d4f86de89588804
on_success: always
on_failure: always
on_start: false
script:
- npm run build
- cd www && npm install && npm run build && cd ..
- npm test
deploy:
- provider: npm
email: niklasvh@gmail.com
api_key:
secure: G/Szpr8q4/D6hp+H/Z9yyluUXtHAwf7LLa1Y07X59/Enlj1h7V5fQ7AW4/iAVM3XbIsrCPWR3dJU9g/ZxpxFg4OovIHVpS2Jr/mahtPYWdHR3pWuSmMW8QD+Twnq2VAFwSgg5Oumq3QxhX3YbCOnZox6+6Uviqk8FO7Z5B0RwW4=
skip_cleanup: true
on:
tags: true
branch: master
repo: niklasvh/html2canvas
- provider: releases
api_key:
secure: "PowO/Jat660k3gHcjgI6DlJz15RM7pLUu11UPsLCtYJ8ZwodppE6Keg0DfVkSFSIZttZor+UssDwP/WOEqfZNLqmXbcj3Gec4xolohet/GOe0KJKKuF/HgggbcxumopxMX6sMVePlMBpkLpHh7tgEAEHBWTlzC1c1a7Xa48fZ7k="
file:
- "dist/html2canvas.js"
- "dist/html2canvas.min.js"
skip_cleanup: true
on:
tags: true
branch: master
repo: niklasvh/html2canvas
- provider: pages
skip_cleanup: true
local_dir: www/public
target_branch: gh-pages
fqdn: html2canvas.hertzen.com
github_token:
secure: "PowO/Jat660k3gHcjgI6DlJz15RM7pLUu11UPsLCtYJ8ZwodppE6Keg0DfVkSFSIZttZor+UssDwP/WOEqfZNLqmXbcj3Gec4xolohet/GOe0KJKKuF/HgggbcxumopxMX6sMVePlMBpkLpHh7tgEAEHBWTlzC1c1a7Xa48fZ7k="
on:
branch: master
repo: niklasvh/html2canvas

View File

@ -1,136 +0,0 @@
### Changelog ###
#### v1.0.0-alpha.10 - 15.2.2018 ####
* Re-introduce `onclone` option (Fix #1434)
* Add `ignoreElements` predicate function option
* Fix version console logging
#### v1.0.0-alpha.9 - 7.1.2018 ####
* Fix dynamic style sheets
* Fix > 50% border-radius values
#### v1.0.0-alpha.8 - 2.1.2018 ####
* Use correct doctype in cloned Document (Fix #1298)
* Fix individual border rendering (Fix #1349)
#### v1.0.0-alpha.7 - 31.12.2017 ####
* Fix form input rendering (#1338)
* Improve word line breaking algorithm
#### v1.0.0-alpha.6 - 28.12.2017 ####
* Fix list-style: none (#1340)
* Extend supported values for pseudo element content
#### v1.0.0-alpha.5 - 21.12.2017 ####
* Fix underline positioning
* Fix canvas rendering on Chrome
* Fix overflow: auto
* Added support for rendering list-style
#### v1.0.0-alpha.4 - 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-alpha.3 - 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-alpha.2 - 7.12.2017 ####
* Fix scroll positions for CanvasRenderer (#1259)
* Fix `data-html2canvas-ignore` attribute (#1253)
* Fix decimal `letter-spacing` values (#1293)
#### v1.0.0-alpha.1 - 5.12.2017 ####
* Complete rewrite of library
##### Breaking Changes #####
* Remove deprecated onrendered callback, calling `html2canvas` returns a `Promise<HTMLCanvasElement>`
* Removed option `type`, same results can be achieved by assigning `x`, `y`, `scrollX`, `scrollY`, `width` and `height` properties.
##### New featues / fixes #####
* Add support for scaling canvas (defaults to device pixel ratio)
* Add support for multiple text-shadows
* Add support for multiple text-decorations
* Add support for text-decoration-color
* Add support for percentage values for border-radius
* Correctly handle px and percentage values in linear-gradients
* Correctly support all angle types for linear-gradients
* Add support for multiple values for background-repeat, background-position and background-size
#### v0.5.0-beta4 - 23.1.2016 ####
* Fix logger requiring access to window object
* Derequire browserify build
* Fix rendering of specific elements when window is scrolled and `type` isn't set to `view`
#### v0.5.0-beta3 - 6.12.2015 ####
* Handle color names in linear gradients
#### v0.5.0-beta2 - 20.10.2015 ####
* Remove Promise polyfill (use native or provide it yourself)
#### v0.5.0-beta1 - 19.10.2015 ####
* Fix bug with unmatched color stops in gradients
* Fix scrolling issues with iOS
* Correctly handle named colors in gradients
* Accept matrix3d transforms
* Fix transparent colors breaking gradients
* Preserve scrolling positions on render
#### v0.5.0-alpha2 - 3.2.2015 ####
* Switch to using browserify for building
* Fix (#517) Chrome stretches background images with 'auto' or single attributes
#### v0.5.0-alpha - 19.1.2015####
* Complete rewrite of library
* Switched interface to return Promise
* Uses hidden iframe window to perform rendering, allowing async rendering and doesn't force the viewport to be scrolled to the top anymore.
* Better support for unicode
* Checkbox/radio button rendering
* SVG rendering
* iframe rendering
* Changed format for proxy requests, permitting binary responses with CORS headers as well
* Fixed many layering issues (see z-index tests)
#### v0.4.1 - 7.9.2013 ####
* Added support for bower
* Improved z-index ordering
* Basic implementation for CSS transformations
* Fixed inline text in top element
* Basic implementation for text-shadow
#### v0.4.0 - 30.1.2013 ####
* Added rendering tests with <a href="https://github.com/niklasvh/webdriver.js">webdriver</a>
* Switched to using grunt for building
* Removed support for IE<9, including any FlashCanvas bits
* Support for border-radius
* Support for multiple background images, size, and clipping
* Support for :before and :after pseudo elements
* Support for placeholder rendering
* Reformatted all tests to small units to test specific features
#### v0.3.4 - 26.6.2012 ####
* Removed (last?) jQuery dependencies (<a href="https://github.com/niklasvh/html2canvas/commit/343b86705fe163766fcf735eb0217130e4bd5b17">niklasvh</a>)
* SVG-powered rendering (<a href="https://github.com/niklasvh/html2canvas/commit/67d3e0d0f59a5a654caf71a2e3be6494ff146c75">niklasvh</a>)
* Radial gradients (<a href="https://github.com/niklasvh/html2canvas/commit/4f22c18043a73c0c3bbf3b5e4d62714c56acd3c7">SunboX</a>)
* Split renderers to their own objects (<a href="https://github.com/niklasvh/html2canvas/commit/94f2f799a457cd29a21cc56ef8c06f1697866739">niklasvh</a>)
* Simplified API, cleaned up code (<a href="https://github.com/niklasvh/html2canvas/commit/c7d526c9eaa6a4abf4754d205fe1dee360c7660e">niklasvh</a>)
#### v0.3.3 - 2.3.2012 ####
* SVG taint fix, and additional taint testing options for rendering (<a href="https://github.com/niklasvh/html2canvas/commit/2dc8b9385e656696cb019d615bdfa1d98b17d5d4">niklasvh</a>)
* Added support for CORS images and option to create canvas as tainted (<a href="https://github.com/niklasvh/html2canvas/commit/3ad49efa0032cde25c6ed32a39e35d1505d3b2ef">niklasvh</a>)
* Improved minification saved ~1K! (<a href="https://github.com/cobexer/html2canvas/commit/b82be022b2b9240bd503e078ac980bde2b953e43">cobexer</a>)
* Added integrated support for Flashcanvas (<a href="https://github.com/niklasvh/html2canvas/commit/e9257191519f67d74fd5e364d8dee3c0963ba5fc">niklasvh</a>)
* Fixed a variety of legacy IE bugs (<a href="https://github.com/niklasvh/html2canvas/commit/b65357c55d0701017bafcd357bc654b54d458f8f">niklasvh</a>)
#### v0.3.2 - 20.2.2012 ####
* Added changelog!
* Added bookmarklet (<a href="https://github.com/niklasvh/html2canvas/commit/b320dd306e1a2d32a3bc5a71b6ebf6d8c060cde5">cobexer</a>)
* Option to select single element to render (<a href="https://github.com/niklasvh/html2canvas/commit/0cb252ada91c84ef411288b317c03e97da1f12ad">niklasvh</a>)
* Fixed closure compiler warnings (<a href="https://github.com/niklasvh/html2canvas/commit/36ff1ec7aadcbdf66851a0b77f0b9e87e4a8e4a1">cobexer</a>)
* Enable profiling in FF (<a href="https://github.com/niklasvh/html2canvas/commit/bbd75286a8406cf9e5aea01fdb7950d547edefb9">cobexer</a>)

37
LICENSE
View File

@ -1,22 +1,21 @@
Copyright (c) 2012 Niklas von Hertzen
/*
* The MIT License
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/

View File

@ -1,89 +0,0 @@
html2canvas
===========
[Homepage](https://html2canvas.hertzen.com) | [Downloads](https://github.com/niklasvh/html2canvas/releases) | [Questions](http://stackoverflow.com/questions/tagged/html2canvas?sort=newest) | [Donate](https://www.gittip.com/niklasvh/)
[![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.svg)](https://travis-ci.org/niklasvh/html2canvas)
[![NPM Downloads](https://img.shields.io/npm/dm/html2canvas.svg)](https://www.npmjs.org/package/html2canvas)
[![NPM Version](https://img.shields.io/npm/v/html2canvas.svg)](https://www.npmjs.org/package/html2canvas)
#### JavaScript HTML renderer ####
The script allows you to take "screenshots" of webpages or parts of it, directly on the users browser. The screenshot is based on the DOM and as such may not be 100% accurate to the real representation as it does not make an actual screenshot, but builds the screenshot based on the information available on the page.
### How does it work? ###
The script renders the current page as a canvas image, by reading the DOM and the different styles applied to the elements.
It does **not require any rendering from the server**, as the whole image is created on the **client's browser**. However, as it is heavily dependent on the browser, this library is *not suitable* to be used in nodejs.
It doesn't magically circumvent any browser content policy restrictions either, so rendering cross-origin content will require a [proxy](https://github.com/niklasvh/html2canvas/wiki/Proxies) to get the content to the [same origin](http://en.wikipedia.org/wiki/Same_origin_policy).
The script is still in a **very experimental state**, so I don't recommend using it in a production environment nor start building applications with it yet, as there will be still major changes made.
### Browser compatibility ###
The library should work fine on the following browsers (with `Promise` polyfill):
* Firefox 3.5+
* Google Chrome
* Opera 12+
* IE9+
* Safari 6+
As each CSS property needs to be manually built to be supported, there are a number of properties that are not yet supported.
### Usage ###
The html2canvas library utilizes `Promise`s and expects them to be available in the global context. If you wish to
support [older browsers](http://caniuse.com/#search=promise) that do not natively support `Promise`s, please include a polyfill such as
[es6-promise](https://github.com/jakearchibald/es6-promise) before including `html2canvas`.
**Note!** These instructions are for using the current dev version of 0.5, for the latest release version (0.4.1), checkout the [old readme](https://github.com/niklasvh/html2canvas/blob/v0.4/readme.md).
To render an `element` with html2canvas, simply call:
` html2canvas(element[, options]);`
The function returns a [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) containing the `<canvas>` element. Simply add a promise fullfillment handler to the promise using `then`:
html2canvas(document.body).then(function(canvas) {
document.body.appendChild(canvas);
});
### Building ###
You can download ready builds [here](https://github.com/niklasvh/html2canvas/releases).
Clone git repository:
$ git clone git://github.com/niklasvh/html2canvas.git
Install dependencies:
$ npm install
Build browser bundle
$ npm run build
### Running tests ###
The library has two sets of tests. The first set is a number of qunit tests that check that different values parsed by browsers are correctly converted in html2canvas. To run these tests with grunt you'll need [phantomjs](http://phantomjs.org/).
The other set of tests run Firefox, Chrome and Internet Explorer with [webdriver](https://github.com/niklasvh/webdriver.js). The selenium standalone server (runs on Java) is required for these tests and can be downloaded from [here](http://code.google.com/p/selenium/downloads/list). They capture an actual screenshot from the test pages and compare the image to the screenshot created by html2canvas and calculate the percentage differences. These tests generally aren't expected to provide 100% matches, but while committing changes, these should generally not go decrease from the baseline values.
Start by downloading the dependencies:
$ npm install
Run tests:
$ npm test
### Examples ###
For more information and examples, please visit the [homepage](https://html2canvas.hertzen.com) or try the [test console](http://html2canvas.hertzen.com/screenshots.html).
### Contributing ###
If you wish to contribute to the project, please send the pull requests to the develop branch. Before submitting any changes, try and test that the changes work with all the support browsers. If some CSS property isn't supported or is incomplete, please create appropriate tests for it as well before submitting any code changes.

83
build.xml Normal file
View File

@ -0,0 +1,83 @@
<?xml version="1.0" encoding="ISO-8859-1"?>
<project name="html2canvas" basedir=".">
<property name="src.dir" location="src"/>
<property name="lib.dir" location="lib"/>
<property name="build.dir" location="build"/>
<property name="dist" location="dist"/>
<property name="jquery-externs" value="jquery-1.4.4.externs.js"/>
<property name="JS_NAME" value="html2canvas.js"/>
<property name="JS_NAME_MIN" value="html2canvas.min.js"/>
<property name="JQUERY_PLUGIN_NAME" value="jquery.plugin.html2canvas.js"/>
<path id="sourcefiles">
<fileset dir="${src.dir}" includes="LICENSE"/>
<fileset dir="." includes="LICENSE"/>
<fileset dir="${src.dir}" includes="Core.js"/>
<fileset dir="${src.dir}" includes="Background.js"/>
<fileset dir="${src.dir}" includes="Border.js"/>
<fileset dir="${src.dir}" includes="Draw.js"/>
<fileset dir="${src.dir}" includes="Forms.js"/>
<fileset dir="${src.dir}" includes="Images.js"/>
<fileset dir="${src.dir}" includes="Parse.js"/>
<fileset dir="${src.dir}" includes="Preload.js"/>
<fileset dir="${src.dir}" includes="Queue.js"/>
<fileset dir="${src.dir}" includes="Renderer.js"/>
<fileset dir="${src.dir}" includes="Lists.js"/>
<fileset dir="${src.dir}" includes="Text.js"/>
<fileset dir="${src.dir}" includes="Traversing.js"/>
<fileset dir="${src.dir}" includes="Util.js"/>
</path>
<path id="jquery-plugin">
<fileset dir="${src.dir}" includes="LICENSE"/>
<fileset dir="." includes="LICENSE"/>
<fileset dir="${src.dir}/plugins" includes="${JQUERY_PLUGIN_NAME}"/>
</path>
<path id="minified">
<fileset dir="${src.dir}" includes="LICENSE"/>
<fileset dir="${build.dir}" includes="tmp.js"/>
</path>
<target name="plugins">
<concat fixlastline="yes" destfile="${build.dir}/${JQUERY_PLUGIN_NAME}">
<path refid="jquery-plugin"/>
</concat>
</target>
<target name="source">
<concat fixlastline="yes" destfile="${build.dir}/${JS_NAME}">
<path refid="sourcefiles"/>
</concat>
</target>
<taskdef name="jscomp" classname="com.google.javascript.jscomp.ant.CompileTask"
classpath="${lib.dir}/compiler.jar"/>
<target name="release">
<jscomp compilationLevel="simple" warning="verbose"
debug="false"
output="${build.dir}/tmp.js">
<externs dir="${lib.dir}">
<file name="${jquery-externs}"/>
</externs>
<sources dir="${build.dir}">
<file name="${JS_NAME}" />
</sources>
</jscomp>
<concat fixlastline="yes" destfile="${build.dir}/${JS_NAME_MIN}">
<path refid="minified"/>
</concat>
</target>
</project>

2159
build/html2canvas.js Normal file

File diff suppressed because it is too large Load Diff

55
build/html2canvas.min.js vendored Normal file
View File

@ -0,0 +1,55 @@
/*
* html2canvas v0.27 <http://html2canvas.hertzen.com>
* Copyright (c) 2011 Niklas von Hertzen. All rights reserved.
* http://www.twitter.com/niklasvh
*
* Released under MIT License
*/
function html2canvas(a,b){this.opts=this.extendObj(b||{},{logging:!1,ready:function(a){document.body.appendChild(a.canvas)},storageReady:function(a){a.Renderer(a.contextStacks)},iframeDefault:"default",flashCanvasPath:"http://html2canvas.hertzen.com/external/flashcanvas/flashcanvas.js",renderViewport:!1,reorderZ:!0,throttle:!0,letterRendering:!1,proxyUrl:null,logger:function(a){window.console&&window.console.log?window.console.log(a):alert(a)},canvasWidth:0,canvasHeight:0,useOverflow:!0,renderOrder:"canvas flash html"});
this.element=a;this.imagesLoaded=0;this.images=[];this.fontData=[];this.numDraws=0;this.contextStacks=[];this.ignoreElements="IFRAME|OBJECT|PARAM";this.needReorder=!1;this.blockElements=/(BR|PARAM)/;this.pageOrigin=window.location.protocol+window.location.host;this.queue=[];this.ignoreRe=RegExp("("+this.ignoreElements+")");this.support={rangeBounds:!1};if(document.createRange){var c=document.createRange();if(c.getBoundingClientRect){var d=document.createElement("boundtest");d.style.height="123px";
d.style.display="block";document.getElementsByTagName("body")[0].appendChild(d);c.selectNode(d);if(c.getBoundingClientRect().height==123)this.support.rangeBounds=!0;document.getElementsByTagName("body")[0].removeChild(d)}}this.init();return this}
html2canvas.prototype.init=function(){var a=this;this.log("Finding background-images");this.images.push("start");this.getImages(this.element);this.log("Finding images");this.each(this.element.ownerDocument.images,function(b,c){a.preloadImage(a.getAttr(c,"src"))});this.images.splice(0,1);this.images.length==0&&this.start()};
html2canvas.prototype.start=function(){if(this.images.length==0||this.imagesLoaded==this.images.length/2){this.log("Finished loading "+this.imagesLoaded+" images, Started parsing");this.bodyOverflow=document.getElementsByTagName("body")[0].style.overflow;document.getElementsByTagName("body")[0].style.overflow="hidden";var a=new this.storageContext($(document).width(),$(document).height());a.opacity=this.getCSS(this.element,"opacity");this.parseElement(this.element,this.newElement(this.element,a))}};
html2canvas.prototype.stackingContext=function(a){this.canvas=document.createElement("canvas");this.canvas.width=a;this.canvas.height=a;if(this.canvas.getContext)this.ctx=this.canvas.getContext("2d");this.ctx.textBaseline="bottom";return this.ctx};
html2canvas.prototype.storageContext=function(a,b){this.storage=[];this.width=a;this.height=b;this.fillRect=function(a,b,e,f){this.storage.push({type:"function",name:"fillRect",arguments:[a,b,e,f]})};this.drawImage=function(a,b,e,f,g,h,i,j,m){this.storage.push({type:"function",name:"drawImage",arguments:[a,b,e,f,g,h,i,j,m]})};this.fillText=function(a,b,e){this.storage.push({type:"function",name:"fillText",arguments:[a,b,e]})};return this};
html2canvas.prototype.finish=function(){this.log("Finished rendering");document.getElementsByTagName("body")[0].style.overflow=this.bodyOverflow;this.opts.ready(this)};
html2canvas.prototype.drawBackground=function(a,b,c){var d=this.getCSS(a,"background-image").split(",")[0],e=this.getCSS(a,"background-repeat").split(",")[0];if(typeof d!="undefined"&&/^(1|none)$/.test(d)==!1&&/^(-webkit|-moz|linear-gradient|-o-)/.test(d)==!1){var d=this.backgroundImageUrl(d),f=this.loadImage(d),a=this.getBackgroundPosition(a,b,f);if(f)switch(e){case "repeat-x":this.drawbackgroundRepeatX(c,f,a,b.left,b.top,b.width,b.height);break;case "repeat-y":this.drawbackgroundRepeatY(c,f,a,b.left,
b.top,b.width,b.height);break;case "no-repeat":var d=b.width-a.left,e=b.height-a.top,g=a.left,h=a.top,i=a.left+b.left,j=a.top+b.top;g<0?(g=Math.abs(g),i+=g,d=Math.min(b.width,f.width-g)):(d=Math.min(d,f.width),g=0);h<0?(h=Math.abs(h),j+=h,e=Math.min(b.height,f.height-h)):(e=Math.min(e,f.height),h=0);if(e>0&&d>0){this.drawImage(c,f,g,h,d,e,i,j,d,e);break}default:a.top-=Math.ceil(a.top/f.height)*f.height;for(d=b.top+a.top;d<b.height+b.top;)e=Math.min(f.height,b.height+b.top-d),e=Math.floor(d+f.height)>
e+d?e+d-d:f.height,d<b.top?(g=b.top-d,d=b.top):g=0,this.drawbackgroundRepeatX(c,f,a,b.left,d,b.width,e),g>0&&(a.top+=g),d=Math.floor(d+f.height)-g}else this.log("Error loading background:"+d)}};html2canvas.prototype.backgroundImageUrl=function(a){a.substr(0,5)=='url("'?(a=a.substr(5),a=a.substr(0,a.length-2)):(a=a.substr(4),a=a.substr(0,a.length-1));return a};
html2canvas.prototype.getBackgroundPosition=function(a,b,c){var d=(this.getCSS(a,"backgroundPosition").split(",")[0]||"0 0").split(" "),e;d.length==1&&(a=d,d=[],d[0]=a,d[1]=a);d[0].toString().indexOf("%")!=-1?(e=parseFloat(d[0])/100,a=b.width*e-c.width*e):a=parseInt(d[0],10);d[1].toString().indexOf("%")!=-1?(e=parseFloat(d[1])/100,b=b.height*e-c.height*e):b=parseInt(d[1],10);c={};c.top=b;c.left=a;return c};
html2canvas.prototype.drawbackgroundRepeatY=function(a,b,c,d,e,f,g){var h=Math.min(b.width,f),i;c.top-=Math.ceil(c.top/b.height)*b.height;for(i=e+c.top;i<g+e;)f=Math.floor(i+b.height)>g+e?g+e-i:b.height,this.drawBackgroundRepeat(a,b,d+c.left,i,h,f,d,e),i=Math.floor(i+b.height)};
html2canvas.prototype.drawbackgroundRepeatX=function(a,b,c,d,e,f,g){var g=Math.min(b.height,g),h,i;c.left-=Math.ceil(c.left/b.width)*b.width;for(i=d+c.left;i<f+d;)h=Math.floor(i+b.width)>f+d?f+d-i:b.width,this.drawBackgroundRepeat(a,b,i,e+c.top,h,g,d,e),i=Math.floor(i+b.width)};html2canvas.prototype.drawBackgroundRepeat=function(a,b,c,d,e,f,g,h){var i=0,j=0;g-c>0&&(i=g-c);h-d>0&&(j=h-d);this.drawImage(a,b,i,j,e-i,f-j,c+i,d+j,e-i,f-j)};
html2canvas.prototype.getBorderData=function(a){var b=[],c=this;this.each(["top","right","bottom","left"],function(d,e){b.push({width:parseInt(c.getCSS(a,"border-"+e+"-width"),10),color:c.getCSS(a,"border-"+e+"-color")})});return b};
html2canvas.prototype.drawBorders=function(a,b,c,d){var e=c.left,f=c.top,g=c.width,h=c.height,i=this.getBorderData(a),j=this;this.each(i,function(a,c){if(c.width>0){var k=e,o=f,n=g,p=h-i[2].width;switch(a){case 0:p=i[0].width;break;case 1:k=e+g-i[1].width;n=i[1].width;break;case 2:o=o+h-i[2].width;p=i[2].width;break;case 3:n=i[3].width}n={left:k,top:o,width:n,height:p};d&&(n=j.clipBounds(n,d));n.width>0&&n.height>0&&j.newRect(b,k,o,n.width,n.height,c.color)}});return i};
html2canvas.prototype.newElement=function(a,b){var c=this.getBounds(a),d=c.left,e=c.top,f=c.width,g=c.height,h;h=this.getCSS(a,"background-color");var i=this.getCSS(a,"position"),b=b||{},j=this.setZ(this.getCSS(a,"zIndex"),i,b.zIndex,a.parentNode),m=this.getCSS(a,"opacity"),l={ctx:new this.storageContext,zIndex:j,opacity:m*b.opacity,cssPosition:i};if(b.clip)l.clip=$.extend({},b.clip),l.clip.height-=b.borders[2].width;if(this.opts.useOverflow&&/(hidden|scroll|auto)/.test(this.getCSS(a,"overflow"))&&
!/(BODY)/i.test(a.nodeName))l.clip=l.clip?this.clipBounds(l.clip,c):c;i=j.children.push(l);m=j.children[i-1].ctx;this.setContextVariable(m,"globalAlpha",l.opacity);var k=this.drawBorders(a,m,c);l.borders=k;this.ignoreRe.test(a.nodeName)&&this.opts.iframeDefault!="transparent"&&(h=this.opts.iframeDefault=="default"?"#efefef":this.opts.iframeDefault);f={left:d+k[3].width,top:e+k[0].width,width:f-(k[1].width+k[3].width),height:g-(k[0].width+k[2].width)};l.clip&&(f=this.clipBounds(f,l.clip));f.height>
0&&f.width>0&&(this.newRect(m,f.left,f.top,f.width,f.height,h),this.drawBackground(a,f,m));switch(a.nodeName){case "IMG":(h=this.loadImage(this.getAttr(a,"src")))?this.drawImage(m,h,0,0,h.width,h.height,d+parseInt(this.getCSS(a,"padding-left"),10)+k[3].width,e+parseInt(this.getCSS(a,"padding-top"),10)+k[0].width,c.width-(k[1].width+k[3].width+parseInt(this.getCSS(a,"padding-left"),10)+parseInt(this.getCSS(a,"padding-right"),10)),c.height-(k[0].width+k[2].width+parseInt(this.getCSS(a,"padding-top"),
10)+parseInt(this.getCSS(a,"padding-bottom"),10))):this.log("Error loading <img>:"+this.getAttr(a,"src"));break;case "INPUT":/^(text|url|email|submit|button|reset)$/.test(a.type)&&a.value.length>0&&this.renderFormValue(a,c,l);break;case "TEXTAREA":a.value.length>0&&this.renderFormValue(a,c,l);break;case "SELECT":a.options.length>0&&this.renderFormValue(a,c,l);break;case "LI":this.drawListItem(a,l,f)}return j.children[i-1]};
html2canvas.prototype.printText=function(a,b,c,d){this.trim(a).length>0&&(d.fillText(a,b,c),this.numDraws++)};html2canvas.prototype.newRect=function(a,b,c,d,e,f){f!="transparent"&&(this.setContextVariable(a,"fillStyle",f),a.fillRect(b,c,d,e),this.numDraws++)};html2canvas.prototype.drawImage=function(a,b,c,d,e,f,g,h,i,j){a.drawImage(b,c,d,e,f,g,h,i,j);this.numDraws++};
html2canvas.prototype.renderFormValue=function(a,b,c){var d=document.createElement("valuewrap"),e=this;this.each(["lineHeight","textAlign","fontFamily","color","fontSize","paddingLeft","paddingTop","width","height","border","borderLeftWidth","borderTopWidth"],function(b,c){d.style[c]=e.getCSS(a,c)});d.style.borderColor="black";d.style.borderStyle="solid";d.style.display="block";d.style.position="absolute";if(/^(submit|reset|button|text|password)$/.test(a.type)||a.nodeName=="SELECT")d.style.lineHeight=
e.getCSS(a,"height");d.style.top=b.top+"px";d.style.left=b.left+"px";b=document.createTextNode(a.nodeName=="SELECT"?a.options[a.selectedIndex].text:a.value);d.appendChild(b);$("body").append(d);this.newText(a,b,c);$(d).remove()};
html2canvas.prototype.getImages=function(a){var b=this;this.ignoreRe.test(a.nodeName)||this.each($(a).contents(),function(a,d){RegExp("("+this.ignoreElements+")").test(d.nodeName)||b.getImages(d)});if(a.nodeType==1||typeof a.nodeType=="undefined")(a=this.getCSS(a,"background-image"))&&a!="1"&&a!="none"&&a.substring(0,7)!="-webkit"&&a.substring(0,3)!="-o-"&&a.substring(0,4)!="-moz"&&this.preloadImage(this.backgroundImageUrl(a.split(",")[0]))};
html2canvas.prototype.loadImage=function(a){a=this.getIndex(this.images,a);return a!=-1?this.images[a+1]:!1};html2canvas.prototype.preloadImage=function(a){if(this.getIndex(this.images,a)==-1)if(this.isSameOrigin(a)){this.images.push(a);var b=new Image,c=this;$(b).load(function(){c.imagesLoaded++;c.start()});b.onerror=function(){c.images.splice(c.images.indexOf(b.src),2);c.start()};b.src=a;this.images.push(b)}else this.opts.proxyUrl&&(this.images.push(a),b=new Image,this.proxyGetImage(a,b),this.images.push(b))};
html2canvas.prototype.proxyGetImage=function(a,b){var c=this,d=document.createElement("a");d.href=a;a=d.href;$.ajax({data:{xhr2:!1,url:a},url:this.opts.proxyUrl,dataType:"jsonp",success:function(d){d.substring(0,6)=="error:"?(c.images.splice(c.images.indexOf(a),2),c.start(),c.log("Proxy was unable to load "+a+" "+d)):(b.onload=function(){c.imagesLoaded++;c.start()},b.src=d)},error:function(){c.images.splice(c.images.indexOf(a),2);c.start()}})};
html2canvas.prototype.Renderer=function(a){var b=this;this.log("Renderer initiated");this.each(this.opts.renderOrder.split(" "),function(c,d){switch(d){case "canvas":b.canvas=document.createElement("canvas");if(b.canvas.getContext)return b.canvasRenderer(a),b.log("Using canvas renderer"),!1;break;case "html":return b.log("Using HTML renderer"),!1}});return this};html2canvas.prototype.throttler=function(){};
html2canvas.prototype.canvasRenderContext=function(a,b){b.textBaseline="bottom";var c=this;a.clip&&(b.save(),b.beginPath(),b.rect(a.clip.left,a.clip.top,a.clip.width,a.clip.height),b.clip());a.ctx.storage&&c.each(a.ctx.storage,function(a,e){switch(e.type){case "variable":b[e.name]=e.arguments;break;case "function":e.name=="fillRect"?b.fillRect(e.arguments[0],e.arguments[1],e.arguments[2],e.arguments[3]):e.name=="fillText"?b.fillText(e.arguments[0],e.arguments[1],e.arguments[2]):e.name=="drawImage"?
e.arguments[8]>0&&e.arguments[7]&&b.drawImage(e.arguments[0],e.arguments[1],e.arguments[2],e.arguments[3],e.arguments[4],e.arguments[5],e.arguments[6],e.arguments[7],e.arguments[8]):c.log(e)}});a.clip&&b.restore()};html2canvas.prototype.canvasRenderStorage=function(a,b){for(;0<a.length;){var c=a.splice(0,1)[0];c.canvasPosition=c.canvasPosition||{};this.canvasRenderContext(c,b)}};
html2canvas.prototype.canvasRenderer=function(a){this.sortZ(this.zStack);a=this.queue;this.canvas.width=Math.max($(document).width(),this.opts.canvasWidth);this.canvas.height=Math.max($(document).height(),this.opts.canvasHeight);this.ctx=this.canvas.getContext("2d");this.canvasRenderStorage(a,this.ctx)};html2canvas.prototype.setContextVariable=function(a,b,c){a.storage?a.storage.push({type:"variable",name:b,arguments:c}):a[b]=c};
html2canvas.prototype.drawListItem=function(a,b,c){var d=this.getCSS(a,"list-style-position",!1);this.getListItem(a);var e=this.getCSS(a,"list-style-type",!1);if(/^(decimal|decimal-leading-zero|upper-alpha|upper-latin|upper-roman|lower-alpha|lower-greek|lower-latin|lower-roman)$/i.test(e)){var f=$(a).index()+1,g;e=="decimal"?g=f:e=="decimal-leading-zero"?g=f.toString().length==1?"0"+f.toString():f.toString():e=="upper-roman"?g=this.getListRoman(f):e=="lower-roman"?g=this.getListRoman(f).toLowerCase():
e=="lower-alpha"?g=this.getListAlpha(f).toLowerCase():e=="upper-alpha"&&(g=this.getListAlpha(f));g+=". ";e=this.getListPosition(a,g);if(d=="inside")this.setFont(b.ctx,a,!1),a=c.left,c=e.bottom,this.printText(g,a,c,b.ctx)}};
html2canvas.prototype.getListPosition=function(a,b){var c=document.createElement("boundelement");c.style.display="inline";var d=a.style.listStyleType;a.style.listStyleType="none";c.appendChild(document.createTextNode(b));a.insertBefore(c,a.firstChild);var e=this.getBounds(c);a.removeChild(c);a.style.listStyleType=d;return e};html2canvas.prototype.getListItem=function(){};html2canvas.prototype.getListAlpha=function(a){var b="";do b=String.fromCharCode(a%26+64)+b,a/=26;while(a*26>26);return b};
html2canvas.prototype.getListRoman=function(a){var b=["M","CM","D","CD","C","XC","L","XL","X","IX","V","IV","I"],c=[1E3,900,500,400,100,90,50,40,10,9,5,4,1],d="";if(!(a<=0||a>=4E3)){for(var e=0;e<b.length;e++)for(;a>=c[e];)a-=c[e],d+=b[e];return d}};
html2canvas.prototype.newText=function(a,b,c){var c=c.ctx,d=this.getCSS(a,"font-family"),e=this.getCSS(a,"font-size"),f=this.getCSS(a,"color"),g=this.getCSS(a,"text-decoration"),h=this.getCSS(a,"text-align"),i=this.getCSS(a,"letter-spacing");b.nodeValue=this.textTransform(b.nodeValue,this.getCSS(a,"text-transform"));if(this.trim(b.nodeValue).length>0){if(g!="none")var j=this.fontMetrics(d,e);h=h.replace(["-webkit-auto"],["auto"]);d=this.opts.letterRendering==!1&&/^(left|right|justify|auto)$/.test(h)&&
/^(normal|none)$/.test(i)?b.nodeValue.split(/(\b| )/):b.nodeValue.split("");this.setFont(c,a,!1);a=b;for(b=0;b<d.length;b++)if(typeof a.nodeValue=="string"){e=a.splitText(d[b].length);if(g!="none"||this.trim(a.nodeValue).length!=0){if(this.support.rangeBounds)document.createRange?(h=document.createRange(),h.selectNode(a)):h=document.body.createTextRange(),h=h.getBoundingClientRect()?h.getBoundingClientRect():{};else{var i=a.parentNode,m=document.createElement("wrapper"),l=a.cloneNode(!0);m.appendChild(a.cloneNode(!0));
i.replaceChild(m,a);h=this.getBounds(m);i.replaceChild(l,m)}this.printText(a.nodeValue,h.left,h.bottom,c);switch(g){case "underline":this.newRect(c,h.left,Math.round(h.top+j.baseline+j.lineWidth),h.width,1,f);break;case "overline":this.newRect(c,h.left,h.top,h.width,1,f);break;case "line-through":this.newRect(c,h.left,Math.ceil(h.top+j.middle+j.lineWidth),h.width,1,f)}}a=e}}};
html2canvas.prototype.setFont=function(a,b,c){var d=this.getCSS(b,"font-family"),e=this.getCSS(b,"font-size"),f=this.getCSS(b,"color"),g=this.getCSS(b,"font-weight"),h=this.getCSS(b,"font-style"),b=this.getCSS(b,"font-variant");switch(g){case 401:g="bold";break;case 400:g="normal"}d=b+" "+g+" "+h+" "+e+" "+d;this.setContextVariable(a,"fillStyle",f);this.setContextVariable(a,"font",d);c?this.setContextVariable(a,"textAlign","right"):this.setContextVariable(a,"textAlign","left")};
html2canvas.prototype.fontMetrics=function(a,b){var c=this.fontData.indexOf(a+"-"+b);if(c>-1)return this.fontData[c+1];c=document.createElement("div");document.getElementsByTagName("body")[0].appendChild(c);$(c).css({visibility:"hidden",fontFamily:a,fontSize:b,margin:0,padding:0});var d=document.createElement("img");d.src="http://html2canvas.hertzen.com/images/8.jpg";d.width=1;d.height=1;$(d).css({margin:0,padding:0});var e=document.createElement("span");$(e).css({fontFamily:a,fontSize:b,margin:0,
padding:0});e.appendChild(document.createTextNode("Hidden Text"));c.appendChild(e);c.appendChild(d);var f=d.offsetTop-e.offsetTop+1;c.removeChild(e);c.appendChild(document.createTextNode("Hidden Text"));$(c).css("line-height","normal");$(d).css("vertical-align","super");d={baseline:f,lineWidth:1,middle:d.offsetTop-c.offsetTop+1};this.fontData.push(a+"-"+b);this.fontData.push(d);$(c).remove();return d};
html2canvas.prototype.textTransform=function(a,b){switch(b){case "lowercase":return a.toLowerCase();case "capitalize":return a.replace(/(^|\s|:|-|\(|\))([a-z])/g,function(a,b,e){return b+e.toUpperCase()});case "uppercase":return a.toUpperCase();default:return a}};html2canvas.prototype.trim=function(a){return a.replace(/^\s*/,"").replace(/\s*$/,"")};
html2canvas.prototype.parseElement=function(a,b){var c=this;this.each(a.children,function(a,e){c.parsing(e,b)});this.log("Render queue stored");this.opts.storageReady(this);this.finish()};html2canvas.prototype.parsing=function(a,b){if(this.getCSS(a,"display")!="none"&&this.getCSS(a,"visibility")!="hidden"){var c=this,b=this.newElement(a,b)||b;this.ignoreRe.test(a.nodeName)||this.each(this.contentsInZ(a),function(d,e){e.nodeType==1?c.parsing(e,b):e.nodeType==3&&c.newText(a,e,b)})}};
html2canvas.prototype.log=function(a){this.opts.logging&&this.opts.logger(a)};html2canvas.prototype.withinBounds=function(a,b){return!a?!0:(a.left<=b.left||b.left+b.width<a.left)&&(a.top<=b.top||b.top+b.height<a.top)};html2canvas.prototype.clipBounds=function(a,b){var c=Math.max(a.left,b.left),d=Math.max(a.top,b.top);return{left:c,top:d,width:Math.min(a.left+a.width,b.left+b.width)-c,height:Math.min(a.top+a.height,b.top+b.height)-d}};
html2canvas.prototype.getBounds=function(a){window.scroll(0,0);if(a.getBoundingClientRect){var a=a.getBoundingClientRect(),b={};b.top=a.top;b.bottom=a.bottom||a.top+a.height;b.left=a.left;b.width=a.width;b.height=a.height;return b}else return b=$(a).offset(),{left:b.left+this.getCSS(a,"border-left-width",!0),top:b.top+this.getCSS(a,"border-top-width",!0),width:$(a).innerWidth(),height:$(a).innerHeight()}};
html2canvas.prototype.each=function(a,b){for(var b=b||function(){},c=0;c<a.length;c++)if(b(c,a[c])===!1)break};html2canvas.prototype.contentsInZ=function(a){return $(a).contents()};html2canvas.prototype.getAttr=function(a,b){return a.getAttribute(b)};html2canvas.prototype.extendObj=function(a,b){for(var c in a)b[c]=a[c];return b};html2canvas.prototype.zContext=function(a){return{zindex:a,children:[]}};
html2canvas.prototype.setZ=function(a,b,c){return!c?this.zStack=new this.zContext(0):a!="auto"?(this.needReorder=!0,a=new this.zContext(a),c.children.push(a),a):c};html2canvas.prototype.sortZ=function(a){var b=[],c=[],d=this;this.each(a.children,function(a,f){f.children&&f.children.length>0?(b.push(f),c.push(f.zindex)):d.queue.push(f)});c.sort(function(a,b){return a-b});this.each(c,function(a,c){for(var g=0;g<=b.length;g++)if(b[g].zindex==c){g=b.splice(g,1);d.sortZ(g[0]);break}})};
html2canvas.prototype.getContents=function(a){return a.nodeName=="iframe"?a.contentDocument||a.contentWindow.document:a.childNodes};html2canvas.prototype.getCSS=function(a,b,c){return c?parseInt($(a).css(b),10):$(a).css(b)};html2canvas.prototype.getIndex=function(a,b){if(a.indexOf)return a.indexOf(b);else{for(var c=0;c<a.length;c++)if(this[c]==b)return c;return-1}};html2canvas.prototype.isSameOrigin=function(a){var b=document.createElement("a");b.href=a;return b.protocol+b.host==this.pageOrigin};

View File

@ -0,0 +1,118 @@
/*
* html2canvas v0.25 <http://html2canvas.hertzen.com>
* Copyright (c) 2011 Niklas von Hertzen. All rights reserved.
* http://www.twitter.com/niklasvh
*
* Released under MIT License
*/
/*
* The MIT License
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
/*
* jQuery helper plugin for examples and tests
*/
(function( $ ){
$.fn.html2canvas = function(options) {
var date = new Date();
var message,
timeoutTimer,
timer = date.getTime();
var object = $.extend({},{
logging: false,
proxyUrl: "http://html2canvas.appspot.com/", // running html2canvas-python proxy
ready: function(renderer) {
var finishTime = new Date();
// console.log((finishTime.getTime()-timer)/1000);
document.body.appendChild(renderer.canvas);
var canvas = $(renderer.canvas);
canvas.css('position','absolute')
.css('left',0).css('top',0);
// $('body').append(canvas);
$(canvas).siblings().toggle();
throwMessage('Screenshot created in '+ ((finishTime.getTime()-timer)/1000) + " seconds<br />Total of "+renderer.numDraws+" draws performed",4000);
$(window).click(function(){
if (!canvas.is(':visible')){
$(canvas).toggle().siblings().toggle();
throwMessage("Canvas Render visible");
} else{
$(canvas).siblings().toggle();
$(canvas).toggle();
throwMessage("Canvas Render hidden");
}
});
}
},options)
new html2canvas(this.get(0), object);
function throwMessage(msg,duration){
window.clearTimeout(timeoutTimer);
timeoutTimer = window.setTimeout(function(){
message.fadeOut(function(){
message.remove();
});
},duration || 2000);
$(message).remove();
message = $('<div />').html(msg).css({
margin:0,
padding:10,
background: "#000",
opacity:0.7,
position:"fixed",
top:10,
right:10,
fontFamily: 'Tahoma' ,
color:'#fff',
fontSize:12,
borderRadius:12,
width:'auto',
height:'auto',
textAlign:'center',
textDecoration:'none'
}).hide().fadeIn().appendTo('body');
}
};
})( jQuery );

2364
demo.html Normal file

File diff suppressed because it is too large Load Diff

186
demo2.html Normal file
View File

@ -0,0 +1,186 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">
<html>
<head>
<script type="text/javascript" src="external/jquery-1.6.2.min.js"></script>
<script type="text/javascript" src="build/html2canvas.js"></script>
<script type="text/javascript" src="build/jquery.plugin.html2canvas.js"></script>
<script type="text/javascript">
$(document).ready(function() {
$('body').html2canvas();
// var ss = $('ul').offset();
// alert(ss.left);
});
</script>
<title>
display/box/float/clear test
</title>
<style type="text/css">
/* last modified: 1 Dec 98 */
html {
font: 10px/1 Verdana, sans-serif;
background-color: blue;
color: white;
}
body {
margin: 1.5em;
border: .5em solid black;
padding: 0;
width: 48em;
background-color: white;
}
dl {
margin: 0;
border: 0;
padding: .5em;
}
dt {
background-color: rgb(204,0,0);
margin: 0;
padding: 1em;
width: 10.638%; /* refers to parent element's width of 47em. = 5em or 50px */
height: 28em;
border: .5em solid black;
float: left;
}
dd {
float: right;
margin: 0 0 0 1em;
border: 1em solid black;
padding: 1em;
width: 34em;
height: 27em;
}
ul {
margin: 0;
border: 0;
padding: 0;
}
li {
display: block; /* i.e., suppress marker */
color: black;
height: 9em;
width: 5em;
margin: 0;
border: .5em solid black;
padding: 1em;
float: left;
background-color: #FC0;
}
#bar {
background-color: black;
color: white;
width: 41.17%; /* = 14em */
border: 0;
margin: 0 1em;
}
#baz {
margin: 1em 0;
border: 0;
padding: 1em;
width: 10em;
height: 10em;
background-color: black;
color: white;
}
form {
margin: 0;
display: inline;
}
p {
margin: 0;
}
form p {
line-height: 1.9;
}
blockquote {
margin: 1em 1em 1em 2em;
border-width: 1em 1.5em 2em .5em;
border-style: solid;
border-color: black;
padding: 1em 0;
width: 5em;
height: 9em;
float: left;
background-color: #FC0;
color: black;
}
address {
font-style: normal;
}
h1 {
background-color: black;
color: white;
float: left;
margin: 1em 0;
border: 0;
padding: 1em;
width: 10em;
height: 10em;
font-weight: normal;
font-size: 1em;
}
</style>
</head>
<body>
<dl>
<dt>
toggle
</dt>
<dd>
<ul>
<li>
the way
</li>
<li id="bar">
<p>
the world ends
</p>
<form action="./" method="get">
<p>
bang
<input type="radio" name="foo" value="off">
</p>
<p>
whimper
<input type="radio" name="foo2" value="on">
</p>
</form>
</li>
<li>
i grow old
</li>
<li id="baz">
pluot?
</li>
</ul>
<blockquote>
<address>
bar maids,
</address>
</blockquote>
<h1>
sing to me, erbarme dich
</h1>
</dd>
</dl>
<p style="color: black; font-size: 1em; line-height: 1.3em; clear: both">
This is a nonsensical document, but syntactically valid HTML 4.0. All 100% conformant CSS1 agents should be able to render the document elements above this paragraph <b>indistinguishably</b> (to the pixel) from this reference rendering, (except font rasterization and form widgets). All discrepancies should be traceable to CSS1 implementation shortcomings. Once you have finished evaluating this test, you can return to the <A HREF="sec5526c.htm" style="text-decoration:none">parent page</A>.
</p>
</body>
</html>

72
demo3.html Normal file
View File

@ -0,0 +1,72 @@
<!DOCTYPE html>
<html>
<head>
<title></title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<script type="text/javascript" src="external/jquery-1.6.2.min.js"></script>
<script type="text/javascript" src="build/html2canvas.js"></script>
<script type="text/javascript" src="build/jquery.plugin.html2canvas.js"></script>
<script type="text/javascript">
$(function(){
$('body').html2canvas();
});
</script>
<style>
.feedback-overlay-black{
background-color:#000;
opacity:0.5;
position:absolute;
top:0;
left:0;
width:100%;
height:100%;
margin:0;
}
</style>
<style>
div{
padding:20px;
margin:0 auto;
border:5px solid black;
}
h1{
border-bottom:2px solid white;
}
h2{
background: #efefef;
padding:10px;
}
</style>
</head>
<body>
<div style="background:red;">
<div style="background:green;">
<div style="background:blue;border-color:white;">
<div style="background:yellow;"><div style="background:orange;"><h1>Heading</h1>
Text that isn't wrapped in anything.
<p>Followed by some text wrapped in a <b>&lt;p&gt; paragraph.</b> </p>
Maybe add a <a href="#">link</a> or a different style of <a href="#" style="background:white;" id="highlight">link with a highlight</a>.
<hr />
<h2>More content</h2>
<div style="width:10px;height:10px;border-width:10px;padding:0;">a</div>
</div></div>
</div>
</div>
</div>
</body>
</html>

View File

@ -1,36 +0,0 @@
---
title: "Options"
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.
| ignoreElements | `(element) => false` | Predicate function which removes the matching elements from the render.
| logging | `true` | Enable logging for debug purposes
| onclone | `null` | Callback function which is called when the Document has been cloned for rendering, can be used to modify the contents that will be rendered without affecting the original source document.
| proxy | `null` | Url to the [proxy](/proxy/) which is to be used for loading cross-origin images. If left empty, cross-origin images won't be loaded.
| removeContainer | `true` | Whether to cleanup the cloned DOM elements html2canvas creates temporarily
| scale | `window.devicePixelRatio` | The scale to use for rendering. Defaults to the browsers device pixel ratio.
| useCORS | `false` | Whether to attempt to load images from a server using CORS
| width | `Element` width | The width of the `canvas`
| height | `Element` height | The height of the `canvas`
| x | `Element` x-offset | Crop canvas x-coordinate
| y | `Element` y-offset| Crop canvas y-coordinate
| scrollX | `Element` scrollX | The x-scroll position to used when rendering element, (for example if the Element uses `position: fixed`)
| scrollY | `Element` scrollY | The y-scroll position to used when rendering element, (for example if the Element uses `position: fixed`)
| windowWidth | `Window.innerWidth` | Window width to use when rendering `Element`, which may affect things like Media queries
| windowHeight | `Window.innerHeight` | Window height to use when rendering `Element`, which may affect things like Media queries
If you wish to exclude certain `Element`s from getting rendered, you can add a `data-html2canvas-ignore` attribute to those elements and html2canvas will exclude them from the rendering.

View File

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

View File

@ -1,42 +0,0 @@
---
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()).

View File

@ -1,85 +0,0 @@
---
title: "Features"
description: "Discover the different features supported by html2canvas"
---
Below is a list of all the supported CSS properties and values.
- background
- background-clip (**Does not support `text`**)
- background-color
- background-image
- url()
- linear-gradient()
- radial-gradient()
- background-origin
- background-position
- background-size
- border
- border-color
- border-radius
- border-style (**Only supports `solid`**)
- border-width
- bottom
- box-sizing
- content
- color
- display
- flex
- float
- font
- font-family
- font-size
- font-style
- font-variant
- font-weight
- height
- left
- letter-spacing
- line-break
- list-style
- list-style-image
- list-style-position
- list-style-type
- margin
- max-height
- max-width
- min-height
- min-width
- opacity
- overflow
- overflow-wrap
- 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-break
- word-spacing
- word-wrap
- z-index
## Unsupported CSS properties
These CSS properties are **NOT** currently supported
- [background-blend-mode](https://github.com/niklasvh/html2canvas/issues/966)
- [border-image](https://github.com/niklasvh/html2canvas/issues/1287)
- [box-decoration-break](https://github.com/niklasvh/html2canvas/issues/552)
- [box-shadow](https://github.com/niklasvh/html2canvas/pull/1086)
- [filter](https://github.com/niklasvh/html2canvas/issues/493)
- [font-variant-ligatures](https://github.com/niklasvh/html2canvas/pull/1085)
- [mix-blend-mode](https://github.com/niklasvh/html2canvas/issues/580)
- [object-fit](https://github.com/niklasvh/html2canvas/issues/1064)
- [repeating-linear-gradient()](https://github.com/niklasvh/html2canvas/issues/1162)
- [writing-mode](https://github.com/niklasvh/html2canvas/issues/1258)
- [zoom](https://github.com/niklasvh/html2canvas/issues/732)

View File

@ -1,30 +0,0 @@
---
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);
});
```

View File

@ -1,12 +0,0 @@
---
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)

View File

@ -1,181 +0,0 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">
<html>
<head>
<title>
display/box/float/clear test
</title>
<style type="text/css">
/* last modified: 1 Dec 98 */
html {
font: 10px/1 Verdana, sans-serif;
background-color: blue;
color: white;
}
body {
margin: 1.5em;
border: .5em solid black;
padding: 0;
width: 48em;
background-color: white;
}
dl {
margin: 0;
border: 0;
padding: .5em;
}
dt {
background-color: rgb(204,0,0);
margin: 0;
padding: 1em;
width: 10.638%; /* refers to parent element's width of 47em. = 5em or 50px */
height: 28em;
border: .5em solid black;
float: left;
}
dd {
float: right;
margin: 0 0 0 1em;
border: 1em solid black;
padding: 1em;
width: 34em;
height: 27em;
}
ul {
margin: 0;
border: 0;
padding: 0;
}
li {
display: block; /* i.e., suppress marker */
color: black;
height: 9em;
width: 5em;
margin: 0;
border: .5em solid black;
padding: 1em;
float: left;
background-color: #FC0;
}
#bar {
background-color: black;
color: white;
width: 41.17%; /* = 14em */
border: 0;
margin: 0 1em;
}
#baz {
margin: 1em 0;
border: 0;
padding: 1em;
width: 10em;
height: 10em;
background-color: black;
color: white;
}
form {
margin: 0;
display: inline;
}
p {
margin: 0;
}
form p {
line-height: 1.9;
}
blockquote {
margin: 1em 1em 1em 2em;
border-width: 1em 1.5em 2em .5em;
border-style: solid;
border-color: black;
padding: 1em 0;
width: 5em;
height: 9em;
float: left;
background-color: #FC0;
color: black;
}
address {
font-style: normal;
}
h1 {
background-color: black;
color: white;
float: left;
margin: 1em 0;
border: 0;
padding: 1em;
width: 10em;
height: 10em;
font-weight: normal;
font-size: 1em;
}
</style>
</head>
<body>
<dl>
<dt>
toggle
</dt>
<dd>
<ul>
<li>
the way
</li>
<li id="bar">
<p>
the world ends
</p>
<form action="./" method="get">
<p>
bang
<input type="radio" name="foo" value="off">
</p>
<p>
whimper
<input type="radio" name="foo2" value="on">
</p>
</form>
</li>
<li>
i grow old
</li>
<li id="baz">
pluot?
</li>
</ul>
<blockquote>
<address>
bar maids,
</address>
</blockquote>
<h1>
sing to me, erbarme dich
</h1>
</dd>
</dl>
<p style="color: black; font-size: 1em; line-height: 1.3em; clear: both">
This is a nonsensical document, but syntactically valid HTML 4.0. All 100% conformant CSS1 agents should be able to render the document elements above this paragraph <b>indistinguishably</b> (to the pixel) from this reference rendering, (except font rasterization and form widgets). All discrepancies should be traceable to CSS1 implementation shortcomings. Once you have finished evaluating this test, you can return to the <A HREF="sec5526c.htm" style="text-decoration:none">parent page</A>.
</p>
<script type="text/javascript" src="../dist/html2canvas.js"></script>
<script type="text/javascript">
html2canvas(document.body).then(function(canvas) {
document.body.appendChild(canvas);
});
</script>
</body>
</html>

View File

@ -1,63 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title></title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<style>
.feedback-overlay-black{
background-color:#000;
opacity:0.5;
position:absolute;
top:0;
left:0;
width:100%;
height:100%;
margin:0;
}
</style>
<style>
div{
padding:20px;
margin:0 auto;
border:5px solid black;
}
h1{
border-bottom:2px solid white;
}
h2{
background: #efefef;
padding:10px;
}
</style>
</head>
<body>
<div style="background:red;">
<div style="background:green;">
<div style="background:blue;border-color:white;">
<div style="background:yellow;"><div style="background:orange;"><h1>Heading</h1>
Text that isn't wrapped in anything.
<p>Followed by some text wrapped in a <b>&lt;p&gt; paragraph.</b> </p>
Maybe add a <a href="#">link</a> or a different style of <a href="#" style="background:white;" id="highlight">link with a highlight</a>.
<hr />
<h2>More content</h2>
<div style="width:10px;height:10px;border-width:10px;padding:0;">a</div>
</div></div>
</div>
</div>
</div>
<script type="text/javascript" src="../dist/html2canvas.js"></script>
<script type="text/javascript">
html2canvas(document.body).then(function(canvas) {
document.body.appendChild(canvas);
});
</script>
</body>
</html>

View File

@ -1,52 +0,0 @@
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8">
<title>Using an existing canvas to draw on</title>
<style>
canvas {
border: 1px solid black;
}
button {
clear: both;
display: block;
}
#content {
background: rgba(100, 255, 255, 0.5);
padding: 50px 10px;
}
</style>
</head>
<body>
<div><h1>HTML content to render:</h1>
<div id="content">Render the content in this element <strong>only</strong> onto the existing canvas element</div>
</div>
<h1>Existing canvas:</h1>
<canvas width="500" height="200"></canvas>
<script type="text/javascript" src="../dist/html2canvas.js"></script>
<button>Run html2canvas</button>
<script type="text/javascript">
var canvas = document.querySelector("canvas");
var ctx = canvas.getContext("2d");
var ctx = canvas.getContext('2d');
ctx.beginPath();
ctx.arc(75,75,50,0,Math.PI*2,true); // Outer circle
ctx.moveTo(110,75);
ctx.arc(75,75,35,0,Math.PI,false); // Mouth (clockwise)
ctx.moveTo(65,65);
ctx.arc(60,65,5,0,Math.PI*2,true); // Left eye
ctx.moveTo(95,65);
ctx.arc(90,65,5,0,Math.PI*2,true); // Right eye
ctx.stroke();
document.querySelector("button").addEventListener("click", function() {
html2canvas(document.querySelector("#content"), {canvas: canvas}).then(function(canvas) {
console.log('Drew on the existing canvas');
});
}, false);
</script>
</body>
</html>

8981
external/jquery-1.6.2.js vendored Normal file

File diff suppressed because it is too large Load Diff

18
external/jquery-1.6.2.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -1,11 +0,0 @@
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 {}

View File

@ -1,176 +0,0 @@
// 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
View File

@ -1,112 +0,0 @@
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();
});

BIN
lib/compiler.jar Normal file

Binary file not shown.

1204
lib/jquery-1.4.4.externs.js Normal file

File diff suppressed because it is too large Load Diff

BIN
loading.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

7483
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,82 +0,0 @@
{
"title": "html2canvas",
"name": "html2canvas",
"description": "Screenshots with JavaScript",
"main": "dist/npm/index.js",
"version": "1.0.0-alpha.10",
"author": {
"name": "Niklas von Hertzen",
"email": "niklasvh@gmail.com",
"url": "https://hertzen.com"
},
"engines": {
"node": ">=4.0.0"
},
"repository": {
"type": "git",
"url": "git@github.com:niklasvh/html2canvas.git"
},
"bugs": {
"url": "https://github.com/niklasvh/html2canvas/issues"
},
"devDependencies": {
"babel-cli": "6.24.1",
"babel-core": "6.25.0",
"babel-eslint": "7.2.3",
"babel-loader": "7.1.1",
"babel-plugin-dev-expression": "0.2.1",
"babel-plugin-transform-es2015-modules-commonjs": "6.26.0",
"babel-plugin-transform-object-rest-spread": "6.23.0",
"babel-preset-es2015": "6.24.1",
"babel-preset-flow": "6.23.0",
"base64-arraybuffer": "0.1.5",
"body-parser": "1.17.2",
"chai": "4.1.1",
"chromeless": "1.2.0",
"cors": "2.8.4",
"eslint": "4.2.0",
"eslint-plugin-flowtype": "2.35.0",
"eslint-plugin-prettier": "2.1.2",
"express": "4.15.4",
"filenamify-url": "1.0.0",
"flow-bin": "0.56.0",
"glob": "7.1.2",
"html2canvas-proxy": "1.0.0",
"jquery": "3.2.1",
"karma": "1.7.0",
"karma-chrome-launcher": "2.2.0",
"karma-edge-launcher": "0.4.1",
"karma-firefox-launcher": "1.0.1",
"karma-ie-launcher": "1.0.0",
"karma-mocha": "1.3.0",
"karma-sauce-launcher": "1.1.0",
"mocha": "3.5.0",
"platform": "1.3.4",
"prettier": "1.5.3",
"promise-polyfill": "6.0.2",
"replace-in-file": "^3.0.0",
"rimraf": "2.6.1",
"serve-index": "1.9.0",
"slash": "1.0.0",
"uglifyjs-webpack-plugin": "^1.1.2",
"webpack": "3.4.1"
},
"scripts": {
"build": "rimraf dist/ && node scripts/create-reftest-list && npm run build:npm && npm run build:browser",
"build:npm": "babel src/ -d dist/npm/ --plugins=dev-expression,transform-es2015-modules-commonjs && replace-in-file __VERSION__ '\"$npm_package_version\"' dist/npm/index.js",
"build:browser": "webpack",
"format": "prettier --single-quote --no-bracket-spacing --tab-width 4 --print-width 100 --write \"{src,www/src,tests,scripts}/**/*.js\"",
"flow": "flow",
"lint": "eslint src/**",
"test": "npm run flow && npm run lint && npm run test:node && npm run karma",
"test:node": "mocha tests/node/*.js",
"karma": "node karma",
"watch": "webpack --progress --colors --watch",
"start": "node tests/server"
},
"homepage": "https://html2canvas.hertzen.com",
"license": "MIT",
"dependencies": {
"css-line-break": "1.0.1"
}
}

36
readme.md Normal file
View File

@ -0,0 +1,36 @@
html2canvas
===========
#### JavaScript HTML renderer ####
This script allows you to take "screenshots" of webpages or parts of it, directly on the users browser. The screenshot is based on the DOM and as such may not be 100% accurate to the real representation as it does not make an actual screenshot, but builds the screenshot based on the information available on the page.
###How does it work?###
The script renders the current page as a canvas image, by reading the DOM and the different styles applied to the elements. However, as many elements are displayed differently on different browsers and operating systems (such as form elements such as radio buttons or checkboxes) as well as
It does <b>not require any rendering from the server</b>, as the whole image is created on the <b>clients browser</b>. However, for browsers without <code>canvas</code> support alternatives such as <a href="http://flashcanvas.net/">flashcanvas</a> or <a href="http://excanvas.sourceforge.net/">ExplorerCanvas</a> are necessary to create the image.
Additionally, to render <code>iframe</code> content or images situated outside of the <a href="http://en.wikipedia.org/wiki/Same_origin_policy">same origin policy</a> a proxy will be necessary to load the content to the users browser.
The script is still in a very experimental state, so I don't recommend using it in a production environment nor start building applications with it yet, as there will be still major changes made. However, please do test it out and report your findings, especially if something should be working, but is displaying it incorrectly.
###Browser compatibility###
The script should work fine on the following browsers:
* Firefox 3.5+
* Google Chrome
* Newer versions of Opera (exactly how new is yet to be determined)
* >=IE9 (Older versions compatible with the use of flashcanvas)
Note that the compatibility will most likely be increased in future builds, as many of the current restrictions have at least partial work arounds, which can be used with older browser versions.
###So what isn't included yet?###
There are still a lot of CSS properties missing, including most CSS3 properties such as <code>text-shadow</code>, <code>box-radius</code> etc. as well as all elements created by the browser, such as radio and checkbox buttons and list icons. I will compile a full list of supported elements and CSS properties soon.
There is no support for <code>frame</code> and <code>object</code> content such as Flash.
### Examples ###
For more information and examples, please visit the <a href="http://html2canvas.hertzen.com">homepage</a> or try the <a href="http://html2canvas.hertzen.com/screenshots.html">test console</a>.

326
screenshots.html Normal file
View File

@ -0,0 +1,326 @@
<!DOCTYPE html>
<html>
<head>
<title>JavaScript screenshot creator</title>
<style type="text/css">
a {
color: #0B0B0B;
background-color: #FDF9EE;
padding: 0 8px;
text-decoration: none;
font: normal 12px/16px "Trebuchet MS", Arial, Helvetica, sans-serif;
}
a:hover {
color: #0B0B0B;
background-color: #EFEBDE;
padding: 0 8px;
text-decoration: none;
font: normal 12px/16px "Trebuchet MS", Arial, Helvetica, sans-serif;
}
body {
font: normal 14px/19px Arial, Helvetica, sans-serif;
background-color: white;
color: #4E4628;
}
textarea {
background-color: #EFEBDE;
color: #0B0B0B;
border: #C3BCA4 1px solid;
font: normal 11px Arial, Helvetica, sans-serif;
width:300px;
height:150px;
}
h2 {
background-color: white;
color: #0B0B0B;
font: normal 28px/46px Georgia, "Times New Roman", Times, serif;
margin:0;
clear:both;
}
h3 {
color: #786E4E;
padding: 0 0 10px 55px;
height: 37px;
font: normal 24px/30px Georgia, "Times New Roman", Times, serif;
}
ul{
float:left;
margin:0;
}
table{
margin:0 auto;
width:400px;
border:1px solid black;
}
#content{
clear:both;
text-align:center;
}
#about{
padding:0 10px;
width:450px;
float:left;
}
</style>
<script type="text/javascript" src="external/jquery-1.6.2.min.js"></script>
<script type="text/javascript" src="build/html2canvas.js?221"></script>
<script type="text/javascript" src="build/jquery.plugin.html2canvas.js"></script>
<script type="text/javascript" src="http://www.hertzen.com/js/ganalytics-heatmap.js"></script>
<script type="text/javascript">
var date = new Date();
var message,
timeoutTimer,
timer;
var proxyUrl = "http://html2canvas.appspot.com";
function addRow(table,field,val){
var tr = $('<tr />').appendTo( $(table));
tr.append($('<td />').css('font-weight','bold').text(field)).append($('<td />').text(val));
}
function throwMessage(msg,duration){
window.clearTimeout(timeoutTimer);
timeoutTimer = window.setTimeout(function(){
message.fadeOut(function(){
message.remove();
});
},duration || 2000);
$(message).remove();
message = $('<div />').html(msg).css({
margin:0,
padding:10,
background: "#000",
opacity:0.7,
position:"fixed",
top:10,
right:10,
fontFamily: 'Tahoma' ,
color:'#fff',
fontSize:12,
borderRadius:12,
width:'auto',
height:'auto',
textAlign:'center',
textDecoration:'none'
}).hide().fadeIn().appendTo('body');
}
$(function(){
$('ul li a').click(function(e){
e.preventDefault();
$('#url').val(this.href);
$('button').click();
})
var iframe,d;
$('input[type="button"]').click(function(){
$(iframe.contentWindow).unbind('load');
$(iframe).contents().find('body').html2canvas({
canvasHeight: d.body.scrollHeight,
canvasWidth: d.body.scrollWidth,
logging:true
});
});
$('button').click(function(){
$(this).prop('disabled',true);
var url = $('#url').val();
$('#content').append($('<img />').attr('src','loading.gif').css('margin-top',40));
var urlParts = document.createElement('a');
urlParts.href = url;
$.ajax({
data: {
xhr2:false,
url:urlParts.href
},
url: proxyUrl,
dataType: "jsonp",
success: function(html){
iframe = document.createElement('iframe');
$(iframe).css({
'visibility':'hidden'
}).width($(window).width()).height($(window).height());
$('#content').append(iframe);
d = iframe.contentWindow.document;
d.open();
$(iframe.contentWindow).load(function(){
timer = date.getTime();
$(iframe).contents().find('body').html2canvas({
canvasHeight: d.body.scrollHeight,
canvasWidth: d.body.scrollWidth,
logging:true,
proxyUrl: proxyUrl,
logger:function(msg){
$('#logger').val(function(e,i){
return i+"\n"+msg;
});
},
ready: function(renderer) {
$('button').prop('disabled',false);
$("#content").empty();
var finishTime = new Date();
var table = $('<table />');
$('#content')
.append('<h2>Screenshot</h2>')
.append(renderer.canvas)
.append('<h3>Details</h3>')
.append(table);
addRow(table,"Creation time",((finishTime.getTime()-timer)/1000) + " seconds");
addRow(table,"Total draws", renderer.numDraws);
addRow(table,"Context stacks", renderer.contextStacks.length);
addRow(table,"Loaded images", renderer.images.length/2);
addRow(table,"Performed z-index reorder", renderer.needReorder);
addRow(table,"Used rangeBounds", renderer.support.rangeBounds);
throwMessage('Screenshot created in '+ ((finishTime.getTime()-timer)/1000) + " seconds<br />Total of "+renderer.numDraws+" draws performed",4000);
}
});
});
$('base').attr('href',urlParts.protocol+"//"+urlParts.hostname+"/");
html = html.replace("<head>","<head><base href='"+urlParts.protocol+"//"+urlParts.hostname+"/' />");
if ($("#disablejs").prop('checked')){
html = html.replace(/\<script/gi,"<!--<script");
html = html.replace(/\<\/script\>/gi,"<\/script>-->");
}
// console.log(html);
d.write(html);
d.close();
}
});
});
});
</script>
<script type="text/javascript">
var _gaq = _gaq || [];_gaq.push(['_setAccount', 'UA-188600-10']);_gaq.push(['_trackPageview']);(function() {var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);})();
</script>
<base />
</head>
<body>
<!-- <div style="background:red;padding:10px;color:#fff">
App engine proxy is <a href="http://twitter.com/#!/Niklasvh/status/96265826713350144">temporarily out of use</a> due to exceeded bandwidth use. Please try again tomorrow or meanwhile check other examples <a href="http://html2canvas.hertzen.com/">here</a>.
</div>-->
<div style="float:left;width:500px;">
<h1>JavaScript screenshot creator</h1>
<label for="url">Website URL:</label>
<input type="url" id="url" value="http://www.yahoo.com" /><button>Get screenshot!</button>
<!-- <input type="button" value="Try anyway" />--><br />
<label for="disablejs">Disable JavaScript (recommended, doesn't work well with the proxy)</label> <input type="checkbox" id="disablejs" checked /><br />
<small>Tested with Google Chrome 12, Firefox 4 and Opera 11.5</small>
</div>
<div style="float:right;">
<div style="margin-left:17px;float:right;">
<!-- Place this tag in your head or just before your close body tag -->
<script type="text/javascript" src="https://apis.google.com/js/plusone.js"></script>
<!-- Place this tag where you want the +1 button to render -->
<g:plusone size="tall"></g:plusone>
</div>
<div style="float:right;">
<a href="http://twitter.com/share" class="twitter-share-button" data-url="http://html2canvas.hertzen.com/" data-text="html2canvas - screenshots with #JavaScript" data-count="vertical" data-via="niklasvh">Tweet</a><script type="text/javascript" src="http://platform.twitter.com/widgets.js"></script>
</div>
</div>
<div style="clear:both;"></div>
<h3>Recommended (tested) pages:</h3>
<ul>
<li><a href="http://www.yahoo.com">yahoo.com</a></li>
<li><a href="http://www.google.com">google.com</a></li>
<li><a href="https://github.com/niklasvh/html2canvas">github.com</a></li>
<li><a href="http://www.smashingmagazine.com">smashingmagazine.com</a></li>
<li><a href="http://www.mashable.com">mashable.com</a></li>
<li><a href="http://www.facebook.com/google">facebook.com/google</a></li>
<li><a href="http://www.youtube.com/">youtube.com</a></li>
<li><a href="http://www.cnn.com/">cnn.com</a></li>
<li><a href="http://www.engadget.com/">engadget.com (lot of elements, very slow)</a></li>
<li><a href="http://eu.battle.net/en/">battle.net</a></li>
</ul>
<div style="float:left;">
<textarea id="logger"></textarea>
</div>
<div id="about"><b> About</b><br />
The whole screenshot is created with JavaScript. The only server interaction that is happening on this page is the proxy for loading the external pages/images into JSONP/CORS enabled page and onwards onto the JavaScript renderer script.
There are a lot of problems of loading external pages, even with a proxy, and as such many pages will not render at all. If you wish to try the script properly, I recommend you get a copy of the source from <a href="https://github.com/niklasvh/html2canvas">here</a> instead.
</div>
<div id="content"></div>
</body>
</html>

View File

@ -1,53 +0,0 @@
'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

@ -1,37 +0,0 @@
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));
});

View File

@ -1,155 +0,0 @@
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;

View File

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

300
src/Background.js Normal file
View File

@ -0,0 +1,300 @@
html2canvas.prototype.drawBackground = function(el,bounds,ctx){
// TODO add support for multi background-images
var background_image = this.getCSS(el,"background-image").split(",")[0];
var background_repeat = this.getCSS(el,"background-repeat").split(",")[0];
if (typeof background_image != "undefined" && /^(1|none)$/.test(background_image)==false && /^(-webkit|-moz|linear-gradient|-o-)/.test(background_image)==false){
background_image = this.backgroundImageUrl(background_image);
var image = this.loadImage(background_image);
var bgp = this.getBackgroundPosition(el,bounds,image),
bgy;
if (image){
switch(background_repeat){
case "repeat-x":
this.drawbackgroundRepeatX(ctx,image,bgp,bounds.left,bounds.top,bounds.width,bounds.height);
break;
case "repeat-y":
this.drawbackgroundRepeatY(ctx,image,bgp,bounds.left,bounds.top,bounds.width,bounds.height);
break;
case "no-repeat":
/*
this.drawBackgroundRepeat(
ctx,
image,
bgp.left+bounds.left, // sx
bgp.top+bounds.top, // sy
Math.min(bounds.width,image.width),
Math.min(bounds.height,image.height),
bounds.left,
bounds.top
);*/
// console.log($(el).css('background-image'));
var bgw = bounds.width-bgp.left,
bgh = bounds.height-bgp.top,
bgsx = bgp.left,
bgsy = bgp.top,
bgdx = bgp.left+bounds.left,
bgdy = bgp.top+bounds.top;
//
// bgw = Math.min(bgw,image.width);
// bgh = Math.min(bgh,image.height);
if (bgsx<0){
bgsx = Math.abs(bgsx);
bgdx += bgsx;
bgw = Math.min(bounds.width,image.width-bgsx);
}else{
bgw = Math.min(bgw,image.width);
bgsx = 0;
}
if (bgsy<0){
bgsy = Math.abs(bgsy);
bgdy += bgsy;
// bgh = bgh-bgsy;
bgh = Math.min(bounds.height,image.height-bgsy);
}else{
bgh = Math.min(bgh,image.height);
bgsy = 0;
}
// bgh = Math.abs(bgh);
// bgw = Math.abs(bgw);
if (bgh>0 && bgw > 0){
this.drawImage(
ctx,
image,
bgsx, // source X : 0
bgsy, // source Y : 1695
bgw, // source Width : 18
bgh, // source Height : 1677
bgdx, // destination X :906
bgdy, // destination Y : 1020
bgw, // destination width : 18
bgh // destination height : 1677
);
// ctx.drawImage(image,(bounds.left+bgp.left),(bounds.top+bgp.top));
break;
}
default:
var height,
add;
bgp.top = bgp.top-Math.ceil(bgp.top/image.height)*image.height;
for(bgy=(bounds.top+bgp.top);bgy<bounds.height+bounds.top;){
var h = Math.min(image.height,(bounds.height+bounds.top)-bgy);
if ( Math.floor(bgy+image.height)>h+bgy){
height = (h+bgy)-bgy;
}else{
height = image.height;
}
// console.log(height);
if (bgy<bounds.top){
add = bounds.top-bgy;
bgy = bounds.top;
}else{
add = 0;
}
this.drawbackgroundRepeatX(ctx,image,bgp,bounds.left,bgy,bounds.width,height);
if (add>0){
bgp.top += add;
}
bgy = Math.floor(bgy+image.height)-add;
}
break;
}
}else{
this.log("Error loading background:" + background_image);
//console.log(images);
}
}
}
/*
* Function to retrieve the actual src of a background-image
*/
html2canvas.prototype.backgroundImageUrl = function(src){
if (src.substr(0,5)=='url("'){
src = src.substr(5);
src = src.substr(0,src.length-2);
}else{
src = src.substr(4);
src = src.substr(0,src.length-1);
}
return src;
}
/*
* Function to retrieve background-position, both in pixels and %
*/
html2canvas.prototype.getBackgroundPosition = function(el,bounds,image){
// TODO add support for multi image backgrounds
var bgpos = this.getCSS(el,"backgroundPosition").split(",")[0] || "0 0";
// var bgpos = $(el).css("backgroundPosition") || "0 0";
var bgposition = bgpos.split(" "),
topPos,
left,
percentage;
if (bgposition.length==1){
var val = bgposition,
bgposition = [];
bgposition[0] = val,
bgposition[1] = val;
}
if (bgposition[0].toString().indexOf("%")!=-1){
percentage = (parseFloat(bgposition[0])/100);
left = ((bounds.width * percentage)-(image.width*percentage));
}else{
left = parseInt(bgposition[0],10);
}
if (bgposition[1].toString().indexOf("%")!=-1){
percentage = (parseFloat(bgposition[1])/100);
topPos = ((bounds.height * percentage)-(image.height*percentage));
}else{
topPos = parseInt(bgposition[1],10);
}
var returnObj = {}
/*
"top": topPos,
"left": left
};*/
returnObj.top = topPos;
returnObj.left = left;
return returnObj;
}
html2canvas.prototype.drawbackgroundRepeatY = function(ctx,image,bgp,x,y,w,h){
var height,
width = Math.min(image.width,w),bgy;
bgp.top = bgp.top-Math.ceil(bgp.top/image.height)*image.height;
for(bgy=(y+bgp.top);bgy<h+y;){
if ( Math.floor(bgy+image.height)>h+y){
height = (h+y)-bgy;
}else{
height = image.height;
}
this.drawBackgroundRepeat(ctx,image,x+bgp.left,bgy,width,height,x,y);
bgy = Math.floor(bgy+image.height);
}
}
html2canvas.prototype.drawbackgroundRepeatX = function(ctx,image,bgp,x,y,w,h){
var height = Math.min(image.height,h),
width,bgx;
bgp.left = bgp.left-Math.ceil(bgp.left/image.width)*image.width;
for(bgx=(x+bgp.left);bgx<w+x;){
if (Math.floor(bgx+image.width)>w+x){
width = (w+x)-bgx;
}else{
width = image.width;
}
this.drawBackgroundRepeat(ctx,image,bgx,(y+bgp.top),width,height,x,y);
bgx = Math.floor(bgx+image.width);
}
}
html2canvas.prototype.drawBackgroundRepeat = function(ctx,image,x,y,width,height,elx,ely){
var sourceX = 0,
sourceY=0;
if (elx-x>0){
sourceX = elx-x;
}
if (ely-y>0){
sourceY = ely-y;
}
this.drawImage(
ctx,
image,
sourceX, // source X
sourceY, // source Y
width-sourceX, // source Width
height-sourceY, // source Height
x+sourceX, // destination X
y+sourceY, // destination Y
width-sourceX, // destination width
height-sourceY // destination height
);
}

85
src/Border.js Normal file
View File

@ -0,0 +1,85 @@
/*
* Function to provide border details for an element
*/
html2canvas.prototype.getBorderData = function(el){
var borders = [];
var _ = this;
this.each(["top","right","bottom","left"],function(i,borderSide){
borders.push({
width: parseInt(_.getCSS(el,'border-'+borderSide+'-width'),10),
color: _.getCSS(el,'border-'+borderSide+'-color')
});
});
return borders;
}
html2canvas.prototype.drawBorders = function(el,ctx, bounds,clip){
var x = bounds.left;
var y = bounds.top;
var w = bounds.width;
var h = bounds.height;
/*
* TODO add support for different border-style's than solid
*/
var borders = this.getBorderData(el);
var _ = this;
this.each(borders,function(borderSide,borderData){
if (borderData.width>0){
var bx = x,
by = y,
bw = w,
bh = h-(borders[2].width);
switch(borderSide){
case 0:
// top border
bh = borders[0].width;
break;
case 1:
// right border
bx = x+w-(borders[1].width);
bw = borders[1].width;
break;
case 2:
// bottom border
by = (by+h)-(borders[2].width);
bh = borders[2].width;
break;
case 3:
// left border
bw = borders[3].width;
break;
}
var borderBounds = {
left:bx,
top:by,
width: bw,
height:bh
};
if (clip){
borderBounds = _.clipBounds(borderBounds,clip);
}
if (borderBounds.width>0 && borderBounds.height>0){
_.newRect(ctx,bx,by,borderBounds.width,borderBounds.height,borderData.color);
}
}
});
return borders;
};

View File

@ -1,370 +0,0 @@
/* @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 => {
let tlh = borderRadius[CORNER.TOP_LEFT][H].getAbsoluteValue(bounds.width);
let tlv = borderRadius[CORNER.TOP_LEFT][V].getAbsoluteValue(bounds.height);
let trh = borderRadius[CORNER.TOP_RIGHT][H].getAbsoluteValue(bounds.width);
let trv = borderRadius[CORNER.TOP_RIGHT][V].getAbsoluteValue(bounds.height);
let brh = borderRadius[CORNER.BOTTOM_RIGHT][H].getAbsoluteValue(bounds.width);
let brv = borderRadius[CORNER.BOTTOM_RIGHT][V].getAbsoluteValue(bounds.height);
let blh = borderRadius[CORNER.BOTTOM_LEFT][H].getAbsoluteValue(bounds.width);
let blv = borderRadius[CORNER.BOTTOM_LEFT][V].getAbsoluteValue(bounds.height);
const factors = [];
factors.push((tlh + trh) / bounds.width);
factors.push((blh + brh) / bounds.width);
factors.push((tlv + blv) / bounds.height);
factors.push((trv + brv) / bounds.height);
const maxFactor = Math.max(...factors);
if (maxFactor > 1) {
tlh /= maxFactor;
tlv /= maxFactor;
trh /= maxFactor;
trv /= maxFactor;
brh /= maxFactor;
brv /= maxFactor;
blh /= maxFactor;
blv /= maxFactor;
}
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)
);
}
};

View File

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

View File

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

260
src/Core.js Normal file
View File

@ -0,0 +1,260 @@
/**
* Creates a render of the element el
* @constructor
*/
function html2canvas(el, userOptions) {
var options = userOptions || {};
this.opts = this.extendObj(options, {
logging: false,
ready: function (stack) {
document.body.appendChild(stack.canvas);
},
storageReady: function(obj){
obj.Renderer(obj.contextStacks);
},
iframeDefault: "default",
flashCanvasPath: "http://html2canvas.hertzen.com/external/flashcanvas/flashcanvas.js",
renderViewport: false,
reorderZ: true,
throttle:true,
letterRendering:false,
proxyUrl: null,
logger: function(a){
if (window.console && window.console.log){
window.console.log(a);
}else{
alert(a);
}
},
canvasWidth:0,
canvasHeight:0,
useOverflow: true,
renderOrder: "canvas flash html"
});
this.element = el;
var imageLoaded,
canvas,
ctx,
bgx,
bgy,
image;
this.imagesLoaded = 0;
this.images = [];
this.fontData = [];
this.numDraws = 0;
this.contextStacks = [];
this.ignoreElements = "IFRAME|OBJECT|PARAM";
this.needReorder = false;
this.blockElements = new RegExp("(BR|PARAM)");
this.pageOrigin = window.location.protocol + window.location.host;
this.queue = [];
this.ignoreRe = new RegExp("("+this.ignoreElements+")");
this.support = {
rangeBounds: false
};
// Test whether we can use ranges to measure bounding boxes
// Opera doesn't provide valid bounds.height/bottom even though it supports the method.
if (document.createRange){
var r = document.createRange();
//this.support.rangeBounds = new Boolean(r.getBoundingClientRect);
if (r.getBoundingClientRect){
var testElement = document.createElement('boundtest');
testElement.style.height = "123px";
testElement.style.display = "block";
document.getElementsByTagName('body')[0].appendChild(testElement);
r.selectNode(testElement);
var rangeBounds = r.getBoundingClientRect();
var rangeHeight = rangeBounds.height;
if (rangeHeight==123){
this.support.rangeBounds = true;
}
document.getElementsByTagName('body')[0].removeChild(testElement);
}
}
// Start script
this.init();
return this;
}
html2canvas.prototype.init = function(){
var _ = this;
/*
this.ctx = new this.stackingContext($(document).width(),$(document).height());
if (!this.ctx){
// canvas not initialized, let's kill it here
this.log('Canvas not available');
return;
}
this.canvas = this.ctx.canvas;
*/
this.log('Finding background-images');
this.images.push('start');
this.getImages(this.element);
this.log('Finding images');
// console.log(this.element.ownerDocument);
this.each(this.element.ownerDocument.images,function(i,e){
_.preloadImage(_.getAttr(e,'src'));
});
this.images.splice(0,1);
// console.log(this.images);
if (this.images.length == 0){
this.start();
}
}
/*
* Check whether all assets have been loaded and start traversing the DOM
*/
html2canvas.prototype.start = function(){
// console.log(this.images);
if (this.images.length == 0 || this.imagesLoaded==this.images.length/2){
this.log('Finished loading '+this.imagesLoaded+' images, Started parsing');
this.bodyOverflow = document.getElementsByTagName('body')[0].style.overflow;
document.getElementsByTagName('body')[0].style.overflow = "hidden";
var rootStack = new this.storageContext($(document).width(),$(document).height());
rootStack.opacity = this.getCSS(this.element,"opacity");
var stack = this.newElement(this.element,rootStack);
this.parseElement(this.element,stack);
}
}
html2canvas.prototype.stackingContext = function(width,height){
this.canvas = document.createElement('canvas');
this.canvas.width = width;
this.canvas.height = width;
if (!this.canvas.getContext){
// TODO include Flashcanvas
/*
var script = document.createElement('script');
script.type = "text/javascript";
script.src = this.opts.flashCanvasPath;
var s = document.getElementsByTagName('script')[0];
s.parentNode.insertBefore(script, s);
if (typeof FlashCanvas != "undefined") {
FlashCanvas.initElement(this.canvas);
this.ctx = this.canvas.getContext('2d');
} */
}else{
this.ctx = this.canvas.getContext('2d');
}
// set common settings for canvas
this.ctx.textBaseline = "bottom";
return this.ctx;
}
html2canvas.prototype.storageContext = function(width,height){
this.storage = [];
this.width = width;
this.height = height;
//this.zIndex;
// todo simplify this whole section
this.fillRect = function(x, y, w, h){
this.storage.push(
{
type: "function",
name:"fillRect",
arguments:[x,y,w,h]
});
};
this.drawImage = function(image,sx,sy,sw,sh,dx,dy,dw,dh){
this.storage.push(
{
type: "function",
name:"drawImage",
arguments:[image,sx,sy,sw,sh,dx,dy,dw,dh]
});
};
this.fillText = function(currentText,x,y){
this.storage.push(
{
type: "function",
name:"fillText",
arguments:[currentText,x,y]
});
}
return this;
}
/*
* Finished rendering, send callback
*/
html2canvas.prototype.finish = function(){
this.log("Finished rendering");
document.getElementsByTagName('body')[0].style.overflow = this.bodyOverflow;
/*
if (this.opts.renderViewport){
// let's crop it to viewport only then
var newCanvas = document.createElement('canvas');
var newctx = newCanvas.getContext('2d');
newCanvas.width = window.innerWidth;
newCanvas.height = window.innerHeight;
}*/
this.opts.ready(this);
}

226
src/Draw.js Normal file
View File

@ -0,0 +1,226 @@
html2canvas.prototype.newElement = function(el,parentStack){
var bounds = this.getBounds(el);
var x = bounds.left;
var y = bounds.top;
var w = bounds.width;
var h = bounds.height;
var _ = this,
image;
var bgcolor = this.getCSS(el,"background-color");
var cssPosition = this.getCSS(el,"position");
parentStack = parentStack || {};
//var zindex = this.formatZ(this.getCSS(el,"zIndex"),cssPosition,parentStack.zIndex,el.parentNode);
var zindex = this.setZ(this.getCSS(el,"zIndex"),cssPosition,parentStack.zIndex,el.parentNode);
//console.log(el.nodeName+":"+zindex+":"+this.getCSS(el,"position")+":"+this.numDraws+":"+this.getCSS(el,"z-index"))
var opacity = this.getCSS(el,"opacity");
var stack = {
ctx: new this.storageContext(),
zIndex: zindex,
opacity: opacity*parentStack.opacity,
cssPosition: cssPosition
};
// TODO correct overflow for absolute content residing under a static position
if (parentStack.clip){
stack.clip = $.extend({}, parentStack.clip);
//stack.clip = parentStack.clip;
stack.clip.height = stack.clip.height - parentStack.borders[2].width;
}
if (this.opts.useOverflow && /(hidden|scroll|auto)/.test(this.getCSS(el,"overflow")) && !/(BODY)/i.test(el.nodeName)){
if (stack.clip){
stack.clip = this.clipBounds(stack.clip,bounds);
}else{
stack.clip = bounds;
}
}
/*
var stackLength = this.contextStacks.push(stack);
var ctx = this.contextStacks[stackLength-1].ctx;
*/
var stackLength = zindex.children.push(stack);
var ctx = zindex.children[stackLength-1].ctx;
this.setContextVariable(ctx,"globalAlpha",stack.opacity);
// draw element borders
var borders = this.drawBorders(el, ctx, bounds);
stack.borders = borders;
// let's modify clip area for child elements, so borders dont get overwritten
/*
if (stack.clip){
stack.clip.width = stack.clip.width-(borders[1].width);
stack.clip.height = stack.clip.height-(borders[2].width);
}
*/
if (this.ignoreRe.test(el.nodeName) && this.opts.iframeDefault != "transparent"){
if (this.opts.iframeDefault=="default"){
bgcolor = "#efefef";
/*
* TODO write X over frame
*/
}else{
bgcolor = this.opts.iframeDefault;
}
}
// draw base element bgcolor
var bgbounds = {
left: x+borders[3].width,
top: y+borders[0].width,
width: w-(borders[1].width+borders[3].width),
height: h-(borders[0].width+borders[2].width)
};
//if (this.withinBounds(stack.clip,bgbounds)){
if (stack.clip){
bgbounds = this.clipBounds(bgbounds,stack.clip);
//}
}
if (bgbounds.height>0 && bgbounds.width>0){
this.newRect(
ctx,
bgbounds.left,
bgbounds.top,
bgbounds.width,
bgbounds.height,
bgcolor
);
this.drawBackground(el,bgbounds,ctx);
}
switch(el.nodeName){
case "IMG":
image = _.loadImage(_.getAttr(el,'src'));
if (image){
// console.log(image.width);
this.drawImage(
ctx,
image,
0, //sx
0, //sy
image.width, //sw
image.height, //sh
x+parseInt(_.getCSS(el,'padding-left'),10) + borders[3].width, //dx
y+parseInt(_.getCSS(el,'padding-top'),10) + borders[0].width, // dy
bounds.width - (borders[1].width + borders[3].width + parseInt(_.getCSS(el,'padding-left'),10) + parseInt(_.getCSS(el,'padding-right'),10)), //dw
bounds.height - (borders[0].width + borders[2].width + parseInt(_.getCSS(el,'padding-top'),10) + parseInt(_.getCSS(el,'padding-bottom'),10)) //dh
);
}else {
this.log("Error loading <img>:" + _.getAttr(el,'src'));
}
break;
case "INPUT":
// TODO add all relevant type's, i.e. HTML5 new stuff
// todo add support for placeholder attribute for browsers which support it
if (/^(text|url|email|submit|button|reset)$/.test(el.type) && el.value.length > 0){
this.renderFormValue(el,bounds,stack);
/*
this just doesn't work well enough
this.newText(el,{
nodeValue:el.value,
splitText: function(){
return this;
},
formValue:true
},stack);
*/
}
break;
case "TEXTAREA":
if (el.value.length > 0){
this.renderFormValue(el,bounds,stack);
}
break;
case "SELECT":
if (el.options.length > 0){
this.renderFormValue(el,bounds,stack);
}
break;
case "LI":
this.drawListItem(el,stack,bgbounds);
break;
}
// return this.contextStacks[stackLength-1];
return zindex.children[stackLength-1];
}
/*
* Function to draw the text on the canvas
*/
html2canvas.prototype.printText = function(currentText,x,y,ctx){
if (this.trim(currentText).length>0){
ctx.fillText(currentText,x,y);
this.numDraws++;
}
}
// Drawing a rectangle
html2canvas.prototype.newRect = function(ctx,x,y,w,h,bgcolor){
if (bgcolor!="transparent"){
this.setContextVariable(ctx,"fillStyle",bgcolor);
ctx.fillRect (x, y, w, h);
this.numDraws++;
}
}
html2canvas.prototype.drawImage = function(ctx,image,sx,sy,sw,sh,dx,dy,dw,dh){
ctx.drawImage(
image,
sx, //sx
sy, //sy
sw, //sw
sh, //sh
dx, //dx
dy, // dy
dw, //dw
dh //dh
);
this.numDraws++;
}

View File

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

View File

@ -1,73 +0,0 @@
/* @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];
}
}

38
src/Forms.js Normal file
View File

@ -0,0 +1,38 @@
html2canvas.prototype.renderFormValue = function(el,bounds,stack){
var valueWrap = document.createElement('valuewrap'),
_ = this;
this.each(['lineHeight','textAlign','fontFamily','color','fontSize','paddingLeft','paddingTop','width','height','border','borderLeftWidth','borderTopWidth'],function(i,style){
valueWrap.style[style] = _.getCSS(el,style);
});
valueWrap.style.borderColor = "black";
valueWrap.style.borderStyle = "solid";
valueWrap.style.display = "block";
valueWrap.style.position = "absolute";
if (/^(submit|reset|button|text|password)$/.test(el.type) || el.nodeName == "SELECT"){
valueWrap.style.lineHeight = _.getCSS(el,"height");
}
valueWrap.style.top = bounds.top+"px";
valueWrap.style.left = bounds.left+"px";
if (el.nodeName == "SELECT"){
// TODO increase accuracy of text position
var textValue = el.options[el.selectedIndex].text;
} else{
var textValue = el.value;
}
var textNode = document.createTextNode(textValue);
valueWrap.appendChild(textNode);
$('body').append(valueWrap);
this.newText(el,textNode,stack);
$(valueWrap).remove();
}

View File

@ -1,558 +0,0 @@
/* @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}%`;
}
})
);
};

129
src/Images.js Normal file
View File

@ -0,0 +1,129 @@
/*
* Function to find all images from <img> and background-image
*/
html2canvas.prototype.getImages = function(el) {
var self = this;
if (!this.ignoreRe.test(el.nodeName)){
// TODO remove jQuery dependancy
this.each($(el).contents(),function(i,element){
var ignRe = new RegExp("("+this.ignoreElements+")");
if (!ignRe.test(element.nodeName)){
self.getImages(element);
}
})
}
if (el.nodeType==1 || typeof el.nodeType == "undefined"){
var background_image = this.getCSS(el,'background-image');
if (background_image && background_image != "1" && background_image != "none" && background_image.substring(0,7)!="-webkit" && background_image.substring(0,3)!="-o-" && background_image.substring(0,4)!="-moz"){
// TODO add multi image background support
var src = this.backgroundImageUrl(background_image.split(",")[0]);
this.preloadImage(src);
}
}
}
/*
* Load image from storage
*/
html2canvas.prototype.loadImage = function(src){
var imgIndex = this.getIndex(this.images,src);
if (imgIndex!=-1){
return this.images[imgIndex+1];
}else{
return false;
}
}
html2canvas.prototype.preloadImage = function(src){
if (this.getIndex(this.images,src)==-1){
if (this.isSameOrigin(src)){
this.images.push(src);
// console.log('a'+src);
var img = new Image();
// TODO remove jQuery dependancy
var _ = this;
$(img).load(function(){
_.imagesLoaded++;
_.start();
});
img.onerror = function(){
_.images.splice(_.images.indexOf(img.src),2);
// _.imagesLoaded++;
_.start();
}
img.src = src;
this.images.push(img);
}else if (this.opts.proxyUrl){
// console.log('b'+src);
this.images.push(src);
var img = new Image();
this.proxyGetImage(src,img);
this.images.push(img);
}
}
}
html2canvas.prototype.proxyGetImage = function(url,img){
var _ = this;
var link = document.createElement("a");
link.href = url;
url = link.href; // work around for pages with base href="" set
// todo remove jQuery dependency and enable xhr2 requests where available (no need for base64 / json)
$.ajax({
data:{
xhr2:false,
url:url
},
url: this.opts.proxyUrl,
dataType: "jsonp",
success: function(a){
if (a.substring(0,6)=="error:"){
_.images.splice(_.images.indexOf(url),2);
_.start();
_.log('Proxy was unable to load '+url+' '+a);
}else{
// document.createElement(a);
// console.log(img);
img.onload = function(){
// console.log('w'+img.width);
_.imagesLoaded++;
_.start();
}
img.src = a;
}
},
error: function(){
_.images.splice(_.images.indexOf(url),2);
// _.imagesLoaded++;
_.start();
}
});
}

View File

@ -1,155 +0,0 @@
/* @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 = 'absolute';
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;
};

7
src/LICENSE Normal file
View File

@ -0,0 +1,7 @@
/*
* html2canvas v0.27 <http://html2canvas.hertzen.com>
* Copyright (c) 2011 Niklas von Hertzen. All rights reserved.
* http://www.twitter.com/niklasvh
*
* Released under MIT License
*/

View File

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

View File

@ -1,711 +0,0 @@
/* @flow */
'use strict';
import type ResourceLoader from './ResourceLoader';
import type {ListStyleType} from './parsing/listStyle';
import {copyCSSStyles, contains} from './Util';
import NodeContainer from './NodeContainer';
import TextContainer from './TextContainer';
import {LIST_STYLE_POSITION, LIST_STYLE_TYPE} from './parsing/listStyle';
import {fromCodePoint} from './Unicode';
// Margin between the enumeration and the list item content
const MARGIN_RIGHT = 7;
const ancestorTypes = ['OL', 'UL', 'MENU'];
export const getListOwner = (container: NodeContainer): ?NodeContainer => {
let parent = container.parent;
if (!parent) {
return null;
}
do {
let isAncestor = ancestorTypes.indexOf(parent.tagName) !== -1;
if (isAncestor) {
return parent;
}
parent = parent.parent;
} while (parent);
return container.parent;
};
export const inlineListItemElement = (
node: HTMLElement,
container: NodeContainer,
resourceLoader: ResourceLoader
): void => {
const listStyle = container.style.listStyle;
if (!listStyle) {
return;
}
const style = node.ownerDocument.defaultView.getComputedStyle(node, null);
const wrapper = node.ownerDocument.createElement('html2canvaswrapper');
copyCSSStyles(style, wrapper);
wrapper.style.position = 'absolute';
wrapper.style.bottom = 'auto';
wrapper.style.display = 'block';
wrapper.style.letterSpacing = 'normal';
switch (listStyle.listStylePosition) {
case LIST_STYLE_POSITION.OUTSIDE:
wrapper.style.left = 'auto';
wrapper.style.right = `${node.ownerDocument.defaultView.innerWidth -
container.bounds.left -
container.style.margin[1].getAbsoluteValue(container.bounds.width) +
MARGIN_RIGHT}px`;
wrapper.style.textAlign = 'right';
break;
case LIST_STYLE_POSITION.INSIDE:
wrapper.style.left = `${container.bounds.left -
container.style.margin[3].getAbsoluteValue(container.bounds.width)}px`;
wrapper.style.right = 'auto';
wrapper.style.textAlign = 'left';
break;
}
let text;
const MARGIN_TOP = container.style.margin[0].getAbsoluteValue(container.bounds.width);
const styleImage = listStyle.listStyleImage;
if (styleImage) {
if (styleImage.method === 'url') {
const image = node.ownerDocument.createElement('img');
image.src = styleImage.args[0];
wrapper.style.top = `${container.bounds.top - MARGIN_TOP}px`;
wrapper.style.width = 'auto';
wrapper.style.height = 'auto';
wrapper.appendChild(image);
} else {
const size = parseFloat(container.style.font.fontSize) * 0.5;
wrapper.style.top = `${container.bounds.top -
MARGIN_TOP +
container.bounds.height -
1.5 * size}px`;
wrapper.style.width = `${size}px`;
wrapper.style.height = `${size}px`;
wrapper.style.backgroundImage = style.listStyleImage;
}
} else if (typeof container.listIndex === 'number') {
text = node.ownerDocument.createTextNode(
createCounterText(container.listIndex, listStyle.listStyleType, true)
);
wrapper.appendChild(text);
wrapper.style.top = `${container.bounds.top - MARGIN_TOP}px`;
}
// $FlowFixMe
const body: HTMLBodyElement = node.ownerDocument.body;
body.appendChild(wrapper);
if (text) {
container.childNodes.push(TextContainer.fromTextNode(text, container));
body.removeChild(wrapper);
} else {
// $FlowFixMe
container.childNodes.push(new NodeContainer(wrapper, container, resourceLoader, 0));
}
};
const ROMAN_UPPER = {
integers: [1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1],
values: ['M', 'CM', 'D', 'CD', 'C', 'XC', 'L', 'XL', 'X', 'IX', 'V', 'IV', 'I']
};
const ARMENIAN = {
integers: [
9000,
8000,
7000,
6000,
5000,
4000,
3000,
2000,
1000,
900,
800,
700,
600,
500,
400,
300,
200,
100,
90,
80,
70,
60,
50,
40,
30,
20,
10,
9,
8,
7,
6,
5,
4,
3,
2,
1
],
values: [
'Ք',
'Փ',
'Ւ',
'Ց',
'Ր',
'Տ',
'Վ',
'Ս',
'Ռ',
'Ջ',
'Պ',
'Չ',
'Ո',
'Շ',
'Ն',
'Յ',
'Մ',
'Ճ',
'Ղ',
'Ձ',
'Հ',
'Կ',
'Ծ',
'Խ',
'Լ',
'Ի',
'Ժ',
'Թ',
'Ը',
'Է',
'Զ',
'Ե',
'Դ',
'Գ',
'Բ',
'Ա'
]
};
const HEBREW = {
integers: [
10000,
9000,
8000,
7000,
6000,
5000,
4000,
3000,
2000,
1000,
400,
300,
200,
100,
90,
80,
70,
60,
50,
40,
30,
20,
19,
18,
17,
16,
15,
10,
9,
8,
7,
6,
5,
4,
3,
2,
1
],
values: [
'י׳',
'ט׳',
'ח׳',
'ז׳',
'ו׳',
'ה׳',
'ד׳',
'ג׳',
'ב׳',
'א׳',
'ת',
'ש',
'ר',
'ק',
'צ',
'פ',
'ע',
'ס',
'נ',
'מ',
'ל',
'כ',
'יט',
'יח',
'יז',
'טז',
'טו',
'י',
'ט',
'ח',
'ז',
'ו',
'ה',
'ד',
'ג',
'ב',
'א'
]
};
const GEORGIAN = {
integers: [
10000,
9000,
8000,
7000,
6000,
5000,
4000,
3000,
2000,
1000,
900,
800,
700,
600,
500,
400,
300,
200,
100,
90,
80,
70,
60,
50,
40,
30,
20,
10,
9,
8,
7,
6,
5,
4,
3,
2,
1
],
values: [
'ჵ',
'ჰ',
'ჯ',
'ჴ',
'ხ',
'ჭ',
'წ',
'ძ',
'ც',
'ჩ',
'შ',
'',
'ღ',
'ქ',
'ფ',
'ჳ',
'ტ',
'ს',
'რ',
'ჟ',
'პ',
'ო',
'ჲ',
'ნ',
'მ',
'ლ',
'კ',
'ი',
'თ',
'ჱ',
'ზ',
'ვ',
'ე',
'დ',
'გ',
'ბ',
'ა'
]
};
const createAdditiveCounter = (
value: number,
min: number,
max: number,
symbols,
fallback: ListStyleType,
suffix: string
) => {
if (value < min || value > max) {
return createCounterText(value, fallback, suffix.length > 0);
}
return (
symbols.integers.reduce((string, integer, index) => {
while (value >= integer) {
value -= integer;
string += symbols.values[index];
}
return string;
}, '') + suffix
);
};
const createCounterStyleWithSymbolResolver = (
value: number,
codePointRangeLength: number,
isNumeric: boolean,
resolver
): string => {
let string = '';
do {
if (!isNumeric) {
value--;
}
string = resolver(value) + string;
value /= codePointRangeLength;
} while (value * codePointRangeLength >= codePointRangeLength);
return string;
};
const createCounterStyleFromRange = (
value: number,
codePointRangeStart: number,
codePointRangeEnd: number,
isNumeric: boolean,
suffix: string
): string => {
const codePointRangeLength = codePointRangeEnd - codePointRangeStart + 1;
return (
(value < 0 ? '-' : '') +
(createCounterStyleWithSymbolResolver(
Math.abs(value),
codePointRangeLength,
isNumeric,
codePoint =>
fromCodePoint(Math.floor(codePoint % codePointRangeLength) + codePointRangeStart)
) +
suffix)
);
};
const createCounterStyleFromSymbols = (
value: number,
symbols: string,
suffix: string = '. '
): string => {
const codePointRangeLength = symbols.length;
return (
createCounterStyleWithSymbolResolver(
Math.abs(value),
codePointRangeLength,
false,
codePoint => symbols[Math.floor(codePoint % codePointRangeLength)]
) + suffix
);
};
const CJK_ZEROS = 1 << 0;
const CJK_TEN_COEFFICIENTS = 1 << 1;
const CJK_TEN_HIGH_COEFFICIENTS = 1 << 2;
const CJK_HUNDRED_COEFFICIENTS = 1 << 3;
const createCJKCounter = (
value: number,
numbers: string,
multipliers: string,
negativeSign: string,
suffix: string,
flags: number
): string => {
if (value < -9999 || value > 9999) {
return createCounterText(value, LIST_STYLE_TYPE.CJK_DECIMAL, suffix.length > 0);
}
let tmp = Math.abs(value);
let string = suffix;
if (tmp === 0) {
return numbers[0] + string;
}
for (let digit = 0; tmp > 0 && digit <= 4; digit++) {
let coefficient = tmp % 10;
if (coefficient === 0 && contains(flags, CJK_ZEROS) && string !== '') {
string = numbers[coefficient] + string;
} else if (
coefficient > 1 ||
(coefficient === 1 && digit === 0) ||
(coefficient === 1 && digit === 1 && contains(flags, CJK_TEN_COEFFICIENTS)) ||
(coefficient === 1 &&
digit === 1 &&
contains(flags, CJK_TEN_HIGH_COEFFICIENTS) &&
value > 100) ||
(coefficient === 1 && digit > 1 && contains(flags, CJK_HUNDRED_COEFFICIENTS))
) {
string = numbers[coefficient] + (digit > 0 ? multipliers[digit - 1] : '') + string;
} else if (coefficient === 1 && digit > 0) {
string = multipliers[digit - 1] + string;
}
tmp = Math.floor(tmp / 10);
}
return (value < 0 ? negativeSign : '') + string;
};
const CHINESE_INFORMAL_MULTIPLIERS = '十百千萬';
const CHINESE_FORMAL_MULTIPLIERS = '拾佰仟萬';
const JAPANESE_NEGATIVE = 'マイナス';
const KOREAN_NEGATIVE = '마이너스 ';
export const createCounterText = (
value: number,
type: ListStyleType,
appendSuffix: boolean
): string => {
const defaultSuffix = appendSuffix ? '. ' : '';
const cjkSuffix = appendSuffix ? '、' : '';
const koreanSuffix = appendSuffix ? ', ' : '';
switch (type) {
case LIST_STYLE_TYPE.DISC:
return '•';
case LIST_STYLE_TYPE.CIRCLE:
return '◦';
case LIST_STYLE_TYPE.SQUARE:
return '◾';
case LIST_STYLE_TYPE.DECIMAL_LEADING_ZERO:
const string = createCounterStyleFromRange(value, 48, 57, true, defaultSuffix);
return string.length < 4 ? `0${string}` : string;
case LIST_STYLE_TYPE.CJK_DECIMAL:
return createCounterStyleFromSymbols(value, '〇一二三四五六七八九', cjkSuffix);
case LIST_STYLE_TYPE.LOWER_ROMAN:
return createAdditiveCounter(
value,
1,
3999,
ROMAN_UPPER,
LIST_STYLE_TYPE.DECIMAL,
defaultSuffix
).toLowerCase();
case LIST_STYLE_TYPE.UPPER_ROMAN:
return createAdditiveCounter(
value,
1,
3999,
ROMAN_UPPER,
LIST_STYLE_TYPE.DECIMAL,
defaultSuffix
);
case LIST_STYLE_TYPE.LOWER_GREEK:
return createCounterStyleFromRange(value, 945, 969, false, defaultSuffix);
case LIST_STYLE_TYPE.LOWER_ALPHA:
return createCounterStyleFromRange(value, 97, 122, false, defaultSuffix);
case LIST_STYLE_TYPE.UPPER_ALPHA:
return createCounterStyleFromRange(value, 65, 90, false, defaultSuffix);
case LIST_STYLE_TYPE.ARABIC_INDIC:
return createCounterStyleFromRange(value, 1632, 1641, true, defaultSuffix);
case LIST_STYLE_TYPE.ARMENIAN:
case LIST_STYLE_TYPE.UPPER_ARMENIAN:
return createAdditiveCounter(
value,
1,
9999,
ARMENIAN,
LIST_STYLE_TYPE.DECIMAL,
defaultSuffix
);
case LIST_STYLE_TYPE.LOWER_ARMENIAN:
return createAdditiveCounter(
value,
1,
9999,
ARMENIAN,
LIST_STYLE_TYPE.DECIMAL,
defaultSuffix
).toLowerCase();
case LIST_STYLE_TYPE.BENGALI:
return createCounterStyleFromRange(value, 2534, 2543, true, defaultSuffix);
case LIST_STYLE_TYPE.CAMBODIAN:
case LIST_STYLE_TYPE.KHMER:
return createCounterStyleFromRange(value, 6112, 6121, true, defaultSuffix);
case LIST_STYLE_TYPE.CJK_EARTHLY_BRANCH:
return createCounterStyleFromSymbols(value, '子丑寅卯辰巳午未申酉戌亥', cjkSuffix);
case LIST_STYLE_TYPE.CJK_HEAVENLY_STEM:
return createCounterStyleFromSymbols(value, '甲乙丙丁戊己庚辛壬癸', cjkSuffix);
case LIST_STYLE_TYPE.CJK_IDEOGRAPHIC:
case LIST_STYLE_TYPE.TRAD_CHINESE_INFORMAL:
return createCJKCounter(
value,
'零一二三四五六七八九',
CHINESE_INFORMAL_MULTIPLIERS,
'負',
cjkSuffix,
CJK_TEN_COEFFICIENTS | CJK_TEN_HIGH_COEFFICIENTS | CJK_HUNDRED_COEFFICIENTS
);
case LIST_STYLE_TYPE.TRAD_CHINESE_FORMAL:
return createCJKCounter(
value,
'零壹貳參肆伍陸柒捌玖',
CHINESE_FORMAL_MULTIPLIERS,
'負',
cjkSuffix,
CJK_ZEROS |
CJK_TEN_COEFFICIENTS |
CJK_TEN_HIGH_COEFFICIENTS |
CJK_HUNDRED_COEFFICIENTS
);
case LIST_STYLE_TYPE.SIMP_CHINESE_INFORMAL:
return createCJKCounter(
value,
'零一二三四五六七八九',
CHINESE_INFORMAL_MULTIPLIERS,
'负',
cjkSuffix,
CJK_TEN_COEFFICIENTS | CJK_TEN_HIGH_COEFFICIENTS | CJK_HUNDRED_COEFFICIENTS
);
case LIST_STYLE_TYPE.SIMP_CHINESE_FORMAL:
return createCJKCounter(
value,
'零壹贰叁肆伍陆柒捌玖',
CHINESE_FORMAL_MULTIPLIERS,
'负',
cjkSuffix,
CJK_ZEROS |
CJK_TEN_COEFFICIENTS |
CJK_TEN_HIGH_COEFFICIENTS |
CJK_HUNDRED_COEFFICIENTS
);
case LIST_STYLE_TYPE.JAPANESE_INFORMAL:
return createCJKCounter(value, '〇一二三四五六七八九', '十百千万', JAPANESE_NEGATIVE, cjkSuffix, 0);
case LIST_STYLE_TYPE.JAPANESE_FORMAL:
return createCJKCounter(
value,
'零壱弐参四伍六七八九',
'拾百千万',
JAPANESE_NEGATIVE,
cjkSuffix,
CJK_ZEROS | CJK_TEN_COEFFICIENTS | CJK_TEN_HIGH_COEFFICIENTS
);
case LIST_STYLE_TYPE.KOREAN_HANGUL_FORMAL:
return createCJKCounter(
value,
'영일이삼사오육칠팔구',
'십백천만',
KOREAN_NEGATIVE,
koreanSuffix,
CJK_ZEROS | CJK_TEN_COEFFICIENTS | CJK_TEN_HIGH_COEFFICIENTS
);
case LIST_STYLE_TYPE.KOREAN_HANJA_INFORMAL:
return createCJKCounter(value, '零一二三四五六七八九', '十百千萬', KOREAN_NEGATIVE, koreanSuffix, 0);
case LIST_STYLE_TYPE.KOREAN_HANJA_FORMAL:
return createCJKCounter(
value,
'零壹貳參四五六七八九',
'拾百千',
KOREAN_NEGATIVE,
koreanSuffix,
CJK_ZEROS | CJK_TEN_COEFFICIENTS | CJK_TEN_HIGH_COEFFICIENTS
);
case LIST_STYLE_TYPE.DEVANAGARI:
return createCounterStyleFromRange(value, 0x966, 0x96f, true, defaultSuffix);
case LIST_STYLE_TYPE.GEORGIAN:
return createAdditiveCounter(
value,
1,
19999,
GEORGIAN,
LIST_STYLE_TYPE.DECIMAL,
defaultSuffix
);
case LIST_STYLE_TYPE.GUJARATI:
return createCounterStyleFromRange(value, 0xae6, 0xaef, true, defaultSuffix);
case LIST_STYLE_TYPE.GURMUKHI:
return createCounterStyleFromRange(value, 0xa66, 0xa6f, true, defaultSuffix);
case LIST_STYLE_TYPE.HEBREW:
return createAdditiveCounter(
value,
1,
10999,
HEBREW,
LIST_STYLE_TYPE.DECIMAL,
defaultSuffix
);
case LIST_STYLE_TYPE.HIRAGANA:
return createCounterStyleFromSymbols(
value,
'あいうえおかきくけこさしすせそたちつてとなにぬねのはひふへほまみむめもやゆよらりるれろわゐゑをん'
);
case LIST_STYLE_TYPE.HIRAGANA_IROHA:
return createCounterStyleFromSymbols(
value,
'いろはにほへとちりぬるをわかよたれそつねならむうゐのおくやまけふこえてあさきゆめみしゑひもせす'
);
case LIST_STYLE_TYPE.KANNADA:
return createCounterStyleFromRange(value, 0xce6, 0xcef, true, defaultSuffix);
case LIST_STYLE_TYPE.KATAKANA:
return createCounterStyleFromSymbols(
value,
'アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヰヱヲン',
cjkSuffix
);
case LIST_STYLE_TYPE.KATAKANA_IROHA:
return createCounterStyleFromSymbols(
value,
'イロハニホヘトチリヌルヲワカヨタレソツネナラムウヰノオクヤマケフコエテアサキユメミシヱヒモセス',
cjkSuffix
);
case LIST_STYLE_TYPE.LAO:
return createCounterStyleFromRange(value, 0xed0, 0xed9, true, defaultSuffix);
case LIST_STYLE_TYPE.MONGOLIAN:
return createCounterStyleFromRange(value, 0x1810, 0x1819, true, defaultSuffix);
case LIST_STYLE_TYPE.MYANMAR:
return createCounterStyleFromRange(value, 0x1040, 0x1049, true, defaultSuffix);
case LIST_STYLE_TYPE.ORIYA:
return createCounterStyleFromRange(value, 0xb66, 0xb6f, true, defaultSuffix);
case LIST_STYLE_TYPE.PERSIAN:
return createCounterStyleFromRange(value, 0x6f0, 0x6f9, true, defaultSuffix);
case LIST_STYLE_TYPE.TAMIL:
return createCounterStyleFromRange(value, 0xbe6, 0xbef, true, defaultSuffix);
case LIST_STYLE_TYPE.TELUGU:
return createCounterStyleFromRange(value, 0xc66, 0xc6f, true, defaultSuffix);
case LIST_STYLE_TYPE.THAI:
return createCounterStyleFromRange(value, 0xe50, 0xe59, true, defaultSuffix);
case LIST_STYLE_TYPE.TIBETAN:
return createCounterStyleFromRange(value, 0xf20, 0xf29, true, defaultSuffix);
case LIST_STYLE_TYPE.DECIMAL:
default:
return createCounterStyleFromRange(value, 48, 57, true, defaultSuffix);
}
};

121
src/Lists.js Normal file
View File

@ -0,0 +1,121 @@
html2canvas.prototype.drawListItem = function(element,stack,elBounds){
var position = this.getCSS(element,"list-style-position",false);
var item = this.getListItem(element),
x,
y;
var type = this.getCSS(element,"list-style-type",false);
if (/^(decimal|decimal-leading-zero|upper-alpha|upper-latin|upper-roman|lower-alpha|lower-greek|lower-latin|lower-roman)$/i.test(type)){
// TODO remove jQuery dependency
var currentIndex = $(element).index()+1,
text;
if (type == "decimal"){
text = currentIndex;
}else if (type == "decimal-leading-zero"){
if (currentIndex.toString().length == 1){
text = currentIndex = "0" + currentIndex.toString();
}else{
text = currentIndex.toString();
}
}else if (type == "upper-roman"){
text = this.getListRoman(currentIndex);
}else if (type == "lower-roman"){
text = this.getListRoman(currentIndex).toLowerCase();
}else if (type == "lower-alpha"){
text = this.getListAlpha(currentIndex).toLowerCase();
}else if (type == "upper-alpha"){
text = this.getListAlpha(currentIndex);
}
text += ". ";
var listBounds = this.getListPosition(element,text);
if (position == "inside"){
this.setFont(stack.ctx,element,false);
x = elBounds.left;
}else{
return; /* TODO really need to figure out some more accurate way to try and find the position.
as defined in http://www.w3.org/TR/CSS21/generate.html#propdef-list-style-position, it does not even have a specified "correct" position, so each browser
may display it whatever way it feels like.
"The position of the list-item marker adjacent to floats is undefined in CSS 2.1. CSS 2.1 does not specify the precise location of the marker box or its position in the painting order"
*/
this.setFont(stack.ctx,element,true);
x = elBounds.left-10;
}
y = listBounds.bottom;
this.printText(text, x, y, stack.ctx);
}
}
html2canvas.prototype.getListPosition = function(element,val){
var boundElement = document.createElement("boundelement");
boundElement.style.display = "inline";
//boundElement.style.width = "1px";
//boundElement.style.height = "1px";
var type = element.style.listStyleType;
element.style.listStyleType = "none";
boundElement.appendChild(document.createTextNode(val));
element.insertBefore(boundElement,element.firstChild);
var bounds = this.getBounds(boundElement);
element.removeChild(boundElement);
element.style.listStyleType = type;
return bounds;
}
html2canvas.prototype.getListItem = function(element){
}
html2canvas.prototype.getListAlpha = function(number){
var tmp = "";
do{
var modulus = number % 26;
tmp = String.fromCharCode((modulus) + 64) + tmp;
number = number / 26;
}while((number*26) > 26);
return tmp;
}
html2canvas.prototype.getListRoman = function(number){
var romanArray = ["M","CM","D","CD","C","XC","L","XL","X","IX","V","IV","I"],
decimal = [1000,900,500,400,100,90,50,40,10,9,5,4,1],
roman = "";
if (number <= 0 || number >= 4000) return;
for (var v=0; v<romanArray.length; v++) {
while (number >= decimal[v]) {
number -= decimal[v];
roman += romanArray[v];
}
}
return roman;
}

View File

@ -1,48 +0,0 @@
/* @flow */
'use strict';
export default class Logger {
enabled: boolean;
start: number;
id: ?string;
constructor(enabled: boolean, id: ?string, start: ?number) {
this.enabled = typeof window !== 'undefined' && 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))
);
}
}
}

View File

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

View File

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

View File

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

View File

@ -1,341 +0,0 @@
/* @flow */
'use strict';
import {createCounterText} from './ListItem';
import {parseListStyleType} from './parsing/listStyle';
export const PSEUDO_CONTENT_ITEM_TYPE = {
TEXT: 0,
IMAGE: 1
};
export const TOKEN_TYPE = {
STRING: 0,
ATTRIBUTE: 1,
URL: 2,
COUNTER: 3,
COUNTERS: 4,
OPENQUOTE: 5,
CLOSEQUOTE: 6
};
export type PseudoContentData = {
counters: {[string]: Array<number>},
quoteDepth: number
};
export type PseudoContentItem = {
type: $Values<typeof PSEUDO_CONTENT_ITEM_TYPE>,
value: string
};
export type Token = {
type: $Values<typeof TOKEN_TYPE>,
value?: string,
name?: string,
format?: string,
glue?: string
};
export const parseCounterReset = (
style: ?CSSStyleDeclaration,
data: PseudoContentData
): Array<string> => {
if (!style || !style.counterReset || style.counterReset === 'none') {
return [];
}
const counterNames: Array<string> = [];
const counterResets = style.counterReset.split(/\s*,\s*/);
const lenCounterResets = counterResets.length;
for (let i = 0; i < lenCounterResets; i++) {
const [counterName, initialValue] = counterResets[i].split(/\s+/);
counterNames.push(counterName);
let counter = data.counters[counterName];
if (!counter) {
counter = data.counters[counterName] = [];
}
counter.push(parseInt(initialValue || 0, 10));
}
return counterNames;
};
export const popCounters = (counterNames: Array<string>, data: PseudoContentData): void => {
const lenCounters = counterNames.length;
for (let i = 0; i < lenCounters; i++) {
data.counters[counterNames[i]].pop();
}
};
export const resolvePseudoContent = (
node: Node,
style: ?CSSStyleDeclaration,
data: PseudoContentData
): ?Array<PseudoContentItem> => {
if (
!style ||
!style.content ||
style.content === 'none' ||
style.content === '-moz-alt-content' ||
style.display === 'none'
) {
return null;
}
const tokens = parseContent(style.content);
const len = tokens.length;
const contentItems: Array<PseudoContentItem> = [];
let s = '';
// increment the counter (if there is a "counter-increment" declaration)
const counterIncrement = style.counterIncrement;
if (counterIncrement && counterIncrement !== 'none') {
const [counterName, incrementValue] = counterIncrement.split(/\s+/);
const counter = data.counters[counterName];
if (counter) {
counter[counter.length - 1] +=
incrementValue === undefined ? 1 : parseInt(incrementValue, 10);
}
}
// build the content string
for (let i = 0; i < len; i++) {
const token = tokens[i];
switch (token.type) {
case TOKEN_TYPE.STRING:
s += token.value || '';
break;
case TOKEN_TYPE.ATTRIBUTE:
if (node instanceof HTMLElement && token.value) {
s += node.getAttribute(token.value) || '';
}
break;
case TOKEN_TYPE.COUNTER:
const counter = data.counters[token.name || ''];
if (counter) {
s += formatCounterValue([counter[counter.length - 1]], '', token.format);
}
break;
case TOKEN_TYPE.COUNTERS:
const counters = data.counters[token.name || ''];
if (counters) {
s += formatCounterValue(counters, token.glue, token.format);
}
break;
case TOKEN_TYPE.OPENQUOTE:
s += getQuote(style, true, data.quoteDepth);
data.quoteDepth++;
break;
case TOKEN_TYPE.CLOSEQUOTE:
data.quoteDepth--;
s += getQuote(style, false, data.quoteDepth);
break;
case TOKEN_TYPE.URL:
if (s) {
contentItems.push({type: PSEUDO_CONTENT_ITEM_TYPE.TEXT, value: s});
s = '';
}
contentItems.push({type: PSEUDO_CONTENT_ITEM_TYPE.IMAGE, value: token.value || ''});
break;
}
}
if (s) {
contentItems.push({type: PSEUDO_CONTENT_ITEM_TYPE.TEXT, value: s});
}
return contentItems;
};
export const parseContent = (content: string, cache?: {[string]: Array<Token>}): Array<Token> => {
if (cache && cache[content]) {
return cache[content];
}
const tokens: Array<Token> = [];
const len = content.length;
let isString = false;
let isEscaped = false;
let isFunction = false;
let str = '';
let functionName = '';
let args = [];
for (let i = 0; i < len; i++) {
const c = content.charAt(i);
switch (c) {
case "'":
case '"':
if (isEscaped) {
str += c;
} else {
isString = !isString;
if (!isFunction && !isString) {
tokens.push({type: TOKEN_TYPE.STRING, value: str});
str = '';
}
}
break;
case '\\':
if (isEscaped) {
str += c;
isEscaped = false;
} else {
isEscaped = true;
}
break;
case '(':
if (isString) {
str += c;
} else {
isFunction = true;
functionName = str;
str = '';
args = [];
}
break;
case ')':
if (isString) {
str += c;
} else if (isFunction) {
if (str) {
args.push(str);
}
switch (functionName) {
case 'attr':
if (args.length > 0) {
tokens.push({type: TOKEN_TYPE.ATTRIBUTE, value: args[0]});
}
break;
case 'counter':
if (args.length > 0) {
const counter: Token = {
type: TOKEN_TYPE.COUNTER,
name: args[0]
};
if (args.length > 1) {
counter.format = args[1];
}
tokens.push(counter);
}
break;
case 'counters':
if (args.length > 0) {
const counters: Token = {
type: TOKEN_TYPE.COUNTERS,
name: args[0]
};
if (args.length > 1) {
counters.glue = args[1];
}
if (args.length > 2) {
counters.format = args[2];
}
tokens.push(counters);
}
break;
case 'url':
if (args.length > 0) {
tokens.push({type: TOKEN_TYPE.URL, value: args[0]});
}
break;
}
isFunction = false;
str = '';
}
break;
case ',':
if (isString) {
str += c;
} else if (isFunction) {
args.push(str);
str = '';
}
break;
case ' ':
case '\t':
if (isString) {
str += c;
} else if (str) {
addOtherToken(tokens, str);
str = '';
}
break;
default:
str += c;
}
if (c !== '\\') {
isEscaped = false;
}
}
if (str) {
addOtherToken(tokens, str);
}
if (cache) {
cache[content] = tokens;
}
return tokens;
};
const addOtherToken = (tokens: Array<Token>, identifier: string): void => {
switch (identifier) {
case 'open-quote':
tokens.push({type: TOKEN_TYPE.OPENQUOTE});
break;
case 'close-quote':
tokens.push({type: TOKEN_TYPE.CLOSEQUOTE});
break;
}
};
const getQuote = (style: CSSStyleDeclaration, isOpening: boolean, quoteDepth: number): string => {
const quotes = style.quotes ? style.quotes.split(/\s+/) : ["'\"'", "'\"'"];
let idx = quoteDepth * 2;
if (idx >= quotes.length) {
idx = quotes.length - 2;
}
if (!isOpening) {
++idx;
}
return quotes[idx].replace(/^["']|["']$/g, '');
};
const formatCounterValue = (counter, glue: ?string, format: ?string): string => {
const len = counter.length;
let result = '';
for (let i = 0; i < len; i++) {
if (i > 0) {
result += glue || '';
}
result += createCounterText(counter[i], parseListStyleType(format || 'decimal'), false);
}
return result;
};

View File

@ -1,450 +1,348 @@
/* @flow */
'use strict';
html2canvas.prototype.Renderer = function(queue){
var _ = this;
this.log('Renderer initiated');
this.each(this.opts.renderOrder.split(" "),function(i,renderer){
switch(renderer){
case "canvas":
_.canvas = document.createElement('canvas');
if (_.canvas.getContext){
_.canvasRenderer(queue);
_.log('Using canvas renderer');
return false;
}
break;
case "flash":
/*
var script = document.createElement('script');
script.type = "text/javascript";
script.src = _.opts.flashCanvasPath;
var s = document.getElementsByTagName('script')[0];
s.parentNode.insertBefore(script, s);
if (typeof FlashCanvas != "undefined") {
_.canvas = initCanvas(document.getElementById("testflash"));
FlashCanvas.initElement(_.canvas);
_.ctx = _.canvas.getContext("2d");
// _.canvas = document.createElement('canvas');
//
_.log('Using Flashcanvas renderer');
_.canvasRenderer(queue);
return false;
}
*/
break;
case "html":
// TODO add renderer
_.log("Using HTML renderer");
return false;
break;
}
});
// this.log('No renderer chosen, rendering quit');
return this;
// this.canvasRenderer(queue);
/*
if (!this.canvas.getContext){
}*/
// TODO include Flashcanvas
/*
var script = document.createElement('script');
script.type = "text/javascript";
script.src = this.opts.flashCanvasPath;
var s = document.getElementsByTagName('script')[0];
s.parentNode.insertBefore(script, s);
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
if (typeof FlashCanvas != "undefined") {
FlashCanvas.initElement(this.canvas);
this.ctx = this.canvas.getContext('2d');
} */
}
export default class Renderer {
target: RenderTarget<*>;
options: RenderOptions;
_opacity: ?number;
constructor(target: RenderTarget<*>, options: RenderOptions) {
this.target = target;
this.options = options;
target.render(options);
html2canvas.prototype.throttler = function(queue){
};
html2canvas.prototype.canvasRenderContext = function(storageContext,ctx){
// set common settings for canvas
ctx.textBaseline = "bottom";
var _ = this;
if (storageContext.clip){
ctx.save();
ctx.beginPath();
// console.log(storageContext);
ctx.rect(storageContext.clip.left,storageContext.clip.top,storageContext.clip.width,storageContext.clip.height);
ctx.clip();
}
if (storageContext.ctx.storage){
_.each(storageContext.ctx.storage,function(a,renderItem){
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
switch(renderItem.type){
case "variable":
ctx[renderItem.name] = renderItem.arguments;
break;
case "function":
if (renderItem.name=="fillRect"){
ctx.fillRect(
renderItem.arguments[0],
renderItem.arguments[1],
renderItem.arguments[2],
renderItem.arguments[3]
);
});
}else if(renderItem.name=="fillText"){
// console.log(renderItem.arguments[0]);
ctx.fillText(renderItem.arguments[0],renderItem.arguments[1],renderItem.arguments[2]);
}else if(renderItem.name=="drawImage"){
// console.log(renderItem);
// console.log(renderItem.arguments[0].width);
if (renderItem.arguments[8] > 0 && renderItem.arguments[7]){
ctx.drawImage(
renderItem.arguments[0],
renderItem.arguments[1],
renderItem.arguments[2],
renderItem.arguments[3],
renderItem.arguments[4],
renderItem.arguments[5],
renderItem.arguments[6],
renderItem.arguments[7],
renderItem.arguments[8]
);
}
}else{
_.log(renderItem);
}
}
break;
default:
}
};
const paths = container.getClipPaths();
if (paths.length) {
this.target.clip(paths, callback);
} else {
callback();
}
});
}
if (storageContext.clip){
ctx.restore();
}
}
renderNodeBackgroundAndBorders(container: NodeContainer) {
const HAS_BACKGROUND =
!container.style.background.backgroundColor.isTransparent() ||
container.style.background.backgroundImage.length;
/*
html2canvas.prototype.canvasRenderContextChildren = function(storageContext,parentctx){
var ctx = storageContext.canvas.getContext('2d');
const hasRenderableBorders = container.style.border.some(
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);
});
}
container.style.border.forEach((border, side) => {
if (
border.borderStyle !== BORDER_STYLE.NONE &&
!border.borderColor.isTransparent()
) {
this.renderBorder(border, side, container.curvedBounds);
}
});
};
if (HAS_BACKGROUND || hasRenderableBorders) {
const paths = container.parent ? container.parent.getClipPaths() : [];
if (paths.length) {
this.target.clip(paths, callback);
} else {
callback();
}
storageContext.canvasPosition = storageContext.canvasPosition || {};
this.canvasRenderContext(storageContext,ctx);
for (var s = 0; s<this.queue.length;){
if (storageContext.parentStack && this.queue[s].canvas === storageContext.parentStack.canvas){
var substorageContext = this.queue.splice(s,1)[0];
if (substorageContext.ctx.storage[5] && substorageContext.ctx.storage[5].arguments[0]=="Highlights"){
console.log('ssssssadasda');
}
this.canvasRenderContextChildren(substorageContext,ctx);
// this.canvasRenderContext(substorageContext,ctx);
// this.canvasRenderStorage(this.queue,ctx);
}else{
s++;
}
}
if (storageContext.ctx.storage[5] && storageContext.ctx.storage[5].arguments[0]=="Highlights"){
$('body').append(parentctx.canvas);
}
//var parentctx = this.canvas.getContext("2d");
if (storageContext.canvas.width>0 && storageContext.canvas.height > 0){
parentctx.drawImage(storageContext.canvas,(storageContext.canvasPosition.x || 0),(storageContext.canvasPosition.y || 0));
}
}
*/
html2canvas.prototype.canvasRenderStorage = function(queue,parentctx){
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);
for (var i = 0; i<queue.length;){
var storageContext = queue.splice(0,1)[0];
// storageContext.removeOverflow = storageContext.removeOverflow || {};
/*
if (storageContext.canvas){
this.canvasRenderContextChildren(storageContext,parentctx);
var ctx = storageContext.canvas.getContext('2d');
storageContext.canvasPosition = storageContext.canvasPosition || {};
this.canvasRenderContext(storageContext,ctx);
for (var s = 0; s<this.queue.length;){
if (this.queue[s].canvas === storageContext.canvas){
var substorageContext = this.queue.splice(s,1)[0];
substorageContext.canvasPosition = storageContext.canvasPosition || {};
this.canvasRenderContext(substorageContext,ctx);
// this.canvasRenderStorage(this.queue,ctx);
}else{
s++;
}
}
//var parentctx = this.canvas.getContext("2d");
if (storageContext.canvas.width>0 && storageContext.canvas.height > 0){
parentctx.drawImage(storageContext.canvas,(storageContext.canvasPosition.x || 0),(storageContext.canvasPosition.y || 0));
}
ctx = parentctx;
}else{
*/
storageContext.canvasPosition = storageContext.canvasPosition || {};
this.canvasRenderContext(storageContext,parentctx);
// }
/*
if (storageContext.newCanvas){
var ctx = _.canvas.getContext("2d");
ctx.drawImage(canvas,(storageContext.removeOverflow.left || 0),(storageContext.removeOverflow.top || 0));
_.ctx = ctx;
}*/
}
}
html2canvas.prototype.canvasRenderer = function(queue){
var _ = this;
this.sortZ(this.zStack);
queue = this.queue;
//console.log(queue);
//queue = this.sortQueue(queue);
this.canvas.width = Math.max($(document).width(),this.opts.canvasWidth);
this.canvas.height = Math.max($(document).height(),this.opts.canvasHeight);
this.ctx = this.canvas.getContext("2d");
this.canvasRenderStorage(queue,this.ctx);
};
/*
* Sort elements based on z-index and position attributes
*/
/*
html2canvas.prototype.sortQueue = function(queue){
if (!this.opts.reorderZ || !this.needReorder) return queue;
var longest = 0;
this.each(queue,function(i,e){
if (longest<e.zIndex.length){
longest = e.zIndex.length;
}
});
var counter = 0;
//console.log(((queue.length).toString().length)-(count.length).toString().length);
this.each(queue,function(i,e){
var more = ((queue.length).toString().length)-((counter).toString().length);
while(longest>e.zIndex.length){
e.zIndex += "0";
}
e.zIndex = e.zIndex+counter;
while((longest+more+(counter).toString().length)>e.zIndex.length){
e.zIndex += "0";
}
counter++;
// console.log(e.zIndex);
});
queue = queue.sort(function(a,b){
if (a.zIndex < b.zIndex) return -1;
if (a.zIndex > b.zIndex) return 1;
return 0;
});
return queue;
}
*/
html2canvas.prototype.setContextVariable = function(ctx,variable,value){
if (!ctx.storage){
ctx[variable] = value;
}else{
ctx.storage.push(
{
type: "variable",
name:variable,
arguments:value
});
}
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;
};
}

View File

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

View File

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

288
src/Text.js Normal file
View File

@ -0,0 +1,288 @@
html2canvas.prototype.newText = function(el,textNode,stack,form){
var ctx = stack.ctx;
var family = this.getCSS(el,"font-family");
var size = this.getCSS(el,"font-size");
var color = this.getCSS(el,"color");
var text_decoration = this.getCSS(el,"text-decoration");
var text_align = this.getCSS(el,"text-align");
var letter_spacing = this.getCSS(el,"letter-spacing");
// apply text-transform:ation to the text
textNode.nodeValue = this.textTransform(textNode.nodeValue,this.getCSS(el,"text-transform"));
var text = this.trim(textNode.nodeValue);
//text = $.trim(text);
if (text.length>0){
if (text_decoration!="none"){
var metrics = this.fontMetrics(family,size);
}
var renderList,
renderWords = false;
text_align = text_align.replace(["-webkit-auto"],["auto"])
if (this.opts.letterRendering == false && /^(left|right|justify|auto)$/.test(text_align) && /^(normal|none)$/.test(letter_spacing)){
// this.setContextVariable(ctx,"textAlign",text_align);
renderWords = true;
renderList = textNode.nodeValue.split(/(\b| )/);
}else{
// this.setContextVariable(ctx,"textAlign","left");
renderList = textNode.nodeValue.split("");
}
this.setFont(ctx,el,false);
/*
if (stack.clip){
ctx.rect (stack.clip.left, stack.clip.top, stack.clip.width, stack.clip.height);
ctx.clip();
}
*/
var oldTextNode = textNode;
for(var c=0;c<renderList.length;c++){
// IE 9 bug
if (typeof oldTextNode.nodeValue != "string"){
continue;
}
// TODO only do the splitting for non-range prints
var newTextNode = oldTextNode.splitText(renderList[c].length);
if (text_decoration!="none" || this.trim(oldTextNode.nodeValue).length != 0){
if (this.support.rangeBounds){
// getBoundingClientRect is supported for ranges
if (document.createRange){
var range = document.createRange();
range.selectNode(oldTextNode);
}else{
// TODO add IE support
var range = document.body.createTextRange();
}
if (range.getBoundingClientRect()){
var bounds = range.getBoundingClientRect();
}else{
var bounds = {};
}
}else{
// it isn't supported, so let's wrap it inside an element instead and the bounds there
var parent = oldTextNode.parentNode;
var wrapElement = document.createElement('wrapper');
var backupText = oldTextNode.cloneNode(true);
wrapElement.appendChild(oldTextNode.cloneNode(true));
parent.replaceChild(wrapElement,oldTextNode);
var bounds = this.getBounds(wrapElement);
parent.replaceChild(backupText,wrapElement);
}
// console.log(range);
// console.log("'"+oldTextNode.nodeValue+"'"+bounds.left)
this.printText(oldTextNode.nodeValue,bounds.left,bounds.bottom,ctx);
switch(text_decoration) {
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.newRect(ctx,bounds.left,Math.round(bounds.top+metrics.baseline+metrics.lineWidth),bounds.width,1,color);
break;
case "overline":
this.newRect(ctx,bounds.left,bounds.top,bounds.width,1,color);
break;
case "line-through":
// TODO try and find exact position for line-through
this.newRect(ctx,bounds.left,Math.ceil(bounds.top+metrics.middle+metrics.lineWidth),bounds.width,1,color);
break;
}
}
oldTextNode = newTextNode;
}
}
}
html2canvas.prototype.setFont = function(ctx,el,align){
var family = this.getCSS(el,"font-family");
var size = this.getCSS(el,"font-size");
var color = this.getCSS(el,"color");
var bold = this.getCSS(el,"font-weight");
var font_style = this.getCSS(el,"font-style");
var font_variant = this.getCSS(el,"font-variant");
switch(bold){
case 401:
bold = "bold";
break;
case 400:
bold = "normal";
break;
}
var font = font_variant+" "+bold+" "+font_style+" "+size+" "+family;
this.setContextVariable(ctx,"fillStyle",color);
this.setContextVariable(ctx,"font",font);
if (align){
this.setContextVariable(ctx,"textAlign","right");
}else{
this.setContextVariable(ctx,"textAlign","left");
}
}
/*
* Function to find baseline for font with a specicic size
*/
html2canvas.prototype.fontMetrics = function(font,fontSize){
var findMetrics = this.fontData.indexOf(font+"-"+fontSize);
if (findMetrics>-1){
return this.fontData[findMetrics+1];
}
var container = document.createElement('div');
document.getElementsByTagName('body')[0].appendChild(container);
// jquery to speed this up, TODO remove jquery dependancy
$(container).css({
visibility:'hidden',
fontFamily:font,
fontSize:fontSize,
margin:0,
padding:0
});
var img = document.createElement('img');
// TODO add another image
img.src = "http://html2canvas.hertzen.com/images/8.jpg";
img.width = 1;
img.height = 1;
$(img).css({
margin:0,
padding:0
});
var span = document.createElement('span');
$(span).css({
fontFamily:font,
fontSize:fontSize,
margin:0,
padding:0
});
span.appendChild(document.createTextNode('Hidden Text'));
container.appendChild(span);
container.appendChild(img);
var baseline = (img.offsetTop-span.offsetTop)+1;
container.removeChild(span);
container.appendChild(document.createTextNode('Hidden Text'));
$(container).css('line-height','normal');
$(img).css("vertical-align","super");
var middle = (img.offsetTop-container.offsetTop)+1;
var metricsObj = {
baseline: baseline,
lineWidth: 1,
middle: middle
};
this.fontData.push(font+"-"+fontSize);
this.fontData.push(metricsObj);
$(container).remove();
return metricsObj;
}
/*
* Function to apply text-transform attribute to text
*/
html2canvas.prototype.textTransform = function(text,transform){
switch(transform){
case "lowercase":
return text.toLowerCase();
break;
case "capitalize":
return text.replace( /(^|\s|:|-|\(|\))([a-z])/g , function(m,p1,p2){
return p1+p2.toUpperCase();
} );
break;
case "uppercase":
return text.toUpperCase();
break;
default:
return text;
}
}
/*
*Function to trim whitespace from text
*/
html2canvas.prototype.trim = function(text) {
return text.replace(/^\s*/, "").replace(/\s*$/, "");
}

View File

@ -1,85 +0,0 @@
/* @flow */
'use strict';
import type NodeContainer from './NodeContainer';
import {Bounds, parseBounds} from './Bounds';
import {TEXT_DECORATION} from './parsing/textDecoration';
import FEATURES from './Feature';
import {breakWords, toCodePoints, fromCodePoint} from './Unicode';
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 letterRendering = parent.style.letterSpacing !== 0;
const textList = letterRendering
? toCodePoints(value).map(i => fromCodePoint(i))
: breakWords(value, parent);
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);
};

View File

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

46
src/Traversing.js Normal file
View File

@ -0,0 +1,46 @@
html2canvas.prototype.parseElement = function(element,stack){
var _ = this;
this.each(element.children,function(index,el){
_.parsing(el,stack);
});
this.log('Render queue stored');
this.opts.storageReady(this);
this.finish();
}
html2canvas.prototype.parsing = function(el,stack){
if (this.getCSS(el,'display') != "none" && this.getCSS(el,'visibility')!="hidden"){
var _ = this;
stack = this.newElement(el,stack) || stack;
var ctx = stack.ctx;
if (!this.ignoreRe.test(el.nodeName)){
// TODO remove jQuery dependancy
this.each(this.contentsInZ(el),function(cid,node){
if (node.nodeType==1){
// element
_.parsing(node,stack);
}else if (node.nodeType==3){
_.newText(el,node,stack);
}
});
}
}
// }
}

View File

@ -1,27 +0,0 @@
/* @flow */
'use strict';
import NodeContainer from './NodeContainer';
import {LineBreaker, fromCodePoint, toCodePoints} from 'css-line-break';
import {OVERFLOW_WRAP} from './parsing/overflowWrap';
export {toCodePoints, fromCodePoint} from 'css-line-break';
export const breakWords = (str: string, parent: NodeContainer): Array<string> => {
const breaker = LineBreaker(str, {
lineBreak: parent.style.lineBreak,
wordBreak:
parent.style.overflowWrap === OVERFLOW_WRAP.BREAK_WORD
? 'break-word'
: parent.style.wordBreak
});
const words = [];
let bk;
while (!(bk = breaker.next()).done) {
words.push(bk.value.slice());
}
return words;
};

View File

@ -1,21 +1,285 @@
/* @flow */
'use strict';
export const contains = (bit: number, value: number): boolean => (bit & value) !== 0;
// Simple logger
html2canvas.prototype.log = function(a){
if (this.opts.logging){
this.opts.logger(a);
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));
html2canvas.prototype.withinBounds = function(src,dst){
if (!src) return true;
// return true;
return (
(src.left <= dst.left || dst.left+dst.width < src.left) &&
(src.top <= dst.top || dst.top+dst.height < src.top)
);
}
html2canvas.prototype.clipBounds = function(src,dst){
var x = Math.max(src.left,dst.left);
var y = Math.max(src.top,dst.top);
var x2 = Math.min((src.left+src.width),(dst.left+dst.width));
var y2 = Math.min((src.top+src.height),(dst.top+dst.height));
return {
left:x,
top:y,
width:x2-x,
height:y2-y
};
}
/**
* Function to provide bounds for element
* @return {Bounds} object with position and dimension information
*/
html2canvas.prototype.getBounds = function(el){
window.scroll(0,0);
if (el.getBoundingClientRect){
var clientRect = el.getBoundingClientRect();
var bounds = {};
// need to create new object, as clientrect bounds can't be modified, thanks pufuwozu
// TODO add scroll position to bounds, so no scrolling of window necessary
bounds.top = clientRect.top;
bounds.bottom = clientRect.bottom || (clientRect.top + clientRect.height);
bounds.left = clientRect.left;
bounds.width = clientRect.width;
bounds.height = clientRect.height;
return bounds;
}else{
// TODO remove jQuery dependancy
var p = $(el).offset();
return {
left: p.left + this.getCSS(el,"border-left-width",true),
top: p.top + this.getCSS(el,"border-top-width",true),
width:$(el).innerWidth(),
height:$(el).innerHeight()
}
}
}
/*
* Function for looping through array
*/
html2canvas.prototype.each = function(arrayLoop,callbackFunc){
callbackFunc = callbackFunc || function(){};
for (var i=0;i<arrayLoop.length;i++){
if (callbackFunc(i,arrayLoop[i]) === false) return;
}
}
/*
* Function to get childNodes of an element in the order they should be rendered (based on z-index)
* reference http://www.w3.org/TR/CSS21/zindex.html
*/
html2canvas.prototype.contentsInZ = function(el){
// TODO remove jQuery dependency
var contents = $(el).contents();
return contents;
}
/*
* Function for fetching the element attribute
*/
html2canvas.prototype.getAttr = function(el,attribute){
return el.getAttribute(attribute);
//return $(el).attr(attribute);
}
/*
* Function to extend object
*/
html2canvas.prototype.extendObj = function(options,defaults){
for (var key in options){
defaults[key] = options[key];
}
return defaults;
}
/*
*todo remove this function
html2canvas.prototype.leadingZero = function(num,size){
var s = "000000000" + num;
return s.substr(s.length-size);
}
*/
html2canvas.prototype.zContext = function(zindex){
return {
zindex: zindex,
children: []
}
}
html2canvas.prototype.setZ = function(zindex,position,parentZ,parentNode){
// TODO fix static elements overlapping relative/absolute elements under same stack, if they are defined after them
if (!parentZ){
this.zStack = new this.zContext(0);
return this.zStack;
}
if (zindex!="auto"){
this.needReorder = true;
var newContext = new this.zContext(zindex);
parentZ.children.push(newContext);
return newContext;
}else {
return parentZ;
}
}
html2canvas.prototype.sortZ = function(zStack){
var subStacks = [];
var stackValues = [];
var _ = this;
this.each(zStack.children, function(i,stackChild){
if (stackChild.children && stackChild.children.length > 0){
subStacks.push(stackChild);
stackValues.push(stackChild.zindex);
}else{
_.queue.push(stackChild);
}
});
stackValues.sort(function(a,b){
return a - b
});
this.each(stackValues, function(i,zValue){
for (var s = 0;s<=subStacks.length;s++){
if (subStacks[s].zindex == zValue){
var stackChild = subStacks.splice(s,1);
_.sortZ(stackChild[0]);
break;
}
}
});
}
/*
*todo remove this function
html2canvas.prototype.formatZ = function(zindex,position,parentZ,parentNode){
if (!parentZ){
parentZ = "0";
}
if (position!="static" && parentZ.charAt(0)=="0"){
this.needReorder = true;
parentZ = "1"+parentZ.slice(1);
}
if (zindex=="auto"){
var parentPosition = this.getCSS(parentNode,"position");
if (parentPosition!="static" && typeof parentPosition != "undefined"){
zindex = 0;
}
else{
return parentZ;
}
}
return target;
};
var b = this.leadingZero(this.numDraws,9);
var s = this.leadingZero(zindex+1,9);
export const SMALL_IMAGE =
'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
// var s = "000000000" + num;
return parentZ+""+""+s+""+b;
}
*/
/*
* Get element childNodes
*/
html2canvas.prototype.getContents = function(el){
return (el.nodeName == "iframe" ) ?
el.contentDocument || el.contentWindow.document :
el.childNodes;
}
/*
* Function for fetching the css attribute
* TODO remove jQuery dependancy
*/
html2canvas.prototype.getCSS = function(el,attribute,intOnly){
if (intOnly){
return parseInt($(el).css(attribute),10);
}else{
return $(el).css(attribute);
}
}
html2canvas.prototype.getIndex = function(array,src){
if (array.indexOf){
return array.indexOf(src);
}else{
for(var i = 0; i < array.length; i++){
if(this[i] == src) return i;
}
return -1;
}
}
html2canvas.prototype.isSameOrigin = function(url){
var link = document.createElement("a");
link.href = url;
return ((link.protocol + link.host) == this.pageOrigin);
}

View File

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

View File

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

View File

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

View File

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

View File

@ -1,12 +0,0 @@
/* @flow */
'use strict';
export default class Size {
width: number;
height: number;
constructor(width: number, height: number) {
this.width = width;
this.height = height;
}
}

View File

@ -1,24 +0,0 @@
/* @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,97 +0,0 @@
/* @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,
ignoreElements?: HTMLElement => boolean,
imageTimeout: number,
logging: boolean,
onclone?: Document => void,
proxy: ?string,
removeContainer: ?boolean,
scale: number,
target: RenderTarget<*>,
useCORS: boolean,
width: number,
height: number,
x: number,
y: number,
scrollX: number,
scrollY: number,
windowWidth: number,
windowHeight: number
};
const html2canvas = (element: HTMLElement, conf: ?Options): Promise<*> => {
const config = conf || {};
const logger = new Logger(typeof config.logging === 'boolean' ? config.logging : true);
logger.log(`html2canvas ${__VERSION__}`);
if (__DEV__ && typeof config.onrendered === 'function') {
logger.error(
`onrendered option is deprecated, html2canvas returns a Promise with the canvas as the value`
);
}
const ownerDocument = element.ownerDocument;
if (!ownerDocument) {
return Promise.reject(`Provided element is not within a Document`);
}
const defaultView = ownerDocument.defaultView;
const scrollX = defaultView.pageXOffset;
const scrollY = defaultView.pageYOffset;
const isDocument = element.tagName === 'HTML' || element.tagName === 'BODY';
const {width, height, left, top} = isDocument
? parseDocumentSize(ownerDocument)
: parseBounds(element, scrollX, scrollY);
const defaultOptions = {
async: true,
allowTaint: false,
backgroundColor: '#ffffff',
imageTimeout: 15000,
logging: true,
proxy: null,
removeContainer: true,
foreignObjectRendering: false,
scale: defaultView.devicePixelRatio || 1,
target: new CanvasRenderer(config.canvas),
useCORS: false,
x: left,
y: top,
width: Math.ceil(width),
height: Math.ceil(height),
windowWidth: defaultView.innerWidth,
windowHeight: defaultView.innerHeight,
scrollX: defaultView.pageXOffset,
scrollY: defaultView.pageYOffset
};
const result = renderElement(element, {...defaultOptions, ...config}, logger);
if (__DEV__) {
return result.catch(e => {
logger.error(e);
throw e;
});
}
return result;
};
html2canvas.CanvasRenderer = CanvasRenderer;
module.exports = html2canvas;

View File

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

View File

@ -1,49 +0,0 @@
/* @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

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

View File

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

View File

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

View File

@ -1,38 +0,0 @@
/* @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

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

View File

@ -1,19 +0,0 @@
/* @flow */
'use strict';
export const LINE_BREAK = {
NORMAL: 'normal',
STRICT: 'strict'
};
export type LineBreak = $Values<typeof LINE_BREAK>;
export const parseLineBreak = (wordBreak: string): LineBreak => {
switch (wordBreak) {
case 'strict':
return LINE_BREAK.STRICT;
case 'normal':
default:
return LINE_BREAK.NORMAL;
}
};

View File

@ -1,209 +0,0 @@
/* @flow */
'use strict';
import type {BackgroundSource} from './background';
import {parseBackgroundImage} from './background';
export type ListStyle = {
listStyleType: ListStyleType,
listStyleImage: ?BackgroundSource,
listStylePosition: ListStylePosition
};
export const LIST_STYLE_POSITION = {
INSIDE: 0,
OUTSIDE: 1
};
export const LIST_STYLE_TYPE = {
NONE: -1,
DISC: 0,
CIRCLE: 1,
SQUARE: 2,
DECIMAL: 3,
CJK_DECIMAL: 4,
DECIMAL_LEADING_ZERO: 5,
LOWER_ROMAN: 6,
UPPER_ROMAN: 7,
LOWER_GREEK: 8,
LOWER_ALPHA: 9,
UPPER_ALPHA: 10,
ARABIC_INDIC: 11,
ARMENIAN: 12,
BENGALI: 13,
CAMBODIAN: 14,
CJK_EARTHLY_BRANCH: 15,
CJK_HEAVENLY_STEM: 16,
CJK_IDEOGRAPHIC: 17,
DEVANAGARI: 18,
ETHIOPIC_NUMERIC: 19,
GEORGIAN: 20,
GUJARATI: 21,
GURMUKHI: 22,
HEBREW: 22,
HIRAGANA: 23,
HIRAGANA_IROHA: 24,
JAPANESE_FORMAL: 25,
JAPANESE_INFORMAL: 26,
KANNADA: 27,
KATAKANA: 28,
KATAKANA_IROHA: 29,
KHMER: 30,
KOREAN_HANGUL_FORMAL: 31,
KOREAN_HANJA_FORMAL: 32,
KOREAN_HANJA_INFORMAL: 33,
LAO: 34,
LOWER_ARMENIAN: 35,
MALAYALAM: 36,
MONGOLIAN: 37,
MYANMAR: 38,
ORIYA: 39,
PERSIAN: 40,
SIMP_CHINESE_FORMAL: 41,
SIMP_CHINESE_INFORMAL: 42,
TAMIL: 43,
TELUGU: 44,
THAI: 45,
TIBETAN: 46,
TRAD_CHINESE_FORMAL: 47,
TRAD_CHINESE_INFORMAL: 48,
UPPER_ARMENIAN: 49,
DISCLOSURE_OPEN: 50,
DISCLOSURE_CLOSED: 51
};
export type ListStylePosition = $Values<typeof LIST_STYLE_POSITION>;
export type ListStyleType = $Values<typeof LIST_STYLE_TYPE>;
export const parseListStyleType = (type: string): ListStyleType => {
switch (type) {
case 'disc':
return LIST_STYLE_TYPE.DISC;
case 'circle':
return LIST_STYLE_TYPE.CIRCLE;
case 'square':
return LIST_STYLE_TYPE.SQUARE;
case 'decimal':
return LIST_STYLE_TYPE.DECIMAL;
case 'cjk-decimal':
return LIST_STYLE_TYPE.CJK_DECIMAL;
case 'decimal-leading-zero':
return LIST_STYLE_TYPE.DECIMAL_LEADING_ZERO;
case 'lower-roman':
return LIST_STYLE_TYPE.LOWER_ROMAN;
case 'upper-roman':
return LIST_STYLE_TYPE.UPPER_ROMAN;
case 'lower-greek':
return LIST_STYLE_TYPE.LOWER_GREEK;
case 'lower-alpha':
return LIST_STYLE_TYPE.LOWER_ALPHA;
case 'upper-alpha':
return LIST_STYLE_TYPE.UPPER_ALPHA;
case 'arabic-indic':
return LIST_STYLE_TYPE.ARABIC_INDIC;
case 'armenian':
return LIST_STYLE_TYPE.ARMENIAN;
case 'bengali':
return LIST_STYLE_TYPE.BENGALI;
case 'cambodian':
return LIST_STYLE_TYPE.CAMBODIAN;
case 'cjk-earthly-branch':
return LIST_STYLE_TYPE.CJK_EARTHLY_BRANCH;
case 'cjk-heavenly-stem':
return LIST_STYLE_TYPE.CJK_HEAVENLY_STEM;
case 'cjk-ideographic':
return LIST_STYLE_TYPE.CJK_IDEOGRAPHIC;
case 'devanagari':
return LIST_STYLE_TYPE.DEVANAGARI;
case 'ethiopic-numeric':
return LIST_STYLE_TYPE.ETHIOPIC_NUMERIC;
case 'georgian':
return LIST_STYLE_TYPE.GEORGIAN;
case 'gujarati':
return LIST_STYLE_TYPE.GUJARATI;
case 'gurmukhi':
return LIST_STYLE_TYPE.GURMUKHI;
case 'hebrew':
return LIST_STYLE_TYPE.HEBREW;
case 'hiragana':
return LIST_STYLE_TYPE.HIRAGANA;
case 'hiragana-iroha':
return LIST_STYLE_TYPE.HIRAGANA_IROHA;
case 'japanese-formal':
return LIST_STYLE_TYPE.JAPANESE_FORMAL;
case 'japanese-informal':
return LIST_STYLE_TYPE.JAPANESE_INFORMAL;
case 'kannada':
return LIST_STYLE_TYPE.KANNADA;
case 'katakana':
return LIST_STYLE_TYPE.KATAKANA;
case 'katakana-iroha':
return LIST_STYLE_TYPE.KATAKANA_IROHA;
case 'khmer':
return LIST_STYLE_TYPE.KHMER;
case 'korean-hangul-formal':
return LIST_STYLE_TYPE.KOREAN_HANGUL_FORMAL;
case 'korean-hanja-formal':
return LIST_STYLE_TYPE.KOREAN_HANJA_FORMAL;
case 'korean-hanja-informal':
return LIST_STYLE_TYPE.KOREAN_HANJA_INFORMAL;
case 'lao':
return LIST_STYLE_TYPE.LAO;
case 'lower-armenian':
return LIST_STYLE_TYPE.LOWER_ARMENIAN;
case 'malayalam':
return LIST_STYLE_TYPE.MALAYALAM;
case 'mongolian':
return LIST_STYLE_TYPE.MONGOLIAN;
case 'myanmar':
return LIST_STYLE_TYPE.MYANMAR;
case 'oriya':
return LIST_STYLE_TYPE.ORIYA;
case 'persian':
return LIST_STYLE_TYPE.PERSIAN;
case 'simp-chinese-formal':
return LIST_STYLE_TYPE.SIMP_CHINESE_FORMAL;
case 'simp-chinese-informal':
return LIST_STYLE_TYPE.SIMP_CHINESE_INFORMAL;
case 'tamil':
return LIST_STYLE_TYPE.TAMIL;
case 'telugu':
return LIST_STYLE_TYPE.TELUGU;
case 'thai':
return LIST_STYLE_TYPE.THAI;
case 'tibetan':
return LIST_STYLE_TYPE.TIBETAN;
case 'trad-chinese-formal':
return LIST_STYLE_TYPE.TRAD_CHINESE_FORMAL;
case 'trad-chinese-informal':
return LIST_STYLE_TYPE.TRAD_CHINESE_INFORMAL;
case 'upper-armenian':
return LIST_STYLE_TYPE.UPPER_ARMENIAN;
case 'disclosure-open':
return LIST_STYLE_TYPE.DISCLOSURE_OPEN;
case 'disclosure-closed':
return LIST_STYLE_TYPE.DISCLOSURE_CLOSED;
case 'none':
default:
return LIST_STYLE_TYPE.NONE;
}
};
export const parseListStyle = (style: CSSStyleDeclaration): ListStyle => {
const listStyleImage = parseBackgroundImage(style.getPropertyValue('list-style-image'));
return {
listStyleType: parseListStyleType(style.getPropertyValue('list-style-type')),
listStyleImage: listStyleImage.length ? listStyleImage[0] : null,
listStylePosition: parseListStylePosition(style.getPropertyValue('list-style-position'))
};
};
const parseListStylePosition = (position: string): ListStylePosition => {
switch (position) {
case 'inside':
return LIST_STYLE_POSITION.INSIDE;
case 'outside':
default:
return LIST_STYLE_POSITION.OUTSIDE;
}
};

View File

@ -1,11 +0,0 @@
/* @flow */
'use strict';
import Length from '../Length';
const SIDES = ['top', 'right', 'bottom', 'left'];
export type Margin = Array<Length>;
export const parseMargin = (style: CSSStyleDeclaration): Margin => {
return SIDES.map(side => new Length(style.getPropertyValue(`margin-${side}`)));
};

View File

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

View File

@ -1,19 +0,0 @@
/* @flow */
'use strict';
export const OVERFLOW_WRAP = {
NORMAL: 0,
BREAK_WORD: 1
};
export type OverflowWrap = $Values<typeof OVERFLOW_WRAP>;
export const parseOverflowWrap = (overflow: string): OverflowWrap => {
switch (overflow) {
case 'break-word':
return OVERFLOW_WRAP.BREAK_WORD;
case 'normal':
default:
return OVERFLOW_WRAP.NORMAL;
}
};

View File

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

View File

@ -1,27 +0,0 @@
/* @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

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

View File

@ -1,94 +0,0 @@
/* @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

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

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