Fix search results being clipped in app list.
[chromium-blink-merge.git] / ui / file_manager / image_loader / image_loader.js
blobc94e13dadc75d685e6826ef76f34dde9fd03b46f
1 // Copyright 2013 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 /**
6  * Loads and resizes an image.
7  * @constructor
8  */
9 function ImageLoader() {
10   /**
11    * Persistent cache object.
12    * @type {Cache}
13    * @private
14    */
15   this.cache_ = new Cache();
17   /**
18    * Manages pending requests and runs them in order of priorities.
19    * @type {Scheduler}
20    * @private
21    */
22   this.scheduler_ = new Scheduler();
24   /**
25    * Piex loader for RAW images.
26    * @private {!PiexLoader}
27    */
28   this.piexLoader_ = new PiexLoader();
30   // Grant permissions to all volumes, initialize the cache and then start the
31   // scheduler.
32   chrome.fileManagerPrivate.getVolumeMetadataList(function(volumeMetadataList) {
33     // Listen for mount events, and grant permissions to volumes being mounted.
34     chrome.fileManagerPrivate.onMountCompleted.addListener(
35         function(event) {
36           if (event.eventType === 'mount' && event.status === 'success') {
37             chrome.fileSystem.requestFileSystem(
38                 {volumeId: event.volumeMetadata.volumeId}, function() {});
39           }
40         });
41     var initPromises = volumeMetadataList.map(function(volumeMetadata) {
42       var requestPromise = new Promise(function(callback) {
43         chrome.fileSystem.requestFileSystem(
44             {volumeId: volumeMetadata.volumeId},
45             /** @type {function(FileSystem=)} */(callback));
46       });
47       return requestPromise;
48     });
49     initPromises.push(new Promise(function(resolve, reject) {
50       this.cache_.initialize(resolve);
51     }.bind(this)));
53     // After all initialization promises are done, start the scheduler.
54     Promise.all(initPromises).then(this.scheduler_.start.bind(this.scheduler_));
55   }.bind(this));
57   // Listen for incoming requests.
58   chrome.runtime.onMessageExternal.addListener(
59       function(request, sender, sendResponse) {
60         if (ImageLoader.ALLOWED_CLIENTS.indexOf(sender.id) !== -1) {
61           // Sending a response may fail if the receiver already went offline.
62           // This is not an error, but a normal and quite common situation.
63           var failSafeSendResponse = function(response) {
64             try {
65               sendResponse(response);
66             }
67             catch (e) {
68               // Ignore the error.
69             }
70           };
71           return this.onMessage_(sender.id,
72                                  /** @type {LoadImageRequest} */ (request),
73                                  failSafeSendResponse);
74         }
75       }.bind(this));
78 /**
79  * List of extensions allowed to perform image requests.
80  *
81  * @const
82  * @type {Array.<string>}
83  */
84 ImageLoader.ALLOWED_CLIENTS = [
85   'hhaomjibdihmijegdhdafkllkbggdgoj',  // File Manager's extension id.
86   'nlkncpkkdoccmpiclbokaimcnedabhhm'  // Gallery extension id.
89 /**
90  * Handles a request. Depending on type of the request, starts or stops
91  * an image task.
92  *
93  * @param {string} senderId Sender's extension id.
94  * @param {LoadImageRequest} request Request message as a hash array.
95  * @param {function(Object)} callback Callback to be called to return response.
96  * @return {boolean} True if the message channel should stay alive until the
97  *     callback is called.
98  * @private
99  */
100 ImageLoader.prototype.onMessage_ = function(senderId, request, callback) {
101   var requestId = senderId + ':' + request.taskId;
102   if (request.cancel) {
103     // Cancel a task.
104     this.scheduler_.remove(requestId);
105     return false;  // No callback calls.
106   } else {
107     // Create a request task and add it to the scheduler (queue).
108     var requestTask = new Request(
109         requestId, this.cache_, this.piexLoader_, request, callback);
110     this.scheduler_.add(requestTask);
111     return true;  // Request will call the callback.
112   }
116  * Returns the singleton instance.
117  * @return {ImageLoader} ImageLoader object.
118  */
119 ImageLoader.getInstance = function() {
120   if (!ImageLoader.instance_)
121     ImageLoader.instance_ = new ImageLoader();
122   return ImageLoader.instance_;
126  * Checks if the options contain any image processing.
128  * @param {number} width Source width.
129  * @param {number} height Source height.
130  * @param {Object} options Resizing options as a hash array.
131  * @return {boolean} True if yes, false if not.
132  */
133 ImageLoader.shouldProcess = function(width, height, options) {
134   var targetDimensions = ImageLoader.resizeDimensions(width, height, options);
136   // Dimensions has to be adjusted.
137   if (targetDimensions.width != width || targetDimensions.height != height)
138     return true;
140   // Orientation has to be adjusted.
141   if (options.orientation)
142     return true;
144   // Non-standard color space has to be converted.
145   if (options.colorSpace && options.colorSpace !== ColorSpace.SRGB)
146     return true;
148   // No changes required.
149   return false;
153  * Calculates dimensions taking into account resize options, such as:
154  * - scale: for scaling,
155  * - maxWidth, maxHeight: for maximum dimensions,
156  * - width, height: for exact requested size.
157  * Returns the target size as hash array with width, height properties.
159  * @param {number} width Source width.
160  * @param {number} height Source height.
161  * @param {Object} options Resizing options as a hash array.
162  * @return {Object} Dimensions, eg. {width: 100, height: 50}.
163  */
164 ImageLoader.resizeDimensions = function(width, height, options) {
165   var sourceWidth = width;
166   var sourceHeight = height;
168   // Flip dimensions for odd orientation values: 1 (90deg) and 3 (270deg).
169   if (options.orientation && options.orientation % 2) {
170     sourceWidth = height;
171     sourceHeight = width;
172   }
174   var targetWidth = sourceWidth;
175   var targetHeight = sourceHeight;
177   if ('scale' in options) {
178     targetWidth = sourceWidth * options.scale;
179     targetHeight = sourceHeight * options.scale;
180   }
182   if (options.maxWidth && targetWidth > options.maxWidth) {
183     var scale = options.maxWidth / targetWidth;
184     targetWidth *= scale;
185     targetHeight *= scale;
186   }
188   if (options.maxHeight && targetHeight > options.maxHeight) {
189     var scale = options.maxHeight / targetHeight;
190     targetWidth *= scale;
191     targetHeight *= scale;
192   }
194   if (options.width)
195     targetWidth = options.width;
197   if (options.height)
198     targetHeight = options.height;
200   targetWidth = Math.round(targetWidth);
201   targetHeight = Math.round(targetHeight);
203   return {width: targetWidth, height: targetHeight};
207  * Performs resizing of the source image into the target canvas.
209  * @param {HTMLCanvasElement|Image} source Source image or canvas.
210  * @param {HTMLCanvasElement} target Target canvas.
211  * @param {Object} options Resizing options as a hash array.
212  */
213 ImageLoader.resize = function(source, target, options) {
214   var targetDimensions = ImageLoader.resizeDimensions(
215       source.width, source.height, options);
217   // Default orientation is 0deg.
218   var orientation = options.orientation || new ImageOrientation(1, 0, 0, 1);
219   var size = orientation.getSizeAfterCancelling(
220       targetDimensions.width, targetDimensions.height);
221   target.width = size.width;
222   target.height = size.height;
224   var targetContext = target.getContext('2d');
225   targetContext.save();
226   orientation.cancelImageOrientation(
227       targetContext, targetDimensions.width, targetDimensions.height);
228   targetContext.drawImage(
229       source,
230       0, 0, source.width, source.height,
231       0, 0, targetDimensions.width, targetDimensions.height);
232   targetContext.restore();
236  * Matrix converts AdobeRGB color space into sRGB color space.
237  * @const {!Array<number>}
238  */
239 ImageLoader.MATRIX_FROM_ADOBE_TO_STANDARD = [
240   1.39836, -0.39836, 0.00000,
241   0.00000,  1.00000, 0.00000,
242   0.00000, -0.04293, 1.04293
246  * Converts the canvas of color space into sRGB.
247  * @param {HTMLCanvasElement} target Target canvas.
248  * @param {ColorSpace} colorSpace Current color space.
249  */
250 ImageLoader.convertColorSpace = function(target, colorSpace) {
251   if (colorSpace === ColorSpace.SRGB)
252     return;
253   if (colorSpace === ColorSpace.ADOBE_RGB) {
254     var matrix = ImageLoader.MATRIX_FROM_ADOBE_TO_STANDARD;
255     var context = target.getContext('2d');
256     var imageData = context.getImageData(0, 0, target.width, target.height);
257     var data = imageData.data;
258     for (var i = 0; i < data.length; i += 4) {
259       // Scale to [0, 1].
260       var adobeR = data[i] / 255;
261       var adobeG = data[i + 1] / 255;
262       var adobeB = data[i + 2] / 255;
264       // Revert gannma transformation.
265       adobeR = adobeR <= 0.0556 ? adobeR / 32 : Math.pow(adobeR, 2.2);
266       adobeG = adobeG <= 0.0556 ? adobeG / 32 : Math.pow(adobeG, 2.2);
267       adobeB = adobeB <= 0.0556 ? adobeB / 32 : Math.pow(adobeB, 2.2);
269       // Convert color space.
270       var sR = matrix[0] * adobeR + matrix[1] * adobeG + matrix[2] * adobeB;
271       var sG = matrix[3] * adobeR + matrix[4] * adobeG + matrix[5] * adobeB;
272       var sB = matrix[6] * adobeR + matrix[7] * adobeG + matrix[8] * adobeB;
274       // Gannma transformation.
275       sR = sR <= 0.0031308 ? 12.92 * sR : 1.055 * Math.pow(sR, 1 / 2.4) - 0.055;
276       sG = sG <= 0.0031308 ? 12.92 * sG : 1.055 * Math.pow(sG, 1 / 2.4) - 0.055;
277       sB = sB <= 0.0031308 ? 12.92 * sB : 1.055 * Math.pow(sB, 1 / 2.4) - 0.055;
279       // Scale to [0, 255].
280       data[i] = Math.max(0, Math.min(255, sR * 255));
281       data[i + 1] = Math.max(0, Math.min(255, sG * 255));
282       data[i + 2] = Math.max(0, Math.min(255, sB * 255));
283     }
284     context.putImageData(imageData, 0, 0);
285   }