html2canvas-pro
Version:
Screenshots with JavaScript. Next generation!
263 lines • 9.73 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.Cache = exports.CacheStorage = void 0;
const features_1 = require("./features");
/**
* CacheStorage (Deprecated static methods)
*
* @deprecated The static methods of CacheStorage are deprecated.
* Use OriginChecker class instead for instance-based origin checking.
*
* For backward compatibility, these methods remain but should not be used in new code.
*/
class CacheStorage {
/**
* @deprecated Use OriginChecker.getOrigin() instead
*/
static getOrigin(url) {
const link = CacheStorage._link;
if (!link) {
return 'about:blank';
}
link.href = url;
link.href = link.href; // IE9, LOL! - http://jsfiddle.net/niklasvh/2e48b/
return link.protocol + link.hostname + link.port;
}
/**
* @deprecated Use OriginChecker.isSameOrigin() instead
*/
static isSameOrigin(src) {
return CacheStorage.getOrigin(src) === CacheStorage._origin;
}
/**
* @deprecated No longer needed. OriginChecker is created per Context.
*/
static setContext(window) {
CacheStorage._link = window.document.createElement('a');
CacheStorage._origin = CacheStorage.getOrigin(window.location.href);
}
}
exports.CacheStorage = CacheStorage;
CacheStorage._origin = 'about:blank';
class Cache {
constructor(context, _options) {
this.context = context;
this._options = _options;
this._cache = new Map();
this._pendingOperations = new Map();
// Default cache size: 100 items
this.maxSize = _options.maxCacheSize ?? 100;
if (this.maxSize < 1) {
throw new Error('Cache maxSize must be at least 1');
}
if (this.maxSize > 10000) {
this.context.logger.warn(`Cache maxSize ${this.maxSize} is very large and may cause memory issues. ` +
`Consider using a smaller value (recommended: 100-1000).`);
}
}
addImage(src) {
// Wait for any pending operations on this key
const pending = this._pendingOperations.get(src);
if (pending) {
return pending;
}
if (this.has(src)) {
// Update last accessed time
const entry = this._cache.get(src);
if (entry) {
entry.lastAccessed = Date.now();
}
return Promise.resolve();
}
if (isBlobImage(src) || isRenderable(src)) {
// Create a pending operation to ensure atomicity
const operation = this._addImageInternal(src);
this._pendingOperations.set(src, operation);
operation.finally(() => {
this._pendingOperations.delete(src);
});
return operation;
}
return Promise.resolve();
}
async _addImageInternal(src) {
// Create image load promise with timeout protection
const timeoutMs = this._options.imageTimeout ?? 15000;
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => {
reject(new Error(`Image load timeout after ${timeoutMs}ms: ${src}`));
}, timeoutMs);
});
// Race between image load and timeout
const imageWithTimeout = Promise.race([this.loadImage(src), timeoutPromise]);
// Handle errors to prevent unhandled rejections
imageWithTimeout.catch((error) => {
this.context.logger.error(`Failed to load image ${src}: ${error instanceof Error ? error.message : 'Unknown error'}`);
});
// Store the promise with timeout in cache
this.set(src, imageWithTimeout);
}
match(src) {
const entry = this._cache.get(src);
if (entry) {
// Update last accessed time on access
entry.lastAccessed = Date.now();
return entry.value;
}
return undefined;
}
/**
* Set a value in cache with LRU eviction
*/
set(key, value) {
// If key already exists, update it without eviction
if (this._cache.has(key)) {
const entry = this._cache.get(key);
entry.value = value;
entry.lastAccessed = Date.now();
return;
}
// For new keys, check if we need to evict
if (this._cache.size >= this.maxSize) {
this.evictLRU();
}
this._cache.set(key, {
value,
lastAccessed: Date.now()
});
}
/**
* Evict least recently used entry
*/
evictLRU() {
let oldestKey = null;
let oldestTime = Infinity;
for (const [key, entry] of this._cache.entries()) {
if (entry.lastAccessed < oldestTime) {
oldestTime = entry.lastAccessed;
oldestKey = key;
}
}
if (oldestKey) {
this._cache.delete(oldestKey);
this.context.logger.debug(`Cache: Evicted LRU entry: ${oldestKey}`);
}
}
/**
* Get cache size
*/
size() {
return this._cache.size;
}
/**
* Get max cache size
*/
getMaxSize() {
return this.maxSize;
}
/**
* Clear all cache entries
*/
clear() {
this._cache.clear();
}
async loadImage(key) {
const originChecker = this.context.originChecker;
const defaultIsSameOrigin = (src) => originChecker.isSameOrigin(src);
const isSameOrigin = typeof this._options.customIsSameOrigin === 'function'
? await this._options.customIsSameOrigin(key, defaultIsSameOrigin)
: defaultIsSameOrigin(key);
const useCORS = !isInlineImage(key) && this._options.useCORS === true && features_1.FEATURES.SUPPORT_CORS_IMAGES && !isSameOrigin;
const useProxy = !isInlineImage(key) &&
!isSameOrigin &&
!isBlobImage(key) &&
typeof this._options.proxy === 'string' &&
features_1.FEATURES.SUPPORT_CORS_XHR &&
!useCORS;
if (!isSameOrigin &&
this._options.allowTaint === false &&
!isInlineImage(key) &&
!isBlobImage(key) &&
!useProxy &&
!useCORS) {
return;
}
let src = key;
if (useProxy) {
src = await this.proxy(src);
}
this.context.logger.debug(`Added image ${key.substring(0, 256)}`);
return await new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = reject;
//ios safari 10.3 taints canvas with data urls unless crossOrigin is set to anonymous
if (isInlineBase64Image(src) || useCORS) {
img.crossOrigin = 'anonymous';
}
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 > 0) {
setTimeout(() => reject(`Timed out (${this._options.imageTimeout}ms) loading image`), this._options.imageTimeout);
}
});
}
has(key) {
return this._cache.has(key);
}
keys() {
return Promise.resolve(Object.keys(this._cache));
}
proxy(src) {
const proxy = this._options.proxy;
if (!proxy) {
throw new Error('No proxy defined');
}
const key = src.substring(0, 256);
return new Promise((resolve, reject) => {
const responseType = features_1.FEATURES.SUPPORT_RESPONSE_TYPE ? 'blob' : 'text';
const xhr = new XMLHttpRequest();
xhr.onload = () => {
if (xhr.status === 200) {
if (responseType === 'text') {
resolve(xhr.response);
}
else {
const reader = new FileReader();
reader.addEventListener('load', () => resolve(reader.result), false);
reader.addEventListener('error', (e) => reject(e), false);
reader.readAsDataURL(xhr.response);
}
}
else {
reject(`Failed to proxy resource ${key} with status code ${xhr.status}`);
}
};
xhr.onerror = reject;
const queryString = proxy.indexOf('?') > -1 ? '&' : '?';
xhr.open('GET', `${proxy}${queryString}url=${encodeURIComponent(src)}&responseType=${responseType}`);
if (responseType !== 'text' && xhr instanceof XMLHttpRequest) {
xhr.responseType = responseType;
}
if (this._options.imageTimeout) {
const timeout = this._options.imageTimeout;
xhr.timeout = timeout;
xhr.ontimeout = () => reject(`Timed out (${timeout}ms) proxying ${key}`);
}
xhr.send();
});
}
}
exports.Cache = Cache;
const INLINE_SVG = /^data:image\/svg\+xml/i;
const INLINE_BASE64 = /^data:image\/.*;base64,/i;
const INLINE_IMG = /^data:image\/.*/i;
const isRenderable = (src) => features_1.FEATURES.SUPPORT_SVG_DRAWING || !isSVG(src);
const isInlineImage = (src) => INLINE_IMG.test(src);
const isInlineBase64Image = (src) => INLINE_BASE64.test(src);
const isBlobImage = (src) => src.substr(0, 4) === 'blob';
const isSVG = (src) => src.substr(-3).toLowerCase() === 'svg' || INLINE_SVG.test(src);
//# sourceMappingURL=cache-storage.js.map