diff --git a/package.json b/package.json index 96383f1..494b823 100644 --- a/package.json +++ b/package.json @@ -30,14 +30,14 @@ "base64-arraybuffer": "0.1.5", "body-parser": "1.17.2", "chai": "4.1.1", - "chromeless": "^1.2.0", + "chromeless": "1.2.0", "cors": "2.8.4", "eslint": "4.2.0", "eslint-plugin-flowtype": "2.35.0", "eslint-plugin-prettier": "2.1.2", "express": "4.15.4", "filenamify-url": "1.0.0", - "flow-bin": "0.50.0", + "flow-bin": "0.56.0", "glob": "7.1.2", "html2canvas-proxy": "1.0.0", "jquery": "3.2.1", diff --git a/src/Clone.js b/src/Clone.js index 6d7d3b0..cdc1ff8 100644 --- a/src/Clone.js +++ b/src/Clone.js @@ -3,11 +3,10 @@ import type {Bounds} from './Bounds'; import type {Options} from './index'; import type Logger from './Logger'; -import type {ImageElement} from './ImageLoader'; import {parseBounds} from './Bounds'; import {Proxy} from './Proxy'; -import ImageLoader from './ImageLoader'; +import ResourceLoader from './ResourceLoader'; import {copyCSSStyles} from './Util'; import {parseBackgroundImage} from './parsing/background'; import CanvasRenderer from './renderer/CanvasRenderer'; @@ -17,7 +16,7 @@ export class DocumentCloner { referenceElement: HTMLElement; clonedReferenceElement: HTMLElement; documentElement: HTMLElement; - imageLoader: ImageLoader<*>; + resourceLoader: ResourceLoader; logger: Logger; options: Options; inlineImages: boolean; @@ -38,7 +37,7 @@ export class DocumentCloner { this.logger = logger; this.options = options; this.renderer = renderer; - this.imageLoader = new ImageLoader(options, logger, window); + this.resourceLoader = new ResourceLoader(options, logger, window); // $FlowFixMe this.documentElement = this.cloneNode(element.ownerDocument.documentElement); } @@ -49,9 +48,14 @@ export class DocumentCloner { Promise.all( parseBackgroundImage(style.backgroundImage).map(backgroundImage => { if (backgroundImage.method === 'url') { - return this.imageLoader + return this.resourceLoader .inlineImage(backgroundImage.args[0]) - .then(img => (img ? `url("${img.src}")` : 'none')) + .then( + img => + img && typeof img.src === 'string' + ? `url("${img.src}")` + : 'none' + ) .catch(e => { if (__DEV__) { this.logger.log(`Unable to load image`, e); @@ -73,7 +77,7 @@ export class DocumentCloner { }); if (node instanceof HTMLImageElement) { - this.imageLoader + this.resourceLoader .inlineImage(node.src) .then(img => { if (img && node instanceof HTMLImageElement && node.parentNode) { @@ -91,6 +95,56 @@ export class DocumentCloner { } } + inlineFonts(document: Document): Promise { + 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) { if (this.copyStyles && node instanceof HTMLCanvasElement) { const img = node.ownerDocument.createElement('img'); @@ -111,7 +165,7 @@ export class DocumentCloner { 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 => { return this.renderer( documentElement, @@ -211,6 +265,67 @@ export class DocumentCloner { } } +type Font = { + src: string, + format: string +}; + +type FontFamily = { + formats: Array, + fontFace: CSSStyleDeclaration +}; + +const getSheetFonts = (sheet: StyleSheet, document: Document): Array => { + // $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 => { + 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) => { if ( ownerDocument.defaultView && @@ -415,7 +530,7 @@ export const cloneWindow = ( options: Options, logger: Logger, renderer: (element: HTMLElement, options: Options, logger: Logger) => Promise<*> -): Promise<[HTMLIFrameElement, HTMLElement, ImageLoader]> => { +): Promise<[HTMLIFrameElement, HTMLElement, ResourceLoader]> => { const cloner = new DocumentCloner(referenceElement, options, logger, false, renderer); const scrollX = ownerDocument.defaultView.pageXOffset; const scrollY = ownerDocument.defaultView.pageYOffset; @@ -445,7 +560,7 @@ export const cloneWindow = ( ? Promise.resolve([ cloneIframeContainer, cloner.clonedReferenceElement, - cloner.imageLoader + cloner.resourceLoader ]) : Promise.reject( __DEV__ diff --git a/src/Feature.js b/src/Feature.js index 4018b21..185bd75 100644 --- a/src/Feature.js +++ b/src/Feature.js @@ -146,7 +146,10 @@ const FEATURES = { // $FlowFixMe - get/set properties not yet supported get SUPPORT_FOREIGNOBJECT_DRAWING() { '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}); return value; }, diff --git a/src/NodeContainer.js b/src/NodeContainer.js index 3218ead..45218aa 100644 --- a/src/NodeContainer.js +++ b/src/NodeContainer.js @@ -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, {ImageElement} from './ImageLoader'; +import type ResourceLoader, {ImageElement} from './ResourceLoader'; 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, + resourceLoader: ResourceLoader, index: number ) { this.parent = parent; @@ -104,7 +104,7 @@ export default class NodeContainer { const position = parsePosition(style.position); 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), borderRadius: (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 ? reformatInputBounds(parseBounds(node, scrollX, scrollY)) : parseBounds(node, scrollX, scrollY); @@ -223,26 +223,25 @@ export default class NodeContainer { } } -const getImage = ( - node: HTMLElement | SVGSVGElement, - imageLoader: ImageLoader -): ?string => { +const getImage = (node: HTMLElement | SVGSVGElement, resourceLoader: ResourceLoader): ?string => { if ( node instanceof node.ownerDocument.defaultView.SVGSVGElement || node instanceof SVGSVGElement ) { const s = new XMLSerializer(); - return imageLoader.loadImage( + return resourceLoader.loadImage( `data:image/svg+xml,${encodeURIComponent(s.serializeToString(node))}` ); } switch (node.tagName) { case 'IMG': // $FlowFixMe - return imageLoader.loadImage(node.currentSrc || node.src); + const img: HTMLImageElement = node; + return resourceLoader.loadImage(img.currentSrc || img.src); case 'CANVAS': // $FlowFixMe - return imageLoader.loadCanvas(node); + const canvas: HTMLCanvasElement = node; + return resourceLoader.loadCanvas(canvas); case 'IFRAME': const iframeKey = node.getAttribute('data-html2canvas-internal-iframe-key'); if (iframeKey) { diff --git a/src/NodeParser.js b/src/NodeParser.js index 70fddb5..3fb854d 100644 --- a/src/NodeParser.js +++ b/src/NodeParser.js @@ -1,6 +1,6 @@ /* @flow */ 'use strict'; -import type ImageLoader, {ImageElement} from './ImageLoader'; +import type ResourceLoader, {ImageElement} from './ResourceLoader'; import type Logger from './Logger'; import StackingContext from './StackingContext'; import NodeContainer from './NodeContainer'; @@ -9,7 +9,7 @@ import {inlineInputElement, inlineTextAreaElement, inlineSelectElement} from './ export const NodeParser = ( node: HTMLElement, - imageLoader: ImageLoader, + resourceLoader: ResourceLoader, logger: Logger ): StackingContext => { if (__DEV__) { @@ -18,10 +18,10 @@ export const NodeParser = ( 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); - parseNodeTree(node, container, stack, imageLoader, index); + parseNodeTree(node, container, stack, resourceLoader, index); if (__DEV__) { logger.log(`Finished parsing node tree`); @@ -36,7 +36,7 @@ const parseNodeTree = ( node: HTMLElement, parent: NodeContainer, stack: StackingContext, - imageLoader: ImageLoader, + resourceLoader: ResourceLoader, index: number ): void => { if (__DEV__ && index > 50000) { @@ -60,7 +60,7 @@ const parseNodeTree = ( (defaultView.parent && childNode instanceof defaultView.parent.HTMLElement) ) { 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 (childNode.tagName === 'INPUT') { // $FlowFixMe @@ -92,12 +92,12 @@ const parseNodeTree = ( ); parentStack.contexts.push(childStack); if (SHOULD_TRAVERSE_CHILDREN) { - parseNodeTree(childNode, container, childStack, imageLoader, index); + parseNodeTree(childNode, container, childStack, resourceLoader, index); } } else { stack.children.push(container); 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 || (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); if (treatAsRealStackingContext || createsStackingContext(container)) { // for treatAsRealStackingContext:false, any positioned descendants and descendants diff --git a/src/Renderer.js b/src/Renderer.js index 2407151..923e095 100644 --- a/src/Renderer.js +++ b/src/Renderer.js @@ -15,7 +15,7 @@ import type {Matrix} from './parsing/transform'; import type {BoundCurves} from './Bounds'; import type {Gradient} from './Gradient'; -import type {ImageStore, ImageElement} from './ImageLoader'; +import type {ResourceStore, ImageElement} from './ResourceLoader'; import type NodeContainer from './NodeContainer'; import type StackingContext from './StackingContext'; import type {TextBounds} from './TextBounds'; @@ -43,7 +43,7 @@ import {BORDER_STYLE} from './parsing/border'; export type RenderOptions = { scale: number, backgroundColor: ?Color, - imageStore: ImageStore, + imageStore: ResourceStore, fontMetrics: FontMetrics, logger: Logger, x: number, diff --git a/src/ImageLoader.js b/src/ResourceLoader.js similarity index 80% rename from src/ImageLoader.js rename to src/ResourceLoader.js index a53fa12..2fcb083 100644 --- a/src/ImageLoader.js +++ b/src/ResourceLoader.js @@ -5,17 +5,17 @@ import type Options from './index'; import type Logger from './Logger'; export type ImageElement = Image | HTMLCanvasElement; -type ImageCache = {[string]: Promise}; +export type Resource = ImageElement; +type ResourceCache = {[string]: Promise}; import FEATURES from './Feature'; import {Proxy} from './Proxy'; -// $FlowFixMe -export default class ImageLoader { +export default class ResourceLoader { origin: string; options: Options; _link: HTMLAnchorElement; - cache: ImageCache; + cache: ResourceCache; logger: Logger; _index: number; _window: WindowProxy; @@ -30,7 +30,7 @@ export default class ImageLoader { } loadImage(src: string): ?string { - if (this.hasImageInCache(src)) { + if (this.hasResourceInCache(src)) { return src; } @@ -58,11 +58,11 @@ export default class ImageLoader { } } - inlineImage(src: string): Promise { + inlineImage(src: string): Promise { if (isInlineImage(src)) { return loadImage(src, this.options.imageTimeout || 0); } - if (this.hasImageInCache(src)) { + if (this.hasResourceInCache(src)) { return this.cache[src]; } if (!this.isSameOrigin(src) && typeof this.options.proxy === 'string') { @@ -74,7 +74,7 @@ export default class ImageLoader { return this.xhrImage(src); } - xhrImage(src: string): Promise { + xhrImage(src: string): Promise { this.cache[src] = new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.onreadystatechange = () => { @@ -88,10 +88,16 @@ export default class ImageLoader { ); } else { const reader = new FileReader(); - // $FlowFixMe - reader.addEventListener('load', () => resolve(reader.result), false); - // $FlowFixMe - reader.addEventListener('error', e => reject(e), false); + reader.addEventListener( + 'load', + () => { + // $FlowFixMe + const result: string = reader.result; + resolve(result); + }, + false + ); + reader.addEventListener('error', (e: Event) => reject(e), false); reader.readAsDataURL(xhr.response); } } @@ -118,7 +124,7 @@ export default class ImageLoader { return key; } - hasImageInCache(key: string): boolean { + hasResourceInCache(key: string): boolean { return typeof this.cache[key] !== 'undefined'; } @@ -177,38 +183,37 @@ export default class ImageLoader { return link.protocol + link.hostname + link.port; } - ready(): Promise> { - const keys = Object.keys(this.cache); - return Promise.all( - keys.map(str => - this.cache[str].catch(e => { - if (__DEV__) { - this.logger.log(`Unable to load image`, e); - } - return null; - }) - ) - ).then(images => { + ready(): Promise { + const keys: Array = Object.keys(this.cache); + const values: Array> = keys.map(str => + this.cache[str].catch(e => { + if (__DEV__) { + this.logger.log(`Unable to load image`, e); + } + return null; + }) + ); + return Promise.all(values).then((images: Array) => { if (__DEV__) { this.logger.log(`Finished loading ${images.length} images`, images); } - return new ImageStore(keys, images); + return new ResourceStore(keys, images); }); } } -export class ImageStore { +export class ResourceStore { _keys: Array; - _images: Array; + _resources: Array; - constructor(keys: Array, images: Array) { + constructor(keys: Array, resources: Array) { this._keys = keys; - this._images = images; + this._resources = resources; } - get(key: string): ?T { + get(key: string): ?Resource { 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 => src.substr(-3).toLowerCase() === 'svg' || INLINE_SVG.test(src); -const loadImage = (src: string, timeout: number) => { +const loadImage = (src: string, timeout: number): Promise => { return new Promise((resolve, reject) => { const img = new Image(); img.onload = () => resolve(img); diff --git a/src/Window.js b/src/Window.js index e83e559..420939a 100644 --- a/src/Window.js +++ b/src/Window.js @@ -57,22 +57,25 @@ export const renderElement = ( logger.log(`Document cloned, using foreignObject rendering`); } - return cloner.imageLoader.ready().then(() => { - const renderer = new ForeignObjectRenderer(cloner.clonedReferenceElement); - return renderer.render({ - backgroundColor, - logger, - scale: options.scale, - x: options.x, - y: options.y, - width: options.width, - height: options.height, - windowWidth: options.windowWidth, - windowHeight: options.windowHeight, - scrollX: options.scrollX, - scrollY: options.scrollY + return cloner + .inlineFonts(ownerDocument) + .then(() => cloner.resourceLoader.ready()) + .then(() => { + const renderer = new ForeignObjectRenderer(cloner.documentElement); + return renderer.render({ + backgroundColor, + logger, + scale: options.scale, + x: options.x, + y: options.y, + width: options.width, + height: options.height, + windowWidth: options.windowWidth, + windowHeight: options.windowHeight, + scrollX: options.scrollX, + scrollY: options.scrollY + }); }); - }); })(new DocumentCloner(element, options, logger, true, renderElement)) : cloneWindow( ownerDocument, @@ -81,19 +84,19 @@ export const renderElement = ( options, logger, renderElement - ).then(([container, clonedElement, imageLoader]) => { + ).then(([container, clonedElement, resourceLoader]) => { if (__DEV__) { logger.log(`Document cloned, using computed rendering`); } - const stack = NodeParser(clonedElement, imageLoader, logger); + const stack = NodeParser(clonedElement, resourceLoader, logger); const clonedDocument = clonedElement.ownerDocument; if (backgroundColor === stack.container.style.background.backgroundColor) { stack.container.style.background.backgroundColor = TRANSPARENT; } - return imageLoader.ready().then(imageStore => { + return resourceLoader.ready().then(imageStore => { if (options.removeContainer === true) { if (container.parentNode) { container.parentNode.removeChild(container); diff --git a/src/parsing/background.js b/src/parsing/background.js index 09d0a41..5c2ac05 100644 --- a/src/parsing/background.js +++ b/src/parsing/background.js @@ -2,7 +2,7 @@ 'use strict'; import type {Path} from '../drawing/Path'; import type {Bounds, BoundCurves} from '../Bounds'; -import type ImageLoader, {ImageElement} from '../ImageLoader'; +import type ResourceLoader, {ImageElement} from '../ResourceLoader'; import Color from '../Color'; import Length from '../Length'; @@ -223,11 +223,11 @@ export const calculateBackgroundRepeatPath = ( export const parseBackground = ( style: CSSStyleDeclaration, - imageLoader: ImageLoader + resourceLoader: ResourceLoader ): Background => { return { backgroundColor: new Color(style.backgroundColor), - backgroundImage: parseBackgroundImages(style, imageLoader), + backgroundImage: parseBackgroundImages(style, resourceLoader), backgroundClip: parseBackgroundClip(style.backgroundClip), backgroundOrigin: parseBackgroundOrigin(style.backgroundOrigin) }; @@ -276,13 +276,13 @@ const parseBackgroundRepeat = (backgroundRepeat: string): BackgroundRepeat => { const parseBackgroundImages = ( style: CSSStyleDeclaration, - imageLoader: ImageLoader + resourceLoader: ResourceLoader ): Array => { const sources: Array = parseBackgroundImage( style.backgroundImage ).map(backgroundImage => { if (backgroundImage.method === 'url') { - const key = imageLoader.loadImage(backgroundImage.args[0]); + const key = resourceLoader.loadImage(backgroundImage.args[0]); backgroundImage.args = key ? [key] : []; } return backgroundImage; diff --git a/src/renderer/CanvasRenderer.js b/src/renderer/CanvasRenderer.js index cd860ae..ee5c3f4 100644 --- a/src/renderer/CanvasRenderer.js +++ b/src/renderer/CanvasRenderer.js @@ -12,7 +12,7 @@ import type {TextShadow} from '../parsing/textShadow'; import type {Matrix} from '../parsing/transform'; import type {Bounds} from '../Bounds'; -import type {ImageElement} from '../ImageLoader'; +import type {ImageElement} from '../ResourceLoader'; import type {Gradient} from '../Gradient'; import type {TextBounds} from '../TextBounds'; diff --git a/src/renderer/ForeignObjectRenderer.js b/src/renderer/ForeignObjectRenderer.js index 4c00eec..f58dd36 100644 --- a/src/renderer/ForeignObjectRenderer.js +++ b/src/renderer/ForeignObjectRenderer.js @@ -12,29 +12,34 @@ export default class ForeignObjectRenderer { this.options = options; this.canvas = document.createElement('canvas'); this.ctx = this.canvas.getContext('2d'); - this.canvas.width = Math.floor(options.width * options.scale); - this.canvas.height = Math.floor(options.height * options.scale); + this.canvas.width = Math.floor(options.width) * options.scale; + this.canvas.height = Math.floor(options.height) * options.scale; this.canvas.style.width = `${options.width}px`; this.canvas.style.height = `${options.height}px`; - this.ctx.scale(this.options.scale, this.options.scale); options.logger.log( - `ForeignObject renderer initialized (${options.width}x${options.height} at ${options.x},${options.y}) with scale ${this - .options.scale}` + `ForeignObject renderer initialized (${options.width}x${options.height} at ${options.x},${options.y}) with scale ${options.scale}` ); const svg = createForeignObjectSVG( - Math.max(options.windowWidth, options.width), - Math.max(options.windowHeight, options.height), - options.scrollX, - options.scrollY, + Math.max(options.windowWidth, options.width) * options.scale, + Math.max(options.windowHeight, options.height) * options.scale, + options.scrollX * options.scale, + options.scrollY * options.scale, this.element ); + return loadSerializedSVG(svg).then(img => { if (options.backgroundColor) { 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; }); } diff --git a/src/renderer/RefTestRenderer.js b/src/renderer/RefTestRenderer.js index 48cd3c2..fa77189 100644 --- a/src/renderer/RefTestRenderer.js +++ b/src/renderer/RefTestRenderer.js @@ -17,7 +17,7 @@ import type {TextShadow} from '../parsing/textShadow'; import type {Matrix} from '../parsing/transform'; import type {Bounds} from '../Bounds'; -import type {ImageElement} from '../ImageLoader'; +import type {ImageElement} from '../ResourceLoader'; import type {Gradient} from '../Gradient'; import type {TextBounds} from '../TextBounds';