diff --git a/src/Clone.js b/src/Clone.js index dec96b1..180980c 100644 --- a/src/Clone.js +++ b/src/Clone.js @@ -3,27 +3,39 @@ import type {Bounds} from './Bounds'; import type {Options} from './index'; import type Logger from './Logger'; +import type {ImageElement} from './ImageLoader'; import ImageLoader from './ImageLoader'; import {copyCSSStyles} from './Util'; import {parseBackgroundImage} from './parsing/background'; +import CanvasRenderer from './renderer/CanvasRenderer'; export class DocumentCloner { scrolledElements: Array<[HTMLElement, number, number]>; referenceElement: HTMLElement; clonedReferenceElement: HTMLElement; documentElement: HTMLElement; - imageLoader: ImageLoader<string>; + imageLoader: ImageLoader<*>; logger: Logger; + options: Options; inlineImages: boolean; copyStyles: boolean; + renderer: (element: HTMLElement, options: Options, logger: Logger) => Promise<*>; - constructor(element: HTMLElement, options: Options, logger: Logger, copyInline: boolean) { + constructor( + element: HTMLElement, + options: Options, + logger: Logger, + copyInline: boolean, + renderer: (element: HTMLElement, options: Options, logger: Logger) => Promise<*> + ) { this.referenceElement = element; this.scrolledElements = []; this.copyStyles = copyInline; this.inlineImages = copyInline; this.logger = logger; + this.options = options; + this.renderer = renderer; this.imageLoader = new ImageLoader(options, logger, window); // $FlowFixMe this.documentElement = this.cloneNode(element.ownerDocument.documentElement); @@ -90,6 +102,51 @@ export class DocumentCloner { } } + if (node instanceof HTMLIFrameElement) { + const tempIframe = node.cloneNode(false); + const iframeKey = generateIframeKey(); + tempIframe.setAttribute('data-html2canvas-internal-iframe-key', iframeKey); + + this.imageLoader.cache[iframeKey] = getIframeDocumentElement(node, this.options) + .then(documentElement => { + return this.renderer( + documentElement, + { + async: this.options.async, + allowTaint: this.options.allowTaint, + backgroundColor: '#ffffff', + canvas: null, + imageTimeout: this.options.imageTimeout, + proxy: this.options.proxy, + removeContainer: this.options.removeContainer, + scale: this.options.scale, + target: new CanvasRenderer(), + type: 'view', + windowWidth: documentElement.ownerDocument.defaultView.innerWidth, + windowHeight: documentElement.ownerDocument.defaultView.innerHeight, + offsetX: documentElement.ownerDocument.defaultView.pageXOffset, + offsetY: documentElement.ownerDocument.defaultView.pageYOffset + }, + this.logger.child(iframeKey) + ); + }) + .then(canvas => { + const iframeCanvas = document.createElement('img'); + iframeCanvas.src = canvas.toDataURL(); + if (tempIframe.parentNode) { + tempIframe.parentNode.replaceChild( + copyCSSStyles( + node.ownerDocument.defaultView.getComputedStyle(node), + iframeCanvas + ), + tempIframe + ); + } + return canvas; + }); + return tempIframe; + } + return node.cloneNode(false); } @@ -99,11 +156,13 @@ export class DocumentCloner { ? document.createTextNode(node.nodeValue) : this.createElementClone(node); - if (this.referenceElement === node && clone instanceof HTMLElement) { + const window = node.ownerDocument.defaultView; + + if (this.referenceElement === node && clone instanceof window.HTMLElement) { this.clonedReferenceElement = clone; } - if (clone instanceof HTMLBodyElement) { + if (clone instanceof window.HTMLBodyElement) { createPseudoHideStyles(clone); } @@ -114,10 +173,10 @@ export class DocumentCloner { } } } - if (node instanceof HTMLElement && clone instanceof HTMLElement) { + if (node instanceof window.HTMLElement && clone instanceof window.HTMLElement) { this.inlineAllImages(inlinePseudoElement(node, clone, PSEUDO_BEFORE)); this.inlineAllImages(inlinePseudoElement(node, clone, PSEUDO_AFTER)); - if (this.copyStyles) { + if (this.copyStyles && !(node instanceof HTMLIFrameElement)) { copyCSSStyles(node.ownerDocument.defaultView.getComputedStyle(node), clone); } this.inlineAllImages(clone); @@ -127,13 +186,11 @@ export class DocumentCloner { switch (node.nodeName) { case 'CANVAS': if (!this.copyStyles) { - // $FlowFixMe cloneCanvasContents(node, clone); } break; case 'TEXTAREA': case 'SELECT': - // $FlowFixMe clone.value = node.value; break; } @@ -248,14 +305,29 @@ const initNode = ([element, x, y]: [HTMLElement, number, number]) => { element.scrollTop = y; }; +const generateIframeKey = (): string => + Math.ceil(Date.now() + Math.random() * 10000000).toString(16); + +const getIframeDocumentElement = ( + node: HTMLIFrameElement, + options: Options +): Promise<HTMLElement> => { + try { + return Promise.resolve(node.contentWindow.document.documentElement); + } catch (e) { + return Promise.reject(); + } +}; + export const cloneWindow = ( ownerDocument: Document, bounds: Bounds, referenceElement: HTMLElement, options: Options, - logger: Logger -): Promise<[HTMLIFrameElement, HTMLElement]> => { - const cloner = new DocumentCloner(referenceElement, options, logger, false); + logger: Logger, + renderer: (element: HTMLElement, options: Options, logger: Logger) => Promise<*> +): Promise<[HTMLIFrameElement, HTMLElement, ImageLoader<ImageElement>]> => { + const cloner = new DocumentCloner(referenceElement, options, logger, false, renderer); const cloneIframeContainer = ownerDocument.createElement('iframe'); cloneIframeContainer.className = 'html2canvas-container'; @@ -275,7 +347,7 @@ export const cloneWindow = ( ); } return new Promise((resolve, reject) => { - let cloneWindow = cloneIframeContainer.contentWindow; + const cloneWindow = cloneIframeContainer.contentWindow; const documentClone = cloneWindow.document; /* Chrome doesn't detect relative background-images assigned in inline <style> sheets when fetched through getComputedStyle @@ -302,7 +374,11 @@ export const cloneWindow = ( cloner.clonedReferenceElement instanceof cloneWindow.HTMLElement || cloner.clonedReferenceElement instanceof HTMLElement ) { - resolve([cloneIframeContainer, cloner.clonedReferenceElement]); + resolve([ + cloneIframeContainer, + cloner.clonedReferenceElement, + cloner.imageLoader + ]); } else { reject( __DEV__ diff --git a/src/Logger.js b/src/Logger.js index bbdf241..c426805 100644 --- a/src/Logger.js +++ b/src/Logger.js @@ -3,9 +3,15 @@ export default class Logger { start: number; + id: ?string; - constructor() { - this.start = Date.now(); + constructor(id: ?string, start: ?number) { + this.start = start ? start : Date.now(); + this.id = id; + } + + child(id: string) { + return new Logger(id, this.start); } // eslint-disable-next-line flowtype/no-weak-types @@ -15,7 +21,10 @@ export default class Logger { .call(window.console.log, window.console) .apply( window.console, - [Date.now() - this.start + 'ms', 'html2canvas:'].concat([].slice.call(args, 0)) + [ + Date.now() - this.start + 'ms', + this.id ? `html2canvas (${this.id}):` : 'html2canvas:' + ].concat([].slice.call(args, 0)) ); } } @@ -27,7 +36,10 @@ export default class Logger { .call(window.console.error, window.console) .apply( window.console, - [Date.now() - this.start + 'ms', 'html2canvas:'].concat([].slice.call(args, 0)) + [ + Date.now() - this.start + 'ms', + this.id ? `html2canvas (${this.id}):` : 'html2canvas:' + ].concat([].slice.call(args, 0)) ); } } diff --git a/src/NodeContainer.js b/src/NodeContainer.js index 17a1f0c..d1558d3 100644 --- a/src/NodeContainer.js +++ b/src/NodeContainer.js @@ -239,6 +239,13 @@ const getImage = ( case 'CANVAS': // $FlowFixMe return imageLoader.loadCanvas(node); + case 'DIV': + const iframeKey = node.getAttribute('data-html2canvas-internal-iframe-key'); + if (iframeKey) { + console.log('ok'); + return iframeKey; + } + break; } return null; diff --git a/src/Window.js b/src/Window.js new file mode 100644 index 0000000..07a8b5e --- /dev/null +++ b/src/Window.js @@ -0,0 +1,131 @@ +/* @flow */ +'use strict'; + +import type {Options} from './index'; + +import Logger from './Logger'; + +import {NodeParser} from './NodeParser'; +import Renderer from './Renderer'; +import ForeignObjectRenderer from './renderer/ForeignObjectRenderer'; + +import Feature from './Feature'; +import {Bounds, parseDocumentSize} from './Bounds'; +import {cloneWindow, DocumentCloner} from './Clone'; +import {FontMetrics} from './Font'; +import Color, {TRANSPARENT} from './Color'; + +export const renderElement = ( + element: HTMLElement, + options: Options, + logger: Logger +): Promise<*> => { + const ownerDocument = element.ownerDocument; + + const windowBounds = new Bounds( + options.offsetX, + options.offsetY, + options.windowWidth, + options.windowHeight + ); + + const bounds = options.type === 'view' ? windowBounds : parseDocumentSize(ownerDocument); + + // http://www.w3.org/TR/css3-background/#special-backgrounds + const documentBackgroundColor = ownerDocument.documentElement + ? new Color(getComputedStyle(ownerDocument.documentElement).backgroundColor) + : TRANSPARENT; + const bodyBackgroundColor = ownerDocument.body + ? new Color(getComputedStyle(ownerDocument.body).backgroundColor) + : TRANSPARENT; + + const backgroundColor = + element === ownerDocument.documentElement + ? documentBackgroundColor.isTransparent() + ? bodyBackgroundColor.isTransparent() + ? options.backgroundColor ? new Color(options.backgroundColor) : null + : bodyBackgroundColor + : documentBackgroundColor + : options.backgroundColor ? new Color(options.backgroundColor) : null; + + // $FlowFixMe + return Feature.SUPPORT_FOREIGNOBJECT_DRAWING.then( + supportForeignObject => + supportForeignObject + ? (cloner => { + if (__DEV__) { + logger.log(`Document cloned, using foreignObject rendering`); + } + + 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, true, renderElement)) + : cloneWindow( + ownerDocument, + windowBounds, + element, + options, + logger, + renderElement + ).then(([container, clonedElement, imageLoader]) => { + if (__DEV__) { + logger.log(`Document cloned, using computed rendering`); + } + + const stack = NodeParser(clonedElement, imageLoader, logger); + const clonedDocument = clonedElement.ownerDocument; + const width = bounds.width; + const height = bounds.height; + + if (backgroundColor === stack.container.style.background.backgroundColor) { + stack.container.style.background.backgroundColor = TRANSPARENT; + } + + 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 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); + } + }); + }) + ); +}; diff --git a/src/index.js b/src/index.js index b2e2476..017528c 100644 --- a/src/index.js +++ b/src/index.js @@ -3,17 +3,9 @@ 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, DocumentCloner} from './Clone'; -import {FontMetrics} from './Font'; -import Color, {TRANSPARENT} from './Color'; +import {renderElement} from './Window'; export type Options = { async: ?boolean, @@ -58,118 +50,7 @@ const html2canvas = (element: HTMLElement, conf: ?Options): Promise<*> => { offsetY: defaultView.pageYOffset }; - const options = {...defaultOptions, ...config}; - - const windowBounds = new Bounds( - options.offsetX, - options.offsetY, - options.windowWidth, - options.windowHeight - ); - - const bounds = options.type === 'view' ? windowBounds : parseDocumentSize(ownerDocument); - - // http://www.w3.org/TR/css3-background/#special-backgrounds - const documentBackgroundColor = ownerDocument.documentElement - ? new Color(getComputedStyle(ownerDocument.documentElement).backgroundColor) - : TRANSPARENT; - const bodyBackgroundColor = ownerDocument.body - ? new Color(getComputedStyle(ownerDocument.body).backgroundColor) - : TRANSPARENT; - - const backgroundColor = - element === ownerDocument.documentElement - ? documentBackgroundColor.isTransparent() - ? bodyBackgroundColor.isTransparent() - ? options.backgroundColor ? new Color(options.backgroundColor) : null - : bodyBackgroundColor - : documentBackgroundColor - : options.backgroundColor ? new Color(options.backgroundColor) : null; - - // $FlowFixMe - const result = Feature.SUPPORT_FOREIGNOBJECT_DRAWING.then( - supportForeignObject => - supportForeignObject - ? (cloner => { - if (__DEV__) { - logger.log(`Document cloned, using foreignObject rendering`); - } - - 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, true)) - : cloneWindow( - ownerDocument, - windowBounds, - element, - options, - logger - ).then(([container, clonedElement]) => { - if (__DEV__) { - logger.log(`Document cloned, using computed rendering`); - } - - 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; - - if (backgroundColor === stack.container.style.background.backgroundColor) { - stack.container.style.background.backgroundColor = TRANSPARENT; - } - - 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 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); - } - }); - }) - ); + const result = renderElement(element, {...defaultOptions, ...config}, logger); if (__DEV__) { return result.catch(e => { diff --git a/tests/reftests/iframe.html b/tests/reftests/iframe.html index e6a5cf1..1aa7f03 100644 --- a/tests/reftests/iframe.html +++ b/tests/reftests/iframe.html @@ -5,6 +5,7 @@ <script type="text/javascript" src="../test.js"></script> </head> <body> -<iframe src="/tests/assets/iframe/frame1.html" width="500" height="500"></iframe> + <div style="background: cornflowerblue; padding: 20px; width: 200px;">Parent document content</div> + <iframe src="/tests/assets/iframe/frame1.html" width="500" height="500"></iframe> </body> </html>