Implement proxied cross-origin iframe rendering

This commit is contained in:
Niklas von Hertzen 2017-09-25 22:53:09 +08:00
parent 57dc7137b2
commit 929b9de6e0
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 {ImageElement} from './ImageLoader';
import {parseBounds} from './Bounds';
import {Proxy} from './Proxy';
import ImageLoader from './ImageLoader';
import {copyCSSStyles} from './Util';
import {parseBackgroundImage} from './parsing/background';
@ -130,20 +132,24 @@ export class DocumentCloner {
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;
});
.then(
canvas =>
new Promise((resolve, reject) => {
const iframeCanvas = document.createElement('img');
iframeCanvas.onload = () => resolve(canvas);
iframeCanvas.onerror = reject;
iframeCanvas.src = canvas.toDataURL();
if (tempIframe.parentNode) {
tempIframe.parentNode.replaceChild(
copyCSSStyles(
node.ownerDocument.defaultView.getComputedStyle(node),
iframeCanvas
),
tempIframe
);
}
})
);
return tempIframe;
}
@ -308,6 +314,8 @@ const initNode = ([element, x, y]: [HTMLElement, number, number]) => {
const generateIframeKey = (): string =>
Math.ceil(Date.now() + Math.random() * 10000000).toString(16);
const DATA_URI_REGEXP = /^data:text\/(.+);(base64)?,(.*)$/i;
const getIframeDocumentElement = (
node: HTMLIFrameElement,
options: Options
@ -315,19 +323,44 @@ const getIframeDocumentElement = (
try {
return Promise.resolve(node.contentWindow.document.documentElement);
} 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,
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);
bounds: Bounds
): Promise<HTMLIFrameElement> => {
const cloneIframeContainer = ownerDocument.createElement('iframe');
cloneIframeContainer.className = 'html2canvas-container';
@ -339,58 +372,81 @@ export const cloneWindow = (
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 {
if (!ownerDocument.body) {
return Promise.reject(
__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) => {
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 documentClone = cloneWindow.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
*/
cloneWindow.onload = cloneIframeContainer.onload = () => {
const interval = setInterval(() => {
if (documentClone.body.childNodes.length > 0) {
cloner.scrolledElements.forEach(initNode);
clearInterval(interval);
if (options.type === 'view') {
cloneWindow.scrollTo(bounds.left, bounds.top);
if (
/(iPad|iPhone|iPod)/g.test(navigator.userAgent) &&
(cloneWindow.scrollY !== bounds.top ||
cloneWindow.scrollX !== bounds.left)
) {
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`
: ''
);
}
if window url is about:blank, we can assign the url to current by writing onto the document
*/
const iframeLoad = iframeLoader(cloneIframeContainer).then(() => {
cloner.scrolledElements.forEach(initNode);
if (options.type === 'view') {
cloneWindow.scrollTo(bounds.left, bounds.top);
if (
/(iPad|iPhone|iPod)/g.test(navigator.userAgent) &&
(cloneWindow.scrollY !== bounds.top || cloneWindow.scrollX !== bounds.left)
) {
documentClone.documentElement.style.top = -bounds.top + 'px';
documentClone.documentElement.style.left = -bounds.left + 'px';
documentClone.documentElement.style.position = 'absolute';
}
}, 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.write('<!DOCTYPE html><html></html>');
@ -401,5 +457,7 @@ export const cloneWindow = (
documentClone.documentElement
);
documentClone.close();
return iframeLoad;
});
};

View File

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

View File

@ -46,13 +46,18 @@ const parseNodeTree = (
for (let childNode = node.firstChild, nextNode; childNode; childNode = nextNode) {
nextNode = childNode.nextSibling;
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) {
parent.childNodes.push(TextContainer.fromTextNode(childNode, parent));
}
} else if (
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) {
const container = new NodeContainer(childNode, parent, imageLoader, index++);
@ -99,7 +104,8 @@ const parseNodeTree = (
}
} else if (
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 treatAsRealStackingContext = createsRealStackingContext(container, childNode);

View File

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

View File

@ -2,12 +2,9 @@
<html>
<head>
<title>cross-origin iframe test</title>
<script>
var h2cOptions = {proxy: "http://localhost:8082"};
</script>
<script type="text/javascript" src="../test.js"></script>
</head>
<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>
</html>