2013-09-07 22:24:41 +04:00
/ *
2014-01-19 20:04:27 +04:00
html2canvas 0.5 . 0 - rc1 < http : //html2canvas.hertzen.com>
Copyright ( c ) 2014 Niklas von Hertzen
2013-09-07 22:24:41 +04:00
Released under MIT License
* /
( function ( window , document , undefined ) {
2014-02-23 19:02:49 +04:00
var html2canvasNodeAttribute = "data-html2canvas-node" ;
2014-01-19 20:04:27 +04:00
window . html2canvas = function ( nodeList , options ) {
2014-01-21 23:41:00 +04:00
options = options || { } ;
if ( options . logging ) {
window . html2canvas . logging = true ;
window . html2canvas . start = Date . now ( ) ;
}
2014-02-15 02:33:09 +04:00
options . async = typeof ( options . async ) === "undefined" ? true : options . async ;
options . removeContainer = typeof ( options . removeContainer ) === "undefined" ? true : options . removeContainer ;
2014-02-23 19:02:49 +04:00
var node = ( ( nodeList === undefined ) ? [ document . documentElement ] : ( ( nodeList . length ) ? nodeList : [ nodeList ] ) ) [ 0 ] ;
node . setAttribute ( html2canvasNodeAttribute , "true" ) ;
return renderDocument ( node . ownerDocument , options , window . innerWidth , window . innerHeight ) . then ( function ( canvas ) {
2014-02-03 21:42:42 +04:00
if ( typeof ( options . onrendered ) === "function" ) {
log ( "options.onrendered is deprecated, html2canvas returns a Promise containing the canvas" ) ;
options . onrendered ( canvas ) ;
}
return canvas ;
} ) ;
} ;
2014-02-08 16:07:20 +04:00
function renderDocument ( document , options , windowWidth , windowHeight ) {
2014-02-15 02:33:09 +04:00
return createWindowClone ( document , windowWidth , windowHeight , options ) . then ( function ( container ) {
2014-01-26 18:06:16 +04:00
log ( "Document cloned" ) ;
2014-02-23 19:02:49 +04:00
var selector = "[" + html2canvasNodeAttribute + "='true']" ;
document . querySelector ( selector ) . removeAttribute ( html2canvasNodeAttribute ) ;
2014-01-26 18:06:16 +04:00
var clonedWindow = container . contentWindow ;
2014-02-23 19:02:49 +04:00
var node = clonedWindow . document . querySelector ( selector ) ;
2014-03-02 18:00:59 +04:00
var support = new Support ( clonedWindow . document ) ;
2014-01-26 18:06:16 +04:00
var imageLoader = new ImageLoader ( options , support ) ;
2014-03-02 18:00:59 +04:00
var bounds = getBounds ( node ) ;
2014-02-08 16:07:20 +04:00
var width = options . type === "view" ? Math . min ( bounds . width , windowWidth ) : documentWidth ( ) ;
var height = options . type === "view" ? Math . min ( bounds . height , windowHeight ) : documentHeight ( ) ;
2014-02-01 20:32:05 +04:00
var renderer = new CanvasRenderer ( width , height , imageLoader ) ;
2014-01-26 18:06:16 +04:00
var parser = new NodeParser ( node , renderer , support , imageLoader , options ) ;
2014-02-03 21:42:42 +04:00
return parser . ready . then ( function ( ) {
2014-02-15 02:33:09 +04:00
log ( "Finished rendering" ) ;
if ( options . removeContainer ) {
container . parentNode . removeChild ( container ) ;
}
2014-03-03 23:19:28 +04:00
return ( options . type !== "view" && ( node === clonedWindow . document . body || node === clonedWindow . document . documentElement ) ) ? renderer . canvas : crop ( renderer . canvas , bounds ) ;
2014-02-03 21:42:42 +04:00
} ) ;
2014-01-26 18:06:16 +04:00
} ) ;
2014-02-03 21:42:42 +04:00
}
2013-09-07 22:24:41 +04:00
2014-03-03 23:19:28 +04:00
function crop ( canvas , bounds ) {
var croppedCanvas = document . createElement ( "canvas" ) ;
2014-03-05 21:19:24 +04:00
var x1 = Math . min ( canvas . width - 1 , Math . max ( 0 , bounds . left ) ) ;
var x2 = Math . min ( canvas . width , Math . max ( 1 , bounds . left + bounds . width ) ) ;
var y1 = Math . min ( canvas . height - 1 , Math . max ( 0 , bounds . top ) ) ;
var y2 = Math . min ( canvas . height , Math . max ( 1 , bounds . top + bounds . height ) ) ;
var width = croppedCanvas . width = x2 - x1 ;
var height = croppedCanvas . height = y2 - y1 ;
2014-03-04 22:42:34 +04:00
log ( "Cropping canvas at:" , "left:" , bounds . left , "top:" , bounds . top , "width:" , bounds . width , "height:" , bounds . height ) ;
2014-03-05 21:19:24 +04:00
log ( "Resulting crop with width" , width , "and height" , height , " with x" , x1 , "and y" , y1 ) ;
croppedCanvas . getContext ( "2d" ) . drawImage ( canvas , x1 , y1 , width , height , 0 , 0 , width , height ) ;
2014-03-03 23:19:28 +04:00
return croppedCanvas ;
}
2014-01-21 23:41:00 +04:00
function documentWidth ( ) {
return Math . max (
Math . max ( document . body . scrollWidth , document . documentElement . scrollWidth ) ,
Math . max ( document . body . offsetWidth , document . documentElement . offsetWidth ) ,
Math . max ( document . body . clientWidth , document . documentElement . clientWidth )
) ;
}
function documentHeight ( ) {
return Math . max (
Math . max ( document . body . scrollHeight , document . documentElement . scrollHeight ) ,
Math . max ( document . body . offsetHeight , document . documentElement . offsetHeight ) ,
Math . max ( document . body . clientHeight , document . documentElement . clientHeight )
) ;
}
2014-02-03 21:42:42 +04:00
function smallImage ( ) {
return "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" ;
}
2014-02-15 02:33:09 +04:00
function createWindowClone ( ownerDocument , width , height , options ) {
2014-01-19 20:04:27 +04:00
var documentElement = ownerDocument . documentElement . cloneNode ( true ) ,
container = ownerDocument . createElement ( "iframe" ) ;
2013-09-07 22:24:41 +04:00
2014-02-03 21:42:42 +04:00
container . style . visibility = "hidden" ;
2014-01-19 20:04:27 +04:00
container . style . position = "absolute" ;
2014-02-23 18:26:23 +04:00
container . style . left = container . style . top = "-10000px" ;
2014-02-01 20:32:05 +04:00
container . width = width ;
container . height = height ;
container . scrolling = "no" ; // ios won't scroll without it
2014-01-19 20:04:27 +04:00
ownerDocument . body . appendChild ( container ) ;
2013-09-07 22:24:41 +04:00
2014-01-26 18:06:16 +04:00
return new Promise ( function ( resolve ) {
var documentClone = container . contentWindow . document ;
/ * C h r o m e d o e s n ' t d e t e c t r e l a t i v e b a c k g r o u n d - i m a g e s a s s i g n e d i n i n l i n e < s t y l e > s h e e t s w h e n f e t c h e d t h r o u g h g e t C o m p u t e d S t y l e
if window url is about : blank , we can assign the url to current by writing onto the document
* /
documentClone . open ( ) ;
2014-02-10 21:53:13 +04:00
documentClone . write ( "<!DOCTYPE html>" ) ;
2014-01-26 18:06:16 +04:00
documentClone . close ( ) ;
2014-03-02 20:03:30 +04:00
documentClone . replaceChild ( removeScriptNodes ( documentClone . adoptNode ( documentElement ) ) , documentClone . documentElement ) ;
2014-02-15 02:33:09 +04:00
if ( options . type === "view" ) {
2014-03-02 20:03:30 +04:00
container . contentWindow . scrollTo ( window . pageXOffset , window . pageYOffset ) ;
2014-02-15 02:33:09 +04:00
}
resolve ( container ) ;
2014-01-26 18:06:16 +04:00
} ) ;
2013-09-07 22:24:41 +04:00
}
2014-03-02 20:03:30 +04:00
function removeScriptNodes ( parent ) {
[ ] . slice . call ( parent . childNodes , 0 ) . filter ( isElementNode ) . forEach ( function ( node ) {
if ( node . tagName === "SCRIPT" ) {
parent . removeChild ( node ) ;
} else {
removeScriptNodes ( node ) ;
}
} ) ;
return parent ;
}
function isElementNode ( node ) {
return node . nodeType === Node . ELEMENT _NODE ;
}
2014-02-03 21:42:42 +04:00
function Font ( family , size ) {
var container = document . createElement ( 'div' ) ,
img = document . createElement ( 'img' ) ,
span = document . createElement ( 'span' ) ,
sampleText = 'Hidden Text' ,
baseline ,
middle ;
container . style . visibility = "hidden" ;
container . style . fontFamily = family ;
container . style . fontSize = size ;
container . style . margin = 0 ;
container . style . padding = 0 ;
document . body . appendChild ( container ) ;
img . src = smallImage ( ) ;
img . width = 1 ;
img . height = 1 ;
img . style . margin = 0 ;
img . style . padding = 0 ;
img . style . verticalAlign = "baseline" ;
span . style . fontFamily = family ;
span . style . fontSize = size ;
span . style . margin = 0 ;
span . style . padding = 0 ;
span . appendChild ( document . createTextNode ( sampleText ) ) ;
container . appendChild ( span ) ;
container . appendChild ( img ) ;
baseline = ( img . offsetTop - span . offsetTop ) + 1 ;
container . removeChild ( span ) ;
container . appendChild ( document . createTextNode ( sampleText ) ) ;
container . style . lineHeight = "normal" ;
img . style . verticalAlign = "super" ;
middle = ( img . offsetTop - container . offsetTop ) + 1 ;
document . body . removeChild ( container ) ;
this . baseline = baseline ;
this . lineWidth = 1 ;
this . middle = middle ;
}
function FontMetrics ( ) {
this . data = { } ;
}
FontMetrics . prototype . getMetrics = function ( family , size ) {
if ( this . data [ family + "-" + size ] === undefined ) {
this . data [ family + "-" + size ] = new Font ( family , size ) ;
}
return this . data [ family + "-" + size ] ;
} ;
function GradientContainer ( imageData ) {
this . src = imageData . value ;
this . colorStops = [ ] ;
this . type = null ;
this . x0 = 0.5 ;
this . y0 = 0.5 ;
this . x1 = 0.5 ;
this . y1 = 0.5 ;
this . promise = Promise . resolve ( true ) ;
}
GradientContainer . prototype . TYPES = {
LINEAR : 1 ,
RADIAL : 2
} ;
2014-02-01 20:52:53 +04:00
function ImageContainer ( src , cors ) {
this . src = src ;
this . image = new Image ( ) ;
var image = this . image ;
this . promise = new Promise ( function ( resolve , reject ) {
image . onload = resolve ;
image . onerror = reject ;
if ( cors ) {
image . crossOrigin = "anonymous" ;
}
image . src = src ;
if ( image . complete === true ) {
resolve ( image ) ;
}
} ) ;
}
function ImageLoader ( options , support ) {
this . link = null ;
2014-01-19 20:04:27 +04:00
this . options = options ;
2014-01-21 23:41:00 +04:00
this . support = support ;
2014-02-01 20:52:53 +04:00
this . origin = window . location . protocol + window . location . host ;
2013-09-07 22:24:41 +04:00
}
2014-02-01 20:52:53 +04:00
ImageLoader . prototype . findImages = function ( nodes ) {
var images = [ ] ;
2014-02-08 16:07:20 +04:00
nodes . filter ( isImage ) . map ( urlImage ) . forEach ( this . addImage ( images , this . loadImage ) , this ) ;
2014-02-01 20:52:53 +04:00
return images ;
2013-09-07 22:24:41 +04:00
} ;
2014-02-01 20:52:53 +04:00
ImageLoader . prototype . findBackgroundImage = function ( images , container ) {
2014-02-03 21:42:42 +04:00
container . parseBackgroundImages ( ) . filter ( this . hasImageBackground ) . forEach ( this . addImage ( images , this . loadImage ) , this ) ;
2014-02-01 20:52:53 +04:00
return images ;
2013-09-07 22:24:41 +04:00
} ;
2014-02-01 20:52:53 +04:00
ImageLoader . prototype . addImage = function ( images , callback ) {
return function ( newImage ) {
if ( ! this . imageExists ( images , newImage ) ) {
images . splice ( 0 , 0 , callback . apply ( this , arguments ) ) ;
2014-02-03 21:42:42 +04:00
log ( 'Added image #' + ( images . length ) , newImage ) ;
2014-01-19 20:04:27 +04:00
}
2014-02-01 20:52:53 +04:00
} ;
2013-11-12 21:35:28 +04:00
} ;
2014-02-03 21:42:42 +04:00
ImageLoader . prototype . hasImageBackground = function ( imageData ) {
return imageData . method !== "none" ;
2013-12-23 18:07:49 +04:00
} ;
2013-09-07 22:24:41 +04:00
2014-02-03 21:42:42 +04:00
ImageLoader . prototype . loadImage = function ( imageData ) {
if ( imageData . method === "url" ) {
var src = imageData . args [ 0 ] ;
if ( src . match ( /data:image\/.*;base64,/i ) ) {
return new ImageContainer ( src . replace ( /url\(['"]{0,}|['"]{0,}\)$/ig , '' ) , false ) ;
} else if ( this . isSameOrigin ( src ) || this . options . allowTaint === true ) {
return new ImageContainer ( src , false ) ;
} else if ( this . support . cors && ! this . options . allowTaint && this . options . useCORS ) {
return new ImageContainer ( src , true ) ;
} else if ( this . options . proxy ) {
2014-02-17 02:04:09 +04:00
return new ProxyImageContainer ( src , this . options . proxy ) ;
2014-02-03 21:42:42 +04:00
} else {
return new DummyImageContainer ( src ) ;
}
} else if ( imageData . method === "linear-gradient" ) {
return new LinearGradientContainer ( imageData ) ;
} else if ( imageData . method === "gradient" ) {
return new WebkitGradientContainer ( imageData ) ;
2014-02-01 20:52:53 +04:00
}
2013-09-07 22:24:41 +04:00
} ;
2014-02-01 20:52:53 +04:00
ImageLoader . prototype . imageExists = function ( images , src ) {
return images . some ( function ( image ) {
return image . src === src ;
} ) ;
2013-09-07 22:24:41 +04:00
} ;
2014-02-01 20:52:53 +04:00
ImageLoader . prototype . isSameOrigin = function ( url ) {
var link = this . link || ( this . link = document . createElement ( "a" ) ) ;
link . href = url ;
link . href = link . href ; // IE9, LOL! - http://jsfiddle.net/niklasvh/2e48b/
var origin = link . protocol + link . host ;
return ( origin === this . origin ) ;
2013-09-07 22:24:41 +04:00
} ;
2013-12-23 18:07:49 +04:00
2014-02-01 20:52:53 +04:00
ImageLoader . prototype . getPromise = function ( container ) {
return container . promise ;
2014-01-19 20:04:27 +04:00
} ;
2013-09-07 22:24:41 +04:00
2014-02-01 20:52:53 +04:00
ImageLoader . prototype . get = function ( src ) {
var found = null ;
return this . images . some ( function ( img ) {
return ( found = img ) . src === src ;
} ) ? found : null ;
2014-01-19 20:04:27 +04:00
} ;
2013-09-07 22:24:41 +04:00
2014-02-01 20:52:53 +04:00
ImageLoader . prototype . fetch = function ( nodes ) {
this . images = nodes . reduce ( bind ( this . findBackgroundImage , this ) , this . findImages ( nodes ) ) ;
this . images . forEach ( function ( image , index ) {
image . promise . then ( function ( ) {
log ( "Succesfully loaded image #" + ( index + 1 ) ) ;
} , function ( ) {
log ( "Failed loading image #" + ( index + 1 ) ) ;
} ) ;
} ) ;
this . ready = Promise . all ( this . images . map ( this . getPromise ) ) ;
log ( "Finished searching images" ) ;
return this ;
2014-01-19 20:04:27 +04:00
} ;
2013-09-07 22:24:41 +04:00
2014-02-01 20:52:53 +04:00
function isImage ( container ) {
return container . node . nodeName === "IMG" ;
2013-09-07 22:24:41 +04:00
}
2014-02-08 16:07:20 +04:00
function urlImage ( container ) {
return {
args : [ container . node . src ] ,
method : "url"
} ;
2014-01-19 20:04:27 +04:00
}
Speed & accuracy improvements to pseudo element rendering.
Previously, pseudo elements would be processed as they were found in the DOM tree, which was
an expensive operation as each element's computed :before and :after style was checked for
'content' styles.
This commit traverses the user's stylesheets for :before and :after selectors, gathers the classes
affected, selects all elements that likely have a pseudo element present, then checks computed style.
If there is actually an element present, it is created but *not* appended to the DOM until after
all elements have been processed.
After all elements have been found and created, they are added to the DOM in a single batch, and the original
pseudo elements are hidden in a single batch. This prevents the layout invalidation / relayout loop that was
occuring previously, and in my tests speeds parsing by as much as 50% or more, depending on how many
pseudo elements your page uses.
Additionally, this commit contains a bugfix to the handling of ":before" pseudo elements; the browser effectively
inserts them as the first child of the element, not before the element. This fixes a few rendering inconsistencies
and complicated pages look almost perfect in my tests.
2013-09-18 09:19:39 +04:00
2014-02-03 21:42:42 +04:00
function LinearGradientContainer ( imageData ) {
GradientContainer . apply ( this , arguments ) ;
this . type = this . TYPES . LINEAR ;
imageData . args [ 0 ] . split ( " " ) . forEach ( function ( position ) {
switch ( position ) {
case "left" :
this . x0 = 0 ;
this . x1 = 1 ;
break ;
case "top" :
this . y0 = 0 ;
this . y1 = 1 ;
break ;
case "right" :
this . x0 = 1 ;
this . x1 = 0 ;
break ;
case "bottom" :
this . y0 = 1 ;
this . y1 = 0 ;
break ;
}
} , this ) ;
imageData . args . slice ( 1 ) . forEach ( function ( colorStop ) {
// console.log(colorStop, colorStop.match(this.stepRegExp));
} , this ) ;
}
LinearGradientContainer . prototype = Object . create ( GradientContainer . prototype ) ;
LinearGradientContainer . prototype . stepRegExp = /((?:rgb|rgba)\(\d{1,3},\s\d{1,3},\s\d{1,3}(?:,\s[0-9\.]+)?\))\s*(\d{1,3})?(%|px)?/ ;
2014-02-01 20:52:53 +04:00
function log ( ) {
if ( window . html2canvas . logging && window . console && window . console . log ) {
2014-03-02 20:03:30 +04:00
Function . prototype . bind . call ( window . console . log , ( window . console ) ) . apply ( window . console , [ ( Date . now ( ) - window . html2canvas . start ) + "ms" , "html2canvas:" ] . concat ( [ ] . slice . call ( arguments , 0 ) ) ) ;
2014-02-01 20:52:53 +04:00
}
2014-01-19 20:04:27 +04:00
}
Speed & accuracy improvements to pseudo element rendering.
Previously, pseudo elements would be processed as they were found in the DOM tree, which was
an expensive operation as each element's computed :before and :after style was checked for
'content' styles.
This commit traverses the user's stylesheets for :before and :after selectors, gathers the classes
affected, selects all elements that likely have a pseudo element present, then checks computed style.
If there is actually an element present, it is created but *not* appended to the DOM until after
all elements have been processed.
After all elements have been found and created, they are added to the DOM in a single batch, and the original
pseudo elements are hidden in a single batch. This prevents the layout invalidation / relayout loop that was
occuring previously, and in my tests speeds parsing by as much as 50% or more, depending on how many
pseudo elements your page uses.
Additionally, this commit contains a bugfix to the handling of ":before" pseudo elements; the browser effectively
inserts them as the first child of the element, not before the element. This fixes a few rendering inconsistencies
and complicated pages look almost perfect in my tests.
2013-09-18 09:19:39 +04:00
2014-02-01 20:52:53 +04:00
function NodeContainer ( node , parent ) {
this . node = node ;
this . parent = parent ;
this . stack = null ;
this . bounds = null ;
2014-03-02 18:00:59 +04:00
this . offsetBounds = null ;
2014-02-01 20:52:53 +04:00
this . visible = null ;
this . computedStyles = null ;
this . styles = { } ;
this . backgroundImages = null ;
2014-03-02 20:03:30 +04:00
this . transformData = null ;
this . transformMatrix = null ;
2014-01-19 20:04:27 +04:00
}
2013-12-23 18:07:49 +04:00
2014-02-01 20:52:53 +04:00
NodeContainer . prototype . assignStack = function ( stack ) {
this . stack = stack ;
stack . children . push ( this ) ;
} ;
2013-09-18 05:54:20 +04:00
2014-02-01 20:52:53 +04:00
NodeContainer . prototype . isElementVisible = function ( ) {
return this . node . nodeType === Node . TEXT _NODE ? this . parent . visible : ( this . css ( 'display' ) !== "none" && this . css ( 'visibility' ) !== "hidden" && ! this . node . hasAttribute ( "data-html2canvas-ignore" ) ) ;
} ;
Speed & accuracy improvements to pseudo element rendering.
Previously, pseudo elements would be processed as they were found in the DOM tree, which was
an expensive operation as each element's computed :before and :after style was checked for
'content' styles.
This commit traverses the user's stylesheets for :before and :after selectors, gathers the classes
affected, selects all elements that likely have a pseudo element present, then checks computed style.
If there is actually an element present, it is created but *not* appended to the DOM until after
all elements have been processed.
After all elements have been found and created, they are added to the DOM in a single batch, and the original
pseudo elements are hidden in a single batch. This prevents the layout invalidation / relayout loop that was
occuring previously, and in my tests speeds parsing by as much as 50% or more, depending on how many
pseudo elements your page uses.
Additionally, this commit contains a bugfix to the handling of ":before" pseudo elements; the browser effectively
inserts them as the first child of the element, not before the element. This fixes a few rendering inconsistencies
and complicated pages look almost perfect in my tests.
2013-09-18 09:19:39 +04:00
2014-02-01 20:52:53 +04:00
NodeContainer . prototype . css = function ( attribute ) {
if ( ! this . computedStyles ) {
2014-02-08 18:52:41 +04:00
this . computedStyles = this . computedStyle ( null ) ;
2014-02-01 20:52:53 +04:00
}
2013-09-18 05:54:20 +04:00
2014-02-01 20:52:53 +04:00
return this . styles [ attribute ] || ( this . styles [ attribute ] = this . computedStyles [ attribute ] ) ;
} ;
Speed & accuracy improvements to pseudo element rendering.
Previously, pseudo elements would be processed as they were found in the DOM tree, which was
an expensive operation as each element's computed :before and :after style was checked for
'content' styles.
This commit traverses the user's stylesheets for :before and :after selectors, gathers the classes
affected, selects all elements that likely have a pseudo element present, then checks computed style.
If there is actually an element present, it is created but *not* appended to the DOM until after
all elements have been processed.
After all elements have been found and created, they are added to the DOM in a single batch, and the original
pseudo elements are hidden in a single batch. This prevents the layout invalidation / relayout loop that was
occuring previously, and in my tests speeds parsing by as much as 50% or more, depending on how many
pseudo elements your page uses.
Additionally, this commit contains a bugfix to the handling of ":before" pseudo elements; the browser effectively
inserts them as the first child of the element, not before the element. This fixes a few rendering inconsistencies
and complicated pages look almost perfect in my tests.
2013-09-18 09:19:39 +04:00
2014-02-23 19:35:46 +04:00
NodeContainer . prototype . prefixedCss = function ( attribute ) {
var prefixes = [ "webkit" , "moz" , "ms" , "o" ] ;
var value = this . css ( attribute ) ;
if ( value === undefined ) {
prefixes . some ( function ( prefix ) {
value = this . css ( prefix + attribute . substr ( 0 , 1 ) . toUpperCase ( ) + attribute . substr ( 1 ) ) ;
return value !== undefined ;
} , this ) ;
}
return value === undefined ? null : value ;
} ;
2014-02-08 18:52:41 +04:00
NodeContainer . prototype . computedStyle = function ( type ) {
return this . node . ownerDocument . defaultView . getComputedStyle ( this . node , type ) ;
} ;
2014-02-01 20:52:53 +04:00
NodeContainer . prototype . cssInt = function ( attribute ) {
var value = parseInt ( this . css ( attribute ) , 10 ) ;
return ( isNaN ( value ) ) ? 0 : value ; // borders in old IE are throwing 'medium' for demo.html
2014-01-19 20:04:27 +04:00
} ;
Speed & accuracy improvements to pseudo element rendering.
Previously, pseudo elements would be processed as they were found in the DOM tree, which was
an expensive operation as each element's computed :before and :after style was checked for
'content' styles.
This commit traverses the user's stylesheets for :before and :after selectors, gathers the classes
affected, selects all elements that likely have a pseudo element present, then checks computed style.
If there is actually an element present, it is created but *not* appended to the DOM until after
all elements have been processed.
After all elements have been found and created, they are added to the DOM in a single batch, and the original
pseudo elements are hidden in a single batch. This prevents the layout invalidation / relayout loop that was
occuring previously, and in my tests speeds parsing by as much as 50% or more, depending on how many
pseudo elements your page uses.
Additionally, this commit contains a bugfix to the handling of ":before" pseudo elements; the browser effectively
inserts them as the first child of the element, not before the element. This fixes a few rendering inconsistencies
and complicated pages look almost perfect in my tests.
2013-09-18 09:19:39 +04:00
2014-02-01 20:52:53 +04:00
NodeContainer . prototype . cssFloat = function ( attribute ) {
var value = parseFloat ( this . css ( attribute ) ) ;
return ( isNaN ( value ) ) ? 0 : value ;
2014-01-19 20:04:27 +04:00
} ;
2013-09-07 22:24:41 +04:00
2014-02-01 20:52:53 +04:00
NodeContainer . prototype . fontWeight = function ( ) {
var weight = this . css ( "fontWeight" ) ;
switch ( parseInt ( weight , 10 ) ) {
case 401 :
weight = "bold" ;
break ;
case 400 :
weight = "normal" ;
2014-01-26 20:10:04 +04:00
break ;
}
2014-02-01 20:52:53 +04:00
return weight ;
2014-01-19 20:04:27 +04:00
} ;
2013-09-07 22:24:41 +04:00
2014-02-01 20:52:53 +04:00
NodeContainer . prototype . parseBackgroundImages = function ( ) {
2014-02-08 16:07:20 +04:00
return this . backgroundImages || ( this . backgroundImages = parseBackgrounds ( this . css ( "backgroundImage" ) ) ) ;
2014-02-01 20:52:53 +04:00
} ;
2013-09-07 22:24:41 +04:00
2014-02-01 20:52:53 +04:00
NodeContainer . prototype . cssList = function ( property , index ) {
var value = ( this . css ( property ) || '' ) . split ( ',' ) ;
value = value [ index || 0 ] || value [ 0 ] || 'auto' ;
value = value . trim ( ) . split ( ' ' ) ;
if ( value . length === 1 ) {
value = [ value [ 0 ] , value [ 0 ] ] ;
2013-09-07 22:24:41 +04:00
}
2014-02-01 20:52:53 +04:00
return value ;
} ;
2013-09-07 22:24:41 +04:00
2014-02-01 20:52:53 +04:00
NodeContainer . prototype . parseBackgroundSize = function ( bounds , image , index ) {
var size = this . cssList ( "backgroundSize" , index ) ;
var width , height ;
2013-09-07 22:24:41 +04:00
2014-02-01 20:52:53 +04:00
if ( isPercentage ( size [ 0 ] ) ) {
width = bounds . width * parseFloat ( size [ 0 ] ) / 100 ;
} else if ( /contain|cover/ . test ( size [ 0 ] ) ) {
var targetRatio = bounds . width / bounds . height , currentRatio = image . width / image . height ;
return ( targetRatio < currentRatio ^ size [ 0 ] === 'contain' ) ? { width : bounds . height * currentRatio , height : bounds . height } : { width : bounds . width , height : bounds . width / currentRatio } ;
2013-09-07 22:24:41 +04:00
} else {
2014-02-01 20:52:53 +04:00
width = parseInt ( size [ 0 ] , 10 ) ;
2013-09-07 22:24:41 +04:00
}
2014-02-01 20:52:53 +04:00
if ( size [ 0 ] === 'auto' && size [ 1 ] === 'auto' ) {
height = image . height ;
} else if ( size [ 1 ] === 'auto' ) {
height = width / image . width * image . height ;
} else if ( isPercentage ( size [ 1 ] ) ) {
height = bounds . height * parseFloat ( size [ 1 ] ) / 100 ;
} else {
height = parseInt ( size [ 1 ] , 10 ) ;
}
2013-09-07 22:24:41 +04:00
2014-02-01 20:52:53 +04:00
if ( size [ 0 ] === 'auto' ) {
width = height / image . height * image . width ;
}
2013-09-07 22:24:41 +04:00
2014-02-01 20:52:53 +04:00
return { width : width , height : height } ;
} ;
2013-09-07 22:24:41 +04:00
2014-02-01 20:52:53 +04:00
NodeContainer . prototype . parseBackgroundPosition = function ( bounds , image , index , backgroundSize ) {
var position = this . cssList ( 'backgroundPosition' , index ) ;
var left , top ;
2013-09-07 22:24:41 +04:00
2014-02-01 20:52:53 +04:00
if ( isPercentage ( position [ 0 ] ) ) {
left = ( bounds . width - ( backgroundSize || image ) . width ) * ( parseFloat ( position [ 0 ] ) / 100 ) ;
} else {
left = parseInt ( position [ 0 ] , 10 ) ;
}
2013-09-07 22:24:41 +04:00
2014-02-01 20:52:53 +04:00
if ( position [ 1 ] === 'auto' ) {
top = left / image . width * image . height ;
} else if ( isPercentage ( position [ 1 ] ) ) {
top = ( bounds . height - ( backgroundSize || image ) . height ) * parseFloat ( position [ 1 ] ) / 100 ;
} else {
top = parseInt ( position [ 1 ] , 10 ) ;
}
2013-09-07 22:24:41 +04:00
2014-02-01 20:52:53 +04:00
if ( position [ 0 ] === 'auto' ) {
left = top / image . height * image . width ;
}
2013-09-07 22:24:41 +04:00
2014-02-01 20:52:53 +04:00
return { left : left , top : top } ;
} ;
2013-09-07 22:24:41 +04:00
2014-02-01 20:52:53 +04:00
NodeContainer . prototype . parseBackgroundRepeat = function ( index ) {
return this . cssList ( "backgroundRepeat" , index ) [ 0 ] ;
} ;
2013-09-18 05:54:20 +04:00
2014-02-03 21:42:42 +04:00
NodeContainer . prototype . parseTextShadows = function ( ) {
var textShadow = this . css ( "textShadow" ) ;
var results = [ ] ;
if ( textShadow && textShadow !== 'none' ) {
var shadows = textShadow . match ( this . TEXT _SHADOW _PROPERTY ) ;
for ( var i = 0 ; shadows && ( i < shadows . length ) ; i ++ ) {
var s = shadows [ i ] . match ( this . TEXT _SHADOW _VALUES ) ;
results . push ( {
color : s [ 0 ] ,
offsetX : s [ 1 ] ? s [ 1 ] . replace ( 'px' , '' ) : 0 ,
offsetY : s [ 2 ] ? s [ 2 ] . replace ( 'px' , '' ) : 0 ,
blur : s [ 3 ] ? s [ 3 ] . replace ( 'px' , '' ) : 0
} ) ;
}
}
return results ;
} ;
2014-02-23 19:35:46 +04:00
NodeContainer . prototype . parseTransform = function ( ) {
2014-03-02 20:03:30 +04:00
if ( ! this . transformData ) {
if ( this . hasTransform ( ) ) {
var offset = this . parseBounds ( ) ;
var origin = this . prefixedCss ( "transformOrigin" ) . split ( " " ) . map ( removePx ) . map ( asFloat ) ;
origin [ 0 ] += offset . left ;
origin [ 1 ] += offset . top ;
this . transformData = {
origin : origin ,
matrix : this . parseTransformMatrix ( )
} ;
} else {
this . transformData = {
origin : [ 0 , 0 ] ,
matrix : [ 1 , 0 , 0 , 1 , 0 , 0 ]
} ;
}
}
return this . transformData ;
} ;
NodeContainer . prototype . parseTransformMatrix = function ( ) {
if ( ! this . transformMatrix ) {
var transform = this . prefixedCss ( "transform" ) ;
var matrix = transform ? parseMatrix ( transform . match ( this . MATRIX _PROPERTY ) ) : null ;
this . transformMatrix = matrix ? matrix : [ 1 , 0 , 0 , 1 , 0 , 0 ] ;
2014-02-23 19:35:46 +04:00
}
2014-03-02 20:03:30 +04:00
return this . transformMatrix ;
2014-03-02 18:00:59 +04:00
} ;
NodeContainer . prototype . parseBounds = function ( ) {
return this . bounds || ( this . bounds = this . hasTransform ( ) ? offsetBounds ( this . node ) : getBounds ( this . node ) ) ;
} ;
NodeContainer . prototype . hasTransform = function ( ) {
2014-03-02 20:03:30 +04:00
return this . parseTransformMatrix ( ) . join ( "," ) !== "1,0,0,1,0,0" ;
2014-02-23 19:35:46 +04:00
} ;
2014-03-02 21:51:46 +04:00
NodeContainer . prototype . getValue = function ( ) {
var value = this . node . value || "" ;
value = ( this . node . tagName === "SELECT" ) ? selectionValue ( this . node ) : value ;
return value . length === 0 ? ( this . node . placeholder || "" ) : value ;
} ;
2014-03-02 20:03:30 +04:00
NodeContainer . prototype . MATRIX _PROPERTY = /(matrix)\((.+)\)/ ;
2014-02-03 21:42:42 +04:00
NodeContainer . prototype . TEXT _SHADOW _PROPERTY = /((rgba|rgb)\([^\)]+\)(\s-?\d+px){0,})/g ;
NodeContainer . prototype . TEXT _SHADOW _VALUES = /(-?\d+px)|(#.+)|(rgb\(.+\))|(rgba\(.+\))/g ;
2014-03-02 21:51:46 +04:00
function selectionValue ( node ) {
var option = node . options [ node . selectedIndex || 0 ] ;
return option ? ( option . text || "" ) : "" ;
}
2014-02-23 19:35:46 +04:00
function parseMatrix ( match ) {
if ( match && match [ 1 ] === "matrix" ) {
return match [ 2 ] . split ( "," ) . map ( function ( s ) {
return parseFloat ( s . trim ( ) ) ;
} ) ;
}
}
2014-02-01 20:52:53 +04:00
function isPercentage ( value ) {
return value . toString ( ) . indexOf ( "%" ) !== - 1 ;
2014-01-21 23:41:00 +04:00
}
2014-02-08 18:52:41 +04:00
function parseBackgrounds ( backgroundImage ) {
var whitespace = ' \r\n\t' ,
method , definition , prefix , prefix _i , block , results = [ ] ,
mode = 0 , numParen = 0 , quote , args ;
var appendResult = function ( ) {
if ( method ) {
if ( definition . substr ( 0 , 1 ) === '"' ) {
definition = definition . substr ( 1 , definition . length - 2 ) ;
}
if ( definition ) {
args . push ( definition ) ;
}
if ( method . substr ( 0 , 1 ) === '-' && ( prefix _i = method . indexOf ( '-' , 1 ) + 1 ) > 0 ) {
prefix = method . substr ( 0 , prefix _i ) ;
method = method . substr ( prefix _i ) ;
}
results . push ( {
prefix : prefix ,
method : method . toLowerCase ( ) ,
value : block ,
args : args ,
image : null
} ) ;
}
args = [ ] ;
method = prefix = definition = block = '' ;
} ;
args = [ ] ;
method = prefix = definition = block = '' ;
backgroundImage . split ( "" ) . forEach ( function ( c ) {
if ( mode === 0 && whitespace . indexOf ( c ) > - 1 ) {
return ;
}
switch ( c ) {
case '"' :
if ( ! quote ) {
quote = c ;
}
else if ( quote === c ) {
quote = null ;
}
break ;
case '(' :
if ( quote ) {
break ;
} else if ( mode === 0 ) {
mode = 1 ;
block += c ;
return ;
} else {
numParen ++ ;
}
break ;
case ')' :
if ( quote ) {
break ;
} else if ( mode === 1 ) {
if ( numParen === 0 ) {
mode = 0 ;
block += c ;
appendResult ( ) ;
return ;
} else {
numParen -- ;
}
}
break ;
case ',' :
if ( quote ) {
break ;
} else if ( mode === 0 ) {
appendResult ( ) ;
return ;
} else if ( mode === 1 ) {
if ( numParen === 0 && ! method . match ( /^url$/i ) ) {
args . push ( definition ) ;
definition = '' ;
block += c ;
return ;
}
}
break ;
}
block += c ;
if ( mode === 0 ) {
method += c ;
} else {
definition += c ;
}
} ) ;
appendResult ( ) ;
return results ;
}
2014-03-02 18:00:59 +04:00
function removePx ( str ) {
return str . replace ( "px" , "" ) ;
}
function asFloat ( str ) {
return parseFloat ( str ) ;
}
function getBounds ( node ) {
if ( node . getBoundingClientRect ) {
var clientRect = node . getBoundingClientRect ( ) ;
var isBody = node . nodeName === "BODY" ;
var width = isBody ? node . scrollWidth : node . offsetWidth ;
return {
top : clientRect . top ,
bottom : clientRect . bottom || ( clientRect . top + clientRect . height ) ,
right : clientRect . left + width ,
left : clientRect . left ,
width : width ,
height : isBody ? node . scrollHeight : node . offsetHeight
} ;
}
return { } ;
}
function offsetBounds ( node ) {
var parent = node . offsetParent ? offsetBounds ( node . offsetParent ) : { top : 0 , left : 0 } ;
return {
top : node . offsetTop + parent . top ,
bottom : node . offsetTop + node . offsetHeight + parent . top ,
right : node . offsetLeft + parent . left + node . offsetWidth ,
left : node . offsetLeft + parent . left ,
width : node . offsetWidth ,
height : node . offsetHeight
} ;
}
2014-02-01 20:52:53 +04:00
function NodeParser ( element , renderer , support , imageLoader , options ) {
log ( "Starting NodeParser" ) ;
this . renderer = renderer ;
2014-01-21 23:41:00 +04:00
this . options = options ;
2014-02-01 20:52:53 +04:00
this . range = null ;
2014-01-21 23:41:00 +04:00
this . support = support ;
2014-02-15 02:33:09 +04:00
this . renderQueue = [ ] ;
2014-02-01 20:52:53 +04:00
this . stack = new StackingContext ( true , 1 , element . ownerDocument , null ) ;
var parent = new NodeContainer ( element , null ) ;
parent . visibile = parent . isElementVisible ( ) ;
2014-02-08 18:52:41 +04:00
this . createPseudoHideStyles ( element . ownerDocument ) ;
this . nodes = flatten ( [ parent ] . concat ( this . getChildren ( parent ) ) . filter ( function ( container ) {
2014-02-01 20:52:53 +04:00
return container . visible = container . isElementVisible ( ) ;
2014-02-08 18:52:41 +04:00
} ) . map ( this . getPseudoElements , this ) ) ;
2014-02-03 21:42:42 +04:00
this . fontMetrics = new FontMetrics ( ) ;
2014-02-01 20:52:53 +04:00
log ( "Fetched nodes" ) ;
this . images = imageLoader . fetch ( this . nodes . filter ( isElement ) ) ;
log ( "Creating stacking contexts" ) ;
this . createStackingContexts ( ) ;
log ( "Sorting stacking contexts" ) ;
this . sortStackingContexts ( this . stack ) ;
2014-02-03 21:42:42 +04:00
this . ready = this . images . ready . then ( bind ( function ( ) {
2014-02-01 20:52:53 +04:00
log ( "Images loaded, starting parsing" ) ;
this . parse ( this . stack ) ;
2014-02-15 02:33:09 +04:00
log ( "Render queue created with " + this . renderQueue . length + " items" ) ;
return new Promise ( bind ( function ( resolve ) {
if ( ! options . async ) {
this . renderQueue . forEach ( this . paint , this ) ;
resolve ( ) ;
} else if ( typeof ( options . async ) === "function" ) {
options . async . call ( this , this . renderQueue , resolve ) ;
} else {
this . renderIndex = 0 ;
this . asyncRenderer ( this . renderQueue , resolve ) ;
}
} , this ) ) ;
2014-02-01 20:52:53 +04:00
} , this ) ) ;
2014-01-21 23:41:00 +04:00
}
2014-02-15 02:33:09 +04:00
NodeParser . prototype . asyncRenderer = function ( queue , resolve , asyncTimer ) {
asyncTimer = asyncTimer || Date . now ( ) ;
this . paint ( queue [ this . renderIndex ++ ] ) ;
if ( queue . length === this . renderIndex ) {
resolve ( ) ;
} else if ( asyncTimer + 20 > Date . now ( ) ) {
this . asyncRenderer ( queue , resolve , asyncTimer ) ;
} else {
setTimeout ( bind ( function ( ) {
this . asyncRenderer ( queue , resolve ) ;
} , this ) , 0 ) ;
}
} ;
2014-02-08 18:52:41 +04:00
NodeParser . prototype . createPseudoHideStyles = function ( document ) {
var hidePseudoElements = document . createElement ( 'style' ) ;
hidePseudoElements . innerHTML = '.' + this . pseudoHideClass + ':before { content: "" !important; display: none !important; }' +
'.' + this . pseudoHideClass + ':after { content: "" !important; display: none !important; }' ;
document . body . appendChild ( hidePseudoElements ) ;
} ;
NodeParser . prototype . getPseudoElements = function ( container ) {
var nodes = [ [ container ] ] ;
2014-02-08 19:42:40 +04:00
if ( container . node . nodeType === Node . ELEMENT _NODE ) {
2014-02-08 18:52:41 +04:00
var before = this . getPseudoElement ( container , ":before" ) ;
var after = this . getPseudoElement ( container , ":after" ) ;
if ( before ) {
container . node . insertBefore ( before [ 0 ] . node , container . node . firstChild ) ;
nodes . push ( before ) ;
}
if ( after ) {
container . node . appendChild ( after [ 0 ] . node ) ;
nodes . push ( after ) ;
}
if ( before || after ) {
container . node . className += " " + this . pseudoHideClass ;
}
}
return flatten ( nodes ) ;
} ;
2014-02-08 19:42:40 +04:00
function toCamelCase ( str ) {
return str . replace ( /(\-[a-z])/g , function ( match ) {
return match . toUpperCase ( ) . replace ( '-' , '' ) ;
} ) ;
}
2014-02-08 18:52:41 +04:00
NodeParser . prototype . getPseudoElement = function ( container , type ) {
var style = container . computedStyle ( type ) ;
if ( ! style || ! style . content || style . content === "none" || style . content === "-moz-alt-content" || style . display === "none" ) {
return null ;
}
var content = stripQuotes ( style . content ) ;
var isImage = content . substr ( 0 , 3 ) === 'url' ;
var pseudoNode = document . createElement ( isImage ? 'img' : 'html2canvaspseudoelement' ) ;
var pseudoContainer = new NodeContainer ( pseudoNode , container ) ;
2014-02-08 19:42:40 +04:00
for ( var i = style . length - 1 ; i >= 0 ; i -- ) {
var property = toCamelCase ( style . item ( i ) ) ;
pseudoNode . style [ property ] = style [ property ] ;
}
2014-02-08 18:52:41 +04:00
pseudoNode . className = this . pseudoHideClass ;
if ( isImage ) {
pseudoNode . src = parseBackgrounds ( content ) [ 0 ] . args [ 0 ] ;
return [ pseudoContainer ] ;
} else {
var text = document . createTextNode ( content ) ;
pseudoNode . appendChild ( text ) ;
return [ pseudoContainer , new TextContainer ( text , pseudoContainer ) ] ;
}
} ;
2014-02-01 20:52:53 +04:00
NodeParser . prototype . getChildren = function ( parentContainer ) {
return flatten ( [ ] . filter . call ( parentContainer . node . childNodes , renderableNode ) . map ( function ( node ) {
var container = [ node . nodeType === Node . TEXT _NODE ? new TextContainer ( node , parentContainer ) : new NodeContainer ( node , parentContainer ) ] . filter ( nonIgnoredElement ) ;
2014-03-02 21:51:46 +04:00
return node . nodeType === Node . ELEMENT _NODE && container . length && node . tagName !== "TEXTAREA" ? ( container [ 0 ] . isElementVisible ( ) ? container . concat ( this . getChildren ( container [ 0 ] ) ) : [ ] ) : container ;
2014-02-01 20:52:53 +04:00
} , this ) ) ;
2014-01-26 20:10:04 +04:00
} ;
2014-02-01 20:52:53 +04:00
NodeParser . prototype . newStackingContext = function ( container , hasOwnStacking ) {
var stack = new StackingContext ( hasOwnStacking , container . cssFloat ( 'opacity' ) , container . node , container . parent ) ;
stack . visible = container . visible ;
2014-02-15 02:33:09 +04:00
var parentStack = hasOwnStacking ? stack . getParentStack ( this ) : stack . parent . stack ;
2014-02-01 20:52:53 +04:00
parentStack . contexts . push ( stack ) ;
container . stack = stack ;
2014-01-26 20:10:04 +04:00
} ;
2014-02-01 20:52:53 +04:00
NodeParser . prototype . createStackingContexts = function ( ) {
this . nodes . forEach ( function ( container ) {
2014-03-02 18:00:59 +04:00
if ( isElement ( container ) && ( this . isRootElement ( container ) || hasOpacity ( container ) || isPositionedForStacking ( container ) || this . isBodyWithTransparentRoot ( container ) || container . hasTransform ( ) ) ) {
2014-02-01 20:52:53 +04:00
this . newStackingContext ( container , true ) ;
2014-02-15 02:33:09 +04:00
} else if ( isElement ( container ) && ( ( isPositioned ( container ) && zIndex0 ( container ) ) || isInlineBlock ( container ) || isFloating ( container ) ) ) {
2014-02-01 20:52:53 +04:00
this . newStackingContext ( container , false ) ;
} else {
container . assignStack ( container . parent . stack ) ;
2014-01-26 20:10:04 +04:00
}
2014-02-01 20:52:53 +04:00
} , this ) ;
2014-01-21 23:41:00 +04:00
} ;
2014-02-01 20:52:53 +04:00
NodeParser . prototype . isBodyWithTransparentRoot = function ( container ) {
return container . node . nodeName === "BODY" && this . renderer . isTransparent ( container . parent . css ( 'backgroundColor' ) ) ;
2014-01-21 23:41:00 +04:00
} ;
2014-02-01 20:52:53 +04:00
NodeParser . prototype . isRootElement = function ( container ) {
2014-02-23 19:02:49 +04:00
return container . parent === null ;
2014-01-21 23:41:00 +04:00
} ;
2014-02-01 20:52:53 +04:00
NodeParser . prototype . sortStackingContexts = function ( stack ) {
stack . contexts . sort ( zIndexSort ) ;
stack . contexts . forEach ( this . sortStackingContexts , this ) ;
} ;
NodeParser . prototype . parseTextBounds = function ( container ) {
return function ( text , index , textList ) {
2014-03-02 18:00:59 +04:00
if ( container . parent . css ( "textDecoration" ) . substr ( 0 , 4 ) !== "none" || text . trim ( ) . length !== 0 ) {
if ( this . support . rangeBounds && ! container . parent . hasTransform ( ) ) {
2014-02-03 21:42:42 +04:00
var offset = textList . slice ( 0 , index ) . join ( "" ) . length ;
2014-02-01 20:52:53 +04:00
return this . getRangeBounds ( container . node , offset , text . length ) ;
} else if ( container . node && typeof ( container . node . data ) === "string" ) {
var replacementNode = container . node . splitText ( text . length ) ;
2014-03-02 18:00:59 +04:00
var bounds = this . getWrapperBounds ( container . node , container . parent . hasTransform ( ) ) ;
2014-02-01 20:52:53 +04:00
container . node = replacementNode ;
return bounds ;
}
2014-03-02 18:00:59 +04:00
} else if ( ! this . support . rangeBounds || container . parent . hasTransform ( ) ) {
2014-02-03 21:42:42 +04:00
container . node = container . node . splitText ( text . length ) ;
2014-02-01 20:52:53 +04:00
}
2014-02-03 21:42:42 +04:00
return { } ;
2014-02-01 20:52:53 +04:00
} ;
2014-01-21 23:41:00 +04:00
} ;
2014-03-02 18:00:59 +04:00
NodeParser . prototype . getWrapperBounds = function ( node , transform ) {
var wrapper = node . ownerDocument . createElement ( 'html2canvaswrapper' ) ;
2014-02-01 20:52:53 +04:00
var parent = node . parentNode ,
backupText = node . cloneNode ( true ) ;
wrapper . appendChild ( node . cloneNode ( true ) ) ;
parent . replaceChild ( wrapper , node ) ;
2014-03-02 18:00:59 +04:00
var bounds = transform ? offsetBounds ( wrapper ) : getBounds ( wrapper ) ;
2014-02-01 20:52:53 +04:00
parent . replaceChild ( backupText , wrapper ) ;
return bounds ;
2014-01-21 23:41:00 +04:00
} ;
2014-02-01 20:52:53 +04:00
NodeParser . prototype . getRangeBounds = function ( node , offset , length ) {
var range = this . range || ( this . range = node . ownerDocument . createRange ( ) ) ;
range . setStart ( node , offset ) ;
range . setEnd ( node , offset + length ) ;
return range . getBoundingClientRect ( ) ;
2014-01-21 23:41:00 +04:00
} ;
2014-03-02 18:00:59 +04:00
function ClearTransform ( ) { }
2014-02-01 20:52:53 +04:00
NodeParser . prototype . parse = function ( stack ) {
// http://www.w3.org/TR/CSS21/visuren.html#z-index
var negativeZindex = stack . contexts . filter ( negativeZIndex ) ; // 2. the child stacking contexts with negative stack levels (most negative first).
var descendantElements = stack . children . filter ( isElement ) ;
var descendantNonFloats = descendantElements . filter ( not ( isFloating ) ) ;
var nonInlineNonPositionedDescendants = descendantNonFloats . filter ( not ( isPositioned ) ) . filter ( not ( inlineLevel ) ) ; // 3 the in-flow, non-inline-level, non-positioned descendants.
var nonPositionedFloats = descendantElements . filter ( not ( isPositioned ) ) . filter ( isFloating ) ; // 4. the non-positioned floats.
var inFlow = descendantNonFloats . filter ( not ( isPositioned ) ) . filter ( inlineLevel ) ; // 5. the in-flow, inline-level, non-positioned descendants, including inline tables and inline blocks.
var stackLevel0 = stack . contexts . concat ( descendantNonFloats . filter ( isPositioned ) ) . filter ( zIndex0 ) ; // 6. the child stacking contexts with stack level 0 and the positioned descendants with stack level 0.
var text = stack . children . filter ( isTextNode ) . filter ( hasText ) ;
var positiveZindex = stack . contexts . filter ( positiveZIndex ) ; // 7. the child stacking contexts with positive stack levels (least positive first).
negativeZindex . concat ( nonInlineNonPositionedDescendants ) . concat ( nonPositionedFloats )
. concat ( inFlow ) . concat ( stackLevel0 ) . concat ( text ) . concat ( positiveZindex ) . forEach ( function ( container ) {
2014-02-15 02:33:09 +04:00
this . renderQueue . push ( container ) ;
2014-02-01 20:52:53 +04:00
if ( isStackingContext ( container ) ) {
this . parse ( container ) ;
2014-03-02 18:00:59 +04:00
this . renderQueue . push ( new ClearTransform ( ) ) ;
2014-02-01 20:52:53 +04:00
}
} , this ) ;
2014-01-21 23:41:00 +04:00
} ;
2014-02-01 20:52:53 +04:00
NodeParser . prototype . paint = function ( container ) {
try {
2014-03-02 18:00:59 +04:00
if ( container instanceof ClearTransform ) {
this . renderer . ctx . restore ( ) ;
} else if ( isTextNode ( container ) ) {
2014-02-01 20:52:53 +04:00
this . paintText ( container ) ;
} else {
this . paintNode ( container ) ;
}
} catch ( e ) {
log ( e ) ;
}
2014-01-21 23:41:00 +04:00
} ;
2014-02-01 20:52:53 +04:00
NodeParser . prototype . paintNode = function ( container ) {
if ( isStackingContext ( container ) ) {
this . renderer . setOpacity ( container . opacity ) ;
2014-03-02 18:00:59 +04:00
this . renderer . ctx . save ( ) ;
if ( container . hasTransform ( ) ) {
this . renderer . setTransform ( container . parseTransform ( ) ) ;
2014-02-23 19:35:46 +04:00
}
2014-02-01 20:52:53 +04:00
}
2014-03-02 18:00:59 +04:00
var bounds = container . parseBounds ( ) ;
2014-02-01 20:52:53 +04:00
var borderData = this . parseBorders ( container ) ;
this . renderer . clip ( borderData . clip , function ( ) {
2014-02-15 02:33:09 +04:00
this . renderer . renderBackground ( container , bounds , borderData . borders . map ( getWidth ) ) ;
2014-02-01 20:52:53 +04:00
} , this ) ;
this . renderer . renderBorders ( borderData . borders ) ;
2014-01-26 20:10:04 +04:00
2014-02-01 20:52:53 +04:00
switch ( container . node . nodeName ) {
case "IMG" :
var imageContainer = this . images . get ( container . node . src ) ;
if ( imageContainer ) {
this . renderer . renderImage ( container , bounds , borderData , imageContainer . image ) ;
} else {
log ( "Error loading <img>" , container . node . src ) ;
}
break ;
2014-03-02 21:51:46 +04:00
case "SELECT" :
case "INPUT" :
case "TEXTAREA" :
this . paintFormValue ( container ) ;
break ;
}
} ;
NodeParser . prototype . paintFormValue = function ( container ) {
if ( container . getValue ( ) . length > 0 ) {
var document = container . node . ownerDocument ;
var wrapper = document . createElement ( 'html2canvaswrapper' ) ;
var properties = [ 'lineHeight' , 'textAlign' , 'fontFamily' , 'fontWeight' , 'fontSize' , 'color' ,
'paddingLeft' , 'paddingTop' , 'paddingRight' , 'paddingBottom' ,
'width' , 'height' , 'borderLeftStyle' , 'borderTopStyle' , 'borderLeftWidth' , 'borderTopWidth' ,
'boxSizing' , 'whiteSpace' , 'wordWrap' ] ;
properties . forEach ( function ( property ) {
try {
wrapper . style [ property ] = container . css ( property ) ;
} catch ( e ) {
// Older IE has issues with "border"
log ( "html2canvas: Parse: Exception caught in renderFormValue: " + e . message ) ;
}
} ) ;
var bounds = container . parseBounds ( ) ;
wrapper . style . position = "absolute" ;
wrapper . style . left = bounds . left + "px" ;
wrapper . style . top = bounds . top + "px" ;
wrapper . textContent = container . getValue ( ) ;
document . body . appendChild ( wrapper ) ;
this . paintText ( new TextContainer ( wrapper . firstChild , container ) ) ;
document . body . removeChild ( wrapper ) ;
2014-01-21 23:41:00 +04:00
}
2014-02-01 20:52:53 +04:00
} ;
2014-01-21 23:41:00 +04:00
2014-02-01 20:52:53 +04:00
NodeParser . prototype . paintText = function ( container ) {
container . applyTextTransform ( ) ;
var textList = container . node . data . split ( ! this . options . letterRendering || noLetterSpacing ( container ) ? /(\b| )/ : "" ) ;
var weight = container . parent . fontWeight ( ) ;
var size = container . parent . css ( 'fontSize' ) ;
var family = container . parent . css ( 'fontFamily' ) ;
2014-02-03 21:42:42 +04:00
var shadows = container . parent . parseTextShadows ( ) ;
2014-02-01 20:52:53 +04:00
this . renderer . font ( container . parent . css ( 'color' ) , container . parent . css ( 'fontStyle' ) , container . parent . css ( 'fontVariant' ) , weight , size , family ) ;
2014-02-03 21:42:42 +04:00
if ( shadows . length ) {
// TODO: support multiple text shadows
this . renderer . fontShadow ( shadows [ 0 ] . color , shadows [ 0 ] . offsetX , shadows [ 0 ] . offsetY , shadows [ 0 ] . blur ) ;
} else {
this . renderer . clearShadow ( ) ;
}
2014-01-21 23:41:00 +04:00
2014-02-01 20:52:53 +04:00
textList . map ( this . parseTextBounds ( container ) , this ) . forEach ( function ( bounds , index ) {
if ( bounds ) {
this . renderer . text ( textList [ index ] , bounds . left , bounds . bottom ) ;
2014-02-03 21:42:42 +04:00
this . renderTextDecoration ( container . parent , bounds , this . fontMetrics . getMetrics ( family , size ) ) ;
2014-02-01 20:52:53 +04:00
}
} , this ) ;
2014-01-21 23:41:00 +04:00
} ;
2014-02-03 21:42:42 +04:00
NodeParser . prototype . renderTextDecoration = function ( container , bounds , metrics ) {
switch ( container . css ( "textDecoration" ) . split ( " " ) [ 0 ] ) {
case "underline" :
// Draws a line at the baseline of the font
// TODO As some browsers display the line as more than 1px if the font-size is big, need to take that into account both in position and size
this . renderer . rectangle ( bounds . left , Math . round ( bounds . top + metrics . baseline + metrics . lineWidth ) , bounds . width , 1 , container . css ( "color" ) ) ;
break ;
case "overline" :
this . renderer . rectangle ( bounds . left , Math . round ( bounds . top ) , bounds . width , 1 , container . css ( "color" ) ) ;
break ;
case "line-through" :
// TODO try and find exact position for line-through
this . renderer . rectangle ( bounds . left , Math . ceil ( bounds . top + metrics . middle + metrics . lineWidth ) , bounds . width , 1 , container . css ( "color" ) ) ;
break ;
}
} ;
2014-02-01 20:52:53 +04:00
NodeParser . prototype . parseBorders = function ( container ) {
var nodeBounds = container . bounds ;
var radius = getBorderRadiusData ( container ) ;
var borders = [ "Top" , "Right" , "Bottom" , "Left" ] . map ( function ( side ) {
return {
width : container . cssInt ( 'border' + side + 'Width' ) ,
color : container . css ( 'border' + side + 'Color' ) ,
args : null
} ;
} ) ;
var borderPoints = calculateCurvePoints ( nodeBounds , radius , borders ) ;
2014-01-21 23:41:00 +04:00
2014-02-01 20:52:53 +04:00
return {
clip : this . parseBackgroundClip ( container , borderPoints , borders , radius , nodeBounds ) ,
borders : borders . map ( function ( border , borderSide ) {
if ( border . width > 0 ) {
var bx = nodeBounds . left ;
var by = nodeBounds . top ;
var bw = nodeBounds . width ;
var bh = nodeBounds . height - ( borders [ 2 ] . width ) ;
2014-01-21 23:41:00 +04:00
2014-02-01 20:52:53 +04:00
switch ( borderSide ) {
case 0 :
// top border
bh = borders [ 0 ] . width ;
border . args = drawSide ( {
c1 : [ bx , by ] ,
c2 : [ bx + bw , by ] ,
c3 : [ bx + bw - borders [ 1 ] . width , by + bh ] ,
c4 : [ bx + borders [ 3 ] . width , by + bh ]
} , radius [ 0 ] , radius [ 1 ] ,
borderPoints . topLeftOuter , borderPoints . topLeftInner , borderPoints . topRightOuter , borderPoints . topRightInner ) ;
break ;
case 1 :
// right border
bx = nodeBounds . left + nodeBounds . width - ( borders [ 1 ] . width ) ;
bw = borders [ 1 ] . width ;
2014-01-21 23:41:00 +04:00
2014-02-01 20:52:53 +04:00
border . args = drawSide ( {
c1 : [ bx + bw , by ] ,
c2 : [ bx + bw , by + bh + borders [ 2 ] . width ] ,
c3 : [ bx , by + bh ] ,
c4 : [ bx , by + borders [ 0 ] . width ]
} , radius [ 1 ] , radius [ 2 ] ,
borderPoints . topRightOuter , borderPoints . topRightInner , borderPoints . bottomRightOuter , borderPoints . bottomRightInner ) ;
break ;
case 2 :
// bottom border
by = ( by + nodeBounds . height ) - ( borders [ 2 ] . width ) ;
bh = borders [ 2 ] . width ;
border . args = drawSide ( {
c1 : [ bx + bw , by + bh ] ,
c2 : [ bx , by + bh ] ,
c3 : [ bx + borders [ 3 ] . width , by ] ,
c4 : [ bx + bw - borders [ 3 ] . width , by ]
} , radius [ 2 ] , radius [ 3 ] ,
borderPoints . bottomRightOuter , borderPoints . bottomRightInner , borderPoints . bottomLeftOuter , borderPoints . bottomLeftInner ) ;
break ;
case 3 :
// left border
bw = borders [ 3 ] . width ;
border . args = drawSide ( {
c1 : [ bx , by + bh + borders [ 2 ] . width ] ,
c2 : [ bx , by ] ,
c3 : [ bx + bw , by + borders [ 0 ] . width ] ,
c4 : [ bx + bw , by + bh ]
} , radius [ 3 ] , radius [ 0 ] ,
borderPoints . bottomLeftOuter , borderPoints . bottomLeftInner , borderPoints . topLeftOuter , borderPoints . topLeftInner ) ;
break ;
}
}
return border ;
} )
} ;
2014-01-21 23:41:00 +04:00
} ;
2014-02-01 20:52:53 +04:00
NodeParser . prototype . parseBackgroundClip = function ( container , borderPoints , borders , radius , bounds ) {
var backgroundClip = container . css ( 'backgroundClip' ) ,
borderArgs = [ ] ;
2014-01-21 23:41:00 +04:00
2014-02-01 20:52:53 +04:00
switch ( backgroundClip ) {
case "content-box" :
case "padding-box" :
parseCorner ( borderArgs , radius [ 0 ] , radius [ 1 ] , borderPoints . topLeftInner , borderPoints . topRightInner , bounds . left + borders [ 3 ] . width , bounds . top + borders [ 0 ] . width ) ;
parseCorner ( borderArgs , radius [ 1 ] , radius [ 2 ] , borderPoints . topRightInner , borderPoints . bottomRightInner , bounds . left + bounds . width - borders [ 1 ] . width , bounds . top + borders [ 0 ] . width ) ;
parseCorner ( borderArgs , radius [ 2 ] , radius [ 3 ] , borderPoints . bottomRightInner , borderPoints . bottomLeftInner , bounds . left + bounds . width - borders [ 1 ] . width , bounds . top + bounds . height - borders [ 2 ] . width ) ;
parseCorner ( borderArgs , radius [ 3 ] , radius [ 0 ] , borderPoints . bottomLeftInner , borderPoints . topLeftInner , bounds . left + borders [ 3 ] . width , bounds . top + bounds . height - borders [ 2 ] . width ) ;
2014-01-21 23:41:00 +04:00
break ;
2014-02-01 20:52:53 +04:00
default :
parseCorner ( borderArgs , radius [ 0 ] , radius [ 1 ] , borderPoints . topLeftOuter , borderPoints . topRightOuter , bounds . left , bounds . top ) ;
parseCorner ( borderArgs , radius [ 1 ] , radius [ 2 ] , borderPoints . topRightOuter , borderPoints . bottomRightOuter , bounds . left + bounds . width , bounds . top ) ;
parseCorner ( borderArgs , radius [ 2 ] , radius [ 3 ] , borderPoints . bottomRightOuter , borderPoints . bottomLeftOuter , bounds . left + bounds . width , bounds . top + bounds . height ) ;
parseCorner ( borderArgs , radius [ 3 ] , radius [ 0 ] , borderPoints . bottomLeftOuter , borderPoints . topLeftOuter , bounds . left , bounds . top + bounds . height ) ;
2014-01-21 23:41:00 +04:00
break ;
}
2014-02-01 20:52:53 +04:00
return borderArgs ;
2014-01-21 23:41:00 +04:00
} ;
2014-02-08 18:52:41 +04:00
NodeParser . prototype . pseudoHideClass = "___html2canvas___pseudoelement" ;
2014-02-01 20:52:53 +04:00
function getCurvePoints ( x , y , r1 , r2 ) {
var kappa = 4 * ( ( Math . sqrt ( 2 ) - 1 ) / 3 ) ;
var ox = ( r1 ) * kappa , // control point offset horizontal
oy = ( r2 ) * kappa , // control point offset vertical
xm = x + r1 , // x-middle
ym = y + r2 ; // y-middle
return {
topLeft : bezierCurve ( { x : x , y : ym } , { x : x , y : ym - oy } , { x : xm - ox , y : y } , { x : xm , y : y } ) ,
topRight : bezierCurve ( { x : x , y : y } , { x : x + ox , y : y } , { x : xm , y : ym - oy } , { x : xm , y : ym } ) ,
bottomRight : bezierCurve ( { x : xm , y : y } , { x : xm , y : y + oy } , { x : x + ox , y : ym } , { x : x , y : ym } ) ,
bottomLeft : bezierCurve ( { x : xm , y : ym } , { x : xm - ox , y : ym } , { x : x , y : y + oy } , { x : x , y : y } )
2014-01-21 23:41:00 +04:00
} ;
2014-02-01 20:52:53 +04:00
}
function calculateCurvePoints ( bounds , borderRadius , borders ) {
var x = bounds . left ,
y = bounds . top ,
width = bounds . width ,
height = bounds . height ,
2014-01-21 23:41:00 +04:00
2014-02-01 20:52:53 +04:00
tlh = borderRadius [ 0 ] [ 0 ] ,
tlv = borderRadius [ 0 ] [ 1 ] ,
trh = borderRadius [ 1 ] [ 0 ] ,
trv = borderRadius [ 1 ] [ 1 ] ,
brh = borderRadius [ 2 ] [ 0 ] ,
brv = borderRadius [ 2 ] [ 1 ] ,
blh = borderRadius [ 3 ] [ 0 ] ,
blv = borderRadius [ 3 ] [ 1 ] ;
2014-01-21 23:41:00 +04:00
2014-02-01 20:52:53 +04:00
var topWidth = width - trh ,
rightHeight = height - brv ,
bottomWidth = width - brh ,
leftHeight = height - blv ;
2014-01-21 23:41:00 +04:00
2014-02-01 20:52:53 +04:00
return {
topLeftOuter : getCurvePoints ( x , y , tlh , tlv ) . topLeft . subdivide ( 0.5 ) ,
topLeftInner : getCurvePoints ( x + borders [ 3 ] . width , y + borders [ 0 ] . width , Math . max ( 0 , tlh - borders [ 3 ] . width ) , Math . max ( 0 , tlv - borders [ 0 ] . width ) ) . topLeft . subdivide ( 0.5 ) ,
topRightOuter : getCurvePoints ( x + topWidth , y , trh , trv ) . topRight . subdivide ( 0.5 ) ,
topRightInner : getCurvePoints ( x + Math . min ( topWidth , width + borders [ 3 ] . width ) , y + borders [ 0 ] . width , ( topWidth > width + borders [ 3 ] . width ) ? 0 : trh - borders [ 3 ] . width , trv - borders [ 0 ] . width ) . topRight . subdivide ( 0.5 ) ,
bottomRightOuter : getCurvePoints ( x + bottomWidth , y + rightHeight , brh , brv ) . bottomRight . subdivide ( 0.5 ) ,
bottomRightInner : getCurvePoints ( x + Math . min ( bottomWidth , width + borders [ 3 ] . width ) , y + Math . min ( rightHeight , height + borders [ 0 ] . width ) , Math . max ( 0 , brh - borders [ 1 ] . width ) , Math . max ( 0 , brv - borders [ 2 ] . width ) ) . bottomRight . subdivide ( 0.5 ) ,
bottomLeftOuter : getCurvePoints ( x , y + leftHeight , blh , blv ) . bottomLeft . subdivide ( 0.5 ) ,
bottomLeftInner : getCurvePoints ( x + borders [ 3 ] . width , y + leftHeight , Math . max ( 0 , blh - borders [ 3 ] . width ) , Math . max ( 0 , blv - borders [ 2 ] . width ) ) . bottomLeft . subdivide ( 0.5 )
} ;
}
2014-01-21 23:41:00 +04:00
2014-02-01 20:52:53 +04:00
function bezierCurve ( start , startControl , endControl , end ) {
var lerp = function ( a , b , t ) {
return {
x : a . x + ( b . x - a . x ) * t ,
y : a . y + ( b . y - a . y ) * t
} ;
} ;
2014-01-21 23:41:00 +04:00
2014-02-01 20:52:53 +04:00
return {
start : start ,
startControl : startControl ,
endControl : endControl ,
end : end ,
subdivide : function ( t ) {
var ab = lerp ( start , startControl , t ) ,
bc = lerp ( startControl , endControl , t ) ,
cd = lerp ( endControl , end , t ) ,
abbc = lerp ( ab , bc , t ) ,
bccd = lerp ( bc , cd , t ) ,
dest = lerp ( abbc , bccd , t ) ;
return [ bezierCurve ( start , ab , abbc , dest ) , bezierCurve ( dest , bccd , cd , end ) ] ;
} ,
curveTo : function ( borderArgs ) {
borderArgs . push ( [ "bezierCurve" , startControl . x , startControl . y , endControl . x , endControl . y , end . x , end . y ] ) ;
} ,
curveToReversed : function ( borderArgs ) {
borderArgs . push ( [ "bezierCurve" , endControl . x , endControl . y , startControl . x , startControl . y , start . x , start . y ] ) ;
}
} ;
}
2014-01-21 23:41:00 +04:00
2014-02-01 20:52:53 +04:00
function drawSide ( borderData , radius1 , radius2 , outer1 , inner1 , outer2 , inner2 ) {
var borderArgs = [ ] ;
2014-01-21 23:41:00 +04:00
2014-02-01 20:52:53 +04:00
if ( radius1 [ 0 ] > 0 || radius1 [ 1 ] > 0 ) {
borderArgs . push ( [ "line" , outer1 [ 1 ] . start . x , outer1 [ 1 ] . start . y ] ) ;
outer1 [ 1 ] . curveTo ( borderArgs ) ;
2014-01-21 23:41:00 +04:00
} else {
2014-02-01 20:52:53 +04:00
borderArgs . push ( [ "line" , borderData . c1 [ 0 ] , borderData . c1 [ 1 ] ] ) ;
2014-01-21 23:41:00 +04:00
}
2014-02-01 20:52:53 +04:00
if ( radius2 [ 0 ] > 0 || radius2 [ 1 ] > 0 ) {
borderArgs . push ( [ "line" , outer2 [ 0 ] . start . x , outer2 [ 0 ] . start . y ] ) ;
outer2 [ 0 ] . curveTo ( borderArgs ) ;
borderArgs . push ( [ "line" , inner2 [ 0 ] . end . x , inner2 [ 0 ] . end . y ] ) ;
inner2 [ 0 ] . curveToReversed ( borderArgs ) ;
2014-01-21 23:41:00 +04:00
} else {
2014-02-01 20:52:53 +04:00
borderArgs . push ( [ "line" , borderData . c2 [ 0 ] , borderData . c2 [ 1 ] ] ) ;
borderArgs . push ( [ "line" , borderData . c3 [ 0 ] , borderData . c3 [ 1 ] ] ) ;
2014-01-21 23:41:00 +04:00
}
2014-02-01 20:52:53 +04:00
if ( radius1 [ 0 ] > 0 || radius1 [ 1 ] > 0 ) {
borderArgs . push ( [ "line" , inner1 [ 1 ] . end . x , inner1 [ 1 ] . end . y ] ) ;
inner1 [ 1 ] . curveToReversed ( borderArgs ) ;
} else {
borderArgs . push ( [ "line" , borderData . c4 [ 0 ] , borderData . c4 [ 1 ] ] ) ;
2014-01-21 23:41:00 +04:00
}
2014-02-01 20:52:53 +04:00
return borderArgs ;
}
2014-01-21 23:41:00 +04:00
2014-02-01 20:52:53 +04:00
function parseCorner ( borderArgs , radius1 , radius2 , corner1 , corner2 , x , y ) {
if ( radius1 [ 0 ] > 0 || radius1 [ 1 ] > 0 ) {
borderArgs . push ( [ "line" , corner1 [ 0 ] . start . x , corner1 [ 0 ] . start . y ] ) ;
corner1 [ 0 ] . curveTo ( borderArgs ) ;
corner1 [ 1 ] . curveTo ( borderArgs ) ;
2014-01-21 23:41:00 +04:00
} else {
2014-02-01 20:52:53 +04:00
borderArgs . push ( [ "line" , x , y ] ) ;
2014-01-21 23:41:00 +04:00
}
2014-02-01 20:52:53 +04:00
if ( radius2 [ 0 ] > 0 || radius2 [ 1 ] > 0 ) {
borderArgs . push ( [ "line" , corner2 [ 0 ] . start . x , corner2 [ 0 ] . start . y ] ) ;
2014-01-21 23:41:00 +04:00
}
2014-02-01 20:52:53 +04:00
}
2014-01-21 23:41:00 +04:00
2014-02-01 20:52:53 +04:00
function negativeZIndex ( container ) {
return container . cssInt ( "zIndex" ) < 0 ;
}
2014-01-21 23:41:00 +04:00
2014-02-01 20:52:53 +04:00
function positiveZIndex ( container ) {
return container . cssInt ( "zIndex" ) > 0 ;
}
2014-01-21 23:41:00 +04:00
2014-02-01 20:52:53 +04:00
function zIndex0 ( container ) {
return container . cssInt ( "zIndex" ) === 0 ;
}
2014-01-21 23:41:00 +04:00
2014-02-01 20:52:53 +04:00
function inlineLevel ( container ) {
return [ "inline" , "inline-block" , "inline-table" ] . indexOf ( container . css ( "display" ) ) !== - 1 ;
}
function isStackingContext ( container ) {
return ( container instanceof StackingContext ) ;
}
function hasText ( container ) {
return container . node . data . trim ( ) . length > 0 ;
}
function noLetterSpacing ( container ) {
return ( /^(normal|none|0px)$/ . test ( container . parent . css ( "letterSpacing" ) ) ) ;
}
function getBorderRadiusData ( container ) {
return [ "TopLeft" , "TopRight" , "BottomRight" , "BottomLeft" ] . map ( function ( side ) {
var value = container . css ( 'border' + side + 'Radius' ) ;
var arr = value . split ( " " ) ;
if ( arr . length <= 1 ) {
arr [ 1 ] = arr [ 0 ] ;
}
return arr . map ( asInt ) ;
} ) ;
}
function renderableNode ( node ) {
return ( node . nodeType === Node . TEXT _NODE || node . nodeType === Node . ELEMENT _NODE ) ;
}
function isPositionedForStacking ( container ) {
var position = container . css ( "position" ) ;
var zIndex = ( position === "absolute" || position === "relative" ) ? container . css ( "zIndex" ) : "auto" ;
return zIndex !== "auto" ;
}
function isPositioned ( container ) {
return container . css ( "position" ) !== "static" ;
}
function isFloating ( container ) {
return container . css ( "float" ) !== "none" ;
}
2014-02-15 02:33:09 +04:00
function isInlineBlock ( container ) {
return [ "inline-block" , "inline-table" ] . indexOf ( container . css ( "display" ) ) !== - 1 ;
}
2014-02-01 20:52:53 +04:00
function not ( callback ) {
var context = this ;
return function ( ) {
return ! callback . apply ( context , arguments ) ;
} ;
}
function isElement ( container ) {
return container . node . nodeType === Node . ELEMENT _NODE ;
}
function isTextNode ( container ) {
return container . node . nodeType === Node . TEXT _NODE ;
}
function zIndexSort ( a , b ) {
return a . cssInt ( "zIndex" ) - b . cssInt ( "zIndex" ) ;
}
function hasOpacity ( container ) {
return container . css ( "opacity" ) < 1 ;
}
function bind ( callback , context ) {
return function ( ) {
return callback . apply ( context , arguments ) ;
} ;
}
function asInt ( value ) {
return parseInt ( value , 10 ) ;
}
2014-02-15 02:33:09 +04:00
function getWidth ( border ) {
return border . width ;
}
2014-02-01 20:52:53 +04:00
function nonIgnoredElement ( nodeContainer ) {
2014-03-02 21:51:46 +04:00
return ( nodeContainer . node . nodeType !== Node . ELEMENT _NODE || [ "SCRIPT" , "HEAD" , "TITLE" , "OBJECT" , "BR" , "OPTION" ] . indexOf ( nodeContainer . node . nodeName ) === - 1 ) ;
2014-02-01 20:52:53 +04:00
}
function flatten ( arrays ) {
return [ ] . concat . apply ( [ ] , arrays ) ;
2014-01-21 23:41:00 +04:00
}
2014-02-08 18:52:41 +04:00
function stripQuotes ( content ) {
var first = content . substr ( 0 , 1 ) ;
return ( first === content . substr ( content . length - 1 ) && first . match ( /'|"/ ) ) ? content . substr ( 1 , content . length - 2 ) : content ;
}
2014-01-26 18:06:16 +04:00
/ *
Copyright ( c ) 2013 Yehuda Katz , Tom Dale , and contributors
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 .
* /
! function ( ) { var a , b , c , d ; ! function ( ) { var e = { } , f = { } ; a = function ( a , b , c ) { e [ a ] = { deps : b , callback : c } } , d = c = b = function ( a ) { function c ( b ) { if ( "." !== b . charAt ( 0 ) ) return b ; for ( var c = b . split ( "/" ) , d = a . split ( "/" ) . slice ( 0 , - 1 ) , e = 0 , f = c . length ; f > e ; e ++ ) { var g = c [ e ] ; if ( ".." === g ) d . pop ( ) ; else { if ( "." === g ) continue ; d . push ( g ) } } return d . join ( "/" ) } if ( d . _eak _seen = e , f [ a ] ) return f [ a ] ; if ( f [ a ] = { } , ! e [ a ] ) throw new Error ( "Could not find module " + a ) ; for ( var g , h = e [ a ] , i = h . deps , j = h . callback , k = [ ] , l = 0 , m = i . length ; m > l ; l ++ ) "exports" === i [ l ] ? k . push ( g = { } ) : k . push ( b ( c ( i [ l ] ) ) ) ; var n = j . apply ( this , k ) ; return f [ a ] = g || n } } ( ) , a ( "promise/all" , [ "./utils" , "exports" ] , function ( a , b ) { "use strict" ; function c ( a ) { var b = this ; if ( ! d ( a ) ) throw new TypeError ( "You must pass an array to all." ) ; return new b ( function ( b , c ) { function d ( a ) { return function ( b ) { f ( a , b ) } } function f ( a , c ) { h [ a ] = c , 0 === -- i && b ( h ) } var g , h = [ ] , i = a . length ; 0 === i && b ( [ ] ) ; for ( var j = 0 ; j < a . length ; j ++ ) g = a [ j ] , g && e ( g . then ) ? g . then ( d ( j ) , c ) : f ( j , g ) } ) } var d = a . isArray , e = a . isFunction ; b . all = c } ) , a ( "promise/asap" , [ "exports" ] , function ( a ) { "use strict" ; function b ( ) { return function ( ) { process . nextTick ( e ) } } function c ( ) { var a = 0 , b = new i ( e ) , c = document . createTextNode ( "" ) ; return b . observe ( c , { characterData : ! 0 } ) , function ( ) { c . data = a = ++ a % 2 } } function d ( ) { return function ( ) { j . setTimeout ( e , 1 ) } } function e ( ) { for ( var a = 0 ; a < k . length ; a ++ ) { var b = k [ a ] , c = b [ 0 ] , d = b [ 1 ] ; c ( d ) } k = [ ] } function f ( a , b ) { var c = k . push ( [ a , b ] ) ; 1 === c && g ( ) } var g , h = "undefined" != typeof window ? window : { } , i = h . MutationObserver || h . WebKitMutationObserver , j = "undefined" != typeof global ? global : this , k = [ ] ; g = "undefined" != typeof process && "[object process]" === { } . toString . call ( process ) ? b ( ) : i ? c ( ) : d ( ) , a . asap = f } ) , a ( "promise/cast" , [ "exports" ] , function ( a ) { "use strict" ; function b ( a ) { if ( a && "object" == typeof a && a . constructor === this ) return a ; var b = this ; return new b ( function ( b ) { b ( a ) } ) } a . cast = b } ) , a ( "promise/config" , [ "exports" ] , function ( a ) { "use strict" ; function b ( a , b ) { return 2 !== arguments . length ? c [ a ] : ( c [ a ] = b , void 0 ) } var c = { instrument : ! 1 } ; a . config = c , a . configure = b } ) , a ( "promise/polyfill" , [ "./promise" , "./utils" , "exports" ] , function ( a , b , c ) { "use strict" ; function d ( ) { var a = "Promise" in window && "cast" in window . Promise && "resolve" in window . Promise && "reject" in window . Promise && "all" in window . Promise && "race" in window . Promise && function ( ) { var a ; return new window . Promise ( function ( b ) { a = b } ) , f ( a ) } ( ) ; a || ( window . Promise = e ) } var e = a . Promise , f = b . isFunction ; c . polyfill = d } ) , a ( "promise/promise" , [ "./config" , "./utils" , "./cast" , "./all" , "./race" , "./resolve" , "./reject" , "./asap" , "exports" ] , function ( a , b , c , d , e , f , g , h , i ) { "use strict" ; function j ( a ) { if ( ! w ( a ) ) throw new TypeError ( "You must pass a resolver function as the first argument to the promise constructor" ) ; if ( ! ( this instanceof j ) ) throw new TypeError ( "Failed to construct 'Promise': Please use the 'new' operator, this object constructor cannot be called as a function." ) ; this . _subscribers = [ ] , k ( a , this ) } function k ( a , b ) { function c ( a ) { p ( b , a ) } function d ( a ) { r ( b , a ) } try { a ( c , d ) } catch ( e ) { d ( e ) } } function l ( a , b , c , d ) { var e , f , g , h , i = w ( c ) ; if ( i ) try { e = c ( d ) , g = ! 0 } catch ( j ) { h = ! 0 , f = j } else e = d , g = ! 0 ; o ( b , e ) || ( i && g ? p ( b , e ) : h ? r ( b , f ) : a === F ? p ( b , e ) : a === G && r ( b , e ) ) } function m ( a , b , c , d ) { var e = a . _subscribers , f = e . length ; e [ f ] = b , e [ f + F ] = c , e [ f + G ] = d } function n ( a , b ) { for ( var c , d , e = a . _subscribers , f = a . _detail , g = 0 ; g < e . length ; g += 3 ) c = e [ g ] , d = e [ g + b ] , l ( b , c , d , f ) ; a . _subscribers = null } function o ( a , b ) { var c , d = null ; try { if ( a === b ) throw new TypeError ( "A promises callback cannot return that same promise." ) ; if ( v ( b ) && ( d = b . then , w ( d ) ) ) return d . call ( b , function ( d ) { return c ? ! 0 : ( c = ! 0 , b !== d ? p ( a , d ) : q ( a , d ) , void 0 ) } , function ( b ) { return c ? ! 0 : ( c = ! 0 , r ( a , b ) , void 0 ) } ) , ! 0 } catch ( e ) { return c ? ! 0 : ( r ( a , e ) , ! 0 ) } return ! 1 } function p ( a , b ) { a === b ? q ( a , b ) : o ( a , b ) || q ( a , b ) } function q ( a , b ) { a . _state === D && ( a . _state = E , a . _detail = b , u . async ( s , a ) ) } function r ( a , b ) { a . _state === D && ( a . _state = E , a . _detail = b , u . async ( t , a ) ) } function s ( a ) { n ( a , a . _state = F ) } function t ( a ) { n ( a , a . _state = G ) } var u = a . config , v = ( a . configure , b . objectOrFunction ) , w = b . isFunction , x = ( b . now , c . cast ) , y = d . all , z = e . race , A = f . resolve , B = g . reject , C = h . asap ; u . async = C ; var D = void 0 , E = 0 , F = 1 , G = 2 ; j . prototype = { constructor : j , _state : void 0 , _detail : void 0 , _subscribers : void 0 , then : function ( a , b ) { var c = this , d
2014-02-17 02:04:09 +04:00
function ProxyImageContainer ( src , proxy ) {
var callbackName = "html2canvas_" + proxyImageCount ++ ;
var script = document . createElement ( "script" ) ;
var link = document . createElement ( "a" ) ;
link . href = src ;
src = link . href ;
var requestUrl = proxy + ( ( proxy . indexOf ( "?" ) > - 1 ) ? "&" : "?" ) + 'url=' + encodeURIComponent ( src ) + '&callback=' + callbackName ;
this . src = src ;
this . image = new Image ( ) ;
var image = this . image ;
this . promise = new Promise ( function ( resolve , reject ) {
image . onload = resolve ;
image . onerror = reject ;
window [ callbackName ] = function ( a ) {
if ( a . substring ( 0 , 6 ) === "error:" ) {
reject ( ) ;
} else {
image . src = a ;
}
window [ callbackName ] = undefined ; // to work with IE<9 // NOTE: that the undefined callback property-name still exists on the window object (for IE<9)
try {
delete window [ callbackName ] ; // for all browser that support this
} catch ( ex ) { }
script . parentNode . removeChild ( script ) ;
} ;
script . setAttribute ( "type" , "text/javascript" ) ;
script . setAttribute ( "src" , requestUrl ) ;
document . body . appendChild ( script ) ;
} ) ;
}
var proxyImageCount = 0 ;
2014-01-21 23:41:00 +04:00
function Renderer ( width , height , images ) {
this . width = width ;
this . height = height ;
this . images = images ;
}
2014-01-26 20:10:04 +04:00
Renderer . prototype . renderImage = function ( container , bounds , borderData , image ) {
var paddingLeft = container . cssInt ( 'paddingLeft' ) ,
paddingTop = container . cssInt ( 'paddingTop' ) ,
paddingRight = container . cssInt ( 'paddingRight' ) ,
paddingBottom = container . cssInt ( 'paddingBottom' ) ,
borders = borderData . borders ;
this . drawImage (
image ,
0 ,
0 ,
image . width ,
image . height ,
bounds . left + paddingLeft + borders [ 3 ] . width ,
bounds . top + paddingTop + borders [ 0 ] . width ,
bounds . width - ( borders [ 1 ] . width + borders [ 3 ] . width + paddingLeft + paddingRight ) ,
bounds . height - ( borders [ 0 ] . width + borders [ 2 ] . width + paddingTop + paddingBottom )
) ;
} ;
2014-02-15 02:33:09 +04:00
Renderer . prototype . renderBackground = function ( container , bounds , borderData ) {
2014-01-19 20:04:27 +04:00
if ( bounds . height > 0 && bounds . width > 0 ) {
this . renderBackgroundColor ( container , bounds ) ;
2014-02-15 02:33:09 +04:00
this . renderBackgroundImage ( container , bounds , borderData ) ;
2013-09-07 22:24:41 +04:00
}
} ;
2014-01-19 20:04:27 +04:00
Renderer . prototype . renderBackgroundColor = function ( container , bounds ) {
var color = container . css ( "backgroundColor" ) ;
if ( ! this . isTransparent ( color ) ) {
2014-01-21 23:41:00 +04:00
this . rectangle ( bounds . left , bounds . top , bounds . width , bounds . height , container . css ( "backgroundColor" ) ) ;
2013-09-07 22:24:41 +04:00
}
2014-01-19 20:04:27 +04:00
} ;
2013-09-07 22:24:41 +04:00
2014-01-19 20:04:27 +04:00
Renderer . prototype . renderBorders = function ( borders ) {
borders . forEach ( this . renderBorder , this ) ;
} ;
2013-09-07 22:24:41 +04:00
2014-01-19 20:04:27 +04:00
Renderer . prototype . renderBorder = function ( data ) {
if ( ! this . isTransparent ( data . color ) && data . args !== null ) {
this . drawShape ( data . args , data . color ) ;
2013-09-07 22:24:41 +04:00
}
2014-01-19 20:04:27 +04:00
} ;
2013-09-07 22:24:41 +04:00
2014-02-15 02:33:09 +04:00
Renderer . prototype . renderBackgroundImage = function ( container , bounds , borderData ) {
2014-01-21 23:41:00 +04:00
var backgroundImages = container . parseBackgroundImages ( ) ;
2014-01-26 18:06:16 +04:00
backgroundImages . reverse ( ) . forEach ( function ( backgroundImage , index , arr ) {
2014-02-03 21:42:42 +04:00
switch ( backgroundImage . method ) {
case "url" :
var image = this . images . get ( backgroundImage . args [ 0 ] ) ;
if ( image ) {
2014-02-15 02:33:09 +04:00
this . renderBackgroundRepeating ( container , bounds , image , arr . length - ( index + 1 ) , borderData ) ;
2014-02-03 21:42:42 +04:00
} else {
log ( "Error loading background-image" , backgroundImage . args [ 0 ] ) ;
}
break ;
case "linear-gradient" :
case "gradient" :
var gradientImage = this . images . get ( backgroundImage . value ) ;
if ( gradientImage ) {
2014-02-15 02:33:09 +04:00
this . renderBackgroundGradient ( gradientImage , bounds , borderData ) ;
2014-02-03 21:42:42 +04:00
} else {
log ( "Error loading background-image" , backgroundImage . args [ 0 ] ) ;
}
break ;
2014-02-08 16:07:20 +04:00
case "none" :
break ;
2014-02-03 21:42:42 +04:00
default :
log ( "Unknown background-image type" , backgroundImage . args [ 0 ] ) ;
2014-01-26 20:10:04 +04:00
}
2014-01-21 23:41:00 +04:00
} , this ) ;
} ;
2013-09-07 22:24:41 +04:00
2014-02-15 02:33:09 +04:00
Renderer . prototype . renderBackgroundRepeating = function ( container , bounds , imageContainer , index , borderData ) {
2014-01-21 23:41:00 +04:00
var size = container . parseBackgroundSize ( bounds , imageContainer . image , index ) ;
var position = container . parseBackgroundPosition ( bounds , imageContainer . image , index , size ) ;
var repeat = container . parseBackgroundRepeat ( index ) ;
switch ( repeat ) {
case "repeat-x" :
case "repeat no-repeat" :
2014-02-15 02:33:09 +04:00
this . backgroundRepeatShape ( imageContainer , position , size , bounds , bounds . left + borderData [ 3 ] , bounds . top + position . top + borderData [ 0 ] , 99999 , imageContainer . image . height , borderData ) ;
2014-01-21 23:41:00 +04:00
break ;
case "repeat-y" :
case "no-repeat repeat" :
2014-02-15 02:33:09 +04:00
this . backgroundRepeatShape ( imageContainer , position , size , bounds , bounds . left + position . left + borderData [ 3 ] , bounds . top + borderData [ 0 ] , imageContainer . image . width , 99999 , borderData ) ;
2014-01-21 23:41:00 +04:00
break ;
case "no-repeat" :
2014-02-15 02:33:09 +04:00
this . backgroundRepeatShape ( imageContainer , position , size , bounds , bounds . left + position . left + borderData [ 3 ] , bounds . top + position . top + borderData [ 0 ] , imageContainer . image . width , imageContainer . image . height , borderData ) ;
2014-01-21 23:41:00 +04:00
break ;
default :
2014-02-15 02:33:09 +04:00
this . renderBackgroundRepeat ( imageContainer , position , size , { top : bounds . top , left : bounds . left } , borderData [ 3 ] , borderData [ 0 ] ) ;
2014-01-21 23:41:00 +04:00
break ;
}
2014-01-19 20:04:27 +04:00
} ;
2013-09-07 22:24:41 +04:00
2014-01-19 20:04:27 +04:00
Renderer . prototype . isTransparent = function ( color ) {
return ( ! color || color === "transparent" || color === "rgba(0, 0, 0, 0)" ) ;
} ;
2013-09-07 22:24:41 +04:00
2014-01-21 23:41:00 +04:00
function CanvasRenderer ( width , height ) {
Renderer . apply ( this , arguments ) ;
2014-01-19 20:04:27 +04:00
this . canvas = document . createElement ( "canvas" ) ;
2014-01-21 23:41:00 +04:00
this . canvas . width = width ;
this . canvas . height = height ;
2014-01-19 20:04:27 +04:00
this . ctx = this . canvas . getContext ( "2d" ) ;
this . ctx . textBaseline = "bottom" ;
2014-02-03 21:42:42 +04:00
this . variables = { } ;
2014-02-01 20:32:05 +04:00
log ( "Initialized CanvasRenderer" ) ;
2014-01-19 20:04:27 +04:00
}
2013-09-07 22:24:41 +04:00
2014-01-19 20:04:27 +04:00
CanvasRenderer . prototype = Object . create ( Renderer . prototype ) ;
2013-09-07 22:24:41 +04:00
2014-01-19 20:04:27 +04:00
CanvasRenderer . prototype . setFillStyle = function ( color ) {
this . ctx . fillStyle = color ;
return this . ctx ;
2013-09-07 22:24:41 +04:00
} ;
2014-01-19 20:04:27 +04:00
CanvasRenderer . prototype . rectangle = function ( left , top , width , height , color ) {
this . setFillStyle ( color ) . fillRect ( left , top , width , height ) ;
} ;
2013-09-07 22:24:41 +04:00
2014-01-19 20:04:27 +04:00
CanvasRenderer . prototype . drawShape = function ( shape , color ) {
this . shape ( shape ) ;
this . setFillStyle ( color ) . fill ( ) ;
2013-09-07 22:24:41 +04:00
} ;
2014-01-26 20:10:04 +04:00
CanvasRenderer . prototype . drawImage = function ( image , sx , sy , sw , sh , dx , dy , dw , dh ) {
this . ctx . drawImage ( image , sx , sy , sw , sh , dx , dy , dw , dh ) ;
} ;
2014-01-19 20:04:27 +04:00
CanvasRenderer . prototype . clip = function ( shape , callback , context ) {
this . ctx . save ( ) ;
this . shape ( shape ) . clip ( ) ;
callback . call ( context ) ;
this . ctx . restore ( ) ;
} ;
2013-09-07 22:24:41 +04:00
2014-01-19 20:04:27 +04:00
CanvasRenderer . prototype . shape = function ( shape ) {
this . ctx . beginPath ( ) ;
shape . forEach ( function ( point , index ) {
this . ctx [ ( index === 0 ) ? "moveTo" : point [ 0 ] + "To" ] . apply ( this . ctx , point . slice ( 1 ) ) ;
} , this ) ;
this . ctx . closePath ( ) ;
return this . ctx ;
2013-09-07 22:24:41 +04:00
} ;
2014-01-19 20:04:27 +04:00
CanvasRenderer . prototype . font = function ( color , style , variant , weight , size , family ) {
this . setFillStyle ( color ) . font = [ style , variant , weight , size , family ] . join ( " " ) ;
2013-09-07 22:24:41 +04:00
} ;
2014-02-03 21:42:42 +04:00
CanvasRenderer . prototype . fontShadow = function ( color , offsetX , offsetY , blur ) {
this . setVariable ( "shadowColor" , color )
. setVariable ( "shadowOffsetY" , offsetX )
. setVariable ( "shadowOffsetX" , offsetY )
. setVariable ( "shadowBlur" , blur ) ;
} ;
CanvasRenderer . prototype . clearShadow = function ( ) {
this . setVariable ( "shadowColor" , "rgba(0,0,0,0)" ) ;
} ;
2014-01-19 20:04:27 +04:00
CanvasRenderer . prototype . setOpacity = function ( opacity ) {
this . ctx . globalAlpha = opacity ;
} ;
2013-09-07 22:24:41 +04:00
2014-02-23 19:35:46 +04:00
CanvasRenderer . prototype . setTransform = function ( transform ) {
this . ctx . translate ( transform . origin [ 0 ] , transform . origin [ 1 ] ) ;
2014-03-02 18:00:59 +04:00
this . ctx . transform . apply ( this . ctx , transform . matrix ) ;
this . ctx . translate ( - transform . origin [ 0 ] , - transform . origin [ 1 ] ) ;
2014-02-23 19:35:46 +04:00
} ;
2014-02-03 21:42:42 +04:00
CanvasRenderer . prototype . setVariable = function ( property , value ) {
if ( this . variables [ property ] !== value ) {
this . variables [ property ] = this . ctx [ property ] = value ;
}
return this ;
} ;
2014-01-19 20:04:27 +04:00
CanvasRenderer . prototype . text = function ( text , left , bottom ) {
this . ctx . fillText ( text , left , bottom ) ;
2013-09-07 22:24:41 +04:00
} ;
2014-01-19 20:04:27 +04:00
2014-02-15 02:33:09 +04:00
CanvasRenderer . prototype . backgroundRepeatShape = function ( imageContainer , backgroundPosition , size , bounds , left , top , width , height , borderData ) {
2014-01-21 23:41:00 +04:00
var shape = [
[ "line" , Math . round ( left ) , Math . round ( top ) ] ,
[ "line" , Math . round ( left + width ) , Math . round ( top ) ] ,
[ "line" , Math . round ( left + width ) , Math . round ( height + top ) ] ,
[ "line" , Math . round ( left ) , Math . round ( height + top ) ]
] ;
this . clip ( shape , function ( ) {
2014-02-15 02:33:09 +04:00
this . renderBackgroundRepeat ( imageContainer , backgroundPosition , size , bounds , borderData [ 3 ] , borderData [ 0 ] ) ;
2014-01-21 23:41:00 +04:00
} , this ) ;
} ;
2014-02-15 02:33:09 +04:00
CanvasRenderer . prototype . renderBackgroundRepeat = function ( imageContainer , backgroundPosition , size , bounds , borderLeft , borderTop ) {
var offsetX = Math . round ( bounds . left + backgroundPosition . left + borderLeft ) , offsetY = Math . round ( bounds . top + backgroundPosition . top + borderTop ) ;
2014-01-26 18:06:16 +04:00
this . setFillStyle ( this . ctx . createPattern ( this . resizeImage ( imageContainer , size ) , "repeat" ) ) ;
2014-01-21 23:41:00 +04:00
this . ctx . translate ( offsetX , offsetY ) ;
this . ctx . fill ( ) ;
this . ctx . translate ( - offsetX , - offsetY ) ;
} ;
2014-02-03 21:42:42 +04:00
CanvasRenderer . prototype . renderBackgroundGradient = function ( gradientImage , bounds ) {
if ( gradientImage instanceof LinearGradientContainer ) {
var gradient = this . ctx . createLinearGradient ( bounds . left , bounds . top , bounds . right , bounds . bottom ) ;
//console.log(gradientImage, bounds, gradient);
}
} ;
2014-01-26 18:06:16 +04:00
CanvasRenderer . prototype . resizeImage = function ( imageContainer , size ) {
var image = imageContainer . image ;
if ( image . width === size . width && image . height === size . height ) {
return image ;
}
var ctx , canvas = document . createElement ( 'canvas' ) ;
canvas . width = size . width ;
canvas . height = size . height ;
ctx = canvas . getContext ( "2d" ) ;
ctx . drawImage ( image , 0 , 0 , image . width , image . height , 0 , 0 , size . width , size . height ) ;
return canvas ;
} ;
2014-01-21 23:41:00 +04:00
function StackingContext ( hasOwnStacking , opacity , element , parent ) {
NodeContainer . call ( this , element , parent ) ;
this . ownStacking = hasOwnStacking ;
this . contexts = [ ] ;
this . children = [ ] ;
this . opacity = ( this . parent ? this . parent . stack . opacity : 1 ) * opacity ;
}
StackingContext . prototype = Object . create ( NodeContainer . prototype ) ;
StackingContext . prototype . getParentStack = function ( context ) {
var parentStack = ( this . parent ) ? this . parent . stack : null ;
return parentStack ? ( parentStack . ownStacking ? parentStack : parentStack . getParentStack ( context ) ) : context . stack ;
} ;
2014-03-02 18:00:59 +04:00
function Support ( document ) {
this . rangeBounds = this . testRangeBounds ( document ) ;
2014-01-21 23:41:00 +04:00
this . cors = this . testCORS ( ) ;
}
2014-03-02 18:00:59 +04:00
Support . prototype . testRangeBounds = function ( document ) {
2014-01-21 23:41:00 +04:00
var range , testElement , rangeBounds , rangeHeight , support = false ;
if ( document . createRange ) {
range = document . createRange ( ) ;
if ( range . getBoundingClientRect ) {
testElement = document . createElement ( 'boundtest' ) ;
testElement . style . height = "123px" ;
testElement . style . display = "block" ;
document . body . appendChild ( testElement ) ;
range . selectNode ( testElement ) ;
rangeBounds = range . getBoundingClientRect ( ) ;
rangeHeight = rangeBounds . height ;
if ( rangeHeight === 123 ) {
support = true ;
}
document . body . removeChild ( testElement ) ;
}
}
return support ;
} ;
Support . prototype . testCORS = function ( ) {
return typeof ( ( new Image ( ) ) . crossOrigin ) !== "undefined" ;
} ;
function TextContainer ( node , parent ) {
NodeContainer . call ( this , node , parent ) ;
}
TextContainer . prototype = Object . create ( NodeContainer . prototype ) ;
TextContainer . prototype . applyTextTransform = function ( ) {
this . node . data = this . transform ( this . parent . css ( "textTransform" ) ) ;
} ;
TextContainer . prototype . transform = function ( transform ) {
var text = this . node . data ;
switch ( transform ) {
case "lowercase" :
return text . toLowerCase ( ) ;
case "capitalize" :
return text . replace ( /(^|\s|:|-|\(|\))([a-z])/g , capitalize ) ;
case "uppercase" :
return text . toUpperCase ( ) ;
default :
return text ;
}
} ;
function capitalize ( m , p1 , p2 ) {
if ( m . length > 0 ) {
return p1 + p2 . toUpperCase ( ) ;
}
}
2014-02-03 21:42:42 +04:00
function WebkitGradientContainer ( imageData ) {
GradientContainer . apply ( this , arguments ) ;
this . type = ( imageData . args [ 0 ] === "linear" ) ? this . TYPES . LINEAR : this . TYPES . RADIAL ;
}
WebkitGradientContainer . prototype = Object . create ( GradientContainer . prototype ) ;
2013-09-07 22:24:41 +04:00
} ) ( window , document ) ;