|  | // Copyright 2013 The Chromium Authors. All rights reserved. | 
|  | // Use of this source code is governed by a BSD-style license that can be | 
|  | // found in the LICENSE file. | 
|  |  | 
|  | /** | 
|  | * Loads and resizes an image. | 
|  | * @constructor | 
|  | */ | 
|  | function ImageLoader() { | 
|  | /** | 
|  | * Persistent cache object. | 
|  | * @type {ImageCache} | 
|  | * @private | 
|  | */ | 
|  | this.cache_ = new ImageCache(); | 
|  |  | 
|  | /** | 
|  | * Manages pending requests and runs them in order of priorities. | 
|  | * @type {Scheduler} | 
|  | * @private | 
|  | */ | 
|  | this.scheduler_ = new Scheduler(); | 
|  |  | 
|  | /** | 
|  | * Piex loader for RAW images. | 
|  | * @private {!PiexLoader} | 
|  | */ | 
|  | this.piexLoader_ = new PiexLoader(); | 
|  |  | 
|  | // Grant permissions to all volumes, initialize the cache and then start the | 
|  | // scheduler. | 
|  | chrome.fileManagerPrivate.getVolumeMetadataList(function(volumeMetadataList) { | 
|  | // Listen for mount events, and grant permissions to volumes being mounted. | 
|  | chrome.fileManagerPrivate.onMountCompleted.addListener( | 
|  | function(event) { | 
|  | if (event.eventType === 'mount' && event.status === 'success') { | 
|  | chrome.fileSystem.requestFileSystem( | 
|  | {volumeId: event.volumeMetadata.volumeId}, function() {}); | 
|  | } | 
|  | }); | 
|  | var initPromises = volumeMetadataList.map(function(volumeMetadata) { | 
|  | var requestPromise = new Promise(function(callback) { | 
|  | chrome.fileSystem.requestFileSystem( | 
|  | {volumeId: volumeMetadata.volumeId}, | 
|  | /** @type {function(FileSystem=)} */(callback)); | 
|  | }); | 
|  | return requestPromise; | 
|  | }); | 
|  | initPromises.push(new Promise(function(resolve, reject) { | 
|  | this.cache_.initialize(resolve); | 
|  | }.bind(this))); | 
|  |  | 
|  | // After all initialization promises are done, start the scheduler. | 
|  | Promise.all(initPromises).then(this.scheduler_.start.bind(this.scheduler_)); | 
|  | }.bind(this)); | 
|  |  | 
|  | // Listen for incoming requests. | 
|  | chrome.runtime.onMessageExternal.addListener( | 
|  | function(request, sender, sendResponse) { | 
|  | if (ImageLoader.ALLOWED_CLIENTS.indexOf(sender.id) !== -1) { | 
|  | // Sending a response may fail if the receiver already went offline. | 
|  | // This is not an error, but a normal and quite common situation. | 
|  | var failSafeSendResponse = function(response) { | 
|  | try { | 
|  | sendResponse(response); | 
|  | } | 
|  | catch (e) { | 
|  | // Ignore the error. | 
|  | } | 
|  | }; | 
|  | if (typeof request.orientation === 'number') { | 
|  | request.orientation = | 
|  | ImageOrientation.fromDriveOrientation(request.orientation); | 
|  | } else { | 
|  | request.orientation = new ImageOrientation(1, 0, 0, 1); | 
|  | } | 
|  | return this.onMessage_(sender.id, | 
|  | /** @type {LoadImageRequest} */ (request), | 
|  | failSafeSendResponse); | 
|  | } | 
|  | }.bind(this)); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * List of extensions allowed to perform image requests. | 
|  | * | 
|  | * @const | 
|  | * @type {Array<string>} | 
|  | */ | 
|  | ImageLoader.ALLOWED_CLIENTS = [ | 
|  | 'hhaomjibdihmijegdhdafkllkbggdgoj',  // File Manager's extension id. | 
|  | 'nlkncpkkdoccmpiclbokaimcnedabhhm',  // Gallery's extension id. | 
|  | 'jcgeabjmjgoblfofpppfkcoakmfobdko',  // Video Player's extension id. | 
|  | ]; | 
|  |  | 
|  | /** | 
|  | * Handles a request. Depending on type of the request, starts or stops | 
|  | * an image task. | 
|  | * | 
|  | * @param {string} senderId Sender's extension id. | 
|  | * @param {!LoadImageRequest} request Request message as a hash array. | 
|  | * @param {function(Object)} callback Callback to be called to return response. | 
|  | * @return {boolean} True if the message channel should stay alive until the | 
|  | *     callback is called. | 
|  | * @private | 
|  | */ | 
|  | ImageLoader.prototype.onMessage_ = function(senderId, request, callback) { | 
|  | var requestId = senderId + ':' + request.taskId; | 
|  | if (request.cancel) { | 
|  | // Cancel a task. | 
|  | this.scheduler_.remove(requestId); | 
|  | return false;  // No callback calls. | 
|  | } else { | 
|  | // Create a request task and add it to the scheduler (queue). | 
|  | var requestTask = new ImageRequest( | 
|  | requestId, this.cache_, this.piexLoader_, request, callback); | 
|  | this.scheduler_.add(requestTask); | 
|  | return true;  // Request will call the callback. | 
|  | } | 
|  | }; | 
|  |  | 
|  | /** | 
|  | * Returns the singleton instance. | 
|  | * @return {ImageLoader} ImageLoader object. | 
|  | */ | 
|  | ImageLoader.getInstance = function() { | 
|  | if (!ImageLoader.instance_) | 
|  | ImageLoader.instance_ = new ImageLoader(); | 
|  | return ImageLoader.instance_; | 
|  | }; | 
|  |  | 
|  | /** | 
|  | * Checks if the options contain any image processing. | 
|  | * | 
|  | * @param {number} width Source width. | 
|  | * @param {number} height Source height. | 
|  | * @param {Object} options Resizing options as a hash array. | 
|  | * @return {boolean} True if yes, false if not. | 
|  | */ | 
|  | ImageLoader.shouldProcess = function(width, height, options) { | 
|  | var targetDimensions = ImageLoader.resizeDimensions(width, height, options); | 
|  |  | 
|  | // Dimensions has to be adjusted. | 
|  | if (targetDimensions.width != width || targetDimensions.height != height) | 
|  | return true; | 
|  |  | 
|  | // Orientation has to be adjusted. | 
|  | if (!options.orientation.isIdentity()) | 
|  | return true; | 
|  |  | 
|  | // Non-standard color space has to be converted. | 
|  | if (options.colorSpace && options.colorSpace !== ColorSpace.SRGB) | 
|  | return true; | 
|  |  | 
|  | // No changes required. | 
|  | return false; | 
|  | }; | 
|  |  | 
|  | /** | 
|  | * Calculates dimensions taking into account resize options, such as: | 
|  | * - scale: for scaling, | 
|  | * - maxWidth, maxHeight: for maximum dimensions, | 
|  | * - width, height: for exact requested size. | 
|  | * Returns the target size as hash array with width, height properties. | 
|  | * | 
|  | * @param {number} width Source width. | 
|  | * @param {number} height Source height. | 
|  | * @param {Object} options Resizing options as a hash array. | 
|  | * @return {Object} Dimensions, eg. {width: 100, height: 50}. | 
|  | */ | 
|  | ImageLoader.resizeDimensions = function(width, height, options) { | 
|  | var scale = options.scale || 1; | 
|  | var targetDimensions = options.orientation.getSizeAfterCancelling( | 
|  | width * scale, height * scale); | 
|  | var targetWidth = targetDimensions.width; | 
|  | var targetHeight = targetDimensions.height; | 
|  |  | 
|  | if (options.maxWidth && targetWidth > options.maxWidth) { | 
|  | var scale = options.maxWidth / targetWidth; | 
|  | targetWidth *= scale; | 
|  | targetHeight *= scale; | 
|  | } | 
|  |  | 
|  | if (options.maxHeight && targetHeight > options.maxHeight) { | 
|  | var scale = options.maxHeight / targetHeight; | 
|  | targetWidth *= scale; | 
|  | targetHeight *= scale; | 
|  | } | 
|  |  | 
|  | if (options.width) | 
|  | targetWidth = options.width; | 
|  |  | 
|  | if (options.height) | 
|  | targetHeight = options.height; | 
|  |  | 
|  | targetWidth = Math.round(targetWidth); | 
|  | targetHeight = Math.round(targetHeight); | 
|  |  | 
|  | return {width: targetWidth, height: targetHeight}; | 
|  | }; | 
|  |  | 
|  | /** | 
|  | * Performs resizing and cropping of the source image into the target canvas. | 
|  | * | 
|  | * @param {HTMLCanvasElement|Image} source Source image or canvas. | 
|  | * @param {HTMLCanvasElement} target Target canvas. | 
|  | * @param {Object} options Resizing options as a hash array. | 
|  | */ | 
|  | ImageLoader.resizeAndCrop = function(source, target, options) { | 
|  | // Calculates copy parameters. | 
|  | var copyParameters = ImageLoader.calculateCopyParameters(source, options); | 
|  | target.width = copyParameters.canvas.width; | 
|  | target.height = copyParameters.canvas.height; | 
|  |  | 
|  | // Apply. | 
|  | var targetContext = | 
|  | /** @type {CanvasRenderingContext2D} */ (target.getContext('2d')); | 
|  | targetContext.save(); | 
|  | options.orientation.cancelImageOrientation( | 
|  | targetContext, copyParameters.target.width, copyParameters.target.height); | 
|  | targetContext.drawImage( | 
|  | source, | 
|  | copyParameters.source.x, | 
|  | copyParameters.source.y, | 
|  | copyParameters.source.width, | 
|  | copyParameters.source.height, | 
|  | copyParameters.target.x, | 
|  | copyParameters.target.y, | 
|  | copyParameters.target.width, | 
|  | copyParameters.target.height); | 
|  | targetContext.restore(); | 
|  | }; | 
|  |  | 
|  | /** | 
|  | * @typedef {{ | 
|  | *   source: {x:number, y:number, width:number, height:number}, | 
|  | *   target: {x:number, y:number, width:number, height:number}, | 
|  | *   canvas: {width:number, height:number} | 
|  | * }} | 
|  | */ | 
|  | ImageLoader.CopyParameters; | 
|  |  | 
|  | /** | 
|  | * Calculates copy parameters. | 
|  | * | 
|  | * @param {HTMLCanvasElement|Image} source Source image or canvas. | 
|  | * @param {Object} options Resizing options as a hash array. | 
|  | * @return {!ImageLoader.CopyParameters} Calculated copy parameters. | 
|  | */ | 
|  | ImageLoader.calculateCopyParameters = function(source, options) { | 
|  | if (options.crop) { | 
|  | // When an image is cropped, target should be a fixed size square. | 
|  | assert(options.width); | 
|  | assert(options.height); | 
|  | assert(options.width === options.height); | 
|  |  | 
|  | // The length of shorter edge becomes dimension of cropped area in the | 
|  | // source. | 
|  | var cropSourceDimension = Math.min(source.width, source.height); | 
|  |  | 
|  | return { | 
|  | source: { | 
|  | x: Math.floor((source.width / 2) - (cropSourceDimension / 2)), | 
|  | y: Math.floor((source.height / 2) - (cropSourceDimension / 2)), | 
|  | width: cropSourceDimension, | 
|  | height: cropSourceDimension | 
|  | }, | 
|  | target: { | 
|  | x: 0, | 
|  | y: 0, | 
|  | width: options.width, | 
|  | height: options.height | 
|  | }, | 
|  | canvas: { | 
|  | width: options.width, | 
|  | height: options.height | 
|  | } | 
|  | }; | 
|  | } | 
|  |  | 
|  | // Target dimension is calculated in the rotated(transformed) coordinate. | 
|  | var targetCanvasDimensions = ImageLoader.resizeDimensions( | 
|  | source.width, source.height, options); | 
|  |  | 
|  | var targetDimensions = options.orientation.getSizeAfterCancelling( | 
|  | targetCanvasDimensions.width, targetCanvasDimensions.height); | 
|  |  | 
|  | return { | 
|  | source: { | 
|  | x: 0, | 
|  | y: 0, | 
|  | width: source.width, | 
|  | height: source.height | 
|  | }, | 
|  | target: { | 
|  | x: 0, | 
|  | y: 0, | 
|  | width: targetDimensions.width, | 
|  | height: targetDimensions.height | 
|  | }, | 
|  | canvas: { | 
|  | width: targetCanvasDimensions.width, | 
|  | height: targetCanvasDimensions.height | 
|  | } | 
|  | }; | 
|  | }; | 
|  |  | 
|  | /** | 
|  | * Matrix converts AdobeRGB color space into sRGB color space. | 
|  | * @const {!Array<number>} | 
|  | */ | 
|  | ImageLoader.MATRIX_FROM_ADOBE_TO_STANDARD = [ | 
|  | 1.39836, -0.39836, 0.00000, | 
|  | 0.00000,  1.00000, 0.00000, | 
|  | 0.00000, -0.04293, 1.04293 | 
|  | ]; | 
|  |  | 
|  | /** | 
|  | * Converts the canvas of color space into sRGB. | 
|  | * @param {HTMLCanvasElement} target Target canvas. | 
|  | * @param {ColorSpace} colorSpace Current color space. | 
|  | */ | 
|  | ImageLoader.convertColorSpace = function(target, colorSpace) { | 
|  | if (colorSpace === ColorSpace.SRGB) | 
|  | return; | 
|  | if (colorSpace === ColorSpace.ADOBE_RGB) { | 
|  | var matrix = ImageLoader.MATRIX_FROM_ADOBE_TO_STANDARD; | 
|  | var context = target.getContext('2d'); | 
|  | var imageData = context.getImageData(0, 0, target.width, target.height); | 
|  | var data = imageData.data; | 
|  | for (var i = 0; i < data.length; i += 4) { | 
|  | // Scale to [0, 1]. | 
|  | var adobeR = data[i] / 255; | 
|  | var adobeG = data[i + 1] / 255; | 
|  | var adobeB = data[i + 2] / 255; | 
|  |  | 
|  | // Revert gannma transformation. | 
|  | adobeR = adobeR <= 0.0556 ? adobeR / 32 : Math.pow(adobeR, 2.2); | 
|  | adobeG = adobeG <= 0.0556 ? adobeG / 32 : Math.pow(adobeG, 2.2); | 
|  | adobeB = adobeB <= 0.0556 ? adobeB / 32 : Math.pow(adobeB, 2.2); | 
|  |  | 
|  | // Convert color space. | 
|  | var sR = matrix[0] * adobeR + matrix[1] * adobeG + matrix[2] * adobeB; | 
|  | var sG = matrix[3] * adobeR + matrix[4] * adobeG + matrix[5] * adobeB; | 
|  | var sB = matrix[6] * adobeR + matrix[7] * adobeG + matrix[8] * adobeB; | 
|  |  | 
|  | // Gannma transformation. | 
|  | sR = sR <= 0.0031308 ? 12.92 * sR : 1.055 * Math.pow(sR, 1 / 2.4) - 0.055; | 
|  | sG = sG <= 0.0031308 ? 12.92 * sG : 1.055 * Math.pow(sG, 1 / 2.4) - 0.055; | 
|  | sB = sB <= 0.0031308 ? 12.92 * sB : 1.055 * Math.pow(sB, 1 / 2.4) - 0.055; | 
|  |  | 
|  | // Scale to [0, 255]. | 
|  | data[i] = Math.max(0, Math.min(255, sR * 255)); | 
|  | data[i + 1] = Math.max(0, Math.min(255, sG * 255)); | 
|  | data[i + 2] = Math.max(0, Math.min(255, sB * 255)); | 
|  | } | 
|  | context.putImageData(imageData, 0, 0); | 
|  | } | 
|  | }; |