mirror of
https://github.com/niklasvh/html2canvas.git
synced 2023-08-10 21:13:10 +03:00
Implement foreignObject renderer
This commit is contained in:
parent
26cdc0441b
commit
a73dbf8067
2
flow-typed/myLibDef.js
vendored
2
flow-typed/myLibDef.js
vendored
@ -7,3 +7,5 @@ declare class SVGSVGElement extends Element {
|
|||||||
|
|
||||||
getPresentationAttribute(name: string): any;
|
getPresentationAttribute(name: string): any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare class HTMLBodyElement extends HTMLElement {}
|
||||||
|
250
src/Clone.js
250
src/Clone.js
@ -2,6 +2,127 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
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 ImageLoader from './ImageLoader';
|
||||||
|
import {copyCSSStyles} from './Util';
|
||||||
|
import {parseBackgroundImage} from './parsing/background';
|
||||||
|
|
||||||
|
export class DocumentCloner {
|
||||||
|
scrolledElements: Array<[HTMLElement, number, number]>;
|
||||||
|
referenceElement: HTMLElement;
|
||||||
|
clonedReferenceElement: HTMLElement;
|
||||||
|
documentElement: HTMLElement;
|
||||||
|
imageLoader: ImageLoader<string>;
|
||||||
|
logger: Logger;
|
||||||
|
inlineImages: boolean;
|
||||||
|
copyStyles: boolean;
|
||||||
|
|
||||||
|
constructor(element: HTMLElement, options: Options, logger: Logger) {
|
||||||
|
this.referenceElement = element;
|
||||||
|
this.scrolledElements = [];
|
||||||
|
this.copyStyles = true;
|
||||||
|
this.inlineImages = true;
|
||||||
|
this.logger = logger;
|
||||||
|
this.imageLoader = new ImageLoader(options, logger, window);
|
||||||
|
// $FlowFixMe
|
||||||
|
this.documentElement = this.cloneNode(element.ownerDocument.documentElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
inlineAllImages(node: ?HTMLElement) {
|
||||||
|
if (this.inlineImages && node) {
|
||||||
|
const style = node.style;
|
||||||
|
Promise.all(
|
||||||
|
parseBackgroundImage(style.backgroundImage).map(backgroundImage => {
|
||||||
|
if (backgroundImage.method === 'url') {
|
||||||
|
return this.imageLoader
|
||||||
|
.inlineImage(backgroundImage.args[0])
|
||||||
|
.then(src => (src ? `url("${src}")` : 'none'));
|
||||||
|
}
|
||||||
|
return Promise.resolve(
|
||||||
|
`${backgroundImage.prefix}${backgroundImage.method}(${backgroundImage.args.join(
|
||||||
|
','
|
||||||
|
)})`
|
||||||
|
);
|
||||||
|
})
|
||||||
|
).then(backgroundImages => {
|
||||||
|
if (backgroundImages.length > 1) {
|
||||||
|
// TODO Multiple backgrounds somehow broken in Chrome
|
||||||
|
style.backgroundColor = '';
|
||||||
|
}
|
||||||
|
style.backgroundImage = backgroundImages.join(',');
|
||||||
|
});
|
||||||
|
|
||||||
|
if (node instanceof HTMLImageElement) {
|
||||||
|
this.imageLoader.inlineImage(node.src).then(src => {
|
||||||
|
if (src && node instanceof HTMLImageElement) {
|
||||||
|
node.src = src;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createElementClone(node: Node) {
|
||||||
|
if (this.copyStyles && node instanceof HTMLCanvasElement) {
|
||||||
|
const img = node.ownerDocument.createElement('img');
|
||||||
|
try {
|
||||||
|
img.src = node.toDataURL();
|
||||||
|
return img;
|
||||||
|
} catch (e) {
|
||||||
|
this.logger.log(`Unable to clone canvas contents, canvas is tainted`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return node.cloneNode(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
cloneNode(node: Node): Node {
|
||||||
|
const clone =
|
||||||
|
node.nodeType === Node.TEXT_NODE
|
||||||
|
? document.createTextNode(node.nodeValue)
|
||||||
|
: this.createElementClone(node);
|
||||||
|
|
||||||
|
if (this.referenceElement === node && clone instanceof HTMLElement) {
|
||||||
|
this.clonedReferenceElement = clone;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clone instanceof HTMLBodyElement) {
|
||||||
|
createPseudoHideStyles(clone);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let child = node.firstChild; child; child = child.nextSibling) {
|
||||||
|
if (child.nodeType !== Node.ELEMENT_NODE || child.nodeName !== 'SCRIPT') {
|
||||||
|
clone.appendChild(this.cloneNode(child));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (node instanceof HTMLElement && clone instanceof HTMLElement) {
|
||||||
|
this.inlineAllImages(inlinePseudoElement(node, clone, PSEUDO_BEFORE));
|
||||||
|
this.inlineAllImages(inlinePseudoElement(node, clone, PSEUDO_AFTER));
|
||||||
|
if (this.copyStyles) {
|
||||||
|
copyCSSStyles(node.ownerDocument.defaultView.getComputedStyle(node), clone);
|
||||||
|
}
|
||||||
|
this.inlineAllImages(clone);
|
||||||
|
if (node.scrollTop !== 0 || node.scrollLeft !== 0) {
|
||||||
|
this.scrolledElements.push([node, node.scrollLeft, node.scrollTop]);
|
||||||
|
}
|
||||||
|
switch (node.nodeName) {
|
||||||
|
case 'CANVAS':
|
||||||
|
if (!this.copyStyles) {
|
||||||
|
// $FlowFixMe
|
||||||
|
cloneCanvasContents(node, clone);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'TEXTAREA':
|
||||||
|
case 'SELECT':
|
||||||
|
// $FlowFixMe
|
||||||
|
clone.value = node.value;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return clone;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const restoreOwnerScroll = (ownerDocument: Document, x: number, y: number) => {
|
const restoreOwnerScroll = (ownerDocument: Document, x: number, y: number) => {
|
||||||
if (
|
if (
|
||||||
@ -28,44 +149,80 @@ const cloneCanvasContents = (canvas: HTMLCanvasElement, clonedCanvas: HTMLCanvas
|
|||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
};
|
};
|
||||||
|
|
||||||
const cloneNode = (
|
const inlinePseudoElement = (
|
||||||
node: Node,
|
node: HTMLElement,
|
||||||
referenceElement: [HTMLElement, ?HTMLElement],
|
clone: HTMLElement,
|
||||||
scrolledElements: Array<[HTMLElement, number, number]>
|
pseudoElt: ':before' | ':after'
|
||||||
) => {
|
): ?HTMLElement => {
|
||||||
const clone =
|
const style = node.ownerDocument.defaultView.getComputedStyle(node, pseudoElt);
|
||||||
node.nodeType === Node.TEXT_NODE
|
if (
|
||||||
? document.createTextNode(node.nodeValue)
|
!style ||
|
||||||
: node.cloneNode(false);
|
!style.content ||
|
||||||
|
style.content === 'none' ||
|
||||||
if (referenceElement[0] === node && clone instanceof HTMLElement) {
|
style.content === '-moz-alt-content' ||
|
||||||
referenceElement[1] = clone;
|
style.display === 'none'
|
||||||
|
) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let child = node.firstChild; child; child = child.nextSibling) {
|
const content = stripQuotes(style.content);
|
||||||
if (child.nodeType !== Node.ELEMENT_NODE || child.nodeName !== 'SCRIPT') {
|
const image = content.match(URL_REGEXP);
|
||||||
clone.appendChild(cloneNode(child, referenceElement, scrolledElements));
|
const anonymousReplacedElement = clone.ownerDocument.createElement(
|
||||||
}
|
image ? 'img' : 'html2canvaspseudoelement'
|
||||||
|
);
|
||||||
|
if (image) {
|
||||||
|
// $FlowFixMe
|
||||||
|
anonymousReplacedElement.src = stripQuotes(image[1]);
|
||||||
|
} else {
|
||||||
|
anonymousReplacedElement.textContent = content;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (node instanceof HTMLElement) {
|
copyCSSStyles(style, anonymousReplacedElement);
|
||||||
if (node.scrollTop !== 0 || node.scrollLeft !== 0) {
|
|
||||||
scrolledElements.push([node, node.scrollLeft, node.scrollTop]);
|
anonymousReplacedElement.className = `${PSEUDO_HIDE_ELEMENT_CLASS_BEFORE} ${PSEUDO_HIDE_ELEMENT_CLASS_AFTER}`;
|
||||||
}
|
clone.className +=
|
||||||
switch (node.nodeName) {
|
pseudoElt === PSEUDO_BEFORE
|
||||||
case 'CANVAS':
|
? ` ${PSEUDO_HIDE_ELEMENT_CLASS_BEFORE}`
|
||||||
// $FlowFixMe
|
: ` ${PSEUDO_HIDE_ELEMENT_CLASS_AFTER}`;
|
||||||
cloneCanvasContents(node, clone);
|
if (pseudoElt === PSEUDO_BEFORE) {
|
||||||
break;
|
clone.insertBefore(anonymousReplacedElement, clone.firstChild);
|
||||||
case 'TEXTAREA':
|
} else {
|
||||||
case 'SELECT':
|
clone.appendChild(anonymousReplacedElement);
|
||||||
// $FlowFixMe
|
|
||||||
clone.value = node.value;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return clone;
|
return anonymousReplacedElement;
|
||||||
|
};
|
||||||
|
|
||||||
|
const stripQuotes = (content: string): string => {
|
||||||
|
const first = content.substr(0, 1);
|
||||||
|
return first === content.substr(content.length - 1) && first.match(/['"]/)
|
||||||
|
? content.substr(1, content.length - 2)
|
||||||
|
: content;
|
||||||
|
};
|
||||||
|
|
||||||
|
const URL_REGEXP = /^url\((.+)\)$/i;
|
||||||
|
const PSEUDO_BEFORE = ':before';
|
||||||
|
const PSEUDO_AFTER = ':after';
|
||||||
|
const PSEUDO_HIDE_ELEMENT_CLASS_BEFORE = '___html2canvas___pseudoelement_before';
|
||||||
|
const PSEUDO_HIDE_ELEMENT_CLASS_AFTER = '___html2canvas___pseudoelement_after';
|
||||||
|
|
||||||
|
const PSEUDO_HIDE_ELEMENT_STYLE = `{
|
||||||
|
content: "" !important;
|
||||||
|
display: none !important;
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const createPseudoHideStyles = (body: HTMLElement) => {
|
||||||
|
createStyles(
|
||||||
|
body,
|
||||||
|
`.${PSEUDO_HIDE_ELEMENT_CLASS_BEFORE}${PSEUDO_BEFORE}${PSEUDO_HIDE_ELEMENT_STYLE}
|
||||||
|
.${PSEUDO_HIDE_ELEMENT_CLASS_AFTER}${PSEUDO_AFTER}${PSEUDO_HIDE_ELEMENT_STYLE}`
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const createStyles = (body: HTMLElement, styles) => {
|
||||||
|
const style = body.ownerDocument.createElement('style');
|
||||||
|
style.innerHTML = styles;
|
||||||
|
body.appendChild(style);
|
||||||
};
|
};
|
||||||
|
|
||||||
const initNode = ([element, x, y]: [HTMLElement, number, number]) => {
|
const initNode = ([element, x, y]: [HTMLElement, number, number]) => {
|
||||||
@ -74,24 +231,13 @@ const initNode = ([element, x, y]: [HTMLElement, number, number]) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const cloneWindow = (
|
export const cloneWindow = (
|
||||||
documentToBeCloned: Document,
|
|
||||||
ownerDocument: Document,
|
ownerDocument: Document,
|
||||||
bounds: Bounds,
|
bounds: Bounds,
|
||||||
referenceElement: HTMLElement,
|
referenceElement: HTMLElement,
|
||||||
options: Options
|
options: Options,
|
||||||
|
logger: Logger
|
||||||
): Promise<[HTMLIFrameElement, HTMLElement]> => {
|
): Promise<[HTMLIFrameElement, HTMLElement]> => {
|
||||||
const scrolledElements = [];
|
const cloner = new DocumentCloner(referenceElement, options, logger);
|
||||||
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');
|
const cloneIframeContainer = ownerDocument.createElement('iframe');
|
||||||
|
|
||||||
cloneIframeContainer.className = 'html2canvas-container';
|
cloneIframeContainer.className = 'html2canvas-container';
|
||||||
@ -120,7 +266,7 @@ export const cloneWindow = (
|
|||||||
cloneWindow.onload = cloneIframeContainer.onload = () => {
|
cloneWindow.onload = cloneIframeContainer.onload = () => {
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
if (documentClone.body.childNodes.length > 0) {
|
if (documentClone.body.childNodes.length > 0) {
|
||||||
scrolledElements.forEach(initNode);
|
cloner.scrolledElements.forEach(initNode);
|
||||||
clearInterval(interval);
|
clearInterval(interval);
|
||||||
if (options.type === 'view') {
|
if (options.type === 'view') {
|
||||||
cloneWindow.scrollTo(bounds.left, bounds.top);
|
cloneWindow.scrollTo(bounds.left, bounds.top);
|
||||||
@ -135,10 +281,10 @@ export const cloneWindow = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
referenceElementSearch[1] instanceof cloneWindow.HTMLElement ||
|
cloner.clonedReferenceElement instanceof cloneWindow.HTMLElement ||
|
||||||
referenceElementSearch[1] instanceof HTMLElement
|
cloner.clonedReferenceElement instanceof HTMLElement
|
||||||
) {
|
) {
|
||||||
resolve([cloneIframeContainer, referenceElementSearch[1]]);
|
resolve([cloneIframeContainer, cloner.clonedReferenceElement]);
|
||||||
} else {
|
} else {
|
||||||
reject(
|
reject(
|
||||||
__DEV__
|
__DEV__
|
||||||
@ -153,9 +299,9 @@ export const cloneWindow = (
|
|||||||
documentClone.open();
|
documentClone.open();
|
||||||
documentClone.write('<!DOCTYPE html><html></html>');
|
documentClone.write('<!DOCTYPE html><html></html>');
|
||||||
// Chrome scrolls the parent document for some reason after the write to the cloned window???
|
// Chrome scrolls the parent document for some reason after the write to the cloned window???
|
||||||
restoreOwnerScroll(documentToBeCloned, bounds.left, bounds.top);
|
restoreOwnerScroll(referenceElement.ownerDocument, bounds.left, bounds.top);
|
||||||
documentClone.replaceChild(
|
documentClone.replaceChild(
|
||||||
documentClone.adoptNode(documentElement),
|
documentClone.adoptNode(cloner.documentElement),
|
||||||
documentClone.documentElement
|
documentClone.documentElement
|
||||||
);
|
);
|
||||||
documentClone.close();
|
documentClone.close();
|
||||||
|
@ -71,6 +71,34 @@ const testSVG = document => {
|
|||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const testForeignObject = document => {
|
||||||
|
const img = new Image();
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
img.src = `data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg'><foreignObject><div></div></foreignObject></svg>`;
|
||||||
|
|
||||||
|
return new Promise(resolve => {
|
||||||
|
const onload = () => {
|
||||||
|
try {
|
||||||
|
ctx.drawImage(img, 0, 0);
|
||||||
|
canvas.toDataURL();
|
||||||
|
} catch (e) {
|
||||||
|
return resolve(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolve(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
img.onload = onload;
|
||||||
|
|
||||||
|
if (img.complete === true) {
|
||||||
|
setTimeout(() => {
|
||||||
|
onload();
|
||||||
|
}, 50);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const FEATURES = {
|
const FEATURES = {
|
||||||
// $FlowFixMe - get/set properties not yet supported
|
// $FlowFixMe - get/set properties not yet supported
|
||||||
get SUPPORT_RANGE_BOUNDS() {
|
get SUPPORT_RANGE_BOUNDS() {
|
||||||
@ -94,6 +122,13 @@ const FEATURES = {
|
|||||||
Object.defineProperty(FEATURES, 'SUPPORT_BASE64_DRAWING', {value: () => value});
|
Object.defineProperty(FEATURES, 'SUPPORT_BASE64_DRAWING', {value: () => value});
|
||||||
return value;
|
return value;
|
||||||
};
|
};
|
||||||
|
},
|
||||||
|
// $FlowFixMe - get/set properties not yet supported
|
||||||
|
get SUPPORT_FOREIGNOBJECT_DRAWING() {
|
||||||
|
'use strict';
|
||||||
|
const value = testForeignObject(document);
|
||||||
|
Object.defineProperty(FEATURES, 'SUPPORT_FOREIGNOBJECT_DRAWING', {value});
|
||||||
|
return value;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -5,15 +5,16 @@ import type Options from './index';
|
|||||||
import type Logger from './Logger';
|
import type Logger from './Logger';
|
||||||
|
|
||||||
export type ImageElement = Image | HTMLCanvasElement;
|
export type ImageElement = Image | HTMLCanvasElement;
|
||||||
type ImageCache = {[string]: Promise<?ImageElement>};
|
type ImageCache<T> = {[string]: Promise<T>};
|
||||||
|
|
||||||
import FEATURES from './Feature';
|
import FEATURES from './Feature';
|
||||||
|
|
||||||
export default class ImageLoader {
|
// $FlowFixMe
|
||||||
|
export default class ImageLoader<T> {
|
||||||
origin: string;
|
origin: string;
|
||||||
options: Options;
|
options: Options;
|
||||||
_link: HTMLAnchorElement;
|
_link: HTMLAnchorElement;
|
||||||
cache: ImageCache;
|
cache: ImageCache<T>;
|
||||||
logger: Logger;
|
logger: Logger;
|
||||||
_index: number;
|
_index: number;
|
||||||
_window: WindowProxy;
|
_window: WindowProxy;
|
||||||
@ -45,6 +46,46 @@ export default class ImageLoader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inlineImage(src: string): Promise<string> {
|
||||||
|
if (isInlineImage(src)) {
|
||||||
|
return Promise.resolve(src);
|
||||||
|
}
|
||||||
|
if (this.hasImageInCache(src)) {
|
||||||
|
return this.cache[src];
|
||||||
|
}
|
||||||
|
// TODO proxy
|
||||||
|
return this.xhrImage(src);
|
||||||
|
}
|
||||||
|
|
||||||
|
xhrImage(src: string): Promise<string> {
|
||||||
|
this.cache[src] = new Promise((resolve, reject) => {
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
xhr.onreadystatechange = () => {
|
||||||
|
if (xhr.readyState === 4) {
|
||||||
|
if (xhr.status !== 200) {
|
||||||
|
reject(`Failed to fetch image ${src} with status code ${xhr.status}`);
|
||||||
|
} else {
|
||||||
|
const reader = new FileReader();
|
||||||
|
// $FlowFixMe
|
||||||
|
reader.addEventListener('load', () => resolve(reader.result), false);
|
||||||
|
// $FlowFixMe
|
||||||
|
reader.addEventListener('error', e => reject(e), false);
|
||||||
|
reader.readAsDataURL(xhr.response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
xhr.ontimeout = () => reject(`Timed out fetching ${src}`);
|
||||||
|
xhr.responseType = 'blob';
|
||||||
|
if (this.options.imageTimeout) {
|
||||||
|
xhr.timeout = this.options.imageTimeout;
|
||||||
|
}
|
||||||
|
xhr.open('GET', src, true);
|
||||||
|
xhr.send();
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.cache[src];
|
||||||
|
}
|
||||||
|
|
||||||
loadCanvas(node: HTMLCanvasElement): string {
|
loadCanvas(node: HTMLCanvasElement): string {
|
||||||
const key = String(this._index++);
|
const key = String(this._index++);
|
||||||
this.cache[key] = Promise.resolve(node);
|
this.cache[key] = Promise.resolve(node);
|
||||||
@ -60,8 +101,8 @@ export default class ImageLoader {
|
|||||||
this.logger.log(`Added image ${key.substring(0, 256)}`);
|
this.logger.log(`Added image ${key.substring(0, 256)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const imageLoadHandler = (supportsDataImages: boolean): Promise<Image> => {
|
const imageLoadHandler = (supportsDataImages: boolean): Promise<Image> =>
|
||||||
return new Promise((resolve, reject) => {
|
new Promise((resolve, reject) => {
|
||||||
const img = new Image();
|
const img = new Image();
|
||||||
img.onload = () => resolve(img);
|
img.onload = () => resolve(img);
|
||||||
//ios safari 10.3 taints canvas with data urls unless crossOrigin is set to anonymous
|
//ios safari 10.3 taints canvas with data urls unless crossOrigin is set to anonymous
|
||||||
@ -78,7 +119,6 @@ export default class ImageLoader {
|
|||||||
}, 500);
|
}, 500);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
|
||||||
|
|
||||||
this.cache[key] =
|
this.cache[key] =
|
||||||
isInlineImage(src) && !isSVG(src)
|
isInlineImage(src) && !isSVG(src)
|
||||||
@ -99,7 +139,7 @@ export default class ImageLoader {
|
|||||||
return link.protocol + link.hostname + link.port;
|
return link.protocol + link.hostname + link.port;
|
||||||
}
|
}
|
||||||
|
|
||||||
ready(): Promise<ImageStore> {
|
ready(): Promise<ImageStore<T>> {
|
||||||
const keys = Object.keys(this.cache);
|
const keys = Object.keys(this.cache);
|
||||||
return Promise.all(
|
return Promise.all(
|
||||||
keys.map(str =>
|
keys.map(str =>
|
||||||
@ -112,23 +152,23 @@ export default class ImageLoader {
|
|||||||
)
|
)
|
||||||
).then(images => {
|
).then(images => {
|
||||||
if (__DEV__) {
|
if (__DEV__) {
|
||||||
this.logger.log('Finished loading images', images);
|
this.logger.log(`Finished loading ${images.length} images`, images);
|
||||||
}
|
}
|
||||||
return new ImageStore(keys, images);
|
return new ImageStore(keys, images);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ImageStore {
|
export class ImageStore<T> {
|
||||||
_keys: Array<string>;
|
_keys: Array<string>;
|
||||||
_images: Array<?ImageElement>;
|
_images: Array<?T>;
|
||||||
|
|
||||||
constructor(keys: Array<string>, images: Array<?ImageElement>) {
|
constructor(keys: Array<string>, images: Array<?T>) {
|
||||||
this._keys = keys;
|
this._keys = keys;
|
||||||
this._images = images;
|
this._images = images;
|
||||||
}
|
}
|
||||||
|
|
||||||
get(key: string): ?ImageElement {
|
get(key: string): ?T {
|
||||||
const index = this._keys.indexOf(key);
|
const index = this._keys.indexOf(key);
|
||||||
return index === -1 ? null : this._images[index];
|
return index === -1 ? null : this._images[index];
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,7 @@ import type {Visibility} from './parsing/visibility';
|
|||||||
import type {zIndex} from './parsing/zIndex';
|
import type {zIndex} from './parsing/zIndex';
|
||||||
|
|
||||||
import type {Bounds, BoundCurves} from './Bounds';
|
import type {Bounds, BoundCurves} from './Bounds';
|
||||||
import type ImageLoader from './ImageLoader';
|
import type ImageLoader, {ImageElement} from './ImageLoader';
|
||||||
import type {Path} from './drawing/Path';
|
import type {Path} from './drawing/Path';
|
||||||
import type TextContainer from './TextContainer';
|
import type TextContainer from './TextContainer';
|
||||||
|
|
||||||
@ -87,7 +87,7 @@ export default class NodeContainer {
|
|||||||
constructor(
|
constructor(
|
||||||
node: HTMLElement | SVGSVGElement,
|
node: HTMLElement | SVGSVGElement,
|
||||||
parent: ?NodeContainer,
|
parent: ?NodeContainer,
|
||||||
imageLoader: ImageLoader,
|
imageLoader: ImageLoader<ImageElement>,
|
||||||
index: number
|
index: number
|
||||||
) {
|
) {
|
||||||
this.parent = parent;
|
this.parent = parent;
|
||||||
@ -219,7 +219,10 @@ export default class NodeContainer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getImage = (node: HTMLElement | SVGSVGElement, imageLoader: ImageLoader): ?string => {
|
const getImage = (
|
||||||
|
node: HTMLElement | SVGSVGElement,
|
||||||
|
imageLoader: ImageLoader<ImageElement>
|
||||||
|
): ?string => {
|
||||||
if (
|
if (
|
||||||
node instanceof node.ownerDocument.defaultView.SVGSVGElement ||
|
node instanceof node.ownerDocument.defaultView.SVGSVGElement ||
|
||||||
node instanceof SVGSVGElement
|
node instanceof SVGSVGElement
|
||||||
|
@ -1,16 +1,15 @@
|
|||||||
/* @flow */
|
/* @flow */
|
||||||
'use strict';
|
'use strict';
|
||||||
import type ImageLoader from './ImageLoader';
|
import type ImageLoader, {ImageElement} from './ImageLoader';
|
||||||
import type Logger from './Logger';
|
import type Logger from './Logger';
|
||||||
import StackingContext from './StackingContext';
|
import StackingContext from './StackingContext';
|
||||||
import NodeContainer from './NodeContainer';
|
import NodeContainer from './NodeContainer';
|
||||||
import TextContainer from './TextContainer';
|
import TextContainer from './TextContainer';
|
||||||
import {inlineInputElement, inlineTextAreaElement, inlineSelectElement} from './Input';
|
import {inlineInputElement, inlineTextAreaElement, inlineSelectElement} from './Input';
|
||||||
import {copyCSSStyles} from './Util';
|
|
||||||
|
|
||||||
export const NodeParser = (
|
export const NodeParser = (
|
||||||
node: HTMLElement,
|
node: HTMLElement,
|
||||||
imageLoader: ImageLoader,
|
imageLoader: ImageLoader<ImageElement>,
|
||||||
logger: Logger
|
logger: Logger
|
||||||
): StackingContext => {
|
): StackingContext => {
|
||||||
if (__DEV__) {
|
if (__DEV__) {
|
||||||
@ -22,7 +21,6 @@ export const NodeParser = (
|
|||||||
const container = new NodeContainer(node, null, imageLoader, index++);
|
const container = new NodeContainer(node, null, imageLoader, index++);
|
||||||
const stack = new StackingContext(container, null, true);
|
const stack = new StackingContext(container, null, true);
|
||||||
|
|
||||||
createPseudoHideStyles(node.ownerDocument);
|
|
||||||
parseNodeTree(node, container, stack, imageLoader, index);
|
parseNodeTree(node, container, stack, imageLoader, index);
|
||||||
|
|
||||||
if (__DEV__) {
|
if (__DEV__) {
|
||||||
@ -33,17 +31,12 @@ export const NodeParser = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
const IGNORED_NODE_NAMES = ['SCRIPT', 'HEAD', 'TITLE', 'OBJECT', 'BR', 'OPTION'];
|
const IGNORED_NODE_NAMES = ['SCRIPT', 'HEAD', 'TITLE', 'OBJECT', 'BR', 'OPTION'];
|
||||||
const URL_REGEXP = /^url\((.+)\)$/i;
|
|
||||||
const PSEUDO_BEFORE = ':before';
|
|
||||||
const PSEUDO_AFTER = ':after';
|
|
||||||
const PSEUDO_HIDE_ELEMENT_CLASS_BEFORE = '___html2canvas___pseudoelement_before';
|
|
||||||
const PSEUDO_HIDE_ELEMENT_CLASS_AFTER = '___html2canvas___pseudoelement_after';
|
|
||||||
|
|
||||||
const parseNodeTree = (
|
const parseNodeTree = (
|
||||||
node: HTMLElement,
|
node: HTMLElement,
|
||||||
parent: NodeContainer,
|
parent: NodeContainer,
|
||||||
stack: StackingContext,
|
stack: StackingContext,
|
||||||
imageLoader: ImageLoader,
|
imageLoader: ImageLoader<ImageElement>,
|
||||||
index: number
|
index: number
|
||||||
): void => {
|
): void => {
|
||||||
if (__DEV__ && index > 50000) {
|
if (__DEV__ && index > 50000) {
|
||||||
@ -62,8 +55,6 @@ const parseNodeTree = (
|
|||||||
childNode instanceof HTMLElement
|
childNode instanceof HTMLElement
|
||||||
) {
|
) {
|
||||||
if (IGNORED_NODE_NAMES.indexOf(childNode.nodeName) === -1) {
|
if (IGNORED_NODE_NAMES.indexOf(childNode.nodeName) === -1) {
|
||||||
inlinePseudoElement(childNode, PSEUDO_BEFORE);
|
|
||||||
inlinePseudoElement(childNode, PSEUDO_AFTER);
|
|
||||||
const container = new NodeContainer(childNode, parent, imageLoader, index++);
|
const container = new NodeContainer(childNode, parent, imageLoader, index++);
|
||||||
if (container.isVisible()) {
|
if (container.isVisible()) {
|
||||||
if (childNode.tagName === 'INPUT') {
|
if (childNode.tagName === 'INPUT') {
|
||||||
@ -132,44 +123,6 @@ const parseNodeTree = (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const inlinePseudoElement = (node: HTMLElement, pseudoElt: ':before' | ':after'): void => {
|
|
||||||
const style = node.ownerDocument.defaultView.getComputedStyle(node, pseudoElt);
|
|
||||||
if (
|
|
||||||
!style ||
|
|
||||||
!style.content ||
|
|
||||||
style.content === 'none' ||
|
|
||||||
style.content === '-moz-alt-content' ||
|
|
||||||
style.display === 'none'
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const content = stripQuotes(style.content);
|
|
||||||
const image = content.match(URL_REGEXP);
|
|
||||||
const anonymousReplacedElement = node.ownerDocument.createElement(
|
|
||||||
image ? 'img' : 'html2canvaspseudoelement'
|
|
||||||
);
|
|
||||||
if (image) {
|
|
||||||
// $FlowFixMe
|
|
||||||
anonymousReplacedElement.src = stripQuotes(image[1]);
|
|
||||||
} else {
|
|
||||||
anonymousReplacedElement.textContent = content;
|
|
||||||
}
|
|
||||||
|
|
||||||
copyCSSStyles(style, anonymousReplacedElement);
|
|
||||||
|
|
||||||
anonymousReplacedElement.className = `${PSEUDO_HIDE_ELEMENT_CLASS_BEFORE} ${PSEUDO_HIDE_ELEMENT_CLASS_AFTER}`;
|
|
||||||
node.className +=
|
|
||||||
pseudoElt === PSEUDO_BEFORE
|
|
||||||
? ` ${PSEUDO_HIDE_ELEMENT_CLASS_BEFORE}`
|
|
||||||
: ` ${PSEUDO_HIDE_ELEMENT_CLASS_AFTER}`;
|
|
||||||
if (pseudoElt === PSEUDO_BEFORE) {
|
|
||||||
node.insertBefore(anonymousReplacedElement, node.firstChild);
|
|
||||||
} else {
|
|
||||||
node.appendChild(anonymousReplacedElement);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const createsRealStackingContext = (
|
const createsRealStackingContext = (
|
||||||
container: NodeContainer,
|
container: NodeContainer,
|
||||||
node: HTMLElement | SVGSVGElement
|
node: HTMLElement | SVGSVGElement
|
||||||
@ -197,31 +150,3 @@ const isBodyWithTransparentRoot = (
|
|||||||
container.parent.style.background.backgroundColor.isTransparent()
|
container.parent.style.background.backgroundColor.isTransparent()
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const stripQuotes = (content: string): string => {
|
|
||||||
const first = content.substr(0, 1);
|
|
||||||
return first === content.substr(content.length - 1) && first.match(/['"]/)
|
|
||||||
? content.substr(1, content.length - 2)
|
|
||||||
: content;
|
|
||||||
};
|
|
||||||
|
|
||||||
const PSEUDO_HIDE_ELEMENT_STYLE = `{
|
|
||||||
content: "" !important;
|
|
||||||
display: none !important;
|
|
||||||
}`;
|
|
||||||
|
|
||||||
const createPseudoHideStyles = (document: Document) => {
|
|
||||||
createStyles(
|
|
||||||
document,
|
|
||||||
`.${PSEUDO_HIDE_ELEMENT_CLASS_BEFORE}${PSEUDO_BEFORE}${PSEUDO_HIDE_ELEMENT_STYLE}
|
|
||||||
.${PSEUDO_HIDE_ELEMENT_CLASS_AFTER}${PSEUDO_AFTER}${PSEUDO_HIDE_ELEMENT_STYLE}`
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const createStyles = (document: Document, styles) => {
|
|
||||||
const style = document.createElement('style');
|
|
||||||
style.innerHTML = styles;
|
|
||||||
if (document.body) {
|
|
||||||
document.body.appendChild(style);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
@ -43,7 +43,7 @@ import {BORDER_STYLE} from './parsing/border';
|
|||||||
export type RenderOptions = {
|
export type RenderOptions = {
|
||||||
scale: number,
|
scale: number,
|
||||||
backgroundColor: ?Color,
|
backgroundColor: ?Color,
|
||||||
imageStore: ImageStore,
|
imageStore: ImageStore<ImageElement>,
|
||||||
fontMetrics: FontMetrics,
|
fontMetrics: FontMetrics,
|
||||||
logger: Logger,
|
logger: Logger,
|
||||||
width: number,
|
width: number,
|
||||||
|
166
src/index.js
166
src/index.js
@ -5,13 +5,15 @@ import type {RenderTarget} from './Renderer';
|
|||||||
|
|
||||||
import {NodeParser} from './NodeParser';
|
import {NodeParser} from './NodeParser';
|
||||||
import Renderer from './Renderer';
|
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 ImageLoader from './ImageLoader';
|
||||||
|
import Feature from './Feature';
|
||||||
import {Bounds, parseDocumentSize} from './Bounds';
|
import {Bounds, parseDocumentSize} from './Bounds';
|
||||||
import {cloneWindow} from './Clone';
|
import {cloneWindow, DocumentCloner} from './Clone';
|
||||||
import Color, {TRANSPARENT} from './Color';
|
|
||||||
import {FontMetrics} from './Font';
|
import {FontMetrics} from './Font';
|
||||||
|
import Color, {TRANSPARENT} from './Color';
|
||||||
|
|
||||||
export type Options = {
|
export type Options = {
|
||||||
async: ?boolean,
|
async: ?boolean,
|
||||||
@ -42,7 +44,7 @@ const html2canvas = (element: HTMLElement, config: Options): Promise<*> => {
|
|||||||
const defaultOptions = {
|
const defaultOptions = {
|
||||||
async: true,
|
async: true,
|
||||||
allowTaint: false,
|
allowTaint: false,
|
||||||
imageTimeout: 10000,
|
imageTimeout: 15000,
|
||||||
proxy: null,
|
proxy: null,
|
||||||
removeContainer: true,
|
removeContainer: true,
|
||||||
scale: defaultView.devicePixelRatio || 1,
|
scale: defaultView.devicePixelRatio || 1,
|
||||||
@ -63,79 +65,105 @@ const html2canvas = (element: HTMLElement, config: Options): Promise<*> => {
|
|||||||
options.windowHeight
|
options.windowHeight
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = cloneWindow(
|
const bounds = options.type === 'view' ? windowBounds : parseDocumentSize(ownerDocument);
|
||||||
ownerDocument,
|
|
||||||
ownerDocument,
|
|
||||||
windowBounds,
|
|
||||||
element,
|
|
||||||
options
|
|
||||||
).then(([container, clonedElement]) => {
|
|
||||||
if (__DEV__) {
|
|
||||||
logger.log(`Document cloned`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const imageLoader = new ImageLoader(
|
// http://www.w3.org/TR/css3-background/#special-backgrounds
|
||||||
options,
|
const documentBackgroundColor = ownerDocument.documentElement
|
||||||
logger,
|
? new Color(getComputedStyle(ownerDocument.documentElement).backgroundColor)
|
||||||
clonedElement.ownerDocument.defaultView
|
: TRANSPARENT;
|
||||||
);
|
const backgroundColor =
|
||||||
const stack = NodeParser(clonedElement, imageLoader, logger);
|
element === ownerDocument.documentElement
|
||||||
const clonedDocument = clonedElement.ownerDocument;
|
? documentBackgroundColor.isTransparent()
|
||||||
const size = options.type === 'view' ? windowBounds : parseDocumentSize(clonedDocument);
|
? ownerDocument.body
|
||||||
const width = size.width;
|
? new Color(getComputedStyle(ownerDocument.body).backgroundColor)
|
||||||
const height = size.height;
|
: null
|
||||||
|
: documentBackgroundColor
|
||||||
|
: null;
|
||||||
|
|
||||||
// http://www.w3.org/TR/css3-background/#special-backgrounds
|
// $FlowFixMe
|
||||||
const backgroundColor =
|
const result = Feature.SUPPORT_FOREIGNOBJECT_DRAWING.then(
|
||||||
clonedElement === clonedDocument.documentElement
|
supportForeignObject =>
|
||||||
? stack.container.style.background.backgroundColor.isTransparent()
|
supportForeignObject
|
||||||
? clonedDocument.body
|
? (cloner => {
|
||||||
? new Color(getComputedStyle(clonedDocument.body).backgroundColor)
|
if (__DEV__) {
|
||||||
: null
|
logger.log(`Document cloned, using foreignObject rendering`);
|
||||||
: stack.container.style.background.backgroundColor
|
}
|
||||||
: null;
|
|
||||||
|
|
||||||
if (backgroundColor === stack.container.style.background.backgroundColor) {
|
return cloner.imageLoader.ready().then(() => {
|
||||||
stack.container.style.background.backgroundColor = TRANSPARENT;
|
const renderer = new ForeignObjectRenderer(cloner.clonedReferenceElement);
|
||||||
}
|
return renderer.render({
|
||||||
|
bounds,
|
||||||
|
backgroundColor,
|
||||||
|
logger,
|
||||||
|
scale: options.scale
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})(new DocumentCloner(element, options, logger))
|
||||||
|
: cloneWindow(
|
||||||
|
ownerDocument,
|
||||||
|
windowBounds,
|
||||||
|
element,
|
||||||
|
options,
|
||||||
|
logger
|
||||||
|
).then(([container, clonedElement]) => {
|
||||||
|
if (__DEV__) {
|
||||||
|
logger.log(`Document cloned, using computed rendering`);
|
||||||
|
}
|
||||||
|
|
||||||
return imageLoader.ready().then(imageStore => {
|
const imageLoader = new ImageLoader(
|
||||||
if (options.removeContainer === true) {
|
options,
|
||||||
if (container.parentNode) {
|
logger,
|
||||||
container.parentNode.removeChild(container);
|
clonedElement.ownerDocument.defaultView
|
||||||
} else if (__DEV__) {
|
);
|
||||||
logger.log(`Cannot detach cloned iframe as it is not in the DOM anymore`);
|
const stack = NodeParser(clonedElement, imageLoader, logger);
|
||||||
}
|
const clonedDocument = clonedElement.ownerDocument;
|
||||||
}
|
const width = bounds.width;
|
||||||
|
const height = bounds.height;
|
||||||
|
|
||||||
const fontMetrics = new FontMetrics(clonedDocument);
|
if (backgroundColor === stack.container.style.background.backgroundColor) {
|
||||||
if (__DEV__) {
|
stack.container.style.background.backgroundColor = TRANSPARENT;
|
||||||
logger.log(`Starting renderer`);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const renderOptions = {
|
return imageLoader.ready().then(imageStore => {
|
||||||
backgroundColor,
|
if (options.removeContainer === true) {
|
||||||
fontMetrics,
|
if (container.parentNode) {
|
||||||
imageStore,
|
container.parentNode.removeChild(container);
|
||||||
logger,
|
} else if (__DEV__) {
|
||||||
scale: options.scale,
|
logger.log(
|
||||||
width,
|
`Cannot detach cloned iframe as it is not in the DOM anymore`
|
||||||
height
|
);
|
||||||
};
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (Array.isArray(options.target)) {
|
const fontMetrics = new FontMetrics(clonedDocument);
|
||||||
return Promise.all(
|
if (__DEV__) {
|
||||||
options.target.map(target => {
|
logger.log(`Starting renderer`);
|
||||||
const renderer = new Renderer(target, renderOptions);
|
}
|
||||||
return renderer.render(stack);
|
|
||||||
})
|
const renderOptions = {
|
||||||
);
|
backgroundColor,
|
||||||
} else {
|
fontMetrics,
|
||||||
const renderer = new Renderer(options.target, renderOptions);
|
imageStore,
|
||||||
return renderer.render(stack);
|
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 => {
|
||||||
|
@ -223,7 +223,7 @@ export const calculateBackgroundRepeatPath = (
|
|||||||
|
|
||||||
export const parseBackground = (
|
export const parseBackground = (
|
||||||
style: CSSStyleDeclaration,
|
style: CSSStyleDeclaration,
|
||||||
imageLoader: ImageLoader
|
imageLoader: ImageLoader<ImageElement>
|
||||||
): Background => {
|
): Background => {
|
||||||
return {
|
return {
|
||||||
backgroundColor: new Color(style.backgroundColor),
|
backgroundColor: new Color(style.backgroundColor),
|
||||||
@ -276,12 +276,17 @@ const parseBackgroundRepeat = (backgroundRepeat: string): BackgroundRepeat => {
|
|||||||
|
|
||||||
const parseBackgroundImages = (
|
const parseBackgroundImages = (
|
||||||
style: CSSStyleDeclaration,
|
style: CSSStyleDeclaration,
|
||||||
imageLoader: ImageLoader
|
imageLoader: ImageLoader<ImageElement>
|
||||||
): Array<BackgroundImage> => {
|
): Array<BackgroundImage> => {
|
||||||
const sources: Array<BackgroundSource> = parseBackgroundImage(
|
const sources: Array<BackgroundSource> = parseBackgroundImage(
|
||||||
style.backgroundImage,
|
style.backgroundImage
|
||||||
imageLoader
|
).map(backgroundImage => {
|
||||||
);
|
if (backgroundImage.method === 'url') {
|
||||||
|
const key = imageLoader.loadImage(backgroundImage.args[0]);
|
||||||
|
backgroundImage.args = key ? [key] : [];
|
||||||
|
}
|
||||||
|
return backgroundImage;
|
||||||
|
});
|
||||||
const positions = style.backgroundPosition.split(',');
|
const positions = style.backgroundPosition.split(',');
|
||||||
const repeats = style.backgroundRepeat.split(',');
|
const repeats = style.backgroundRepeat.split(',');
|
||||||
const sizes = style.backgroundSize.split(',');
|
const sizes = style.backgroundSize.split(',');
|
||||||
@ -318,7 +323,7 @@ const parseBackgoundPosition = (position: string): Length => {
|
|||||||
return new Length(position);
|
return new Length(position);
|
||||||
};
|
};
|
||||||
|
|
||||||
const parseBackgroundImage = (image: string, imageLoader: ImageLoader): Array<BackgroundSource> => {
|
export const parseBackgroundImage = (image: string): Array<BackgroundSource> => {
|
||||||
const whitespace = /^\s$/;
|
const whitespace = /^\s$/;
|
||||||
const results = [];
|
const results = [];
|
||||||
|
|
||||||
@ -346,10 +351,6 @@ const parseBackgroundImage = (image: string, imageLoader: ImageLoader): Array<Ba
|
|||||||
method = method.substr(prefix_i);
|
method = method.substr(prefix_i);
|
||||||
}
|
}
|
||||||
method = method.toLowerCase();
|
method = method.toLowerCase();
|
||||||
if (method === 'url') {
|
|
||||||
const key = imageLoader.loadImage(args[0]);
|
|
||||||
args = key ? [key] : [];
|
|
||||||
}
|
|
||||||
if (method !== 'none') {
|
if (method !== 'none') {
|
||||||
results.push({
|
results.push({
|
||||||
prefix,
|
prefix,
|
||||||
|
52
src/renderer/ForeignObjectRenderer.js
Normal file
52
src/renderer/ForeignObjectRenderer.js
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import type {RenderOptions} from '../Renderer';
|
||||||
|
|
||||||
|
export default class ForeignObjectRenderer {
|
||||||
|
options: RenderOptions;
|
||||||
|
element: HTMLElement;
|
||||||
|
|
||||||
|
constructor(element: HTMLElement) {
|
||||||
|
this.element = element;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(options) {
|
||||||
|
this.options = options;
|
||||||
|
this.canvas = document.createElement('canvas');
|
||||||
|
this.ctx = this.canvas.getContext('2d');
|
||||||
|
this.canvas.width = Math.floor(options.bounds.width * options.scale);
|
||||||
|
this.canvas.height = Math.floor(options.bounds.height * options.scale);
|
||||||
|
this.canvas.style.width = `${options.bounds.width}px`;
|
||||||
|
this.canvas.style.height = `${options.bounds.height}px`;
|
||||||
|
this.ctx.scale(this.options.scale, this.options.scale);
|
||||||
|
|
||||||
|
options.logger.log(`ForeignObject renderer initialized with scale ${this.options.scale}`);
|
||||||
|
|
||||||
|
const xmlns = 'http://www.w3.org/2000/svg';
|
||||||
|
const svg = document.createElementNS(xmlns, 'svg');
|
||||||
|
const foreignObject = document.createElementNS(xmlns, 'foreignObject');
|
||||||
|
svg.setAttributeNS(null, 'width', options.bounds.width);
|
||||||
|
svg.setAttributeNS(null, 'height', options.bounds.height);
|
||||||
|
|
||||||
|
foreignObject.setAttributeNS(null, 'width', '100%');
|
||||||
|
foreignObject.setAttributeNS(null, 'height', '100%');
|
||||||
|
foreignObject.setAttributeNS(null, 'externalResourcesRequired', 'true');
|
||||||
|
svg.appendChild(foreignObject);
|
||||||
|
|
||||||
|
foreignObject.appendChild(this.element);
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const img = new Image();
|
||||||
|
img.onload = () => {
|
||||||
|
if (options.backgroundColor) {
|
||||||
|
this.ctx.fillStyle = options.backgroundColor.toString();
|
||||||
|
this.ctx.fillRect(0, 0, options.bounds.width, options.bounds.height);
|
||||||
|
}
|
||||||
|
this.ctx.drawImage(img, 0, 0);
|
||||||
|
resolve(this.canvas);
|
||||||
|
};
|
||||||
|
|
||||||
|
img.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(
|
||||||
|
new XMLSerializer().serializeToString(svg)
|
||||||
|
)}`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user