| // Copyright 2013 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import {LruCache} from 'chrome://file-manager/common/js/lru_cache.js'; |
| |
| import {cacheKey, type CacheValue, createCancel, type LoadImageRequest, LoadImageResponse, LoadImageResponseStatus} from './load_image_request.js'; |
| |
| let instance: ImageLoaderClient|null = null; |
| |
| /** |
| * Client used to connect to the remote ImageLoader extension. Client class runs |
| * in the extension, where the client.js is included (eg. Files app). |
| * It sends remote requests using IPC to the ImageLoader class and forwards |
| * its responses. |
| * |
| * Implements cache, which is stored in the calling extension. |
| */ |
| export class ImageLoaderClient { |
| private lastTaskId_: number = 0; |
| /** |
| * LRU cache for images. |
| */ |
| private cache_ = new LruCache<CacheValue>(CACHE_MEMORY_LIMIT); |
| |
| /** |
| * Returns a singleton instance. |
| */ |
| static getInstance(): ImageLoaderClient { |
| if (!instance) { |
| instance = new ImageLoaderClient(); |
| } |
| return instance; |
| } |
| |
| /** |
| * Sends a message to the Image Loader extension. |
| * @param request The image request. |
| * @param callback Response handling callback. The response is passed as a |
| * hash array. |
| */ |
| private static sendMessage_( |
| request: LoadImageRequest, callback?: (r: LoadImageResponse) => void) { |
| chrome.runtime.sendMessage(EXTENSION_ID, request, {}, callback); |
| } |
| |
| /** |
| * Loads and resizes and image. |
| * |
| * @param callback Response handling callback. |
| * @return Remote task id or null if loaded from cache. |
| */ |
| load(request: LoadImageRequest, callback: (r: LoadImageResponse) => void): |
| null|number { |
| // Replace the client origin with the image loader extension origin. |
| request.url = request.url ?? ''; |
| request.url = request.url.replace(CLIENT_URL_REGEX, IMAGE_LOADER_URL); |
| request.url = request.url.replace(CLIENT_SWA_REGEX, IMAGE_LOADER_URL); |
| |
| // Try to load from cache, if available. |
| const key = cacheKey(request); |
| if (key) { |
| if (request.cache) { |
| // Load from cache. |
| let cachedValue: CacheValue|null = this.cache_.get(key); |
| // Check if the image in cache is up to date. If not, then remove it. |
| // It relies on comparing `null` equals to `undefined`. |
| // eslint-disable-next-line eqeqeq |
| if (cachedValue && cachedValue.timestamp != request.timestamp) { |
| this.cache_.remove(key); |
| cachedValue = null; |
| } |
| if (cachedValue && cachedValue.data && cachedValue.width && |
| cachedValue.height) { |
| callback( |
| new LoadImageResponse(LoadImageResponseStatus.SUCCESS, null, { |
| width: cachedValue.width, |
| height: cachedValue.height, |
| ifd: cachedValue.ifd, |
| data: cachedValue.data, |
| })); |
| return null; |
| } |
| } else { |
| // Remove from cache. |
| this.cache_.remove(key); |
| } |
| } |
| |
| // Not available in cache, performing a request to a remote extension. |
| this.lastTaskId_++; |
| request.taskId = this.lastTaskId_; |
| |
| ImageLoaderClient.sendMessage_(request, (resultData) => { |
| if (chrome.runtime.lastError) { |
| console.warn(chrome.runtime.lastError.message); |
| callback(new LoadImageResponse( |
| LoadImageResponseStatus.ERROR, request.taskId!)); |
| return; |
| } |
| const result = resultData; |
| // Save to cache. |
| if (key && request.cache) { |
| const value: CacheValue|null = |
| LoadImageResponse.cacheValue(result, request.timestamp); |
| if (value) { |
| this.cache_.put(key, value, value.data.length); |
| } |
| } |
| callback(result); |
| }); |
| return request.taskId; |
| } |
| |
| /** |
| * Cancels the request. Note the original callback may still be invoked if |
| * this message doesn't reach the ImageLoader before it starts processing. |
| * @param taskId Task id returned by ImageLoaderClient.load(). |
| */ |
| cancel(taskId: number) { |
| ImageLoaderClient.sendMessage_(createCancel(taskId), (_result) => {}); |
| } |
| |
| // Helper functions. |
| |
| /** |
| * Loads and resizes and image. |
| * |
| * @param image Image node to load the requested picture into. |
| * @param onSuccess Callback for success. |
| * @param onError Callback for failure. |
| * @return Remote task id or null if loaded from cache. |
| */ |
| static loadToImage( |
| request: LoadImageRequest, image: HTMLImageElement, |
| onSuccess: VoidCallback, onError: VoidCallback): null|number { |
| const callback = (result: LoadImageResponse) => { |
| if (!result || result.status === LoadImageResponseStatus.ERROR) { |
| onError(); |
| return; |
| } |
| image.src = result.data!; |
| onSuccess(); |
| }; |
| |
| return ImageLoaderClient.getInstance().load(request, callback); |
| } |
| } |
| |
| /** |
| * Image loader's extension id. |
| */ |
| const EXTENSION_ID = 'pmfjbimdmchhbnneeidfognadeopoehp'; |
| |
| /** |
| * Image loader client extension request URL matcher. |
| */ |
| const CLIENT_URL_REGEX = /filesystem:chrome-extension:\/\/[a-z]+/; |
| |
| /** |
| * Image loader client chrome://file-manager request URL matcher. |
| */ |
| const CLIENT_SWA_REGEX = /filesystem:chrome:\/\/file-manager/; |
| |
| /** |
| * All client request URL match CLIENT_URL_REGEX and all are |
| * rewritten: the client extension id part of the request URL is replaced with |
| * the image loader extension id. |
| */ |
| const IMAGE_LOADER_URL = 'filesystem:chrome-extension://' + EXTENSION_ID; |
| |
| /** |
| * Memory limit for images data in bytes. |
| */ |
| const CACHE_MEMORY_LIMIT = 20 * 1024 * 1024; // 20 MB. |