mirror of
https://github.com/niklasvh/html2canvas.git
synced 2023-08-10 21:13:10 +03:00
199 lines
6.3 KiB
JavaScript
199 lines
6.3 KiB
JavaScript
/* @flow */
|
|
'use strict';
|
|
|
|
import type Options from './index';
|
|
import type Logger from './Logger';
|
|
|
|
export type ImageElement = Image | HTMLCanvasElement;
|
|
type ImageCache<T> = {[string]: Promise<T>};
|
|
|
|
import FEATURES from './Feature';
|
|
|
|
// $FlowFixMe
|
|
export default class ImageLoader<T> {
|
|
origin: string;
|
|
options: Options;
|
|
_link: HTMLAnchorElement;
|
|
cache: ImageCache<T>;
|
|
logger: Logger;
|
|
_index: number;
|
|
_window: WindowProxy;
|
|
|
|
constructor(options: Options, logger: Logger, window: WindowProxy) {
|
|
this.options = options;
|
|
this._window = window;
|
|
this.origin = this.getOrigin(window.location.href);
|
|
this.cache = {};
|
|
this.logger = logger;
|
|
this._index = 0;
|
|
}
|
|
|
|
loadImage(src: string): ?string {
|
|
if (this.hasImageInCache(src)) {
|
|
return src;
|
|
}
|
|
|
|
if (isSVG(src)) {
|
|
if (this.options.allowTaint === true || FEATURES.SUPPORT_SVG_DRAWING) {
|
|
return this.addImage(src, src);
|
|
}
|
|
} else {
|
|
if (
|
|
this.options.allowTaint === true ||
|
|
isInlineBase64Image(src) ||
|
|
this.isSameOrigin(src)
|
|
) {
|
|
return this.addImage(src, src);
|
|
} else if (typeof this.options.proxy === 'string' && !this.isSameOrigin(src)) {
|
|
// TODO proxy
|
|
}
|
|
}
|
|
}
|
|
|
|
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.responseType = 'blob';
|
|
if (this.options.imageTimeout) {
|
|
const timeout = this.options.imageTimeout;
|
|
xhr.timeout = timeout;
|
|
xhr.ontimeout = () =>
|
|
reject(__DEV__ ? `Timed out (${timeout}ms) fetching ${src}` : '');
|
|
}
|
|
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);
|
|
return key;
|
|
}
|
|
|
|
hasImageInCache(key: string): boolean {
|
|
return typeof this.cache[key] !== 'undefined';
|
|
}
|
|
|
|
addImage(key: string, src: string): string {
|
|
if (__DEV__) {
|
|
this.logger.log(`Added image ${key.substring(0, 256)}`);
|
|
}
|
|
|
|
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
|
|
if (!supportsDataImages) {
|
|
img.crossOrigin = 'anonymous';
|
|
}
|
|
|
|
img.onerror = reject;
|
|
img.src = src;
|
|
if (img.complete === true) {
|
|
// Inline XML images may fail to parse, throwing an Error later on
|
|
setTimeout(() => {
|
|
resolve(img);
|
|
}, 500);
|
|
}
|
|
if (this.options.imageTimeout) {
|
|
const timeout = this.options.imageTimeout;
|
|
setTimeout(
|
|
() => reject(__DEV__ ? `Timed out (${timeout}ms) fetching ${src}` : ''),
|
|
timeout
|
|
);
|
|
}
|
|
});
|
|
|
|
this.cache[key] =
|
|
isInlineBase64Image(src) && !isSVG(src)
|
|
? // $FlowFixMe
|
|
FEATURES.SUPPORT_BASE64_DRAWING(src).then(imageLoadHandler)
|
|
: imageLoadHandler(true);
|
|
return key;
|
|
}
|
|
|
|
isSameOrigin(url: string): boolean {
|
|
return this.getOrigin(url) === this.origin;
|
|
}
|
|
|
|
getOrigin(url: string): string {
|
|
const link = this._link || (this._link = this._window.document.createElement('a'));
|
|
link.href = url;
|
|
link.href = link.href; // IE9, LOL! - http://jsfiddle.net/niklasvh/2e48b/
|
|
return link.protocol + link.hostname + link.port;
|
|
}
|
|
|
|
ready(): Promise<ImageStore<T>> {
|
|
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 => {
|
|
if (__DEV__) {
|
|
this.logger.log(`Finished loading ${images.length} images`, images);
|
|
}
|
|
return new ImageStore(keys, images);
|
|
});
|
|
}
|
|
}
|
|
|
|
export class ImageStore<T> {
|
|
_keys: Array<string>;
|
|
_images: Array<?T>;
|
|
|
|
constructor(keys: Array<string>, images: Array<?T>) {
|
|
this._keys = keys;
|
|
this._images = images;
|
|
}
|
|
|
|
get(key: string): ?T {
|
|
const index = this._keys.indexOf(key);
|
|
return index === -1 ? null : this._images[index];
|
|
}
|
|
}
|
|
|
|
const INLINE_SVG = /^data:image\/svg\+xml/i;
|
|
const INLINE_BASE64 = /^data:image\/.*;base64,/i;
|
|
const INLINE_IMG = /^data:image\/.*/i;
|
|
|
|
const isInlineImage = (src: string): boolean => INLINE_IMG.test(src);
|
|
const isInlineBase64Image = (src: string): boolean => INLINE_BASE64.test(src);
|
|
|
|
const isSVG = (src: string): boolean =>
|
|
src.substr(-3).toLowerCase() === 'svg' || INLINE_SVG.test(src);
|