Implement foreignObject renderer

This commit is contained in:
Niklas von Hertzen 2017-08-17 23:14:44 +08:00
parent 26cdc0441b
commit a73dbf8067
10 changed files with 457 additions and 225 deletions

View File

@ -7,3 +7,5 @@ declare class SVGSVGElement extends Element {
getPresentationAttribute(name: string): any;
}
declare class HTMLBodyElement extends HTMLElement {}

View File

@ -2,6 +2,127 @@
'use strict';
import type {Bounds} from './Bounds';
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) => {
if (
@ -28,44 +149,80 @@ const cloneCanvasContents = (canvas: HTMLCanvasElement, clonedCanvas: HTMLCanvas
} 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;
const inlinePseudoElement = (
node: HTMLElement,
clone: HTMLElement,
pseudoElt: ':before' | ':after'
): ?HTMLElement => {
const style = node.ownerDocument.defaultView.getComputedStyle(node, pseudoElt);
if (
!style ||
!style.content ||
style.content === 'none' ||
style.content === '-moz-alt-content' ||
style.display === 'none'
) {
return;
}
for (let child = node.firstChild; child; child = child.nextSibling) {
if (child.nodeType !== Node.ELEMENT_NODE || child.nodeName !== 'SCRIPT') {
clone.appendChild(cloneNode(child, referenceElement, scrolledElements));
}
const content = stripQuotes(style.content);
const image = content.match(URL_REGEXP);
const anonymousReplacedElement = clone.ownerDocument.createElement(
image ? 'img' : 'html2canvaspseudoelement'
);
if (image) {
// $FlowFixMe
anonymousReplacedElement.src = stripQuotes(image[1]);
} else {
anonymousReplacedElement.textContent = content;
}
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;
}
copyCSSStyles(style, anonymousReplacedElement);
anonymousReplacedElement.className = `${PSEUDO_HIDE_ELEMENT_CLASS_BEFORE} ${PSEUDO_HIDE_ELEMENT_CLASS_AFTER}`;
clone.className +=
pseudoElt === PSEUDO_BEFORE
? ` ${PSEUDO_HIDE_ELEMENT_CLASS_BEFORE}`
: ` ${PSEUDO_HIDE_ELEMENT_CLASS_AFTER}`;
if (pseudoElt === PSEUDO_BEFORE) {
clone.insertBefore(anonymousReplacedElement, clone.firstChild);
} else {
clone.appendChild(anonymousReplacedElement);
}
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]) => {
@ -74,24 +231,13 @@ const initNode = ([element, x, y]: [HTMLElement, number, number]) => {
};
export const cloneWindow = (
documentToBeCloned: Document,
ownerDocument: Document,
bounds: Bounds,
referenceElement: HTMLElement,
options: Options
options: Options,
logger: Logger
): 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 cloner = new DocumentCloner(referenceElement, options, logger);
const cloneIframeContainer = ownerDocument.createElement('iframe');
cloneIframeContainer.className = 'html2canvas-container';
@ -120,7 +266,7 @@ export const cloneWindow = (
cloneWindow.onload = cloneIframeContainer.onload = () => {
const interval = setInterval(() => {
if (documentClone.body.childNodes.length > 0) {
scrolledElements.forEach(initNode);
cloner.scrolledElements.forEach(initNode);
clearInterval(interval);
if (options.type === 'view') {
cloneWindow.scrollTo(bounds.left, bounds.top);
@ -135,10 +281,10 @@ export const cloneWindow = (
}
}
if (
referenceElementSearch[1] instanceof cloneWindow.HTMLElement ||
referenceElementSearch[1] instanceof HTMLElement
cloner.clonedReferenceElement instanceof cloneWindow.HTMLElement ||
cloner.clonedReferenceElement instanceof HTMLElement
) {
resolve([cloneIframeContainer, referenceElementSearch[1]]);
resolve([cloneIframeContainer, cloner.clonedReferenceElement]);
} else {
reject(
__DEV__
@ -153,9 +299,9 @@ export const cloneWindow = (
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);
restoreOwnerScroll(referenceElement.ownerDocument, bounds.left, bounds.top);
documentClone.replaceChild(
documentClone.adoptNode(documentElement),
documentClone.adoptNode(cloner.documentElement),
documentClone.documentElement
);
documentClone.close();

View File

@ -71,6 +71,34 @@ const testSVG = document => {
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 = {
// $FlowFixMe - get/set properties not yet supported
get SUPPORT_RANGE_BOUNDS() {
@ -94,6 +122,13 @@ const FEATURES = {
Object.defineProperty(FEATURES, 'SUPPORT_BASE64_DRAWING', {value: () => 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;
}
};

View File

@ -5,15 +5,16 @@ import type Options from './index';
import type Logger from './Logger';
export type ImageElement = Image | HTMLCanvasElement;
type ImageCache = {[string]: Promise<?ImageElement>};
type ImageCache<T> = {[string]: Promise<T>};
import FEATURES from './Feature';
export default class ImageLoader {
// $FlowFixMe
export default class ImageLoader<T> {
origin: string;
options: Options;
_link: HTMLAnchorElement;
cache: ImageCache;
cache: ImageCache<T>;
logger: Logger;
_index: number;
_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 {
const key = String(this._index++);
this.cache[key] = Promise.resolve(node);
@ -60,8 +101,8 @@ export default class ImageLoader {
this.logger.log(`Added image ${key.substring(0, 256)}`);
}
const imageLoadHandler = (supportsDataImages: boolean): Promise<Image> => {
return new Promise((resolve, reject) => {
const imageLoadHandler = (supportsDataImages: boolean): Promise<Image> =>
new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
//ios safari 10.3 taints canvas with data urls unless crossOrigin is set to anonymous
@ -78,7 +119,6 @@ export default class ImageLoader {
}, 500);
}
});
};
this.cache[key] =
isInlineImage(src) && !isSVG(src)
@ -99,7 +139,7 @@ export default class ImageLoader {
return link.protocol + link.hostname + link.port;
}
ready(): Promise<ImageStore> {
ready(): Promise<ImageStore<T>> {
const keys = Object.keys(this.cache);
return Promise.all(
keys.map(str =>
@ -112,23 +152,23 @@ export default class ImageLoader {
)
).then(images => {
if (__DEV__) {
this.logger.log('Finished loading images', images);
this.logger.log(`Finished loading ${images.length} images`, images);
}
return new ImageStore(keys, images);
});
}
}
export class ImageStore {
export class ImageStore<T> {
_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._images = images;
}
get(key: string): ?ImageElement {
get(key: string): ?T {
const index = this._keys.indexOf(key);
return index === -1 ? null : this._images[index];
}

View File

@ -18,7 +18,7 @@ import type {Visibility} from './parsing/visibility';
import type {zIndex} from './parsing/zIndex';
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 TextContainer from './TextContainer';
@ -87,7 +87,7 @@ export default class NodeContainer {
constructor(
node: HTMLElement | SVGSVGElement,
parent: ?NodeContainer,
imageLoader: ImageLoader,
imageLoader: ImageLoader<ImageElement>,
index: number
) {
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 (
node instanceof node.ownerDocument.defaultView.SVGSVGElement ||
node instanceof SVGSVGElement

View File

@ -1,16 +1,15 @@
/* @flow */
'use strict';
import type ImageLoader from './ImageLoader';
import type ImageLoader, {ImageElement} from './ImageLoader';
import type Logger from './Logger';
import StackingContext from './StackingContext';
import NodeContainer from './NodeContainer';
import TextContainer from './TextContainer';
import {inlineInputElement, inlineTextAreaElement, inlineSelectElement} from './Input';
import {copyCSSStyles} from './Util';
export const NodeParser = (
node: HTMLElement,
imageLoader: ImageLoader,
imageLoader: ImageLoader<ImageElement>,
logger: Logger
): StackingContext => {
if (__DEV__) {
@ -22,7 +21,6 @@ export const NodeParser = (
const container = new NodeContainer(node, null, imageLoader, index++);
const stack = new StackingContext(container, null, true);
createPseudoHideStyles(node.ownerDocument);
parseNodeTree(node, container, stack, imageLoader, index);
if (__DEV__) {
@ -33,17 +31,12 @@ export const NodeParser = (
};
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 = (
node: HTMLElement,
parent: NodeContainer,
stack: StackingContext,
imageLoader: ImageLoader,
imageLoader: ImageLoader<ImageElement>,
index: number
): void => {
if (__DEV__ && index > 50000) {
@ -62,8 +55,6 @@ const parseNodeTree = (
childNode instanceof HTMLElement
) {
if (IGNORED_NODE_NAMES.indexOf(childNode.nodeName) === -1) {
inlinePseudoElement(childNode, PSEUDO_BEFORE);
inlinePseudoElement(childNode, PSEUDO_AFTER);
const container = new NodeContainer(childNode, parent, imageLoader, index++);
if (container.isVisible()) {
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 = (
container: NodeContainer,
node: HTMLElement | SVGSVGElement
@ -197,31 +150,3 @@ const isBodyWithTransparentRoot = (
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);
}
};

View File

@ -43,7 +43,7 @@ import {BORDER_STYLE} from './parsing/border';
export type RenderOptions = {
scale: number,
backgroundColor: ?Color,
imageStore: ImageStore,
imageStore: ImageStore<ImageElement>,
fontMetrics: FontMetrics,
logger: Logger,
width: number,

View File

@ -5,13 +5,15 @@ import type {RenderTarget} from './Renderer';
import {NodeParser} from './NodeParser';
import Renderer from './Renderer';
import ForeignObjectRenderer from './renderer/ForeignObjectRenderer';
import CanvasRenderer from './renderer/CanvasRenderer';
import Logger from './Logger';
import ImageLoader from './ImageLoader';
import Feature from './Feature';
import {Bounds, parseDocumentSize} from './Bounds';
import {cloneWindow} from './Clone';
import Color, {TRANSPARENT} from './Color';
import {cloneWindow, DocumentCloner} from './Clone';
import {FontMetrics} from './Font';
import Color, {TRANSPARENT} from './Color';
export type Options = {
async: ?boolean,
@ -42,7 +44,7 @@ const html2canvas = (element: HTMLElement, config: Options): Promise<*> => {
const defaultOptions = {
async: true,
allowTaint: false,
imageTimeout: 10000,
imageTimeout: 15000,
proxy: null,
removeContainer: true,
scale: defaultView.devicePixelRatio || 1,
@ -63,79 +65,105 @@ const html2canvas = (element: HTMLElement, config: Options): Promise<*> => {
options.windowHeight
);
const result = cloneWindow(
ownerDocument,
ownerDocument,
windowBounds,
element,
options
).then(([container, clonedElement]) => {
if (__DEV__) {
logger.log(`Document cloned`);
}
const bounds = options.type === 'view' ? windowBounds : parseDocumentSize(ownerDocument);
const imageLoader = new ImageLoader(
options,
logger,
clonedElement.ownerDocument.defaultView
);
const stack = NodeParser(clonedElement, imageLoader, logger);
const clonedDocument = clonedElement.ownerDocument;
const size = options.type === 'view' ? windowBounds : parseDocumentSize(clonedDocument);
const width = size.width;
const height = size.height;
// http://www.w3.org/TR/css3-background/#special-backgrounds
const documentBackgroundColor = ownerDocument.documentElement
? new Color(getComputedStyle(ownerDocument.documentElement).backgroundColor)
: TRANSPARENT;
const backgroundColor =
element === ownerDocument.documentElement
? documentBackgroundColor.isTransparent()
? ownerDocument.body
? new Color(getComputedStyle(ownerDocument.body).backgroundColor)
: null
: documentBackgroundColor
: null;
// http://www.w3.org/TR/css3-background/#special-backgrounds
const backgroundColor =
clonedElement === clonedDocument.documentElement
? stack.container.style.background.backgroundColor.isTransparent()
? clonedDocument.body
? new Color(getComputedStyle(clonedDocument.body).backgroundColor)
: null
: stack.container.style.background.backgroundColor
: null;
// $FlowFixMe
const result = Feature.SUPPORT_FOREIGNOBJECT_DRAWING.then(
supportForeignObject =>
supportForeignObject
? (cloner => {
if (__DEV__) {
logger.log(`Document cloned, using foreignObject rendering`);
}
if (backgroundColor === stack.container.style.background.backgroundColor) {
stack.container.style.background.backgroundColor = TRANSPARENT;
}
return cloner.imageLoader.ready().then(() => {
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 => {
if (options.removeContainer === true) {
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 imageLoader = new ImageLoader(
options,
logger,
clonedElement.ownerDocument.defaultView
);
const stack = NodeParser(clonedElement, imageLoader, logger);
const clonedDocument = clonedElement.ownerDocument;
const width = bounds.width;
const height = bounds.height;
const fontMetrics = new FontMetrics(clonedDocument);
if (__DEV__) {
logger.log(`Starting renderer`);
}
if (backgroundColor === stack.container.style.background.backgroundColor) {
stack.container.style.background.backgroundColor = TRANSPARENT;
}
const renderOptions = {
backgroundColor,
fontMetrics,
imageStore,
logger,
scale: options.scale,
width,
height
};
return imageLoader.ready().then(imageStore => {
if (options.removeContainer === true) {
if (container.parentNode) {
container.parentNode.removeChild(container);
} else if (__DEV__) {
logger.log(
`Cannot detach cloned iframe as it is not in the DOM anymore`
);
}
}
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);
}
});
});
const fontMetrics = new FontMetrics(clonedDocument);
if (__DEV__) {
logger.log(`Starting renderer`);
}
const renderOptions = {
backgroundColor,
fontMetrics,
imageStore,
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__) {
return result.catch(e => {

View File

@ -223,7 +223,7 @@ export const calculateBackgroundRepeatPath = (
export const parseBackground = (
style: CSSStyleDeclaration,
imageLoader: ImageLoader
imageLoader: ImageLoader<ImageElement>
): Background => {
return {
backgroundColor: new Color(style.backgroundColor),
@ -276,12 +276,17 @@ const parseBackgroundRepeat = (backgroundRepeat: string): BackgroundRepeat => {
const parseBackgroundImages = (
style: CSSStyleDeclaration,
imageLoader: ImageLoader
imageLoader: ImageLoader<ImageElement>
): Array<BackgroundImage> => {
const sources: Array<BackgroundSource> = parseBackgroundImage(
style.backgroundImage,
imageLoader
);
style.backgroundImage
).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 repeats = style.backgroundRepeat.split(',');
const sizes = style.backgroundSize.split(',');
@ -318,7 +323,7 @@ const parseBackgoundPosition = (position: string): Length => {
return new Length(position);
};
const parseBackgroundImage = (image: string, imageLoader: ImageLoader): Array<BackgroundSource> => {
export const parseBackgroundImage = (image: string): Array<BackgroundSource> => {
const whitespace = /^\s$/;
const results = [];
@ -346,10 +351,6 @@ const parseBackgroundImage = (image: string, imageLoader: ImageLoader): Array<Ba
method = method.substr(prefix_i);
}
method = method.toLowerCase();
if (method === 'url') {
const key = imageLoader.loadImage(args[0]);
args = key ? [key] : [];
}
if (method !== 'none') {
results.push({
prefix,

View 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)
)}`;
});
}
}