Implement proxied cross-origin iframe rendering

This commit is contained in:
MoyuScript 2017-09-25 22:53:09 +08:00
parent f84cdb80b3
commit f20dbe8c8e
6 changed files with 139 additions and 76 deletions

View File

@ -5,6 +5,8 @@ import type {Options} from './index';
import type Logger from './Logger'; import type Logger from './Logger';
import type {ImageElement} from './ImageLoader'; import type {ImageElement} from './ImageLoader';
import {parseBounds} from './Bounds';
import {Proxy} from './Proxy';
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';
@ -130,20 +132,24 @@ export class DocumentCloner {
this.logger.child(iframeKey) this.logger.child(iframeKey)
); );
}) })
.then(canvas => { .then(
const iframeCanvas = document.createElement('img'); canvas =>
iframeCanvas.src = canvas.toDataURL(); new Promise((resolve, reject) => {
if (tempIframe.parentNode) { const iframeCanvas = document.createElement('img');
tempIframe.parentNode.replaceChild( iframeCanvas.onload = () => resolve(canvas);
copyCSSStyles( iframeCanvas.onerror = reject;
node.ownerDocument.defaultView.getComputedStyle(node), iframeCanvas.src = canvas.toDataURL();
iframeCanvas if (tempIframe.parentNode) {
), tempIframe.parentNode.replaceChild(
tempIframe copyCSSStyles(
); node.ownerDocument.defaultView.getComputedStyle(node),
} iframeCanvas
return canvas; ),
}); tempIframe
);
}
})
);
return tempIframe; return tempIframe;
} }
@ -308,6 +314,8 @@ const initNode = ([element, x, y]: [HTMLElement, number, number]) => {
const generateIframeKey = (): string => const generateIframeKey = (): string =>
Math.ceil(Date.now() + Math.random() * 10000000).toString(16); Math.ceil(Date.now() + Math.random() * 10000000).toString(16);
const DATA_URI_REGEXP = /^data:text\/(.+);(base64)?,(.*)$/i;
const getIframeDocumentElement = ( const getIframeDocumentElement = (
node: HTMLIFrameElement, node: HTMLIFrameElement,
options: Options options: Options
@ -315,19 +323,44 @@ const getIframeDocumentElement = (
try { try {
return Promise.resolve(node.contentWindow.document.documentElement); return Promise.resolve(node.contentWindow.document.documentElement);
} catch (e) { } catch (e) {
return Promise.reject(); return options.proxy
? Proxy(node.src, options)
.then(html => {
const match = html.match(DATA_URI_REGEXP);
if (!match) {
return Promise.reject();
}
return match[2] === 'base64'
? window.atob(decodeURIComponent(match[3]))
: decodeURIComponent(match[3]);
})
.then(html =>
createIframeContainer(
node.ownerDocument,
parseBounds(node)
).then(cloneIframeContainer => {
const cloneWindow = cloneIframeContainer.contentWindow;
const documentClone = cloneWindow.document;
documentClone.open();
documentClone.write(html);
const iframeLoad = iframeLoader(cloneIframeContainer).then(
() => documentClone.documentElement
);
documentClone.close();
return iframeLoad;
})
)
: Promise.reject();
} }
}; };
export const cloneWindow = ( const createIframeContainer = (
ownerDocument: Document, ownerDocument: Document,
bounds: Bounds, bounds: Bounds
referenceElement: HTMLElement, ): Promise<HTMLIFrameElement> => {
options: Options,
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'); const cloneIframeContainer = ownerDocument.createElement('iframe');
cloneIframeContainer.className = 'html2canvas-container'; cloneIframeContainer.className = 'html2canvas-container';
@ -339,58 +372,81 @@ export const cloneWindow = (
cloneIframeContainer.width = bounds.width.toString(); cloneIframeContainer.width = bounds.width.toString();
cloneIframeContainer.height = bounds.height.toString(); cloneIframeContainer.height = bounds.height.toString();
cloneIframeContainer.scrolling = 'no'; // ios won't scroll without it cloneIframeContainer.scrolling = 'no'; // ios won't scroll without it
if (ownerDocument.body) { if (!ownerDocument.body) {
ownerDocument.body.appendChild(cloneIframeContainer);
} else {
return Promise.reject( return Promise.reject(
__DEV__ ? `Body element not found in Document that is getting rendered` : '' __DEV__ ? `Body element not found in Document that is getting rendered` : ''
); );
} }
ownerDocument.body.appendChild(cloneIframeContainer);
return Promise.resolve(cloneIframeContainer);
};
const iframeLoader = (cloneIframeContainer: HTMLIFrameElement): Promise<HTMLIFrameElement> => {
const cloneWindow = cloneIframeContainer.contentWindow;
const documentClone = cloneWindow.document;
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
cloneWindow.onload = cloneIframeContainer.onload = documentClone.onreadystatechange = () => {
const interval = setInterval(() => {
if (
documentClone.body.childNodes.length > 0 &&
documentClone.readyState === 'complete'
) {
clearInterval(interval);
resolve(cloneIframeContainer);
}
}, 50);
};
});
};
export const cloneWindow = (
ownerDocument: Document,
bounds: Bounds,
referenceElement: HTMLElement,
options: Options,
logger: Logger,
renderer: (element: HTMLElement, options: Options, logger: Logger) => Promise<*>
): Promise<[HTMLIFrameElement, HTMLElement, ImageLoader<ImageElement>]> => {
const cloner = new DocumentCloner(referenceElement, options, logger, false, renderer);
return createIframeContainer(ownerDocument, bounds).then(cloneIframeContainer => {
const 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
if window url is about:blank, we can assign the url to current by writing onto the document if window url is about:blank, we can assign the url to current by writing onto the document
*/ */
cloneWindow.onload = cloneIframeContainer.onload = () => {
const interval = setInterval(() => { const iframeLoad = iframeLoader(cloneIframeContainer).then(() => {
if (documentClone.body.childNodes.length > 0) { cloner.scrolledElements.forEach(initNode);
cloner.scrolledElements.forEach(initNode); if (options.type === 'view') {
clearInterval(interval); cloneWindow.scrollTo(bounds.left, bounds.top);
if (options.type === 'view') { if (
cloneWindow.scrollTo(bounds.left, bounds.top); /(iPad|iPhone|iPod)/g.test(navigator.userAgent) &&
if ( (cloneWindow.scrollY !== bounds.top || cloneWindow.scrollX !== bounds.left)
/(iPad|iPhone|iPod)/g.test(navigator.userAgent) && ) {
(cloneWindow.scrollY !== bounds.top || documentClone.documentElement.style.top = -bounds.top + 'px';
cloneWindow.scrollX !== bounds.left) documentClone.documentElement.style.left = -bounds.left + 'px';
) { documentClone.documentElement.style.position = 'absolute';
documentClone.documentElement.style.top = -bounds.top + 'px';
documentClone.documentElement.style.left = -bounds.left + 'px';
documentClone.documentElement.style.position = 'absolute';
}
}
if (
cloner.clonedReferenceElement instanceof cloneWindow.HTMLElement ||
cloner.clonedReferenceElement instanceof
ownerDocument.defaultView.HTMLElement ||
cloner.clonedReferenceElement instanceof HTMLElement
) {
resolve([
cloneIframeContainer,
cloner.clonedReferenceElement,
cloner.imageLoader
]);
} else {
reject(
__DEV__
? `Error finding the ${referenceElement.nodeName} in the cloned document`
: ''
);
}
} }
}, 50); }
}; return cloner.clonedReferenceElement instanceof cloneWindow.HTMLElement ||
cloner.clonedReferenceElement instanceof ownerDocument.defaultView.HTMLElement ||
cloner.clonedReferenceElement instanceof HTMLElement
? Promise.resolve([
cloneIframeContainer,
cloner.clonedReferenceElement,
cloner.imageLoader
])
: Promise.reject(
__DEV__
? `Error finding the ${referenceElement.nodeName} in the cloned document`
: ''
);
});
documentClone.open(); documentClone.open();
documentClone.write('<!DOCTYPE html><html></html>'); documentClone.write('<!DOCTYPE html><html></html>');
@ -401,5 +457,7 @@ export const cloneWindow = (
documentClone.documentElement documentClone.documentElement
); );
documentClone.close(); documentClone.close();
return iframeLoad;
}); });
}; };

