Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / ui / file_manager / image_loader / image_loader.js
blobe6781f83892dcf147d9c45150cf47372b279ede2
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 {ImageCache}
13    * @private
14    */
15   this.cache_ = new ImageCache();
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           if (typeof request.orientation === 'number') {
72             request.orientation =
73                 ImageOrientation.fromDriveOrientation(request.orientation);
74           } else {
75             request.orientation = new ImageOrientation(1, 0, 0, 1);
76           }
77           return this.onMessage_(sender.id,
78                                  /** @type {LoadImageRequest} */ (request),
79                                  failSafeSendResponse);
80         }
81       }.bind(this));
84 /**
85  * List of extensions allowed to perform image requests.
86  *
87  * @const
88  * @type {Array<string>}
89  */
90 ImageLoader.ALLOWED_CLIENTS = [
91   'hhaomjibdihmijegdhdafkllkbggdgoj',  // File Manager's extension id.
92   'nlkncpkkdoccmpiclbokaimcnedabhhm'  // Gallery extension id.
95 /**
96  * Handles a request. Depending on type of the request, starts or stops
97  * an image task.
98  *
99  * @param {string} senderId Sender's extension id.
100  * @param {!LoadImageRequest} request Request message as a hash array.
101  * @param {function(Object)} callback Callback to be called to return response.
102  * @return {boolean} True if the message channel should stay alive until the
103  *     callback is called.
104  * @private
105  */
106 ImageLoader.prototype.onMessage_ = function(senderId, request, callback) {
107   var requestId = senderId + ':' + request.taskId;
108   if (request.cancel) {
109     // Cancel a task.
110     this.scheduler_.remove(requestId);
111     return false;  // No callback calls.
112   } else {
113     // Create a request task and add it to the scheduler (queue).
114     var requestTask = new ImageRequest(
115         requestId, this.cache_, this.piexLoader_, request, callback);
116     this.scheduler_.add(requestTask);
117     return true;  // Request will call the callback.
118   }
122  * Returns the singleton instance.
123  * @return {ImageLoader} ImageLoader object.
124  */
125 ImageLoader.getInstance = function() {
126   if (!ImageLoader.instance_)
127     ImageLoader.instance_ = new ImageLoader();
128   return ImageLoader.instance_;
132  * Checks if the options contain any image processing.
134  * @param {number} width Source width.
135  * @param {number} height Source height.
136  * @param {Object} options Resizing options as a hash array.
137  * @return {boolean} True if yes, false if not.
138  */
139 ImageLoader.shouldProcess = function(width, height, options) {
140   var targetDimensions = ImageLoader.resizeDimensions(width, height, options);
142   // Dimensions has to be adjusted.
143   if (targetDimensions.width != width || targetDimensions.height != height)
144     return true;
146   // Orientation has to be adjusted.
147   if (!options.orientation.isIdentity())
148     return true;
150   // Non-standard color space has to be converted.
151   if (options.colorSpace && options.colorSpace !== ColorSpace.SRGB)
152     return true;
154   // No changes required.
155   return false;
159  * Calculates dimensions taking into account resize options, such as:
160  * - scale: for scaling,
161  * - maxWidth, maxHeight: for maximum dimensions,
162  * - width, height: for exact requested size.
163  * Returns the target size as hash array with width, height properties.
165  * @param {number} width Source width.
166  * @param {number} height Source height.
167  * @param {Object} options Resizing options as a hash array.
168  * @return {Object} Dimensions, eg. {width: 100, height: 50}.
169  */
170 ImageLoader.resizeDimensions = function(width, height, options) {
171   var scale = options.scale || 1;
172   var targetDimensions = options.orientation.getSizeAfterCancelling(
173       width * scale, height * scale);
174   var targetWidth = targetDimensions.width;
175   var targetHeight = targetDimensions.height;
177   if (options.maxWidth && targetWidth > options.maxWidth) {
178     var scale = options.maxWidth / targetWidth;
179     targetWidth *= scale;
180     targetHeight *= scale;
181   }
183   if (options.maxHeight && targetHeight > options.maxHeight) {
184     var scale = options.maxHeight / targetHeight;
185     targetWidth *= scale;
186     targetHeight *= scale;
187   }
189   if (options.width)
190     targetWidth = options.width;
192   if (options.height)
193     targetHeight = options.height;
195   targetWidth = Math.round(targetWidth);
196   targetHeight = Math.round(targetHeight);
198   return {width: targetWidth, height: targetHeight};
202  * Performs resizing and cropping of the source image into the target canvas.
204  * @param {HTMLCanvasElement|Image} source Source image or canvas.
205  * @param {HTMLCanvasElement} target Target canvas.
206  * @param {Object} options Resizing options as a hash array.
207  */
208 ImageLoader.resizeAndCrop = function(source, target, options) {
209   // Calculates copy parameters.
210   var copyParameters = ImageLoader.calculateCopyParameters(source, options);
211   target.width = copyParameters.canvas.width;
212   target.height = copyParameters.canvas.height;
214   // Apply.
215   var targetContext =
216       /** @type {CanvasRenderingContext2D} */ (target.getContext('2d'));
217   targetContext.save();
218   options.orientation.cancelImageOrientation(
219       targetContext, copyParameters.target.width, copyParameters.target.height);
220   targetContext.drawImage(
221       source,
222       copyParameters.source.x,
223       copyParameters.source.y,
224       copyParameters.source.width,
225       copyParameters.source.height,
226       copyParameters.target.x,
227       copyParameters.target.y,
228       copyParameters.target.width,
229       copyParameters.target.height);
230   targetContext.restore();
234  * @typedef {{
235  *   source: {x:number, y:number, width:number, height:number},
236  *   target: {x:number, y:number, width:number, height:number},
237  *   canvas: {width:number, height:number}
238  * }}
239  */
240 ImageLoader.CopyParameters;
243  * Calculates copy parameters.
245  * @param {HTMLCanvasElement|Image} source Source image or canvas.
246  * @param {Object} options Resizing options as a hash array.
247  * @return {!ImageLoader.CopyParameters} Calculated copy parameters.
248  */
249 ImageLoader.calculateCopyParameters = function(source, options) {
250   if (options.crop) {
251     // When an image is cropped, target should be a fixed size square.
252     assert(options.width);
253     assert(options.height);
254     assert(options.width === options.height);
256     // The length of shorter edge becomes dimension of cropped area in the
257     // source.
258     var cropSourceDimension = Math.min(source.width, source.height);
260     return {
261       source: {
262         x: Math.floor((source.width / 2) - (cropSourceDimension / 2)),
263         y: Math.floor((source.height / 2) - (cropSourceDimension / 2)),
264         width: cropSourceDimension,
265         height: cropSourceDimension
266       },
267       target: {
268         x: 0,
269         y: 0,
270         width: options.width,
271         height: options.height
272       },
273       canvas: {
274         width: options.width,
275         height: options.height
276       }
277     };
278   }
280   // Target dimension is calculated in the rotated(transformed) coordinate.
281   var targetCanvasDimensions = ImageLoader.resizeDimensions(
282       source.width, source.height, options);
284   var targetDimensions = options.orientation.getSizeAfterCancelling(
285       targetCanvasDimensions.width, targetCanvasDimensions.height);
287   return {
288     source: {
289       x: 0,
290       y: 0,
291       width: source.width,
292       height: source.height
293     },
294     target: {
295       x: 0,
296       y: 0,
297       width: targetDimensions.width,
298       height: targetDimensions.height
299     },
300     canvas: {
301       width: targetCanvasDimensions.width,
302       height: targetCanvasDimensions.height
303     }
304   };
308  * Matrix converts AdobeRGB color space into sRGB color space.
309  * @const {!Array<number>}
310  */
311 ImageLoader.MATRIX_FROM_ADOBE_TO_STANDARD = [
312   1.39836, -0.39836, 0.00000,
313   0.00000,  1.00000, 0.00000,
314   0.00000, -0.04293, 1.04293
318  * Converts the canvas of color space into sRGB.
319  * @param {HTMLCanvasElement} target Target canvas.
320  * @param {ColorSpace} colorSpace Current color space.
321  */
322 ImageLoader.convertColorSpace = function(target, colorSpace) {
323   if (colorSpace === ColorSpace.SRGB)
324     return;
325   if (colorSpace === ColorSpace.ADOBE_RGB) {
326     var matrix = ImageLoader.MATRIX_FROM_ADOBE_TO_STANDARD;
327     var context = target.getContext('2d');
328     var imageData = context.getImageData(0, 0, target.width, target.height);
329     var data = imageData.data;
330     for (var i = 0; i < data.length; i += 4) {
331       // Scale to [0, 1].
332       var adobeR = data[i] / 255;
333       var adobeG = data[i + 1] / 255;
334       var adobeB = data[i + 2] / 255;
336       // Revert gannma transformation.
337       adobeR = adobeR <= 0.0556 ? adobeR / 32 : Math.pow(adobeR, 2.2);
338       adobeG = adobeG <= 0.0556 ? adobeG / 32 : Math.pow(adobeG, 2.2);
339       adobeB = adobeB <= 0.0556 ? adobeB / 32 : Math.pow(adobeB, 2.2);
341       // Convert color space.
342       var sR = matrix[0] * adobeR + matrix[1] * adobeG + matrix[2] * adobeB;
343       var sG = matrix[3] * adobeR + matrix[4] * adobeG + matrix[5] * adobeB;
344       var sB = matrix[6] * adobeR + matrix[7] * adobeG + matrix[8] * adobeB;
346       // Gannma transformation.
347       sR = sR <= 0.0031308 ? 12.92 * sR : 1.055 * Math.pow(sR, 1 / 2.4) - 0.055;
348       sG = sG <= 0.0031308 ? 12.92 * sG : 1.055 * Math.pow(sG, 1 / 2.4) - 0.055;
349       sB = sB <= 0.0031308 ? 12.92 * sB : 1.055 * Math.pow(sB, 1 / 2.4) - 0.055;
351       // Scale to [0, 255].
352       data[i] = Math.max(0, Math.min(255, sR * 255));
353       data[i + 1] = Math.max(0, Math.min(255, sG * 255));
354       data[i + 2] = Math.max(0, Math.min(255, sB * 255));
355     }
356     context.putImageData(imageData, 0, 0);
357   }