1 // Copyright (c) 2012 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.
8 * Command queue is the only way to modify images.
10 * Command execution is asynchronous (callback-based).
12 * @param {Document} document Document to create canvases in.
13 * @param {HTMLCanvasElement} canvas The canvas with the original image.
14 * @param {function(callback)} saveFunction Function to save the image.
17 function CommandQueue(document, canvas, saveFunction) {
18 this.document_ = document;
21 this.subscribers_ = [];
22 this.currentImage_ = canvas;
24 // Current image may be null or not-null but with width = height = 0.
25 // Copying an image with zero dimensions causes js errors.
26 if (this.currentImage_) {
27 this.baselineImage_ = document.createElement('canvas');
28 this.baselineImage_.width = this.currentImage_.width;
29 this.baselineImage_.height = this.currentImage_.height;
30 if (this.currentImage_.width > 0 && this.currentImage_.height > 0) {
31 var context = this.baselineImage_.getContext('2d');
32 context.drawImage(this.currentImage_, 0, 0);
35 this.baselineImage_ = null;
38 this.previousImage_ = document.createElement('canvas');
39 this.previousImageAvailable_ = false;
41 this.saveFunction_ = saveFunction;
47 * Attach the UI elements to the command queue.
48 * Once the UI is attached the results of image manipulations are displayed.
50 * @param {ImageView} imageView The ImageView object to display the results.
51 * @param {ImageEditor.Prompt} prompt Prompt to use with this CommandQueue.
52 * @param {function(boolean)} lock Function to enable/disable buttons etc.
54 CommandQueue.prototype.attachUI = function(imageView, prompt, lock) {
63 * Execute the action when the queue is not busy.
64 * @param {function} callback Callback.
66 CommandQueue.prototype.executeWhenReady = function(callback) {
68 this.subscribers_.push(callback);
70 setTimeout(callback, 0);
74 * @return {boolean} True if the command queue is busy.
76 CommandQueue.prototype.isBusy = function() { return this.busy_ };
79 * Set the queue state to busy. Lock the UI.
82 CommandQueue.prototype.setBusy_ = function() {
84 throw new Error('CommandQueue already busy');
88 if (this.UIContext_.lock)
89 this.UIContext_.lock(true);
91 ImageUtil.trace.resetTimer('command-busy');
95 * Set the queue state to not busy. Unlock the UI and execute pending actions.
98 CommandQueue.prototype.clearBusy_ = function() {
100 throw new Error('Inconsistent CommandQueue already not busy');
104 // Execute the actions requested while the queue was busy.
105 while (this.subscribers_.length)
106 this.subscribers_.shift()();
108 if (this.UIContext_.lock)
109 this.UIContext_.lock(false);
111 ImageUtil.trace.reportTimer('command-busy');
115 * Commit the image change: save and unlock the UI.
116 * @param {number=} opt_delay Delay in ms (to avoid disrupting the animation).
119 CommandQueue.prototype.commit_ = function(opt_delay) {
120 setTimeout(this.saveFunction_.bind(null, this.clearBusy_.bind(this)),
125 * Internal function to execute the command in a given context.
127 * @param {Command} command The command to execute.
128 * @param {Object} uiContext The UI context.
129 * @param {function} callback Completion callback.
132 CommandQueue.prototype.doExecute_ = function(command, uiContext, callback) {
133 if (!this.currentImage_)
134 throw new Error('Cannot operate on null image');
136 // Remember one previous image so that the first undo is as fast as possible.
137 this.previousImage_.width = this.currentImage_.width;
138 this.previousImage_.height = this.currentImage_.height;
139 this.previousImageAvailable_ = true;
140 var context = this.previousImage_.getContext('2d');
141 context.drawImage(this.currentImage_, 0, 0);
146 function(result, opt_delay) {
147 this.currentImage_ = result;
154 * Executes the command.
156 * @param {Command} command Command to execute.
157 * @param {boolean=} opt_keep_redo True if redo stack should not be cleared.
159 CommandQueue.prototype.execute = function(command, opt_keep_redo) {
165 this.undo_.push(command);
167 this.doExecute_(command, this.UIContext_, this.commit_.bind(this));
171 * @return {boolean} True if Undo is applicable.
173 CommandQueue.prototype.canUndo = function() {
174 return this.undo_.length != 0;
178 * Undo the most recent command.
180 CommandQueue.prototype.undo = function() {
182 throw new Error('Cannot undo');
186 var command = this.undo_.pop();
187 this.redo_.push(command);
191 function complete() {
192 var delay = command.revertView(
193 self.currentImage_, self.UIContext_.imageView);
197 if (this.previousImageAvailable_) {
198 // First undo after an execute call.
199 this.currentImage_.width = this.previousImage_.width;
200 this.currentImage_.height = this.previousImage_.height;
201 var context = this.currentImage_.getContext('2d');
202 context.drawImage(this.previousImage_, 0, 0);
205 this.previousImage_.width = 0;
206 this.previousImage_.height = 0;
207 this.previousImageAvailable_ = false;
210 // TODO(kaznacheev) Consider recalculating previousImage_ right here
211 // by replaying the commands in the background.
213 this.currentImage_.width = this.baselineImage_.width;
214 this.currentImage_.height = this.baselineImage_.height;
215 var context = this.currentImage_.getContext('2d');
216 context.drawImage(this.baselineImage_, 0, 0);
218 var replay = function(index) {
219 if (index < self.undo_.length)
220 self.doExecute_(self.undo_[index], {}, replay.bind(null, index + 1));
231 * @return {boolean} True if Redo is applicable.
233 CommandQueue.prototype.canRedo = function() {
234 return this.redo_.length != 0;
238 * Repeat the command that was recently un-done.
240 CommandQueue.prototype.redo = function() {
242 throw new Error('Cannot redo');
244 this.execute(this.redo_.pop(), true);
248 * Closes internal buffers. Call to ensure, that internal buffers are freed
249 * as soon as possible.
251 CommandQueue.prototype.close = function() {
252 // Free memory used by the undo buffer.
253 this.previousImage_.width = 0;
254 this.previousImage_.height = 0;
255 this.previousImageAvailable_ = false;
257 if (this.baselineImage_) {
258 this.baselineImage_.width = 0;
259 this.baselineImage_.height = 0;
264 * Command object encapsulates an operation on an image and a way to visualize
267 * @param {string} name Command name.
270 function Command(name) {
275 * @return {string} String representation of the command.
277 Command.prototype.toString = function() {
278 return 'Command ' + this.name_;
282 * Execute the command and visualize its results.
284 * The two actions are combined into one method because sometimes it is nice
285 * to be able to show partial results for slower operations.
287 * @param {Document} document Document on which to execute command.
288 * @param {HTMLCanvasElement} srcCanvas Canvas to execute on.
289 * @param {function(HTMLCanvasElement, number)} callback Callback to call on
291 * @param {Object} uiContext Context to work in.
293 Command.prototype.execute = function(document, srcCanvas, callback, uiContext) {
294 console.error('Command.prototype.execute not implemented');
298 * Visualize reversion of the operation.
300 * @param {HTMLCanvasElement} canvas Image data to use.
301 * @param {ImageView} imageView ImageView to revert.
302 * @return {number} Animation duration in ms.
304 Command.prototype.revertView = function(canvas, imageView) {
305 imageView.replace(canvas);
310 * Creates canvas to render on.
312 * @param {Document} document Document to create canvas in.
313 * @param {HTMLCanvasElement} srcCanvas to copy optional dimensions from.
314 * @param {number=} opt_width new canvas width.
315 * @param {number=} opt_height new canvas height.
316 * @return {HTMLCanvasElement} Newly created canvas.
319 Command.prototype.createCanvas_ = function(
320 document, srcCanvas, opt_width, opt_height) {
321 var result = document.createElement('canvas');
322 result.width = opt_width || srcCanvas.width;
323 result.height = opt_height || srcCanvas.height;
330 * @param {number} rotate90 Rotation angle in 90 degree increments (signed).
334 Command.Rotate = function(rotate90) {
335 Command.call(this, 'rotate(' + rotate90 * 90 + 'deg)');
336 this.rotate90_ = rotate90;
339 Command.Rotate.prototype = { __proto__: Command.prototype };
342 Command.Rotate.prototype.execute = function(
343 document, srcCanvas, callback, uiContext) {
344 var result = this.createCanvas_(
347 (this.rotate90_ & 1) ? srcCanvas.height : srcCanvas.width,
348 (this.rotate90_ & 1) ? srcCanvas.width : srcCanvas.height);
349 ImageUtil.drawImageTransformed(
350 result, srcCanvas, 1, 1, this.rotate90_ * Math.PI / 2);
352 if (uiContext.imageView) {
353 delay = uiContext.imageView.replaceAndAnimate(result, null, this.rotate90_);
355 setTimeout(callback, 0, result, delay);
359 Command.Rotate.prototype.revertView = function(canvas, imageView) {
360 return imageView.replaceAndAnimate(canvas, null, -this.rotate90_);
367 * @param {Rect} imageRect Crop rectangle in image coordinates.
371 Command.Crop = function(imageRect) {
372 Command.call(this, 'crop' + imageRect.toString());
373 this.imageRect_ = imageRect;
376 Command.Crop.prototype = { __proto__: Command.prototype };
379 Command.Crop.prototype.execute = function(
380 document, srcCanvas, callback, uiContext) {
381 var result = this.createCanvas_(
382 document, srcCanvas, this.imageRect_.width, this.imageRect_.height);
383 Rect.drawImage(result.getContext('2d'), srcCanvas, null, this.imageRect_);
385 if (uiContext.imageView) {
386 delay = uiContext.imageView.replaceAndAnimate(result, this.imageRect_, 0);
388 setTimeout(callback, 0, result, delay);
392 Command.Crop.prototype.revertView = function(canvas, imageView) {
393 return imageView.animateAndReplace(canvas, this.imageRect_);
400 * @param {string} name Command name.
401 * @param {function(ImageData,ImageData,number,number)} filter Filter function.
402 * @param {string} message Message to display when done.
406 Command.Filter = function(name, filter, message) {
407 Command.call(this, name);
408 this.filter_ = filter;
409 this.message_ = message;
412 Command.Filter.prototype = { __proto__: Command.prototype };
415 Command.Filter.prototype.execute = function(
416 document, srcCanvas, callback, uiContext) {
417 var result = this.createCanvas_(document, srcCanvas);
423 function onProgressVisible(updatedRow, rowCount) {
424 if (updatedRow == rowCount) {
425 uiContext.imageView.replace(result);
427 uiContext.prompt.show(self.message_, 2000);
430 var viewport = uiContext.imageView.viewport_;
432 var imageStrip = new Rect(viewport.getImageBounds());
433 imageStrip.top = previousRow;
434 imageStrip.height = updatedRow - previousRow;
436 var screenStrip = new Rect(viewport.getImageBoundsOnScreen());
437 screenStrip.top = Math.round(viewport.imageToScreenY(previousRow));
439 Math.round(viewport.imageToScreenY(updatedRow)) - screenStrip.top;
441 uiContext.imageView.paintDeviceRect(
442 viewport.screenToDeviceRect(screenStrip), result, imageStrip);
443 previousRow = updatedRow;
447 function onProgressInvisible(updatedRow, rowCount) {
448 if (updatedRow == rowCount) {
453 filter.applyByStrips(result, srcCanvas, this.filter_,
454 uiContext.imageView ? onProgressVisible : onProgressInvisible);