Inline fonts for ForeignObjectRenderer

This commit is contained in:
Niklas von Hertzen 2017-10-18 20:34:17 +08:00
parent f16d581f04
commit 9445b0b598
12 changed files with 234 additions and 104 deletions

View File

@ -30,14 +30,14 @@
"base64-arraybuffer": "0.1.5", "base64-arraybuffer": "0.1.5",
"body-parser": "1.17.2", "body-parser": "1.17.2",
"chai": "4.1.1", "chai": "4.1.1",
"chromeless": "^1.2.0", "chromeless": "1.2.0",
"cors": "2.8.4", "cors": "2.8.4",
"eslint": "4.2.0", "eslint": "4.2.0",
"eslint-plugin-flowtype": "2.35.0", "eslint-plugin-flowtype": "2.35.0",
"eslint-plugin-prettier": "2.1.2", "eslint-plugin-prettier": "2.1.2",
"express": "4.15.4", "express": "4.15.4",
"filenamify-url": "1.0.0", "filenamify-url": "1.0.0",
"flow-bin": "0.50.0", "flow-bin": "0.56.0",
"glob": "7.1.2", "glob": "7.1.2",
"html2canvas-proxy": "1.0.0", "html2canvas-proxy": "1.0.0",
"jquery": "3.2.1", "jquery": "3.2.1",

View File

