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.
6 * Loads and resizes an image.
9 function ImageLoader() {
11 * Persistent cache object.
15 this.cache_ = new ImageCache();
18 * Manages pending requests and runs them in order of priorities.
22 this.scheduler_ = new Scheduler();
25 * Piex loader for RAW images.
26 * @private {!PiexLoader}
28 this.piexLoader_ = new PiexLoader();
30 // Grant permissions to all volumes, initialize the cache and then start the
32 chrome.fileManagerPrivate.getVolumeMetadataList(function(volumeMetadataList) {
33 // Listen for mount events, and grant permissions to volumes being mounted.
34 chrome.fileManagerPrivate.onMountCompleted.addListener(
36 if (event.eventType === 'mount' && event.status === 'success') {
37 chrome.fileSystem.requestFileSystem(
38 {volumeId: event.volumeMetadata.volumeId}, function() {});
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));
47 return requestPromise;
49 initPromises.push(new Promise(function(resolve, reject) {
50 this.cache_.initialize(resolve);
53 // After all initialization promises are done, start the scheduler.
54 Promise.all(initPromises).then(this.scheduler_.start.bind(this.scheduler_));
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) {
65 sendResponse(response);
71 if (typeof request.orientation === 'number') {
73 ImageOrientation.fromDriveOrientation(request.orientation);
75 request.orientation = new ImageOrientation(1, 0, 0, 1);
77 return this.onMessage_(sender.id,
78 /** @type {LoadImageRequest} */ (request),
79 failSafeSendResponse);
85 * List of extensions allowed to perform image requests.
88 * @type {Array<string>}
90 ImageLoader.ALLOWED_CLIENTS = [
91 'hhaomjibdihmijegdhdafkllkbggdgoj', // File Manager's extension id.
92 'nlkncpkkdoccmpiclbokaimcnedabhhm' // Gallery extension id.
96 * Handles a request. Depending on type of the request, starts or stops
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.
106 ImageLoader.prototype.onMessage_ = function(senderId, request, callback) {
107 var requestId = senderId + ':' + request.taskId;
108 if (request.cancel) {
110 this.scheduler_.remove(requestId);
111 return false; // No callback calls.
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.
122 * Returns the singleton instance.
123 * @return {ImageLoader} ImageLoader object.
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.
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)
146 // Orientation has to be adjusted.
147 if (!options.orientation.isIdentity())
150 // Non-standard color space has to be converted.
151 if (options.colorSpace && options.colorSpace !== ColorSpace.SRGB)
154 // No changes required.
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}.
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;
183 if (options.maxHeight && targetHeight > options.maxHeight) {
184 var scale = options.maxHeight / targetHeight;
185 targetWidth *= scale;
186 targetHeight *= scale;
190 targetWidth = options.width;
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.
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;
216 /** @type {CanvasRenderingContext2D} */ (target.getContext('2d'));
217 targetContext.save();
218 options.orientation.cancelImageOrientation(
219 targetContext, copyParameters.target.width, copyParameters.target.height);
220 targetContext.drawImage(
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();
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}
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.
249 ImageLoader.calculateCopyParameters = function(source, options) {
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
258 var cropSourceDimension = Math.min(source.width, source.height);
262 x: Math.floor((source.width / 2) - (cropSourceDimension / 2)),
263 y: Math.floor((source.height / 2) - (cropSourceDimension / 2)),
264 width: cropSourceDimension,
265 height: cropSourceDimension
270 width: options.width,
271 height: options.height
274 width: options.width,
275 height: options.height
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);
292 height: source.height
297 width: targetDimensions.width,
298 height: targetDimensions.height
301 width: targetCanvasDimensions.width,
302 height: targetCanvasDimensions.height
308 * Matrix converts AdobeRGB color space into sRGB color space.
309 * @const {!Array<number>}
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.
322 ImageLoader.convertColorSpace = function(target, colorSpace) {
323 if (colorSpace === ColorSpace.SRGB)
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) {
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));
356 context.putImageData(imageData, 0, 0);