mirror of
https://github.com/niklasvh/html2canvas.git
synced 2023-08-10 21:13:10 +03:00
Implement proxied cross-origin iframe rendering
This commit is contained in:
parent
f84cdb80b3
commit
f20dbe8c8e
188
src/Clone.js
188
src/Clone.js
@ -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;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -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;
|
||||||
|
@ -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);
|
||||||
|
@ -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}`
|
||||||
|
@ -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) {
|
||||||
|
@ -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>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user