@ -3,11 +3,10 @@
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 type Logger from './Logger';
import type {ImageElement} from './ImageLoader';
import {parseBounds} from './Bounds'; import {parseBounds} from './Bounds';
import {Proxy} from './Proxy'; import {Proxy} from './Proxy';
import ImageLoader from './ImageLoader'; import ResourceLoader from './ResourceLoader';
import {copyCSSStyles} from './Util'; import {copyCSSStyles} from './Util';
import {parseBackgroundImage} from './parsing/background'; import {parseBackgroundImage} from './parsing/background';
import CanvasRenderer from './renderer/CanvasRenderer'; import CanvasRenderer from './renderer/CanvasRenderer';
@ -17,7 +16,7 @@ export class DocumentCloner {
referenceElement: HTMLElement; referenceElement: HTMLElement;
clonedReferenceElement: HTMLElement; clonedReferenceElement: HTMLElement;
documentElement: HTMLElement; documentElement: HTMLElement;
imageLoader: ImageLoader<*>; resourceLoader: ResourceLoader;
logger: Logger; logger: Logger;
options: Options; options: Options;
inlineImages: boolean; inlineImages: boolean;
@ -38,7 +37,7 @@ export class DocumentCloner {
this.logger = logger; this.logger = logger;
this.options = options; this.options = options;
this.renderer = renderer; this.renderer = renderer;
this.imageLoader = new ImageLoader(options, logger, window); this.resourceLoader = new ResourceLoader(options, logger, window);
// $FlowFixMe // $FlowFixMe
this.documentElement = this.cloneNode(element.ownerDocument.documentElement); this.documentElement = this.cloneNode(element.ownerDocument.documentElement);
} }
@ -49,9 +48,14 @@ export class DocumentCloner {
Promise.all( Promise.all(
parseBackgroundImage(style.backgroundImage).map(backgroundImage => { parseBackgroundImage(style.backgroundImage).map(backgroundImage => {
if (backgroundImage.method === 'url') { if (backgroundImage.method === 'url') {
return this.imageLoader return this.resourceLoader
.inlineImage(backgroundImage.args[0]) .inlineImage(backgroundImage.args[0])
.then(img => (img ? `url("${img.src}")` : 'none')) .then(
img =>
img && typeof img.src === 'string'
? `url("${img.src}")`
: 'none'
)
.catch(e => { .catch(e => {
if (__DEV__) { if (__DEV__) {
this.logger.log(`Unable to load image`, e); this.logger.log(`Unable to load image`, e);
@ -73,7 +77,7 @@ export class DocumentCloner {
}); });
if (node instanceof HTMLImageElement) { if (node instanceof HTMLImageElement) {
this.imageLoader this.resourceLoader
.inlineImage(node.src) .inlineImage(node.src)
.then(img => { .then(img => {
if (img && node instanceof HTMLImageElement && node.parentNode) { if (img && node instanceof HTMLImageElement && node.parentNode) {
@ -91,6 +95,56 @@ export class DocumentCloner {
} }
} }
inlineFonts(document: Document): Promise<void> {
return Promise.all(
Array.from(document.styleSheets).map(sheet => {
if (sheet.href) {
return fetch(sheet.href)
.then(res => res.text())
.then(text => createStyleSheetFontsFromText(text, sheet.href))
.catch(e => {
if (__DEV__) {
this.logger.log(`Unable to load stylesheet`, e);
}
return [];
});
}
return getSheetFonts(sheet, document);
})
)
.then(fonts => fonts.reduce((acc, font) => acc.concat(font), []))
.then(fonts =>
Promise.all(
fonts.map(font =>
fetch(font.formats[0].src)
.then(response => response.blob())
.then(
blob =>
new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onerror = reject;
reader.onload = () => {
// $FlowFixMe
const result: string = reader.result;
resolve(result);
};
reader.readAsDataURL(blob);
})
)
.then(dataUri => {
font.fontFace.setProperty('src', `url("${dataUri}")`);
return `@font-face {${font.fontFace.cssText} `;
})
)
)
)
.then(fontCss => {
const style = document.createElement('style');
style.textContent = fontCss.join('\n');
this.documentElement.appendChild(style);
});
}
createElementClone(node: Node) { createElementClone(node: Node) {
if (this.copyStyles && node instanceof HTMLCanvasElement) { if (this.copyStyles && node instanceof HTMLCanvasElement) {
const img = node.ownerDocument.createElement('img'); const img = node.ownerDocument.createElement('img');
@ -111,7 +165,7 @@ export class DocumentCloner {
const {width, height} = parseBounds(node, 0, 0); const {width, height} = parseBounds(node, 0, 0);
this.imageLoader.cache[iframeKey] = getIframeDocumentElement(node, this.options) this.resourceLoader.cache[iframeKey] = getIframeDocumentElement(node, this.options)
.then(documentElement => { .then(documentElement => {
return this.renderer( return this.renderer(
documentElement, documentElement,
@ -211,6 +265,67 @@ export class DocumentCloner {
} }
} }
type Font = {
src: string,
format: string
};
type FontFamily = {
formats: Array<Font>,
fontFace: CSSStyleDeclaration
};
const getSheetFonts = (sheet: StyleSheet, document: Document): Array<FontFamily> => {
// $FlowFixMe
return (sheet.cssRules ? Array.from(sheet.cssRules) : [])
.filter(rule => rule.type === CSSRule.FONT_FACE_RULE)
.map(rule => {
const src = parseBackgroundImage(rule.style.getPropertyValue('src'));
const formats = [];
for (let i = 0; i < src.length; i++) {
if (src[i].method === 'url' && src[i + 1] && src[i + 1].method === 'format') {
const a = document.createElement('a');
a.href = src[i].args[0];
if (document.body) {
document.body.appendChild(a);
}
const font = {
src: a.href,
format: src[i + 1].args[0]
};
formats.push(font);
}
}
return {
// TODO select correct format for browser),
formats: formats.filter(font => /^woff/i.test(font.format)),
fontFace: rule.style
};
})
.filter(font => font.formats.length);
};
const createStyleSheetFontsFromText = (text: string, baseHref: string): Array<FontFamily> => {
const doc = document.implementation.createHTMLDocument('');
const base = document.createElement('base');
// $FlowFixMe
base.href = baseHref;
const style = document.createElement('style');
style.textContent = text;
if (doc.head) {
doc.head.appendChild(base);
}
if (doc.body) {
doc.body.appendChild(style);
}
return style.sheet ? getSheetFonts(style.sheet, doc) : [];
};
const restoreOwnerScroll = (ownerDocument: Document, x: number, y: number) => { const restoreOwnerScroll = (ownerDocument: Document, x: number, y: number) => {
if ( if (
ownerDocument.defaultView && ownerDocument.defaultView &&
@ -415,7 +530,7 @@ export const cloneWindow = (
options: Options, options: Options,
logger: Logger, logger: Logger,
renderer: (element: HTMLElement, options: Options, logger: Logger) => Promise<*> renderer: (element: HTMLElement, options: Options, logger: Logger) => Promise<*>
): Promise<[HTMLIFrameElement, HTMLElement, ImageLoader<ImageElement>]> => { ): Promise<[HTMLIFrameElement, HTMLElement, ResourceLoader]> => {
const cloner = new DocumentCloner(referenceElement, options, logger, false, renderer); const cloner = new DocumentCloner(referenceElement, options, logger, false, renderer);
const scrollX = ownerDocument.defaultView.pageXOffset; const scrollX = ownerDocument.defaultView.pageXOffset;
const scrollY = ownerDocument.defaultView.pageYOffset; const scrollY = ownerDocument.defaultView.pageYOffset;
@ -445,7 +560,7 @@ export const cloneWindow = (
? Promise.resolve([ ? Promise.resolve([
cloneIframeContainer, cloneIframeContainer,
cloner.clonedReferenceElement, cloner.clonedReferenceElement,
cloner.imageLoader cloner.resourceLoader
]) ])
: Promise.reject( : Promise.reject(
__DEV__ __DEV__

View File

@ -146,7 +146,10 @@ const FEATURES = {
// $FlowFixMe - get/set properties not yet supported // $FlowFixMe - get/set properties not yet supported
get SUPPORT_FOREIGNOBJECT_DRAWING() { get SUPPORT_FOREIGNOBJECT_DRAWING() {
'use strict'; 'use strict';
const value = testForeignObject(document); const value =
typeof Array.from === 'function' && typeof window.fetch === 'function'
? testForeignObject(document)
: Promise.resolve(false);
Object.defineProperty(FEATURES, 'SUPPORT_FOREIGNOBJECT_DRAWING', {value}); Object.defineProperty(FEATURES, 'SUPPORT_FOREIGNOBJECT_DRAWING', {value});
return value; return value;
}, },

View File

@ -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, {ImageElement} from './ImageLoader'; import type ResourceLoader, {ImageElement} from './ResourceLoader';
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<ImageElement>, resourceLoader: ResourceLoader,
index: number index: number
) { ) {
this.parent = parent; this.parent = parent;
@ -104,7 +104,7 @@ export default class NodeContainer {
const position = parsePosition(style.position); const position = parsePosition(style.position);
this.style = { this.style = {
background: IS_INPUT ? INPUT_BACKGROUND : parseBackground(style, imageLoader), background: IS_INPUT ? INPUT_BACKGROUND : parseBackground(style, resourceLoader),
border: IS_INPUT ? INPUT_BORDERS : parseBorder(style), border: IS_INPUT ? INPUT_BORDERS : parseBorder(style),
borderRadius: borderRadius:
(node instanceof defaultView.HTMLInputElement || (node instanceof defaultView.HTMLInputElement ||
@ -148,7 +148,7 @@ export default class NodeContainer {
); );
}); });
} }
this.image = getImage(node, imageLoader); this.image = getImage(node, resourceLoader);
this.bounds = IS_INPUT this.bounds = IS_INPUT
? reformatInputBounds(parseBounds(node, scrollX, scrollY)) ? reformatInputBounds(parseBounds(node, scrollX, scrollY))
: parseBounds(node, scrollX, scrollY); : parseBounds(node, scrollX, scrollY);
@ -223,26 +223,25 @@ export default class NodeContainer {
} }
} }
const getImage = ( const getImage = (node: HTMLElement | SVGSVGElement, resourceLoader: ResourceLoader): ?string => {
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
) { ) {
const s = new XMLSerializer(); const s = new XMLSerializer();
return imageLoader.loadImage( return resourceLoader.loadImage(
`data:image/svg+xml,${encodeURIComponent(s.serializeToString(node))}` `data:image/svg+xml,${encodeURIComponent(s.serializeToString(node))}`
); );
} }
switch (node.tagName) { switch (node.tagName) {
case 'IMG': case 'IMG':
// $FlowFixMe // $FlowFixMe
return imageLoader.loadImage(node.currentSrc || node.src); const img: HTMLImageElement = node;
return resourceLoader.loadImage(img.currentSrc || img.src);
case 'CANVAS': case 'CANVAS':
// $FlowFixMe // $FlowFixMe
return imageLoader.loadCanvas(node); const canvas: HTMLCanvasElement = node;
return resourceLoader.loadCanvas(canvas);
case 'IFRAME': case 'IFRAME':
const iframeKey = node.getAttribute('data-html2canvas-internal-iframe-key'); const iframeKey = node.getAttribute('data-html2canvas-internal-iframe-key');
if (iframeKey) { if (iframeKey) {

View File

@ -1,6 +1,6 @@
/* @flow */ /* @flow */
'use strict'; 'use strict';
import type ImageLoader, {ImageElement} from './ImageLoader'; import type ResourceLoader, {ImageElement} from './ResourceLoader';
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';
@ -9,7 +9,7 @@ import {inlineInputElement, inlineTextAreaElement, inlineSelectElement} from './
export const NodeParser = ( export const NodeParser = (
node: HTMLElement, node: HTMLElement,
imageLoader: ImageLoader<ImageElement>, resourceLoader: ResourceLoader,
logger: Logger logger: Logger
): StackingContext => { ): StackingContext => {
if (__DEV__) { if (__DEV__) {
@ -18,10 +18,10 @@ export const NodeParser = (
let index = 0; let index = 0;
const container = new NodeContainer(node, null, imageLoader, index++); const container = new NodeContainer(node, null, resourceLoader, index++);
const stack = new StackingContext(container, null, true); const stack = new StackingContext(container, null, true);
parseNodeTree(node, container, stack, imageLoader, index); parseNodeTree(node, container, stack, resourceLoader, index);
if (__DEV__) { if (__DEV__) {
logger.log(`Finished parsing node tree`); logger.log(`Finished parsing node tree`);
@ -36,7 +36,7 @@ const parseNodeTree = (
node: HTMLElement, node: HTMLElement,
parent: NodeContainer, parent: NodeContainer,
stack: StackingContext, stack: StackingContext,
imageLoader: ImageLoader<ImageElement>, resourceLoader: ResourceLoader,
index: number index: number
): void => { ): void => {
if (__DEV__ && index > 50000) { if (__DEV__ && index > 50000) {
@ -60,7 +60,7 @@ const parseNodeTree = (
(defaultView.parent && childNode instanceof defaultView.parent.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, resourceLoader, index++);
if (container.isVisible()) { if (container.isVisible()) {
if (childNode.tagName === 'INPUT') { if (childNode.tagName === 'INPUT') {
// $FlowFixMe // $FlowFixMe
@ -92,12 +92,12 @@ const parseNodeTree = (
); );
parentStack.contexts.push(childStack); parentStack.contexts.push(childStack);
if (SHOULD_TRAVERSE_CHILDREN) { if (SHOULD_TRAVERSE_CHILDREN) {
parseNodeTree(childNode, container, childStack, imageLoader, index); parseNodeTree(childNode, container, childStack, resourceLoader, index);
} }
} else { } else {
stack.children.push(container); stack.children.push(container);
if (SHOULD_TRAVERSE_CHILDREN) { if (SHOULD_TRAVERSE_CHILDREN) {
parseNodeTree(childNode, container, stack, imageLoader, index); parseNodeTree(childNode, container, stack, resourceLoader, index);
} }
} }
} }
@ -107,7 +107,7 @@ const parseNodeTree = (
childNode instanceof SVGSVGElement || childNode instanceof SVGSVGElement ||
(defaultView.parent && childNode instanceof defaultView.parent.SVGSVGElement) (defaultView.parent && childNode instanceof defaultView.parent.SVGSVGElement)
) { ) {
const container = new NodeContainer(childNode, parent, imageLoader, index++); const container = new NodeContainer(childNode, parent, resourceLoader, index++);
const treatAsRealStackingContext = createsRealStackingContext(container, childNode); const treatAsRealStackingContext = createsRealStackingContext(container, childNode);
if (treatAsRealStackingContext || createsStackingContext(container)) { if (treatAsRealStackingContext || createsStackingContext(container)) {
// for treatAsRealStackingContext:false, any positioned descendants and descendants // for treatAsRealStackingContext:false, any positioned descendants and descendants

View File

@ -15,7 +15,7 @@ import type {Matrix} from './parsing/transform';
import type {BoundCurves} from './Bounds'; import type {BoundCurves} from './Bounds';
import type {Gradient} from './Gradient'; import type {Gradient} from './Gradient';
import type {ImageStore, ImageElement} from './ImageLoader'; import type {ResourceStore, ImageElement} from './ResourceLoader';
import type NodeContainer from './NodeContainer'; import type NodeContainer from './NodeContainer';
import type StackingContext from './StackingContext'; import type StackingContext from './StackingContext';
import type {TextBounds} from './TextBounds'; import type {TextBounds} from './TextBounds';
@ -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<ImageElement>, imageStore: ResourceStore,
fontMetrics: FontMetrics, fontMetrics: FontMetrics,
logger: Logger, logger: Logger,
x: number, x: number,

View File

@ -5,17 +5,17 @@ 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<T> = {[string]: Promise<T>}; export type Resource = ImageElement;
type ResourceCache = {[string]: Promise<Resource>};
import FEATURES from './Feature'; import FEATURES from './Feature';
import {Proxy} from './Proxy'; import {Proxy} from './Proxy';
// $FlowFixMe export default class ResourceLoader {
export default class ImageLoader<T> {
origin: string; origin: string;
options: Options; options: Options;
_link: HTMLAnchorElement; _link: HTMLAnchorElement;
cache: ImageCache<T>; cache: ResourceCache;
logger: Logger; logger: Logger;
_index: number; _index: number;
_window: WindowProxy; _window: WindowProxy;
@ -30,7 +30,7 @@ export default class ImageLoader<T> {
} }
loadImage(src: string): ?string { loadImage(src: string): ?string {
if (this.hasImageInCache(src)) { if (this.hasResourceInCache(src)) {
return src; return src;
} }
@ -58,11 +58,11 @@ export default class ImageLoader<T> {
} }
} }
inlineImage(src: string): Promise<Image> { inlineImage(src: string): Promise<Resource> {
if (isInlineImage(src)) { if (isInlineImage(src)) {
return loadImage(src, this.options.imageTimeout || 0); return loadImage(src, this.options.imageTimeout || 0);
} }
if (this.hasImageInCache(src)) { if (this.hasResourceInCache(src)) {
return this.cache[src]; return this.cache[src];
} }
if (!this.isSameOrigin(src) && typeof this.options.proxy === 'string') { if (!this.isSameOrigin(src) && typeof this.options.proxy === 'string') {
@ -74,7 +74,7 @@ export default class ImageLoader<T> {
return this.xhrImage(src); return this.xhrImage(src);
} }
xhrImage(src: string): Promise<Image> { xhrImage(src: string): Promise<Resource> {
this.cache[src] = new Promise((resolve, reject) => { this.cache[src] = new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest(); const xhr = new XMLHttpRequest();
xhr.onreadystatechange = () => { xhr.onreadystatechange = () => {
@ -88,10 +88,16 @@ export default class ImageLoader<T> {
); );
} else { } else {
const reader = new FileReader(); const reader = new FileReader();
// $FlowFixMe reader.addEventListener(
reader.addEventListener('load', () => resolve(reader.result), false); 'load',
// $FlowFixMe () => {
reader.addEventListener('error', e => reject(e), false); // $FlowFixMe
const result: string = reader.result;
resolve(result);
},
false
);
reader.addEventListener('error', (e: Event) => reject(e), false);
reader.readAsDataURL(xhr.response); reader.readAsDataURL(xhr.response);
} }
} }
@ -118,7 +124,7 @@ export default class ImageLoader<T> {
return key; return key;
} }
hasImageInCache(key: string): boolean { hasResourceInCache(key: string): boolean {
return typeof this.cache[key] !== 'undefined'; return typeof this.cache[key] !== 'undefined';
} }
@ -177,38 +183,37 @@ export default class ImageLoader<T> {
return link.protocol + link.hostname + link.port; return link.protocol + link.hostname + link.port;
} }
ready(): Promise<ImageStore<T>> { ready(): Promise<ResourceStore> {
const keys = Object.keys(this.cache); const keys: Array<string> = Object.keys(this.cache);
return Promise.all( const values: Array<Promise<?Resource>> = keys.map(str =>
keys.map(str => this.cache[str].catch(e => {
this.cache[str].catch(e => { if (__DEV__) {
if (__DEV__) { this.logger.log(`Unable to load image`, e);
this.logger.log(`Unable to load image`, e); }
} return null;
return null; })
}) );
) return Promise.all(values).then((images: Array<?Resource>) => {
).then(images => {
if (__DEV__) { if (__DEV__) {
this.logger.log(`Finished loading ${images.length} images`, images); this.logger.log(`Finished loading ${images.length} images`, images);
} }
return new ImageStore(keys, images); return new ResourceStore(keys, images);
}); });
} }
} }
export class ImageStore<T> { export class ResourceStore {
_keys: Array<string>; _keys: Array<string>;
_images: Array<?T>; _resources: Array<?Resource>;
constructor(keys: Array<string>, images: Array<?T>) { constructor(keys: Array<string>, resources: Array<?Resource>) {
this._keys = keys; this._keys = keys;
this._images = images; this._resources = resources;
} }
get(key: string): ?T { get(key: string): ?Resource {
const index = this._keys.indexOf(key); const index = this._keys.indexOf(key);
return index === -1 ? null : this._images[index]; return index === -1 ? null : this._resources[index];
} }
} }
@ -222,7 +227,7 @@ const isInlineBase64Image = (src: string): boolean => INLINE_BASE64.test(src);
const isSVG = (src: string): boolean => const isSVG = (src: string): boolean =>
src.substr(-3).toLowerCase() === 'svg' || INLINE_SVG.test(src); src.substr(-3).toLowerCase() === 'svg' || INLINE_SVG.test(src);
const loadImage = (src: string, timeout: number) => { const loadImage = (src: string, timeout: number): Promise<Image> => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const img = new Image(); const img = new Image();
img.onload = () => resolve(img); img.onload = () => resolve(img);

View File

@ -57,22 +57,25 @@ export const renderElement = (
logger.log(`Document cloned, using foreignObject rendering`); logger.log(`Document cloned, using foreignObject rendering`);
} }
return cloner.imageLoader.ready().then(() => { return cloner
const renderer = new ForeignObjectRenderer(cloner.clonedReferenceElement); .inlineFonts(ownerDocument)
return renderer.render({ .then(() => cloner.resourceLoader.ready())
backgroundColor, .then(() => {
logger, const renderer = new ForeignObjectRenderer(cloner.documentElement);
scale: options.scale, return renderer.render({
x: options.x, backgroundColor,
y: options.y, logger,
width: options.width, scale: options.scale,
height: options.height, x: options.x,
windowWidth: options.windowWidth, y: options.y,
windowHeight: options.windowHeight, width: options.width,
scrollX: options.scrollX, height: options.height,
scrollY: options.scrollY windowWidth: options.windowWidth,
windowHeight: options.windowHeight,
scrollX: options.scrollX,
scrollY: options.scrollY
});
}); });
});
})(new DocumentCloner(element, options, logger, true, renderElement)) })(new DocumentCloner(element, options, logger, true, renderElement))
: cloneWindow( : cloneWindow(
ownerDocument, ownerDocument,
@ -81,19 +84,19 @@ export const renderElement = (
options, options,
logger, logger,
renderElement renderElement
).then(([container, clonedElement, imageLoader]) => { ).then(([container, clonedElement, resourceLoader]) => {
if (__DEV__) { if (__DEV__) {
logger.log(`Document cloned, using computed rendering`); logger.log(`Document cloned, using computed rendering`);
} }
const stack = NodeParser(clonedElement, imageLoader, logger); const stack = NodeParser(clonedElement, resourceLoader, logger);
const clonedDocument = clonedElement.ownerDocument; const clonedDocument = clonedElement.ownerDocument;
if (backgroundColor === stack.container.style.background.backgroundColor) { if (backgroundColor === stack.container.style.background.backgroundColor) {
stack.container.style.background.backgroundColor = TRANSPARENT; stack.container.style.background.backgroundColor = TRANSPARENT;
} }
return imageLoader.ready().then(imageStore => { return resourceLoader.ready().then(imageStore => {
if (options.removeContainer === true) { if (options.removeContainer === true) {
if (container.parentNode) { if (container.parentNode) {
container.parentNode.removeChild(container); container.parentNode.removeChild(container);

View File

@ -2,7 +2,7 @@
'use strict'; 'use strict';
import type {Path} from '../drawing/Path'; import type {Path} from '../drawing/Path';
import type {Bounds, BoundCurves} from '../Bounds'; import type {Bounds, BoundCurves} from '../Bounds';
import type ImageLoader, {ImageElement} from '../ImageLoader'; import type ResourceLoader, {ImageElement} from '../ResourceLoader';
import Color from '../Color'; import Color from '../Color';
import Length from '../Length'; import Length from '../Length';
@ -223,11 +223,11 @@ export const calculateBackgroundRepeatPath = (
export const parseBackground = ( export const parseBackground = (
style: CSSStyleDeclaration, style: CSSStyleDeclaration,
imageLoader: ImageLoader<ImageElement> resourceLoader: ResourceLoader
): Background => { ): Background => {
return { return {
backgroundColor: new Color(style.backgroundColor), backgroundColor: new Color(style.backgroundColor),
backgroundImage: parseBackgroundImages(style, imageLoader), backgroundImage: parseBackgroundImages(style, resourceLoader),
backgroundClip: parseBackgroundClip(style.backgroundClip), backgroundClip: parseBackgroundClip(style.backgroundClip),
backgroundOrigin: parseBackgroundOrigin(style.backgroundOrigin) backgroundOrigin: parseBackgroundOrigin(style.backgroundOrigin)
}; };
@ -276,13 +276,13 @@ const parseBackgroundRepeat = (backgroundRepeat: string): BackgroundRepeat => {
const parseBackgroundImages = ( const parseBackgroundImages = (
style: CSSStyleDeclaration, style: CSSStyleDeclaration,
imageLoader: ImageLoader<ImageElement> resourceLoader: ResourceLoader
): Array<BackgroundImage> => { ): Array<BackgroundImage> => {
const sources: Array<BackgroundSource> = parseBackgroundImage( const sources: Array<BackgroundSource> = parseBackgroundImage(
style.backgroundImage style.backgroundImage
).map(backgroundImage => { ).map(backgroundImage => {
if (backgroundImage.method === 'url') { if (backgroundImage.method === 'url') {
const key = imageLoader.loadImage(backgroundImage.args[0]); const key = resourceLoader.loadImage(backgroundImage.args[0]);
backgroundImage.args = key ? [key] : []; backgroundImage.args = key ? [key] : [];
} }
return backgroundImage; return backgroundImage;

View File

@ -12,7 +12,7 @@ import type {TextShadow} from '../parsing/textShadow';
import type {Matrix} from '../parsing/transform'; import type {Matrix} from '../parsing/transform';
import type {Bounds} from '../Bounds'; import type {Bounds} from '../Bounds';
import type {ImageElement} from '../ImageLoader'; import type {ImageElement} from '../ResourceLoader';
import type {Gradient} from '../Gradient'; import type {Gradient} from '../Gradient';
import type {TextBounds} from '../TextBounds'; import type {TextBounds} from '../TextBounds';

View File

@ -12,29 +12,34 @@ export default class ForeignObjectRenderer {
this.options = options; this.options = options;
this.canvas = document.createElement('canvas'); this.canvas = document.createElement('canvas');
this.ctx = this.canvas.getContext('2d'); this.ctx = this.canvas.getContext('2d');
this.canvas.width = Math.floor(options.width * options.scale); this.canvas.width = Math.floor(options.width) * options.scale;
this.canvas.height = Math.floor(options.height * options.scale); this.canvas.height = Math.floor(options.height) * options.scale;
this.canvas.style.width = `${options.width}px`; this.canvas.style.width = `${options.width}px`;
this.canvas.style.height = `${options.height}px`; this.canvas.style.height = `${options.height}px`;
this.ctx.scale(this.options.scale, this.options.scale);
options.logger.log( options.logger.log(
`ForeignObject renderer initialized (${options.width}x${options.height} at ${options.x},${options.y}) with scale ${this `ForeignObject renderer initialized (${options.width}x${options.height} at ${options.x},${options.y}) with scale ${options.scale}`
.options.scale}`
); );
const svg = createForeignObjectSVG( const svg = createForeignObjectSVG(
Math.max(options.windowWidth, options.width), Math.max(options.windowWidth, options.width) * options.scale,
Math.max(options.windowHeight, options.height), Math.max(options.windowHeight, options.height) * options.scale,
options.scrollX, options.scrollX * options.scale,
options.scrollY, options.scrollY * options.scale,
this.element this.element
); );
return loadSerializedSVG(svg).then(img => { return loadSerializedSVG(svg).then(img => {
if (options.backgroundColor) { if (options.backgroundColor) {
this.ctx.fillStyle = options.backgroundColor.toString(); this.ctx.fillStyle = options.backgroundColor.toString();
this.ctx.fillRect(0, 0, options.width, options.height); this.ctx.fillRect(
0,
0,
options.width * options.scale,
options.height * options.scale
);
} }
this.ctx.drawImage(img, -options.x, -options.y);
this.ctx.drawImage(img, -options.x * options.scale, -options.y * options.scale);
return this.canvas; return this.canvas;
}); });
} }

View File

@ -17,7 +17,7 @@ import type {TextShadow} from '../parsing/textShadow';
import type {Matrix} from '../parsing/transform'; import type {Matrix} from '../parsing/transform';
import type {Bounds} from '../Bounds'; import type {Bounds} from '../Bounds';
import type {ImageElement} from '../ImageLoader'; import type {ImageElement} from '../ResourceLoader';
import type {Gradient} from '../Gradient'; import type {Gradient} from '../Gradient';
import type {TextBounds} from '../TextBounds'; import type {TextBounds} from '../TextBounds';