Implement Iframe rendering

This commit is contained in:
Niklas von Hertzen 2017-09-11 22:36:23 +08:00
parent aa47a3a3a6
commit 90a8422938
6 changed files with 247 additions and 139 deletions

View File

@ -3,27 +3,39 @@
import type {Bounds} from './Bounds'; import type {Bounds} from './Bounds';
import type {Options} from './index'; import type {Options} from './index';
import type Logger from './Logger'; import type Logger from './Logger';
import type {ImageElement} from './ImageLoader';
import ImageLoader from './ImageLoader'; import ImageLoader from './ImageLoader';
import {copyCSSStyles} from './Util'; import {copyCSSStyles} from './Util';
import {parseBackgroundImage} from './parsing/background'; import {parseBackgroundImage} from './parsing/background';
import CanvasRenderer from './renderer/CanvasRenderer';
export class DocumentCloner { export class DocumentCloner {
scrolledElements: Array<[HTMLElement, number, number]>; scrolledElements: Array<[HTMLElement, number, number]>;
referenceElement: HTMLElement; referenceElement: HTMLElement;
clonedReferenceElement: HTMLElement; clonedReferenceElement: HTMLElement;
documentElement: HTMLElement; documentElement: HTMLElement;
imageLoader: ImageLoader<string>; imageLoader: ImageLoader<*>;
logger: Logger; logger: Logger;
options: Options;
inlineImages: boolean; inlineImages: boolean;
copyStyles: 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.referenceElement = element;
this.scrolledElements = []; this.scrolledElements = [];
this.copyStyles = copyInline; this.copyStyles = copyInline;
this.inlineImages = copyInline; this.inlineImages = copyInline;
this.logger = logger; this.logger = logger;
this.options = options;
this.renderer = renderer;
this.imageLoader = new ImageLoader(options, logger, window); this.imageLoader = new ImageLoader(options, logger, window);
// $FlowFixMe // $FlowFixMe
this.documentElement = this.cloneNode(element.ownerDocument.documentElement); 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); return node.cloneNode(false);
} }
@ -99,11 +156,13 @@ export class DocumentCloner {
? document.createTextNode(node.nodeValue) ? document.createTextNode(node.nodeValue)
: this.createElementClone(node); : 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; this.clonedReferenceElement = clone;
} }
if (clone instanceof HTMLBodyElement) { if (clone instanceof window.HTMLBodyElement) {
createPseudoHideStyles(clone); 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_BEFORE));
this.inlineAllImages(inlinePseudoElement(node, clone, PSEUDO_AFTER)); this.inlineAllImages(inlinePseudoElement(node, clone, PSEUDO_AFTER));
if (this.copyStyles) { if (this.copyStyles && !(node instanceof HTMLIFrameElement)) {
copyCSSStyles(node.ownerDocument.defaultView.getComputedStyle(node), clone); copyCSSStyles(node.ownerDocument.defaultView.getComputedStyle(node), clone);
} }
this.inlineAllImages(clone); this.inlineAllImages(clone);
@ -127,13 +186,11 @@ export class DocumentCloner {
switch (node.nodeName) { switch (node.nodeName) {
case 'CANVAS': case 'CANVAS':
if (!this.copyStyles) { if (!this.copyStyles) {
// $FlowFixMe
cloneCanvasContents(node, clone); cloneCanvasContents(node, clone);
} }
break; break;
case 'TEXTAREA': case 'TEXTAREA':
case 'SELECT': case 'SELECT':
// $FlowFixMe
clone.value = node.value; clone.value = node.value;
break; break;
} }
@ -248,14 +305,29 @@ const initNode = ([element, x, y]: [HTMLElement, number, number]) => {
element.scrollTop = y; 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 = ( export const cloneWindow = (
ownerDocument: Document, ownerDocument: Document,
bounds: Bounds, bounds: Bounds,
referenceElement: HTMLElement, referenceElement: HTMLElement,
options: Options, options: Options,
logger: Logger logger: Logger,
): Promise<[HTMLIFrameElement, HTMLElement]> => { renderer: (element: HTMLElement, options: Options, logger: Logger) => Promise<*>
const cloner = new DocumentCloner(referenceElement, options, logger, false); ): Promise<[HTMLIFrameElement, HTMLElement, ImageLoader<ImageElement>]> => {
const cloner = new DocumentCloner(referenceElement, options, logger, false, renderer);
const cloneIframeContainer = ownerDocument.createElement('iframe'); const cloneIframeContainer = ownerDocument.createElement('iframe');
cloneIframeContainer.className = 'html2canvas-container'; cloneIframeContainer.className = 'html2canvas-container';
@ -275,7 +347,7 @@ export const cloneWindow = (
); );
} }
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let cloneWindow = cloneIframeContainer.contentWindow; const cloneWindow = cloneIframeContainer.contentWindow;
const documentClone = cloneWindow.document; const documentClone = cloneWindow.document;
/* Chrome doesn't detect relative background-images assigned in inline <style> sheets when fetched through getComputedStyle /* 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 cloneWindow.HTMLElement ||
cloner.clonedReferenceElement instanceof HTMLElement cloner.clonedReferenceElement instanceof HTMLElement
) { ) {
resolve([cloneIframeContainer, cloner.clonedReferenceElement]); resolve([
cloneIframeContainer,
cloner.clonedReferenceElement,
cloner.imageLoader
]);
} else { } else {
reject( reject(
__DEV__ __DEV__

View File

@ -3,9 +3,15 @@
export default class Logger { export default class Logger {
start: number; start: number;
id: ?string;
constructor() { constructor(id: ?string, start: ?number) {
this.start = Date.now(); 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 // eslint-disable-next-line flowtype/no-weak-types
@ -15,7 +21,10 @@ export default class Logger {
.call(window.console.log, window.console) .call(window.console.log, window.console)
.apply( .apply(
window.console, 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) .call(window.console.error, window.console)
.apply( .apply(
window.console, 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))
); );
} }
} }

View File

@ -239,6 +239,13 @@ const getImage = (
case 'CANVAS': case 'CANVAS':
// $FlowFixMe // $FlowFixMe
return imageLoader.loadCanvas(node); 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; return null;

131
src/Window.js Normal file
View File

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

View File

@ -3,17 +3,9 @@
import type {RenderTarget} from './Renderer'; import type {RenderTarget} from './Renderer';
import {NodeParser} from './NodeParser';
import Renderer from './Renderer';
import ForeignObjectRenderer from './renderer/ForeignObjectRenderer';
import CanvasRenderer from './renderer/CanvasRenderer'; import CanvasRenderer from './renderer/CanvasRenderer';
import Logger from './Logger'; import Logger from './Logger';
import ImageLoader from './ImageLoader'; import {renderElement} from './Window';
import Feature from './Feature';
import {Bounds, parseDocumentSize} from './Bounds';
import {cloneWindow, DocumentCloner} from './Clone';
import {FontMetrics} from './Font';
import Color, {TRANSPARENT} from './Color';
export type Options = { export type Options = {
async: ?boolean, async: ?boolean,
@ -58,118 +50,7 @@ const html2canvas = (element: HTMLElement, conf: ?Options): Promise<*> => {
offsetY: defaultView.pageYOffset offsetY: defaultView.pageYOffset
}; };
const options = {...defaultOptions, ...config}; const result = renderElement(element, {...defaultOptions, ...config}, logger);
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);
}
});
})
);
if (__DEV__) { if (__DEV__) {
return result.catch(e => { return result.catch(e => {

View File

@ -5,6 +5,7 @@
<script type="text/javascript" src="../test.js"></script> <script type="text/javascript" src="../test.js"></script>
</head> </head>
<body> <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> </body>
</html> </html>