Clone document before parsing it

This commit is contained in:
Niklas von Hertzen 2017-08-01 20:54:18 +08:00
parent 7a3bad2fcb
commit 478155af64
5 changed files with 230 additions and 36 deletions

View File

@ -49,10 +49,8 @@ export const parseBounds = (node: HTMLElement, isTransformed: boolean): Bounds =
}; };
const offsetBounds = (node: HTMLElement): Bounds => { const offsetBounds = (node: HTMLElement): Bounds => {
const parent = // //$FlowFixMe
node.offsetParent instanceof HTMLElement const parent = node.offsetParent ? offsetBounds(node.offsetParent) : {top: 0, left: 0};
? offsetBounds(node.offsetParent)
: {top: 0, left: 0};
return new Bounds( return new Bounds(
node.offsetLeft + parent.left, node.offsetLeft + parent.left,

160
src/Clone.js Normal file
View File

@ -0,0 +1,160 @@
/* @flow */
'use strict';
import type {Bounds} from './Bounds';
import type {Options} from './index';
const restoreOwnerScroll = (ownerDocument: Document, x: number, y: number) => {
if (
ownerDocument.defaultView &&
(x !== ownerDocument.defaultView.pageXOffset || y !== ownerDocument.defaultView.pageYOffset)
) {
ownerDocument.defaultView.scrollTo(x, y);
}
};
const cloneCanvasContents = (canvas: HTMLCanvasElement, clonedCanvas: HTMLCanvasElement) => {
try {
if (clonedCanvas) {
clonedCanvas.width = canvas.width;
clonedCanvas.height = canvas.height;
clonedCanvas
.getContext('2d')
.putImageData(
canvas.getContext('2d').getImageData(0, 0, canvas.width, canvas.height),
0,
0
);
}
} 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;
}
for (let child = node.firstChild; child; child = child.nextSibling) {
if (child.nodeType !== Node.ELEMENT_NODE || child.nodeName !== 'SCRIPT') {
clone.appendChild(cloneNode(child, referenceElement, scrolledElements));
}
}
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;
}
}
return clone;
};
const initNode = ([element, x, y]: [HTMLElement, number, number]) => {
element.scrollLeft = x;
element.scrollTop = y;
};
export const cloneWindow = (
documentToBeCloned: Document,
ownerDocument: Document,
bounds: Bounds,
referenceElement: HTMLElement,
options: Options
): 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 cloneIframeContainer = ownerDocument.createElement('iframe');
cloneIframeContainer.className = 'html2canvas-container';
cloneIframeContainer.style.visibility = 'hidden';
cloneIframeContainer.style.position = 'fixed';
cloneIframeContainer.style.left = '-10000px';
cloneIframeContainer.style.top = '0px';
cloneIframeContainer.style.border = '0';
cloneIframeContainer.width = bounds.width.toString();
cloneIframeContainer.height = bounds.height.toString();
cloneIframeContainer.scrolling = 'no'; // ios won't scroll without it
if (ownerDocument.body) {
ownerDocument.body.appendChild(cloneIframeContainer);
} else {
return Promise.reject(
__DEV__ ? `Body element not found in Document that is getting rendered` : ''
);
}
return new Promise((resolve, reject) => {
const documentClone = cloneIframeContainer.contentWindow.document;
/* Chrome doesn't detect relative background-images assigned in inline <style> sheets when fetched through getComputedStyle
if window url is about:blank, we can assign the url to current by writing onto the document
*/
cloneIframeContainer.contentWindow.onload = cloneIframeContainer.onload = () => {
console.log('iframe load');
const interval = setInterval(() => {
if (documentClone.body.childNodes.length > 0) {
scrolledElements.forEach(initNode);
clearInterval(interval);
if (options.type === 'view') {
cloneIframeContainer.contentWindow.scrollTo(bounds.left, bounds.top);
if (
/(iPad|iPhone|iPod)/g.test(navigator.userAgent) &&
(cloneIframeContainer.contentWindow.scrollY !== bounds.top ||
cloneIframeContainer.contentWindow.scrollX !== bounds.left)
) {
documentClone.documentElement.style.top = -bounds.top + 'px';
documentClone.documentElement.style.left = -bounds.left + 'px';
documentClone.documentElement.style.position = 'absolute';
}
}
if (referenceElementSearch[1] instanceof HTMLElement) {
resolve([cloneIframeContainer, referenceElementSearch[1]]);
} else {
reject(
__DEV__
? `Error finding the ${referenceElement.nodeName} in the cloned document`
: ''
);
}
}
}, 50);
};
documentClone.open();
documentClone.write('<!DOCTYPE html><html></html>');
// Chrome scrolls the parent document for some reason after the write to the cloned window???
restoreOwnerScroll(documentToBeCloned, bounds.left, bounds.top);
documentClone.replaceChild(
documentClone.adoptNode(documentElement),
documentClone.documentElement
);
documentClone.close();
});
};

