Update To 11.40.268.0
[platform/framework/web/crosswalk.git] / src / ui / file_manager / gallery / js / image_editor / commands.js
1 // Copyright 2014 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.
4
5 /**
6  * Command queue is the only way to modify images.
7  * Supports undo/redo.
8  * Command execution is asynchronous (callback-based).
9  *
10  * @param {Document} document Document to create canvases in.
11  * @param {HTMLCanvasElement} canvas The canvas with the original image.
12  * @param {function(function())} saveFunction Function to save the image.
13  * @constructor
14  */
15 function CommandQueue(document, canvas, saveFunction) {
16   this.document_ = document;
17   this.undo_ = [];
18   this.redo_ = [];
19   this.subscribers_ = [];
20   this.currentImage_ = canvas;
21
22   // Current image may be null or not-null but with width = height = 0.
23   // Copying an image with zero dimensions causes js errors.
24   if (this.currentImage_) {
25     this.baselineImage_ = document.createElement('canvas');
26     this.baselineImage_.width = this.currentImage_.width;
27     this.baselineImage_.height = this.currentImage_.height;
28     if (this.currentImage_.width > 0 && this.currentImage_.height > 0) {
29       var context = this.baselineImage_.getContext('2d');
30       context.drawImage(this.currentImage_, 0, 0);
31     }
32   } else {
33     this.baselineImage_ = null;
34   }
35
36   this.previousImage_ = document.createElement('canvas');
37   this.previousImageAvailable_ = false;
38
39   this.saveFunction_ = saveFunction;
40   this.busy_ = false;
41   this.UIContext_ = {};
42 }
43
44 /**
45  * Attach the UI elements to the command queue.
46  * Once the UI is attached the results of image manipulations are displayed.
47  *
48  * @param {ImageView} imageView The ImageView object to display the results.
49  * @param {ImageEditor.Prompt} prompt Prompt to use with this CommandQueue.
50  * @param {function(boolean)} lock Function to enable/disable buttons etc.
51  */
52 CommandQueue.prototype.attachUI = function(imageView, prompt, lock) {
53   this.UIContext_ = {
54     imageView: imageView,
55     prompt: prompt,
56     lock: lock
57   };
58 };
59
60 /**
61  * Execute the action when the queue is not busy.
62  * @param {function()} callback Callback.
63  */
64 CommandQueue.prototype.executeWhenReady = function(callback) {
65   if (this.isBusy())
66     this.subscribers_.push(callback);
67   else
68     setTimeout(callback, 0);
69 };
70
71 /**
72  * @return {boolean} True if the command queue is busy.
73  */
74 CommandQueue.prototype.isBusy = function() { return this.busy_ };
75
76 /**
77  * Set the queue state to busy. Lock the UI.
78  * @private
79  */
80 CommandQueue.prototype.setBusy_ = function() {
81   if (this.busy_)
82     throw new Error('CommandQueue already busy');
83
84   this.busy_ = true;
85
86   if (this.UIContext_.lock)
87     this.UIContext_.lock(true);
88
89   ImageUtil.trace.resetTimer('command-busy');
90 };
91
92 /**
93  * Set the queue state to not busy. Unlock the UI and execute pending actions.
94  * @private
95  */
96 CommandQueue.prototype.clearBusy_ = function() {
97   if (!this.busy_)
98     throw new Error('Inconsistent CommandQueue already not busy');
99
100   this.busy_ = false;
101
102   // Execute the actions requested while the queue was busy.
103   while (this.subscribers_.length)
104     this.subscribers_.shift()();
105
106   if (this.UIContext_.lock)
107     this.UIContext_.lock(false);
108
109   ImageUtil.trace.reportTimer('command-busy');
110 };
111
112 /**
113  * Commit the image change: save and unlock the UI.
114  * @param {number=} opt_delay Delay in ms (to avoid disrupting the animation).
115  * @private
116  */
117 CommandQueue.prototype.commit_ = function(opt_delay) {
118   setTimeout(this.saveFunction_.bind(null, this.clearBusy_.bind(this)),
119       opt_delay || 0);
120 };
121
122 /**
123  * Internal function to execute the command in a given context.
124  *
125  * @param {Command} command The command to execute.
126  * @param {Object} uiContext The UI context.
127  * @param {function(number=)} callback Completion callback.
128  * @private
129  */
130 CommandQueue.prototype.doExecute_ = function(command, uiContext, callback) {
131   if (!this.currentImage_)
132     throw new Error('Cannot operate on null image');
133
134   // Remember one previous image so that the first undo is as fast as possible.
135   this.previousImage_.width = this.currentImage_.width;
136   this.previousImage_.height = this.currentImage_.height;
137   this.previousImageAvailable_ = true;
138   var context = this.previousImage_.getContext('2d');
139   context.drawImage(this.currentImage_, 0, 0);
140
141   command.execute(
142       this.document_,
143       this.currentImage_,
144       function(result, opt_delay) {
145         this.currentImage_ = result;
146         callback(opt_delay);
147       }.bind(this),
148       uiContext);
149 };
150
151 /**
152  * Executes the command.
153  *
154  * @param {Command} command Command to execute.
155  * @param {boolean=} opt_keep_redo True if redo stack should not be cleared.
156  */
157 CommandQueue.prototype.execute = function(command, opt_keep_redo) {
158   this.setBusy_();
159
160   if (!opt_keep_redo)
161     this.redo_ = [];
162
163   this.undo_.push(command);
164
165   this.doExecute_(command, this.UIContext_, this.commit_.bind(this));
166 };
167
168 /**
169  * @return {boolean} True if Undo is applicable.
170  */
171 CommandQueue.prototype.canUndo = function() {
172   return this.undo_.length != 0;
173 };
174
175 /**
176  * Undo the most recent command.
177  */
178 CommandQueue.prototype.undo = function() {
179   if (!this.canUndo())
180     throw new Error('Cannot undo');
181
182   this.setBusy_();
183
184   var command = this.undo_.pop();
185   this.redo_.push(command);
186
187   var self = this;
188
189   function complete() {
190     var delay = command.revertView(
191         self.currentImage_, self.UIContext_.imageView);
192     self.commit_(delay);
193   }
194
195   if (this.previousImageAvailable_) {
196     // First undo after an execute call.
197     this.currentImage_.width = this.previousImage_.width;
198     this.currentImage_.height = this.previousImage_.height;
199     var context = this.currentImage_.getContext('2d');
200     context.drawImage(this.previousImage_, 0, 0);
201
202     // Free memory.
203     this.previousImage_.width = 0;
204     this.previousImage_.height = 0;
205     this.previousImageAvailable_ = false;
206
207     complete();
208     // TODO(kaznacheev) Consider recalculating previousImage_ right here
209     // by replaying the commands in the background.
210   } else {
211     this.currentImage_.width = this.baselineImage_.width;
212     this.currentImage_.height = this.baselineImage_.height;
213     var context = this.currentImage_.getContext('2d');
214     context.drawImage(this.baselineImage_, 0, 0);
215
216     var replay = function(index) {
217       if (index < self.undo_.length)
218         self.doExecute_(self.undo_[index], {}, replay.bind(null, index + 1));
219       else {
220         complete();
221       }
222     };
223
224     replay(0);
225   }
226 };
227
228 /**
229  * @return {boolean} True if Redo is applicable.
230  */
231 CommandQueue.prototype.canRedo = function() {
232   return this.redo_.length != 0;
233 };
234
235 /**
236  * Repeat the command that was recently un-done.
237  */
238 CommandQueue.prototype.redo = function() {
239   if (!this.canRedo())
240     throw new Error('Cannot redo');
241
242   this.execute(this.redo_.pop(), true);
243 };
244
245 /**
246  * Closes internal buffers. Call to ensure, that internal buffers are freed
247  * as soon as possible.
248  */
249 CommandQueue.prototype.close = function() {
250   // Free memory used by the undo buffer.
251   this.previousImage_.width = 0;
252   this.previousImage_.height = 0;
253   this.previousImageAvailable_ = false;
254
255   if (this.baselineImage_) {
256     this.baselineImage_.width = 0;
257     this.baselineImage_.height = 0;
258   }
259 };
260
261 /**
262  * Command object encapsulates an operation on an image and a way to visualize
263  * its result.
264  *
265  * @param {string} name Command name.
266  * @constructor
267  */
268 function Command(name) {
269   this.name_ = name;
270 }
271
272 /**
273  * @return {string} String representation of the command.
274  */
275 Command.prototype.toString = function() {
276   return 'Command ' + this.name_;
277 };
278
279 /**
280  * Execute the command and visualize its results.
281  *
282  * The two actions are combined into one method because sometimes it is nice
283  * to be able to show partial results for slower operations.
284  *
285  * @param {Document} document Document on which to execute command.
286  * @param {HTMLCanvasElement} srcCanvas Canvas to execute on.
287  * @param {function(HTMLCanvasElement, number)} callback Callback to call on
288  *   completion.
289  * @param {Object} uiContext Context to work in.
290  */
291 Command.prototype.execute = function(document, srcCanvas, callback, uiContext) {
292   console.error('Command.prototype.execute not implemented');
293 };
294
295 /**
296  * Visualize reversion of the operation.
297  *
298  * @param {HTMLCanvasElement} canvas Image data to use.
299  * @param {ImageView} imageView ImageView to revert.
300  * @return {number} Animation duration in ms.
301  */
302 Command.prototype.revertView = function(canvas, imageView) {
303   imageView.replace(canvas);
304   return 0;
305 };
306
307 /**
308  * Creates canvas to render on.
309  *
310  * @param {Document} document Document to create canvas in.
311  * @param {HTMLCanvasElement} srcCanvas to copy optional dimensions from.
312  * @param {number=} opt_width new canvas width.
313  * @param {number=} opt_height new canvas height.
314  * @return {HTMLCanvasElement} Newly created canvas.
315  * @private
316  */
317 Command.prototype.createCanvas_ = function(
318     document, srcCanvas, opt_width, opt_height) {
319   var result = document.createElement('canvas');
320   result.width = opt_width || srcCanvas.width;
321   result.height = opt_height || srcCanvas.height;
322   return result;
323 };
324
325
326 /**
327  * Rotate command
328  * @param {number} rotate90 Rotation angle in 90 degree increments (signed).
329  * @constructor
330  * @extends {Command}
331  */
332 Command.Rotate = function(rotate90) {
333   Command.call(this, 'rotate(' + rotate90 * 90 + 'deg)');
334   this.rotate90_ = rotate90;
335 };
336
337 Command.Rotate.prototype = { __proto__: Command.prototype };
338
339 /** @override */
340 Command.Rotate.prototype.execute = function(
341     document, srcCanvas, callback, uiContext) {
342   var result = this.createCanvas_(
343       document,
344       srcCanvas,
345       (this.rotate90_ & 1) ? srcCanvas.height : srcCanvas.width,
346       (this.rotate90_ & 1) ? srcCanvas.width : srcCanvas.height);
347   ImageUtil.drawImageTransformed(
348       result, srcCanvas, 1, 1, this.rotate90_ * Math.PI / 2);
349   var delay;
350   if (uiContext.imageView) {
351     delay = uiContext.imageView.replaceAndAnimate(result, null, this.rotate90_);
352   }
353   setTimeout(callback, 0, result, delay);
354 };
355
356 /** @override */
357 Command.Rotate.prototype.revertView = function(canvas, imageView) {
358   return imageView.replaceAndAnimate(canvas, null, -this.rotate90_);
359 };
360
361
362 /**
363  * Crop command.
364  *
365  * @param {ImageRect} imageRect Crop rectangle in image coordinates.
366  * @constructor
367  * @extends {Command}
368  */
369 Command.Crop = function(imageRect) {
370   Command.call(this, 'crop' + imageRect.toString());
371   this.imageRect_ = imageRect;
372 };
373
374 Command.Crop.prototype = { __proto__: Command.prototype };
375
376 /** @override */
377 Command.Crop.prototype.execute = function(
378     document, srcCanvas, callback, uiContext) {
379   var result = this.createCanvas_(
380       document, srcCanvas, this.imageRect_.width, this.imageRect_.height);
381   ImageRect.drawImage(
382       result.getContext('2d'), srcCanvas, null, this.imageRect_);
383   var delay;
384   if (uiContext.imageView) {
385     delay = uiContext.imageView.replaceAndAnimate(result, this.imageRect_, 0);
386   }
387   setTimeout(callback, 0, result, delay);
388 };
389
390 /** @override */
391 Command.Crop.prototype.revertView = function(canvas, imageView) {
392   return imageView.animateAndReplace(canvas, this.imageRect_);
393 };
394
395
396 /**
397  * Filter command.
398  *
399  * @param {string} name Command name.
400  * @param {function(ImageData,ImageData,number,number)} filter Filter function.
401  * @param {string} message Message to display when done.
402  * @constructor
403  * @extends {Command}
404  */
405 Command.Filter = function(name, filter, message) {
406   Command.call(this, name);
407   this.filter_ = filter;
408   this.message_ = message;
409 };
410
411 Command.Filter.prototype = { __proto__: Command.prototype };
412
413 /** @override */
414 Command.Filter.prototype.execute = function(
415     document, srcCanvas, callback, uiContext) {
416   var result = this.createCanvas_(document, srcCanvas);
417   var self = this;
418   var previousRow = 0;
419
420   function onProgressVisible(updatedRow, rowCount) {
421     if (updatedRow == rowCount) {
422       uiContext.imageView.replace(result);
423       if (self.message_)
424         uiContext.prompt.show(self.message_, 2000);
425       callback(result);
426     } else {
427       var viewport = uiContext.imageView.viewport_;
428
429       var imageStrip = new ImageRect(viewport.getImageBounds());
430       imageStrip.top = previousRow;
431       imageStrip.height = updatedRow - previousRow;
432
433       var screenStrip = new ImageRect(viewport.getImageBoundsOnScreen());
434       screenStrip.top = Math.round(viewport.imageToScreenY(previousRow));
435       screenStrip.height =
436           Math.round(viewport.imageToScreenY(updatedRow)) - screenStrip.top;
437
438       uiContext.imageView.paintDeviceRect(result, imageStrip);
439       previousRow = updatedRow;
440     }
441   }
442
443   function onProgressInvisible(updatedRow, rowCount) {
444     if (updatedRow == rowCount) {
445       callback(result);
446     }
447   }
448
449   filter.applyByStrips(result, srcCanvas, this.filter_,
450       uiContext.imageView ? onProgressVisible : onProgressInvisible);
451 };