Index: chrome/browser/resources/file_manager/js/image_editor/image_adjust.js |
diff --git a/chrome/browser/resources/file_manager/js/image_editor/image_adjust.js b/chrome/browser/resources/file_manager/js/image_editor/image_adjust.js |
index c47283f903424acba0c4cc03783d32bc2a36e6a4..cf53f40d07537394ef05e4ba7f1dc5de7d50b8c8 100644 |
--- a/chrome/browser/resources/file_manager/js/image_editor/image_adjust.js |
+++ b/chrome/browser/resources/file_manager/js/image_editor/image_adjust.js |
@@ -5,81 +5,347 @@ |
/** |
* The base class for simple filters that only modify the image content |
* but do not modify the image dimensions. |
+ * @constructor |
*/ |
-ImageEditor.Mode.Adjust = function(displayName, filterFunc) { |
+ImageEditor.Mode.Adjust = function(displayName) { |
ImageEditor.Mode.call(this, displayName); |
- this.filterFunc_ = filterFunc; |
-} |
+ this.viewportGeneration_ = 0; |
+}; |
ImageEditor.Mode.Adjust.prototype = {__proto__: ImageEditor.Mode.prototype}; |
-ImageEditor.Mode.Adjust.prototype.rollback = function() { |
- if (!this.backup_) return; // Did not do anything yet. |
- this.getContent().drawImageData(this.backup_, 0, 0); |
- this.backup_ = null; |
+/* |
+ * ImageEditor.Mode methods overridden. |
+ */ |
+ |
+ImageEditor.Mode.Adjust.prototype.commit = function() { |
+ if (!this.filter_) return; // Did not do anything yet. |
+ |
+ // Applying the filter to the entire image takes some time, so we do |
+ // it in small increments, providing visual feedback. |
+ // TODO: provide modal progress indicator. |
+ |
+ // First hide the preview and show the original image. |
this.repaint(); |
-}; |
-ImageEditor.Mode.Adjust.prototype.update = function(options) { |
- if (!this.backup_) { |
- this.backup_ = this.getContent().copyImageData(); |
- this.scratch_ = this.getContent().copyImageData(); |
+ var self = this; |
+ |
+ function repaintStrip(fromRow, toRow) { |
+ var imageStrip = new Rect(self.getViewport().getImageBounds()); |
+ imageStrip.top = fromRow; |
+ imageStrip.height = toRow - fromRow; |
+ |
+ var screenStrip = new Rect(self.getViewport().getImageBoundsOnScreen()); |
+ screenStrip.top = Math.round(self.getViewport().imageToScreenY(fromRow)); |
+ screenStrip.height = Math.round(self.getViewport().imageToScreenY(toRow)) - |
+ screenStrip.top; |
+ |
+ self.getBuffer().repaintScreenRect(screenStrip, imageStrip); |
} |
ImageUtil.trace.resetTimer('filter'); |
- this.filterFunc_(this.scratch_, this.backup_, options); |
- ImageUtil.trace.reportTimer('filter'); |
- this.getContent().drawImageData(this.scratch_, 0, 0); |
+ |
+ var lastUpdatedRow = 0; |
+ |
+ filter.applyByStrips( |
+ this.getContent().getCanvas().getContext('2d'), |
+ this.filter_, |
+ function (updatedRow, rowCount) { |
+ repaintStrip(lastUpdatedRow, updatedRow); |
+ lastUpdatedRow = updatedRow; |
+ if (updatedRow == rowCount) { |
+ ImageUtil.trace.reportTimer('filter'); |
+ self.getContent().invalidateCaches(); |
+ self.repaint(); |
+ } |
+ }); |
+}; |
+ |
+ImageEditor.Mode.Adjust.prototype.rollback = function() { |
+ this.filter_ = null; |
+ this.previewImageData_ = null; |
+}; |
+ |
+ImageEditor.Mode.Adjust.prototype.update = function(options) { |
+ // We assume filter names are used in the UI directly. |
+ // This will have to change with i18n. |
+ this.filter_ = this.createFilter(options); |
+ this.previewValid_ = false; |
this.repaint(); |
}; |
/** |
- * A simple filter that multiplies every component of a pixel by some number. |
+ * Clip and scale the source image data for the preview. |
+ * Use the cached copy if the viewport has not changed. |
+ */ |
+ImageEditor.Mode.Adjust.prototype.updatePreviewImage = function() { |
+ if (!this.previewImageData_ || |
+ this.viewportGeneration_ != this.getViewport().getCacheGeneration()) { |
+ this.viewportGeneration_ = this.getViewport().getCacheGeneration(); |
+ |
+ var imageRect = this.getPreviewRect(this.getViewport().getImageClipped()); |
+ var screenRect = this.getPreviewRect(this.getViewport().getScreenClipped()); |
+ |
+ // Copy the visible part of the image at the current screen scale. |
+ var canvas = this.getContent().createBlankCanvas( |
+ screenRect.width, screenRect.height); |
+ var context = canvas.getContext('2d'); |
+ Rect.drawImage(context, this.getContent().getCanvas(), null, imageRect); |
+ this.originalImageData = |
+ context.getImageData(0, 0, screenRect.width, screenRect.height); |
+ this.previewImageData_ = |
+ context.getImageData(0, 0, screenRect.width, screenRect.height); |
+ this.previewValid_ = false; |
+ } |
+ |
+ if (this.filter_ && !this.previewValid_) { |
+ ImageUtil.trace.resetTimer('preview'); |
+ this.filter_(this.previewImageData_, this.originalImageData, 0, 0); |
+ ImageUtil.trace.reportTimer('preview'); |
+ this.previewValid_ = true; |
+ } |
+}; |
+ |
+ImageEditor.Mode.Adjust.prototype.draw = function(context) { |
+ this.updatePreviewImage(); |
+ |
+ var screenClipped = this.getViewport().getScreenClipped(); |
+ |
+ var previewRect = this.getPreviewRect(screenClipped); |
+ context.putImageData( |
+ this.previewImageData_, previewRect.left, previewRect.top); |
+ |
+ if (previewRect.width < screenClipped.width && |
+ previewRect.height < screenClipped.height) { |
+ // Some part of the original image is not covered by the preview, |
+ // shade it out. |
+ context.globalAlpha = 0.75; |
+ context.fillStyle = '#000000'; |
+ context.strokeStyle = '#000000'; |
+ Rect.fillBetween( |
+ context, previewRect, this.getViewport().getScreenBounds()); |
+ Rect.outline(context, previewRect); |
+ } |
+}; |
+ |
+/* |
+ * Own methods |
+ */ |
+ |
+ImageEditor.Mode.Adjust.prototype.createFilter = function(options) { |
+ return filter.create(this.displayName.toLowerCase(), options); |
+}; |
+ |
+ImageEditor.Mode.Adjust.prototype.getPreviewRect = function(rect) { |
+ if (this.getViewport().getScale() >= 1) { |
+ return rect; |
+ } else { |
+ var bounds = this.getViewport().getImageBounds(); |
+ var screen = this.getViewport().getScreenClipped(); |
+ |
+ screen = screen.inflate(-screen.width / 8, -screen.height / 8); |
+ |
+ return rect.inflate(-rect.width / 2, -rect.height / 2). |
+ inflate(Math.min(screen.width, bounds.width) / 2, |
+ Math.min(screen.height, bounds.height) / 2); |
+ } |
+}; |
+ |
+/** |
+ * A base class for color filters that are scale independent (i.e. can |
+ * be applied to a scaled image with basicaly the same effect). |
+ * Displays a histogram. |
+ * @constructor |
*/ |
-ImageEditor.Mode.Brightness = function() { |
- ImageEditor.Mode.Adjust.call( |
- this, 'Brightness', ImageEditor.Mode.Brightness.filter); |
-} |
+ImageEditor.Mode.ColorFilter = function() { |
+ ImageEditor.Mode.Adjust.apply(this, arguments); |
+}; |
-ImageEditor.Mode.Brightness.prototype = |
+ImageEditor.Mode.ColorFilter.prototype = |
{__proto__: ImageEditor.Mode.Adjust.prototype}; |
-ImageEditor.Mode.register(ImageEditor.Mode.Brightness); |
+ImageEditor.Mode.ColorFilter.prototype.setUp = function() { |
+ ImageEditor.Mode.Adjust.prototype.setUp.apply(this, arguments); |
+ this.histogram_ = |
+ new ImageEditor.Mode.Histogram(this.getViewport(), this.getContent()); |
+}; |
-ImageEditor.Mode.Brightness.UI_RANGE = 100; |
+ImageEditor.Mode.ColorFilter.prototype.draw = function(context) { |
+ ImageEditor.Mode.Adjust.prototype.draw.apply(this, arguments); |
+ this.histogram_.draw(context); |
+}; |
-ImageEditor.Mode.Brightness.prototype.createTools = function(toolbar) { |
- toolbar.addRange( |
- 'brightness', |
- -ImageEditor.Mode.Brightness.UI_RANGE, |
- 0, |
- ImageEditor.Mode.Brightness.UI_RANGE); |
+ImageEditor.Mode.ColorFilter.prototype.getPreviewRect = function(rect) { |
+ return rect; |
}; |
-ImageEditor.Mode.Brightness.filter = function(dst, src, options) { |
- // Translate from -100..100 range to 1/5..5 |
- var factor = |
- Math.pow(5, options.brightness / ImageEditor.Mode.Brightness.UI_RANGE); |
+ImageEditor.Mode.ColorFilter.prototype.createFilter = function(options) { |
+ var filterFunc = |
+ ImageEditor.Mode.Adjust.prototype.createFilter.apply(this, arguments); |
+ this.histogram_.update(filterFunc); |
+ return filterFunc; |
+}; |
- var dstData = dst.data; |
- var srcData = src.data; |
- var width = src.width; |
- var height = src.height; |
+ImageEditor.Mode.ColorFilter.prototype.rollback = function() { |
+ ImageEditor.Mode.Adjust.prototype.rollback.apply(this, arguments); |
+ this.histogram_.update(null); |
+}; |
- function scale(value) { |
- return value * factor; |
+/** |
+ * A histogram container. |
+ * @constructor |
+ */ |
+ImageEditor.Mode.Histogram = function(viewport, content) { |
+ this.viewport_ = viewport; |
+ |
+ var canvas = content.getCanvas(); |
+ var downScale = Math.max(1, Math.sqrt(canvas.width * canvas.height / 10000)); |
+ var thumbnail = content.copyCanvas(canvas.width / downScale, |
+ canvas.height / downScale); |
+ var context = thumbnail.getContext('2d'); |
+ |
+ this.originalImageData_ = |
+ context.getImageData(0, 0, thumbnail.width, thumbnail.height); |
+ this.filteredImageData_ = |
+ context.getImageData(0, 0, thumbnail.width, thumbnail.height); |
+ |
+ this.update(); |
+}; |
+ |
+ImageEditor.Mode.Histogram.prototype.getData = function() { return this.data_ }; |
+ |
+ImageEditor.Mode.Histogram.BUCKET_WIDTH = 8; |
+ImageEditor.Mode.Histogram.BAR_WIDTH = 2; |
+ImageEditor.Mode.Histogram.RIGHT = 5; |
+ImageEditor.Mode.Histogram.TOP = 5; |
+ |
+ImageEditor.Mode.Histogram.prototype.update = function(filterFunc) { |
+ if (filterFunc) { |
+ filterFunc(this.filteredImageData_, this.originalImageData_, 0, 0); |
+ this.data_ = filter.getHistogram(this.filteredImageData_); |
+ } else { |
+ this.data_ = filter.getHistogram(this.originalImageData_); |
} |
+}; |
+ |
+ImageEditor.Mode.Histogram.prototype.draw = function(context) { |
+ var screen = this.viewport_.getScreenBounds(); |
+ |
+ var barCount = 2 + 3 * (256 / ImageEditor.Mode.Histogram.BUCKET_WIDTH); |
+ var width = ImageEditor.Mode.Histogram.BAR_WIDTH * barCount; |
+ var height = Math.round(width / 2); |
+ var rect = new Rect( |
+ screen.left + screen.width - ImageEditor.Mode.Histogram.RIGHT - width, |
+ ImageEditor.Mode.Histogram.TOP, |
+ width, |
+ height); |
- var values = ImageUtil.precomputeByteFunction(scale, 255); |
+ context.globalAlpha = 1; |
+ context.fillStyle = '#E0E0E0'; |
+ context.strokeStyle = '#000000'; |
+ context.lineCap = 'square'; |
+ Rect.fill(context, rect); |
+ Rect.outline(context, rect); |
- var index = 0; |
- for (var y = 0; y != height; y++) { |
- for (var x = 0; x != width; x++ ) { |
- dstData[index] = values[srcData[index]]; index++; |
- dstData[index] = values[srcData[index]]; index++; |
- dstData[index] = values[srcData[index]]; index++; |
- dstData[index] = 0xFF; index++; |
+ function drawChannel(channel, style, offsetX, offsetY) { |
+ context.strokeStyle = style; |
+ context.beginPath(); |
+ for (var i = 0; i != 256; i += ImageEditor.Mode.Histogram.BUCKET_WIDTH) { |
+ var barHeight = channel[i]; |
+ for (var b = 1; b < ImageEditor.Mode.Histogram.BUCKET_WIDTH; b++) |
+ barHeight = Math.max(barHeight, channel[i + b]); |
+ barHeight = Math.min(barHeight, height); |
+ for (var j = 0; j != ImageEditor.Mode.Histogram.BAR_WIDTH; j++) { |
+ context.moveTo(offsetX, offsetY); |
+ context.lineTo(offsetX, offsetY - barHeight); |
+ offsetX++; |
+ } |
+ offsetX += 2 * ImageEditor.Mode.Histogram.BAR_WIDTH; |
} |
+ context.closePath(); |
+ context.stroke(); |
} |
+ |
+ var offsetX = rect.left + 0.5 + ImageEditor.Mode.Histogram.BAR_WIDTH; |
+ var offsetY = rect.top + rect.height; |
+ |
+ drawChannel(this.data_.r, '#F00000', offsetX, offsetY); |
+ offsetX += ImageEditor.Mode.Histogram.BAR_WIDTH; |
+ drawChannel(this.data_.g, '#00F000', offsetX, offsetY); |
+ offsetX += ImageEditor.Mode.Histogram.BAR_WIDTH; |
+ drawChannel(this.data_.b, '#0000F0', offsetX, offsetY); |
+}; |
+ |
+/** |
+ * Exposure/contrast filter. |
+ * @constructor |
+ */ |
+ImageEditor.Mode.Exposure = function() { |
+ ImageEditor.Mode.ColorFilter.call(this, 'Exposure'); |
}; |
+ImageEditor.Mode.Exposure.prototype = |
+ {__proto__: ImageEditor.Mode.ColorFilter.prototype}; |
+ |
+ImageEditor.Mode.register(ImageEditor.Mode.Exposure); |
+ |
+ImageEditor.Mode.Exposure.prototype.createTools = function(toolbar) { |
+ toolbar.addRange('brightness', -1, 0, 1, 100); |
+ toolbar.addRange('contrast', -1, 0, 1, 100); |
+}; |
+ |
+/** |
+ * Autofix. |
+ * @constructor |
+ */ |
+ImageEditor.Mode.Autofix = function() { |
+ ImageEditor.Mode.ColorFilter.call(this, 'Autofix'); |
+}; |
+ |
+ImageEditor.Mode.Autofix.prototype = |
+ {__proto__: ImageEditor.Mode.ColorFilter.prototype}; |
+ |
+ImageEditor.Mode.register(ImageEditor.Mode.Autofix); |
+ |
+ImageEditor.Mode.Autofix.prototype.createTools = function(toolbar) { |
+ var self = this; |
+ toolbar.addButton('Apply', function() { |
+ self.update({histogram: self.histogram_.getData()}); |
+ }); |
+}; |
+ |
+/** |
+ * Blur filter. |
+ * @constructor |
+ */ |
+ImageEditor.Mode.Blur = function() { |
+ ImageEditor.Mode.Adjust.call(this, 'Blur'); |
+}; |
+ |
+ImageEditor.Mode.Blur.prototype = |
+ {__proto__: ImageEditor.Mode.Adjust.prototype}; |
+ |
+ImageEditor.Mode.register(ImageEditor.Mode.Blur); |
+ |
+ImageEditor.Mode.Blur.prototype.createTools = function(toolbar) { |
+ toolbar.addRange('strength', 0, 0, 1, 100); |
+ toolbar.addRange('radius', 1, 1, 3); |
+}; |
+ |
+/** |
+ * Sharpen filter. |
+ * @constructor |
+ */ |
+ImageEditor.Mode.Sharpen = function() { |
+ ImageEditor.Mode.Adjust.call(this, 'Sharpen'); |
+}; |
+ |
+ImageEditor.Mode.Sharpen.prototype = |
+ {__proto__: ImageEditor.Mode.Adjust.prototype}; |
+ |
+ImageEditor.Mode.register(ImageEditor.Mode.Sharpen); |
+ |
+ImageEditor.Mode.Sharpen.prototype.createTools = function(toolbar) { |
+ toolbar.addRange('strength', 0, 0, 1, 100); |
+ toolbar.addRange('radius', 1, 1, 3); |
+}; |