View File

@ -87,10 +87,10 @@ export default class NodeContainer {
transform: parseTransform(style), transform: parseTransform(style),
zIndex: parseZIndex(style.zIndex) zIndex: parseZIndex(style.zIndex)
}; };
this.image = this.image =
node instanceof HTMLImageElement // $FlowFixMe
? imageLoader.loadImage(node.currentSrc || node.src) node.tagName === 'IMG' ? imageLoader.loadImage(node.currentSrc || node.src) : null;
: null;
this.bounds = parseBounds(node, this.isTransformed()); this.bounds = parseBounds(node, this.isTransformed());
if (__DEV__) { if (__DEV__) {
this.name = `${node.tagName.toLowerCase()}${node.id this.name = `${node.tagName.toLowerCase()}${node.id

View File

@ -36,17 +36,20 @@ const parseNodeTree = (
imageLoader: ImageLoader imageLoader: ImageLoader
): void => { ): void => {
node.childNodes.forEach((childNode: Node) => { node.childNodes.forEach((childNode: Node) => {
if (childNode instanceof Text) { if (childNode.nodeType === Node.TEXT_NODE) {
//$FlowFixMe
if (childNode.data.trim().length > 0) { if (childNode.data.trim().length > 0) {
//$FlowFixMe
parent.textNodes.push(new TextContainer(childNode, parent)); parent.textNodes.push(new TextContainer(childNode, parent));
} }
} else if (childNode instanceof HTMLElement) { } else if (childNode.nodeType === Node.ELEMENT_NODE) {
if (IGNORED_NODE_NAMES.indexOf(childNode.nodeName) === -1) { if (IGNORED_NODE_NAMES.indexOf(childNode.nodeName) === -1) {
const container = new NodeContainer(childNode, parent, imageLoader); const childElement = flowRefineToHTMLElement(childNode);
const container = new NodeContainer(childElement, parent, imageLoader);
if (container.isVisible()) { if (container.isVisible()) {
const treatAsRealStackingContext = createsRealStackingContext( const treatAsRealStackingContext = createsRealStackingContext(
container, container,
childNode childElement
); );
if (treatAsRealStackingContext || createsStackingContext(container)) { if (treatAsRealStackingContext || createsStackingContext(container)) {
// for treatAsRealStackingContext:false, any positioned descendants and descendants // for treatAsRealStackingContext:false, any positioned descendants and descendants
@ -61,10 +64,10 @@ const parseNodeTree = (
treatAsRealStackingContext treatAsRealStackingContext
); );
parentStack.contexts.push(childStack); parentStack.contexts.push(childStack);
parseNodeTree(childNode, container, childStack, imageLoader); parseNodeTree(childElement, container, childStack, imageLoader);
} else { } else {
stack.children.push(container); stack.children.push(container);
parseNodeTree(childNode, container, stack, imageLoader); parseNodeTree(childElement, container, stack, imageLoader);
} }
} }
} }
@ -93,3 +96,6 @@ const isBodyWithTransparentRoot = (container: NodeContainer, node: HTMLElement):
container.parent.style.background.backgroundColor.isTransparent() container.parent.style.background.backgroundColor.isTransparent()
); );
}; };
//$FlowFixMe
const flowRefineToHTMLElement = (node: Node): HTMLElement => node;

View File

@ -5,6 +5,8 @@ import {NodeParser} from './NodeParser';
import CanvasRenderer from './CanvasRenderer'; import CanvasRenderer from './CanvasRenderer';
import Logger from './Logger'; import Logger from './Logger';
import ImageLoader from './ImageLoader'; import ImageLoader from './ImageLoader';
import {Bounds} from './Bounds';
import {cloneWindow} from './Clone';
import Color from './Color'; import Color from './Color';
export type Options = { export type Options = {
@ -12,36 +14,64 @@ export type Options = {
imageTimeout: number, imageTimeout: number,
proxy: string, proxy: string,
canvas: HTMLCanvasElement, canvas: HTMLCanvasElement,
allowTaint: true allowTaint: true,
type: string
}; };
const html2canvas = (element: HTMLElement, options: Options): Promise<HTMLCanvasElement> => { const html2canvas = (element: HTMLElement, options: Options): Promise<HTMLCanvasElement> => {
const logger = new Logger(); const logger = new Logger();
const imageLoader = new ImageLoader(options, logger);
const stack = NodeParser(element, imageLoader, logger);
const canvas = document.createElement('canvas');
const scale = window.devicePixelRatio; const ownerDocument = element.ownerDocument;
const width = window.innerWidth; const defaultView = ownerDocument.defaultView;
const height = window.innerHeight; const windowBounds = new Bounds(
canvas.width = Math.floor(width * scale); defaultView.pageXOffset,
canvas.height = Math.floor(height * scale); defaultView.pageYOffset,
canvas.style.width = `${width}px`; defaultView.innerWidth,
canvas.style.height = `${height}px`; defaultView.innerHeight
);
// http://www.w3.org/TR/css3-background/#special-backgrounds return cloneWindow(
const backgroundColor = ownerDocument,
element === element.ownerDocument.documentElement ownerDocument,
? stack.container.style.background.backgroundColor.isTransparent() windowBounds,
? element.ownerDocument.body instanceof HTMLElement element,
? new Color(getComputedStyle(element.ownerDocument.body).backgroundColor) options
: null ).then(([container, clonedElement]) => {
: stack.container.style.background.backgroundColor if (__DEV__) {
: null; logger.log(`Document cloned`);
}
return imageLoader.ready().then(imageStore => { const imageLoader = new ImageLoader(options, logger);
const renderer = new CanvasRenderer(canvas, {scale, backgroundColor, imageStore}); const stack = NodeParser(clonedElement, imageLoader, logger);
return renderer.render(stack); const canvas = ownerDocument.createElement('canvas');
const scale = defaultView.devicePixelRatio;
const width = window.innerWidth;
const height = window.innerHeight;
canvas.width = Math.floor(width * scale);
canvas.height = Math.floor(height * scale);
canvas.style.width = `${width}px`;
canvas.style.height = `${height}px`;
// http://www.w3.org/TR/css3-background/#special-backgrounds
const backgroundColor =
clonedElement === ownerDocument.documentElement
? stack.container.style.background.backgroundColor.isTransparent()
? ownerDocument.body instanceof HTMLElement
? new Color(getComputedStyle(ownerDocument.body).backgroundColor)
: null
: stack.container.style.background.backgroundColor
: null;
return imageLoader.ready().then(imageStore => {
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 renderer = new CanvasRenderer(canvas, {scale, backgroundColor, imageStore});
return renderer.render(stack);
});
}); });
}; };