UNPKG

html2canvas-pro

Version:

Screenshots with JavaScript. Next generation!

263 lines 9.73 kB
"use strict"; 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