View File

@ -239,10 +239,9 @@ const getImage = (
case 'CANVAS': case 'CANVAS':
// $FlowFixMe // $FlowFixMe
return imageLoader.loadCanvas(node); return imageLoader.loadCanvas(node);
case 'DIV': case 'IFRAME':
const iframeKey = node.getAttribute('data-html2canvas-internal-iframe-key'); const iframeKey = node.getAttribute('data-html2canvas-internal-iframe-key');
if (iframeKey) { if (iframeKey) {
console.log('ok');
return iframeKey; return iframeKey;
} }
break; break;

View File

@ -46,13 +46,18 @@ const parseNodeTree = (
for (let childNode = node.firstChild, nextNode; childNode; childNode = nextNode) { for (let childNode = node.firstChild, nextNode; childNode; childNode = nextNode) {
nextNode = childNode.nextSibling; nextNode = childNode.nextSibling;
const defaultView = childNode.ownerDocument.defaultView; const defaultView = childNode.ownerDocument.defaultView;
if (childNode instanceof defaultView.Text || childNode instanceof Text) { if (
childNode instanceof defaultView.Text ||
childNode instanceof Text ||
(defaultView.parent && childNode instanceof defaultView.parent.Text)
) {
if (childNode.data.trim().length > 0) { if (childNode.data.trim().length > 0) {
parent.childNodes.push(TextContainer.fromTextNode(childNode, parent)); parent.childNodes.push(TextContainer.fromTextNode(childNode, parent));
} }
} else if ( } else if (
childNode instanceof defaultView.HTMLElement || childNode instanceof defaultView.HTMLElement ||
childNode instanceof HTMLElement childNode instanceof HTMLElement ||
(defaultView.parent && childNode instanceof defaultView.parent.HTMLElement)
) { ) {
if (IGNORED_NODE_NAMES.indexOf(childNode.nodeName) === -1) { if (IGNORED_NODE_NAMES.indexOf(childNode.nodeName) === -1) {
const container = new NodeContainer(childNode, parent, imageLoader, index++); const container = new NodeContainer(childNode, parent, imageLoader, index++);
@ -99,7 +104,8 @@ const parseNodeTree = (
} }
} else if ( } else if (
childNode instanceof defaultView.SVGSVGElement || childNode instanceof defaultView.SVGSVGElement ||
childNode instanceof SVGSVGElement childNode instanceof SVGSVGElement ||
(defaultView.parent && childNode instanceof defaultView.parent.SVGSVGElement)
) { ) {
const container = new NodeContainer(childNode, parent, imageLoader, index++); const container = new NodeContainer(childNode, parent, imageLoader, index++);
const treatAsRealStackingContext = createsRealStackingContext(container, childNode); const treatAsRealStackingContext = createsRealStackingContext(container, childNode);

View File

@ -31,7 +31,7 @@ export const Proxy = (src: string, options: Options): Promise<string> => {
} else { } else {
reject( reject(
__DEV__ __DEV__
? `Failed to proxy image ${src.substring( ? `Failed to proxy resource ${src.substring(
0, 0,
256 256
)} with status code ${xhr.status}` )} with status code ${xhr.status}`

View File

@ -38,7 +38,10 @@ export default class CanvasRenderer implements RenderTarget<HTMLCanvasElement> {
this.ctx.scale(this.options.scale, this.options.scale); this.ctx.scale(this.options.scale, this.options.scale);
this.ctx.textBaseline = 'bottom'; this.ctx.textBaseline = 'bottom';
options.logger.log(`Canvas renderer initialized with scale ${this.options.scale}`); options.logger.log(
`Canvas renderer initialized (${options.width}x${options.height}) with scale ${this
.options.scale}`
);
} }
clip(clipPaths: Array<Path>, callback: () => void) { clip(clipPaths: Array<Path>, callback: () => void) {

View File

@ -2,12 +2,9 @@
<html> <html>
<head> <head>
<title>cross-origin iframe test</title> <title>cross-origin iframe test</title>
<script>
var h2cOptions = {proxy: "http://localhost:8082"};
</script>
<script type="text/javascript" src="../test.js"></script> <script type="text/javascript" src="../test.js"></script>
</head> </head>
<body> <body>
<iframe src="https://html2canvas.hertzen.com/" width="800" height="800"></iframe> <iframe src="http://localhost:8081/assets/iframe/frame1.html" width="800" height="800"></iframe>
</body> </body>
</html> </html>