From a73dbf8067c92e4f25f4db71f75643f0647cb1ac Mon Sep 17 00:00:00 2001 From: Niklas von Hertzen Date: Thu, 17 Aug 2017 23:14:44 +0800 Subject: [PATCH] Implement foreignObject renderer --- flow-typed/myLibDef.js | 2 + src/Clone.js | 250 ++++++++++++++++++++------ src/Feature.js | 35 ++++ src/ImageLoader.js | 64 +++++-- src/NodeContainer.js | 9 +- src/NodeParser.js | 81 +-------- src/Renderer.js | 2 +- src/index.js | 166 ++++++++++------- src/parsing/background.js | 21 +-- src/renderer/ForeignObjectRenderer.js | 52 ++++++ 10 files changed, 457 insertions(+), 225 deletions(-) create mode 100644 src/renderer/ForeignObjectRenderer.js diff --git a/flow-typed/myLibDef.js b/flow-typed/myLibDef.js index 03fae64..e9ac982 100644 --- a/flow-typed/myLibDef.js +++ b/flow-typed/myLibDef.js @@ -7,3 +7,5 @@ declare class SVGSVGElement extends Element { getPresentationAttribute(name: string): any; } + +declare class HTMLBodyElement extends HTMLElement {} diff --git a/src/Clone.js b/src/Clone.js index bc9aa51..06da9d6 100644 --- a/src/Clone.js +++ b/src/Clone.js @@ -2,6 +2,127 @@ 'use strict'; import type {Bounds} from './Bounds'; import type {Options} from './index'; +import type Logger from './Logger'; + +import ImageLoader from './ImageLoader'; +import {copyCSSStyles} from './Util'; +import {parseBackgroundImage} from './parsing/background'; + +export class DocumentCloner { + scrolledElements: Array<[HTMLElement, number, number]>; + referenceElement: HTMLElement; + clonedReferenceElement: HTMLElement; + documentElement: HTMLElement; + imageLoader: ImageLoader; + logger: Logger; + inlineImages: boolean; + copyStyles: boolean; + + constructor(element: HTMLElement, options: Options, logger: Logger) { + this.referenceElement = element; + this.scrolledElements = []; + this.copyStyles = true; + this.inlineImages = true; + this.logger = logger; + this.imageLoader = new ImageLoader(options, logger, window); + // $FlowFixMe + this.documentElement = this.cloneNode(element.ownerDocument.documentElement); + } + + inlineAllImages(node: ?HTMLElement) { + if (this.inlineImages && node) { + const style = node.style; + Promise.all( + parseBackgroundImage(style.backgroundImage).map(backgroundImage => { + if (backgroundImage.method === 'url') { + return this.imageLoader + .inlineImage(backgroundImage.args[0]) + .then(src => (src ? `url("${src}")` : 'none')); + } + 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.imageLoader.inlineImage(node.src).then(src => { + if (src && node instanceof HTMLImageElement) { + node.src = src; + } + }); + } + } + } + + createElementClone(node: Node) { + if (this.copyStyles && node instanceof HTMLCanvasElement) { + const img = node.ownerDocument.createElement('img'); + try { + img.src = node.toDataURL(); + return img; + } catch (e) { + this.logger.log(`Unable to clone canvas contents, canvas is tainted`); + } + } + + return node.cloneNode(false); + } + + cloneNode(node: Node): Node { + const clone = + node.nodeType === Node.TEXT_NODE + ? document.createTextNode(node.nodeValue) + : this.createElementClone(node); + + if (this.referenceElement === node && clone instanceof HTMLElement) { + this.clonedReferenceElement = clone; + } + + if (clone instanceof HTMLBodyElement) { + createPseudoHideStyles(clone); + } + + for (let child = node.firstChild; child; child = child.nextSibling) { + if (child.nodeType !== Node.ELEMENT_NODE || child.nodeName !== 'SCRIPT') { + clone.appendChild(this.cloneNode(child)); + } + } + if (node instanceof HTMLElement && clone instanceof HTMLElement) { + this.inlineAllImages(inlinePseudoElement(node, clone, PSEUDO_BEFORE)); + this.inlineAllImages(inlinePseudoElement(node, clone, PSEUDO_AFTER)); + if (this.copyStyles) { + copyCSSStyles(node.ownerDocument.defaultView.getComputedStyle(node), clone); + } + this.inlineAllImages(clone); + if (node.scrollTop !== 0 || node.scrollLeft !== 0) { + this.scrolledElements.push([node, node.scrollLeft, node.scrollTop]); + } + switch (node.nodeName) { + case 'CANVAS': + if (!this.copyStyles) { + // $FlowFixMe + cloneCanvasContents(node, clone); + } + break; + case 'TEXTAREA': + case 'SELECT': + // $FlowFixMe + clone.value = node.value; + break; + } + } + return clone; + } +} const restoreOwnerScroll = (ownerDocument: Document, x: number, y: number) => { if ( @@ -28,44 +149,80 @@ const cloneCanvasContents = (canvas: HTMLCanvasElement, clonedCanvas: HTMLCanvas } catch (e) {} }; -const cloneNode = ( - node: Node, - referenceElement: [HTMLElement, ?HTMLElement], - scrolledElements: Array<[HTMLElement, number, number]> -) => { - const clone = - node.nodeType === Node.TEXT_NODE - ? document.createTextNode(node.nodeValue) - : node.cloneNode(false); - - if (referenceElement[0] === node && clone instanceof HTMLElement) { - referenceElement[1] = clone; +const inlinePseudoElement = ( + node: HTMLElement, + clone: HTMLElement, + pseudoElt: ':before' | ':after' +): ?HTMLElement => { + const style = node.ownerDocument.defaultView.getComputedStyle(node, pseudoElt); + if ( + !style || + !style.content || + style.content === 'none' || + style.content === '-moz-alt-content' || + style.display === 'none' + ) { + return; } - for (let child = node.firstChild; child; child = child.nextSibling) { - if (child.nodeType !== Node.ELEMENT_NODE || child.nodeName !== 'SCRIPT') { - clone.appendChild(cloneNode(child, referenceElement, scrolledElements)); - } + const content = stripQuotes(style.content); + const image = content.match(URL_REGEXP); + const anonymousReplacedElement = clone.ownerDocument.createElement( + image ? 'img' : 'html2canvaspseudoelement' + ); + if (image) { + // $FlowFixMe + anonymousReplacedElement.src = stripQuotes(image[1]); + } else { + anonymousReplacedElement.textContent = content; } - if (node instanceof HTMLElement) { - if (node.scrollTop !== 0 || node.scrollLeft !== 0) { - scrolledElements.push([node, node.scrollLeft, node.scrollTop]); - } - switch (node.nodeName) { - case 'CANVAS': - // $FlowFixMe - cloneCanvasContents(node, clone); - break; - case 'TEXTAREA': - case 'SELECT': - // $FlowFixMe - clone.value = node.value; - break; - } + copyCSSStyles(style, anonymousReplacedElement); + + anonymousReplacedElement.className = `${PSEUDO_HIDE_ELEMENT_CLASS_BEFORE} ${PSEUDO_HIDE_ELEMENT_CLASS_AFTER}`; + clone.className += + pseudoElt === PSEUDO_BEFORE + ? ` ${PSEUDO_HIDE_ELEMENT_CLASS_BEFORE}` + : ` ${PSEUDO_HIDE_ELEMENT_CLASS_AFTER}`; + if (pseudoElt === PSEUDO_BEFORE) { + clone.insertBefore(anonymousReplacedElement, clone.firstChild); + } else { + clone.appendChild(anonymousReplacedElement); } - return clone; + return anonymousReplacedElement; +}; + +const stripQuotes = (content: string): string => { + const first = content.substr(0, 1); + return first === content.substr(content.length - 1) && first.match(/['"]/) + ? content.substr(1, content.length - 2) + : content; +}; + +const URL_REGEXP = /^url\((.+)\)$/i; +const PSEUDO_BEFORE = ':before'; +const PSEUDO_AFTER = ':after'; +const PSEUDO_HIDE_ELEMENT_CLASS_BEFORE = '___html2canvas___pseudoelement_before'; +const PSEUDO_HIDE_ELEMENT_CLASS_AFTER = '___html2canvas___pseudoelement_after'; + +const PSEUDO_HIDE_ELEMENT_STYLE = `{ + content: "" !important; + display: none !important; +}`; + +const createPseudoHideStyles = (body: HTMLElement) => { + createStyles( + body, + `.${PSEUDO_HIDE_ELEMENT_CLASS_BEFORE}${PSEUDO_BEFORE}${PSEUDO_HIDE_ELEMENT_STYLE} + .${PSEUDO_HIDE_ELEMENT_CLASS_AFTER}${PSEUDO_AFTER}${PSEUDO_HIDE_ELEMENT_STYLE}` + ); +}; + +const createStyles = (body: HTMLElement, styles) => { + const style = body.ownerDocument.createElement('style'); + style.innerHTML = styles; + body.appendChild(style); }; const initNode = ([element, x, y]: [HTMLElement, number, number]) => { @@ -74,24 +231,13 @@ const initNode = ([element, x, y]: [HTMLElement, number, number]) => { }; export const cloneWindow = ( - documentToBeCloned: Document, ownerDocument: Document, bounds: Bounds, referenceElement: HTMLElement, - options: Options + options: Options, + logger: Logger ): Promise<[HTMLIFrameElement, HTMLElement]> => { - const scrolledElements = []; - const referenceElementSearch = [referenceElement, null]; - if (!(documentToBeCloned.documentElement instanceof HTMLElement)) { - return Promise.reject(__DEV__ ? `Invalid document provided for cloning` : ''); - } - - const documentElement = cloneNode( - documentToBeCloned.documentElement, - referenceElementSearch, - scrolledElements - ); - + const cloner = new DocumentCloner(referenceElement, options, logger); const cloneIframeContainer = ownerDocument.createElement('iframe'); cloneIframeContainer.className = 'html2canvas-container'; @@ -120,7 +266,7 @@ export const cloneWindow = ( cloneWindow.onload = cloneIframeContainer.onload = () => { const interval = setInterval(() => { if (documentClone.body.childNodes.length > 0) { - scrolledElements.forEach(initNode); + cloner.scrolledElements.forEach(initNode); clearInterval(interval); if (options.type === 'view') { cloneWindow.scrollTo(bounds.left, bounds.top); @@ -135,10 +281,10 @@ export const cloneWindow = ( } } if ( - referenceElementSearch[1] instanceof cloneWindow.HTMLElement || - referenceElementSearch[1] instanceof HTMLElement + cloner.clonedReferenceElement instanceof cloneWindow.HTMLElement || + cloner.clonedReferenceElement instanceof HTMLElement ) { - resolve([cloneIframeContainer, referenceElementSearch[1]]); + resolve([cloneIframeContainer, cloner.clonedReferenceElement]); } else { reject( __DEV__ @@ -153,9 +299,9 @@ export const cloneWindow = ( documentClone.open(); documentClone.write(''); // Chrome scrolls the parent document for some reason after the write to the cloned window??? - restoreOwnerScroll(documentToBeCloned, bounds.left, bounds.top); + restoreOwnerScroll(referenceElement.ownerDocument, bounds.left, bounds.top); documentClone.replaceChild( - documentClone.adoptNode(documentElement), + documentClone.adoptNode(cloner.documentElement), documentClone.documentElement ); documentClone.close(); diff --git a/src/Feature.js b/src/Feature.js index 3679e78..b1b2777 100644 --- a/src/Feature.js +++ b/src/Feature.js @@ -71,6 +71,34 @@ const testSVG = document => { return true; }; +const testForeignObject = document => { + const img = new Image(); + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + img.src = `data:image/svg+xml,
`; + + return new Promise(resolve => { + const onload = () => { + try { + ctx.drawImage(img, 0, 0); + canvas.toDataURL(); + } catch (e) { + return resolve(false); + } + + return resolve(true); + }; + + img.onload = onload; + + if (img.complete === true) { + setTimeout(() => { + onload(); + }, 50); + } + }); +}; + const FEATURES = { // $FlowFixMe - get/set properties not yet supported get SUPPORT_RANGE_BOUNDS() { @@ -94,6 +122,13 @@ const FEATURES = { 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 = testForeignObject(document); + Object.defineProperty(FEATURES, 'SUPPORT_FOREIGNOBJECT_DRAWING', {value}); + return value; } }; diff --git a/src/ImageLoader.js b/src/ImageLoader.js index 0a3daca..d6cdc21 100644 --- a/src/ImageLoader.js +++ b/src/ImageLoader.js @@ -5,15 +5,16 @@ import type Options from './index'; import type Logger from './Logger'; export type ImageElement = Image | HTMLCanvasElement; -type ImageCache = {[string]: Promise}; +type ImageCache = {[string]: Promise}; import FEATURES from './Feature'; -export default class ImageLoader { +// $FlowFixMe +export default class ImageLoader { origin: string; options: Options; _link: HTMLAnchorElement; - cache: ImageCache; + cache: ImageCache; logger: Logger; _index: number; _window: WindowProxy; @@ -45,6 +46,46 @@ export default class ImageLoader { } } + inlineImage(src: string): Promise { + if (isInlineImage(src)) { + return Promise.resolve(src); + } + if (this.hasImageInCache(src)) { + return this.cache[src]; + } + // TODO proxy + return this.xhrImage(src); + } + + xhrImage(src: string): Promise { + 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} with status code ${xhr.status}`); + } 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); + } + } + }; + xhr.ontimeout = () => reject(`Timed out fetching ${src}`); + xhr.responseType = 'blob'; + if (this.options.imageTimeout) { + xhr.timeout = this.options.imageTimeout; + } + xhr.open('GET', src, true); + xhr.send(); + }); + + return this.cache[src]; + } + loadCanvas(node: HTMLCanvasElement): string { const key = String(this._index++); this.cache[key] = Promise.resolve(node); @@ -60,8 +101,8 @@ export default class ImageLoader { this.logger.log(`Added image ${key.substring(0, 256)}`); } - const imageLoadHandler = (supportsDataImages: boolean): Promise => { - return new Promise((resolve, reject) => { + const imageLoadHandler = (supportsDataImages: boolean): Promise => + 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 @@ -78,7 +119,6 @@ export default class ImageLoader { }, 500); } }); - }; this.cache[key] = isInlineImage(src) && !isSVG(src) @@ -99,7 +139,7 @@ export default class ImageLoader { return link.protocol + link.hostname + link.port; } - ready(): Promise { + ready(): Promise> { const keys = Object.keys(this.cache); return Promise.all( keys.map(str => @@ -112,23 +152,23 @@ export default class ImageLoader { ) ).then(images => { if (__DEV__) { - this.logger.log('Finished loading images', images); + this.logger.log(`Finished loading ${images.length} images`, images); } return new ImageStore(keys, images); }); } } -export class ImageStore { +export class ImageStore { _keys: Array; - _images: Array; + _images: Array; - constructor(keys: Array, images: Array) { + constructor(keys: Array, images: Array) { this._keys = keys; this._images = images; } - get(key: string): ?ImageElement { + get(key: string): ?T { const index = this._keys.indexOf(key); return index === -1 ? null : this._images[index]; } diff --git a/src/NodeContainer.js b/src/NodeContainer.js index 598cc96..17a1f0c 100644 --- a/src/NodeContainer.js +++ b/src/NodeContainer.js @@ -18,7 +18,7 @@ import type {Visibility} from './parsing/visibility'; import type {zIndex} from './parsing/zIndex'; import type {Bounds, BoundCurves} from './Bounds'; -import type ImageLoader from './ImageLoader'; +import type ImageLoader, {ImageElement} from './ImageLoader'; import type {Path} from './drawing/Path'; import type TextContainer from './TextContainer'; @@ -87,7 +87,7 @@ export default class NodeContainer { constructor( node: HTMLElement | SVGSVGElement, parent: ?NodeContainer, - imageLoader: ImageLoader, + imageLoader: ImageLoader, index: number ) { this.parent = parent; @@ -219,7 +219,10 @@ export default class NodeContainer { } } -const getImage = (node: HTMLElement | SVGSVGElement, imageLoader: ImageLoader): ?string => { +const getImage = ( + node: HTMLElement | SVGSVGElement, + imageLoader: ImageLoader +): ?string => { if ( node instanceof node.ownerDocument.defaultView.SVGSVGElement || node instanceof SVGSVGElement diff --git a/src/NodeParser.js b/src/NodeParser.js index 661ce2b..aca7718 100644 --- a/src/NodeParser.js +++ b/src/NodeParser.js @@ -1,16 +1,15 @@ /* @flow */ 'use strict'; -import type ImageLoader from './ImageLoader'; +import type ImageLoader, {ImageElement} from './ImageLoader'; import type Logger from './Logger'; import StackingContext from './StackingContext'; import NodeContainer from './NodeContainer'; import TextContainer from './TextContainer'; import {inlineInputElement, inlineTextAreaElement, inlineSelectElement} from './Input'; -import {copyCSSStyles} from './Util'; export const NodeParser = ( node: HTMLElement, - imageLoader: ImageLoader, + imageLoader: ImageLoader, logger: Logger ): StackingContext => { if (__DEV__) { @@ -22,7 +21,6 @@ export const NodeParser = ( const container = new NodeContainer(node, null, imageLoader, index++); const stack = new StackingContext(container, null, true); - createPseudoHideStyles(node.ownerDocument); parseNodeTree(node, container, stack, imageLoader, index); if (__DEV__) { @@ -33,17 +31,12 @@ export const NodeParser = ( }; const IGNORED_NODE_NAMES = ['SCRIPT', 'HEAD', 'TITLE', 'OBJECT', 'BR', 'OPTION']; -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 parseNodeTree = ( node: HTMLElement, parent: NodeContainer, stack: StackingContext, - imageLoader: ImageLoader, + imageLoader: ImageLoader, index: number ): void => { if (__DEV__ && index > 50000) { @@ -62,8 +55,6 @@ const parseNodeTree = ( childNode instanceof HTMLElement ) { if (IGNORED_NODE_NAMES.indexOf(childNode.nodeName) === -1) { - inlinePseudoElement(childNode, PSEUDO_BEFORE); - inlinePseudoElement(childNode, PSEUDO_AFTER); const container = new NodeContainer(childNode, parent, imageLoader, index++); if (container.isVisible()) { if (childNode.tagName === 'INPUT') { @@ -132,44 +123,6 @@ const parseNodeTree = ( } }; -const inlinePseudoElement = (node: HTMLElement, pseudoElt: ':before' | ':after'): void => { - const style = node.ownerDocument.defaultView.getComputedStyle(node, pseudoElt); - if ( - !style || - !style.content || - style.content === 'none' || - style.content === '-moz-alt-content' || - style.display === 'none' - ) { - return; - } - - const content = stripQuotes(style.content); - const image = content.match(URL_REGEXP); - const anonymousReplacedElement = node.ownerDocument.createElement( - image ? 'img' : 'html2canvaspseudoelement' - ); - if (image) { - // $FlowFixMe - anonymousReplacedElement.src = stripQuotes(image[1]); - } else { - anonymousReplacedElement.textContent = content; - } - - copyCSSStyles(style, anonymousReplacedElement); - - anonymousReplacedElement.className = `${PSEUDO_HIDE_ELEMENT_CLASS_BEFORE} ${PSEUDO_HIDE_ELEMENT_CLASS_AFTER}`; - node.className += - pseudoElt === PSEUDO_BEFORE - ? ` ${PSEUDO_HIDE_ELEMENT_CLASS_BEFORE}` - : ` ${PSEUDO_HIDE_ELEMENT_CLASS_AFTER}`; - if (pseudoElt === PSEUDO_BEFORE) { - node.insertBefore(anonymousReplacedElement, node.firstChild); - } else { - node.appendChild(anonymousReplacedElement); - } -}; - const createsRealStackingContext = ( container: NodeContainer, node: HTMLElement | SVGSVGElement @@ -197,31 +150,3 @@ const isBodyWithTransparentRoot = ( container.parent.style.background.backgroundColor.isTransparent() ); }; - -const stripQuotes = (content: string): string => { - const first = content.substr(0, 1); - return first === content.substr(content.length - 1) && first.match(/['"]/) - ? content.substr(1, content.length - 2) - : content; -}; - -const PSEUDO_HIDE_ELEMENT_STYLE = `{ - content: "" !important; - display: none !important; -}`; - -const createPseudoHideStyles = (document: Document) => { - createStyles( - document, - `.${PSEUDO_HIDE_ELEMENT_CLASS_BEFORE}${PSEUDO_BEFORE}${PSEUDO_HIDE_ELEMENT_STYLE} - .${PSEUDO_HIDE_ELEMENT_CLASS_AFTER}${PSEUDO_AFTER}${PSEUDO_HIDE_ELEMENT_STYLE}` - ); -}; - -const createStyles = (document: Document, styles) => { - const style = document.createElement('style'); - style.innerHTML = styles; - if (document.body) { - document.body.appendChild(style); - } -}; diff --git a/src/Renderer.js b/src/Renderer.js index 9d42a60..d924eda 100644 --- a/src/Renderer.js +++ b/src/Renderer.js @@ -43,7 +43,7 @@ import {BORDER_STYLE} from './parsing/border'; export type RenderOptions = { scale: number, backgroundColor: ?Color, - imageStore: ImageStore, + imageStore: ImageStore, fontMetrics: FontMetrics, logger: Logger, width: number, diff --git a/src/index.js b/src/index.js index 2f80a39..38a1376 100644 --- a/src/index.js +++ b/src/index.js @@ -5,13 +5,15 @@ import type {RenderTarget} from './Renderer'; import {NodeParser} from './NodeParser'; import Renderer from './Renderer'; +import ForeignObjectRenderer from './renderer/ForeignObjectRenderer'; import CanvasRenderer from './renderer/CanvasRenderer'; import Logger from './Logger'; import ImageLoader from './ImageLoader'; +import Feature from './Feature'; import {Bounds, parseDocumentSize} from './Bounds'; -import {cloneWindow} from './Clone'; -import Color, {TRANSPARENT} from './Color'; +import {cloneWindow, DocumentCloner} from './Clone'; import {FontMetrics} from './Font'; +import Color, {TRANSPARENT} from './Color'; export type Options = { async: ?boolean, @@ -42,7 +44,7 @@ const html2canvas = (element: HTMLElement, config: Options): Promise<*> => { const defaultOptions = { async: true, allowTaint: false, - imageTimeout: 10000, + imageTimeout: 15000, proxy: null, removeContainer: true, scale: defaultView.devicePixelRatio || 1, @@ -63,79 +65,105 @@ const html2canvas = (element: HTMLElement, config: Options): Promise<*> => { options.windowHeight ); - const result = cloneWindow( - ownerDocument, - ownerDocument, - windowBounds, - element, - options - ).then(([container, clonedElement]) => { - if (__DEV__) { - logger.log(`Document cloned`); - } + const bounds = options.type === 'view' ? windowBounds : parseDocumentSize(ownerDocument); - const imageLoader = new ImageLoader( - options, - logger, - clonedElement.ownerDocument.defaultView - ); - const stack = NodeParser(clonedElement, imageLoader, logger); - const clonedDocument = clonedElement.ownerDocument; - const size = options.type === 'view' ? windowBounds : parseDocumentSize(clonedDocument); - const width = size.width; - const height = size.height; + // http://www.w3.org/TR/css3-background/#special-backgrounds + const documentBackgroundColor = ownerDocument.documentElement + ? new Color(getComputedStyle(ownerDocument.documentElement).backgroundColor) + : TRANSPARENT; + const backgroundColor = + element === ownerDocument.documentElement + ? documentBackgroundColor.isTransparent() + ? ownerDocument.body + ? new Color(getComputedStyle(ownerDocument.body).backgroundColor) + : null + : documentBackgroundColor + : null; - // http://www.w3.org/TR/css3-background/#special-backgrounds - const backgroundColor = - clonedElement === clonedDocument.documentElement - ? stack.container.style.background.backgroundColor.isTransparent() - ? clonedDocument.body - ? new Color(getComputedStyle(clonedDocument.body).backgroundColor) - : null - : stack.container.style.background.backgroundColor - : null; + // $FlowFixMe + const result = Feature.SUPPORT_FOREIGNOBJECT_DRAWING.then( + supportForeignObject => + supportForeignObject + ? (cloner => { + if (__DEV__) { + logger.log(`Document cloned, using foreignObject rendering`); + } - if (backgroundColor === stack.container.style.background.backgroundColor) { - stack.container.style.background.backgroundColor = TRANSPARENT; - } + return cloner.imageLoader.ready().then(() => { + const renderer = new ForeignObjectRenderer(cloner.clonedReferenceElement); + return renderer.render({ + bounds, + backgroundColor, + logger, + scale: options.scale + }); + }); + })(new DocumentCloner(element, options, logger)) + : cloneWindow( + ownerDocument, + windowBounds, + element, + options, + logger + ).then(([container, clonedElement]) => { + if (__DEV__) { + logger.log(`Document cloned, using computed rendering`); + } - return imageLoader.ready().then(imageStore => { - if (options.removeContainer === true) { - if (container.parentNode) { - container.parentNode.removeChild(container); - } else if (__DEV__) { - logger.log(`Cannot detach cloned iframe as it is not in the DOM anymore`); - } - } + const imageLoader = new ImageLoader( + options, + logger, + clonedElement.ownerDocument.defaultView + ); + const stack = NodeParser(clonedElement, imageLoader, logger); + const clonedDocument = clonedElement.ownerDocument; + const width = bounds.width; + const height = bounds.height; - const fontMetrics = new FontMetrics(clonedDocument); - if (__DEV__) { - logger.log(`Starting renderer`); - } + if (backgroundColor === stack.container.style.background.backgroundColor) { + stack.container.style.background.backgroundColor = TRANSPARENT; + } - const renderOptions = { - backgroundColor, - fontMetrics, - imageStore, - logger, - scale: options.scale, - width, - height - }; + return imageLoader.ready().then(imageStore => { + if (options.removeContainer === true) { + if (container.parentNode) { + container.parentNode.removeChild(container); + } else if (__DEV__) { + logger.log( + `Cannot detach cloned iframe as it is not in the DOM anymore` + ); + } + } - if (Array.isArray(options.target)) { - return Promise.all( - options.target.map(target => { - const renderer = new Renderer(target, renderOptions); - return renderer.render(stack); - }) - ); - } else { - const renderer = new Renderer(options.target, renderOptions); - return renderer.render(stack); - } - }); - }); + const fontMetrics = new FontMetrics(clonedDocument); + if (__DEV__) { + logger.log(`Starting renderer`); + } + + const renderOptions = { + backgroundColor, + fontMetrics, + imageStore, + logger, + scale: options.scale, + width, + height + }; + + if (Array.isArray(options.target)) { + return Promise.all( + options.target.map(target => { + const renderer = new Renderer(target, renderOptions); + return renderer.render(stack); + }) + ); + } else { + const renderer = new Renderer(options.target, renderOptions); + return renderer.render(stack); + } + }); + }) + ); if (__DEV__) { return result.catch(e => { diff --git a/src/parsing/background.js b/src/parsing/background.js index 364c009..09d0a41 100644 --- a/src/parsing/background.js +++ b/src/parsing/background.js @@ -223,7 +223,7 @@ export const calculateBackgroundRepeatPath = ( export const parseBackground = ( style: CSSStyleDeclaration, - imageLoader: ImageLoader + imageLoader: ImageLoader ): Background => { return { backgroundColor: new Color(style.backgroundColor), @@ -276,12 +276,17 @@ const parseBackgroundRepeat = (backgroundRepeat: string): BackgroundRepeat => { const parseBackgroundImages = ( style: CSSStyleDeclaration, - imageLoader: ImageLoader + imageLoader: ImageLoader ): Array => { const sources: Array = parseBackgroundImage( - style.backgroundImage, - imageLoader - ); + style.backgroundImage + ).map(backgroundImage => { + if (backgroundImage.method === 'url') { + const key = imageLoader.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(','); @@ -318,7 +323,7 @@ const parseBackgoundPosition = (position: string): Length => { return new Length(position); }; -const parseBackgroundImage = (image: string, imageLoader: ImageLoader): Array => { +export const parseBackgroundImage = (image: string): Array => { const whitespace = /^\s$/; const results = []; @@ -346,10 +351,6 @@ const parseBackgroundImage = (image: string, imageLoader: ImageLoader): Array { + const img = new Image(); + img.onload = () => { + if (options.backgroundColor) { + this.ctx.fillStyle = options.backgroundColor.toString(); + this.ctx.fillRect(0, 0, options.bounds.width, options.bounds.height); + } + this.ctx.drawImage(img, 0, 0); + resolve(this.canvas); + }; + + img.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent( + new XMLSerializer().serializeToString(svg) + )}`; + }); + } +}