OLD | NEW |
1 // Copyright (c) 2011 The Chromium Authors. All rights reserved. | 1 // Copyright (c) 2011 The Chromium Authors. All rights reserved. |
2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
4 | 4 |
5 /** | 5 /** |
6 * The base class for simple filters that only modify the image content | 6 * The base class for simple filters that only modify the image content |
7 * but do not modify the image dimensions. | 7 * but do not modify the image dimensions. |
8 */ | 8 * @constructor |
9 ImageEditor.Mode.Adjust = function(displayName, filterFunc) { | 9 */ |
| 10 ImageEditor.Mode.Adjust = function(displayName) { |
10 ImageEditor.Mode.call(this, displayName); | 11 ImageEditor.Mode.call(this, displayName); |
11 this.filterFunc_ = filterFunc; | 12 this.viewportGeneration_ = 0; |
12 } | 13 }; |
13 | 14 |
14 ImageEditor.Mode.Adjust.prototype = {__proto__: ImageEditor.Mode.prototype}; | 15 ImageEditor.Mode.Adjust.prototype = {__proto__: ImageEditor.Mode.prototype}; |
15 | 16 |
| 17 /* |
| 18 * ImageEditor.Mode methods overridden. |
| 19 */ |
| 20 |
| 21 ImageEditor.Mode.Adjust.prototype.commit = function() { |
| 22 if (!this.filter_) return; // Did not do anything yet. |
| 23 |
| 24 // Applying the filter to the entire image takes some time, so we do |
| 25 // it in small increments, providing visual feedback. |
| 26 // TODO: provide modal progress indicator. |
| 27 |
| 28 // First hide the preview and show the original image. |
| 29 this.repaint(); |
| 30 |
| 31 var self = this; |
| 32 |
| 33 function repaintStrip(fromRow, toRow) { |
| 34 var imageStrip = new Rect(self.getViewport().getImageBounds()); |
| 35 imageStrip.top = fromRow; |
| 36 imageStrip.height = toRow - fromRow; |
| 37 |
| 38 var screenStrip = new Rect(self.getViewport().getImageBoundsOnScreen()); |
| 39 screenStrip.top = Math.round(self.getViewport().imageToScreenY(fromRow)); |
| 40 screenStrip.height = Math.round(self.getViewport().imageToScreenY(toRow)) - |
| 41 screenStrip.top; |
| 42 |
| 43 self.getBuffer().repaintScreenRect(screenStrip, imageStrip); |
| 44 } |
| 45 |
| 46 ImageUtil.trace.resetTimer('filter'); |
| 47 |
| 48 var lastUpdatedRow = 0; |
| 49 |
| 50 filter.applyByStrips( |
| 51 this.getContent().getCanvas().getContext('2d'), |
| 52 this.filter_, |
| 53 function (updatedRow, rowCount) { |
| 54 repaintStrip(lastUpdatedRow, updatedRow); |
| 55 lastUpdatedRow = updatedRow; |
| 56 if (updatedRow == rowCount) { |
| 57 ImageUtil.trace.reportTimer('filter'); |
| 58 self.getContent().invalidateCaches(); |
| 59 self.repaint(); |
| 60 } |
| 61 }); |
| 62 }; |
| 63 |
16 ImageEditor.Mode.Adjust.prototype.rollback = function() { | 64 ImageEditor.Mode.Adjust.prototype.rollback = function() { |
17 if (!this.backup_) return; // Did not do anything yet. | 65 this.filter_ = null; |
18 this.getContent().drawImageData(this.backup_, 0, 0); | 66 this.previewImageData_ = null; |
19 this.backup_ = null; | 67 }; |
| 68 |
| 69 ImageEditor.Mode.Adjust.prototype.update = function(options) { |
| 70 // We assume filter names are used in the UI directly. |
| 71 // This will have to change with i18n. |
| 72 this.filter_ = this.createFilter(options); |
| 73 this.previewValid_ = false; |
20 this.repaint(); | 74 this.repaint(); |
21 }; | 75 }; |
22 | 76 |
23 ImageEditor.Mode.Adjust.prototype.update = function(options) { | 77 /** |
24 if (!this.backup_) { | 78 * Clip and scale the source image data for the preview. |
25 this.backup_ = this.getContent().copyImageData(); | 79 * Use the cached copy if the viewport has not changed. |
26 this.scratch_ = this.getContent().copyImageData(); | 80 */ |
27 } | 81 ImageEditor.Mode.Adjust.prototype.updatePreviewImage = function() { |
28 | 82 if (!this.previewImageData_ || |
29 ImageUtil.trace.resetTimer('filter'); | 83 this.viewportGeneration_ != this.getViewport().getCacheGeneration()) { |
30 this.filterFunc_(this.scratch_, this.backup_, options); | 84 this.viewportGeneration_ = this.getViewport().getCacheGeneration(); |
31 ImageUtil.trace.reportTimer('filter'); | 85 |
32 this.getContent().drawImageData(this.scratch_, 0, 0); | 86 var imageRect = this.getPreviewRect(this.getViewport().getImageClipped()); |
33 this.repaint(); | 87 var screenRect = this.getPreviewRect(this.getViewport().getScreenClipped()); |
34 }; | 88 |
35 | 89 // Copy the visible part of the image at the current screen scale. |
36 /** | 90 var canvas = this.getContent().createBlankCanvas( |
37 * A simple filter that multiplies every component of a pixel by some number. | 91 screenRect.width, screenRect.height); |
38 */ | 92 var context = canvas.getContext('2d'); |
39 ImageEditor.Mode.Brightness = function() { | 93 Rect.drawImage(context, this.getContent().getCanvas(), null, imageRect); |
40 ImageEditor.Mode.Adjust.call( | 94 this.originalImageData = |
41 this, 'Brightness', ImageEditor.Mode.Brightness.filter); | 95 context.getImageData(0, 0, screenRect.width, screenRect.height); |
42 } | 96 this.previewImageData_ = |
43 | 97 context.getImageData(0, 0, screenRect.width, screenRect.height); |
44 ImageEditor.Mode.Brightness.prototype = | 98 this.previewValid_ = false; |
| 99 } |
| 100 |
| 101 if (this.filter_ && !this.previewValid_) { |
| 102 ImageUtil.trace.resetTimer('preview'); |
| 103 this.filter_(this.previewImageData_, this.originalImageData, 0, 0); |
| 104 ImageUtil.trace.reportTimer('preview'); |
| 105 this.previewValid_ = true; |
| 106 } |
| 107 }; |
| 108 |
| 109 ImageEditor.Mode.Adjust.prototype.draw = function(context) { |
| 110 this.updatePreviewImage(); |
| 111 |
| 112 var screenClipped = this.getViewport().getScreenClipped(); |
| 113 |
| 114 var previewRect = this.getPreviewRect(screenClipped); |
| 115 context.putImageData( |
| 116 this.previewImageData_, previewRect.left, previewRect.top); |
| 117 |
| 118 if (previewRect.width < screenClipped.width && |
| 119 previewRect.height < screenClipped.height) { |
| 120 // Some part of the original image is not covered by the preview, |
| 121 // shade it out. |
| 122 context.globalAlpha = 0.75; |
| 123 context.fillStyle = '#000000'; |
| 124 context.strokeStyle = '#000000'; |
| 125 Rect.fillBetween( |
| 126 context, previewRect, this.getViewport().getScreenBounds()); |
| 127 Rect.outline(context, previewRect); |
| 128 } |
| 129 }; |
| 130 |
| 131 /* |
| 132 * Own methods |
| 133 */ |
| 134 |
| 135 ImageEditor.Mode.Adjust.prototype.createFilter = function(options) { |
| 136 return filter.create(this.displayName.toLowerCase(), options); |
| 137 }; |
| 138 |
| 139 ImageEditor.Mode.Adjust.prototype.getPreviewRect = function(rect) { |
| 140 if (this.getViewport().getScale() >= 1) { |
| 141 return rect; |
| 142 } else { |
| 143 var bounds = this.getViewport().getImageBounds(); |
| 144 var screen = this.getViewport().getScreenClipped(); |
| 145 |
| 146 screen = screen.inflate(-screen.width / 8, -screen.height / 8); |
| 147 |
| 148 return rect.inflate(-rect.width / 2, -rect.height / 2). |
| 149 inflate(Math.min(screen.width, bounds.width) / 2, |
| 150 Math.min(screen.height, bounds.height) / 2); |
| 151 } |
| 152 }; |
| 153 |
| 154 /** |
| 155 * A base class for color filters that are scale independent (i.e. can |
| 156 * be applied to a scaled image with basicaly the same effect). |
| 157 * Displays a histogram. |
| 158 * @constructor |
| 159 */ |
| 160 ImageEditor.Mode.ColorFilter = function() { |
| 161 ImageEditor.Mode.Adjust.apply(this, arguments); |
| 162 }; |
| 163 |
| 164 ImageEditor.Mode.ColorFilter.prototype = |
45 {__proto__: ImageEditor.Mode.Adjust.prototype}; | 165 {__proto__: ImageEditor.Mode.Adjust.prototype}; |
46 | 166 |
47 ImageEditor.Mode.register(ImageEditor.Mode.Brightness); | 167 ImageEditor.Mode.ColorFilter.prototype.setUp = function() { |
48 | 168 ImageEditor.Mode.Adjust.prototype.setUp.apply(this, arguments); |
49 ImageEditor.Mode.Brightness.UI_RANGE = 100; | 169 this.histogram_ = |
50 | 170 new ImageEditor.Mode.Histogram(this.getViewport(), this.getContent()); |
51 ImageEditor.Mode.Brightness.prototype.createTools = function(toolbar) { | 171 }; |
52 toolbar.addRange( | 172 |
53 'brightness', | 173 ImageEditor.Mode.ColorFilter.prototype.draw = function(context) { |
54 -ImageEditor.Mode.Brightness.UI_RANGE, | 174 ImageEditor.Mode.Adjust.prototype.draw.apply(this, arguments); |
55 0, | 175 this.histogram_.draw(context); |
56 ImageEditor.Mode.Brightness.UI_RANGE); | 176 }; |
57 }; | 177 |
58 | 178 ImageEditor.Mode.ColorFilter.prototype.getPreviewRect = function(rect) { |
59 ImageEditor.Mode.Brightness.filter = function(dst, src, options) { | 179 return rect; |
60 // Translate from -100..100 range to 1/5..5 | 180 }; |
61 var factor = | 181 |
62 Math.pow(5, options.brightness / ImageEditor.Mode.Brightness.UI_RANGE); | 182 ImageEditor.Mode.ColorFilter.prototype.createFilter = function(options) { |
63 | 183 var filterFunc = |
64 var dstData = dst.data; | 184 ImageEditor.Mode.Adjust.prototype.createFilter.apply(this, arguments); |
65 var srcData = src.data; | 185 this.histogram_.update(filterFunc); |
66 var width = src.width; | 186 return filterFunc; |
67 var height = src.height; | 187 }; |
68 | 188 |
69 function scale(value) { | 189 ImageEditor.Mode.ColorFilter.prototype.rollback = function() { |
70 return value * factor; | 190 ImageEditor.Mode.Adjust.prototype.rollback.apply(this, arguments); |
71 } | 191 this.histogram_.update(null); |
72 | 192 }; |
73 var values = ImageUtil.precomputeByteFunction(scale, 255); | 193 |
74 | 194 /** |
75 var index = 0; | 195 * A histogram container. |
76 for (var y = 0; y != height; y++) { | 196 * @constructor |
77 for (var x = 0; x != width; x++ ) { | 197 */ |
78 dstData[index] = values[srcData[index]]; index++; | 198 ImageEditor.Mode.Histogram = function(viewport, content) { |
79 dstData[index] = values[srcData[index]]; index++; | 199 this.viewport_ = viewport; |
80 dstData[index] = values[srcData[index]]; index++; | 200 |
81 dstData[index] = 0xFF; index++; | 201 var canvas = content.getCanvas(); |
| 202 var downScale = Math.max(1, Math.sqrt(canvas.width * canvas.height / 10000)); |
| 203 var thumbnail = content.copyCanvas(canvas.width / downScale, |
| 204 canvas.height / downScale); |
| 205 var context = thumbnail.getContext('2d'); |
| 206 |
| 207 this.originalImageData_ = |
| 208 context.getImageData(0, 0, thumbnail.width, thumbnail.height); |
| 209 this.filteredImageData_ = |
| 210 context.getImageData(0, 0, thumbnail.width, thumbnail.height); |
| 211 |
| 212 this.update(); |
| 213 }; |
| 214 |
| 215 ImageEditor.Mode.Histogram.prototype.getData = function() { return this.data_ }; |
| 216 |
| 217 ImageEditor.Mode.Histogram.BUCKET_WIDTH = 8; |
| 218 ImageEditor.Mode.Histogram.BAR_WIDTH = 2; |
| 219 ImageEditor.Mode.Histogram.RIGHT = 5; |
| 220 ImageEditor.Mode.Histogram.TOP = 5; |
| 221 |
| 222 ImageEditor.Mode.Histogram.prototype.update = function(filterFunc) { |
| 223 if (filterFunc) { |
| 224 filterFunc(this.filteredImageData_, this.originalImageData_, 0, 0); |
| 225 this.data_ = filter.getHistogram(this.filteredImageData_); |
| 226 } else { |
| 227 this.data_ = filter.getHistogram(this.originalImageData_); |
| 228 } |
| 229 }; |
| 230 |
| 231 ImageEditor.Mode.Histogram.prototype.draw = function(context) { |
| 232 var screen = this.viewport_.getScreenBounds(); |
| 233 |
| 234 var barCount = 2 + 3 * (256 / ImageEditor.Mode.Histogram.BUCKET_WIDTH); |
| 235 var width = ImageEditor.Mode.Histogram.BAR_WIDTH * barCount; |
| 236 var height = Math.round(width / 2); |
| 237 var rect = new Rect( |
| 238 screen.left + screen.width - ImageEditor.Mode.Histogram.RIGHT - width, |
| 239 ImageEditor.Mode.Histogram.TOP, |
| 240 width, |
| 241 height); |
| 242 |
| 243 context.globalAlpha = 1; |
| 244 context.fillStyle = '#E0E0E0'; |
| 245 context.strokeStyle = '#000000'; |
| 246 context.lineCap = 'square'; |
| 247 Rect.fill(context, rect); |
| 248 Rect.outline(context, rect); |
| 249 |
| 250 function drawChannel(channel, style, offsetX, offsetY) { |
| 251 context.strokeStyle = style; |
| 252 context.beginPath(); |
| 253 for (var i = 0; i != 256; i += ImageEditor.Mode.Histogram.BUCKET_WIDTH) { |
| 254 var barHeight = channel[i]; |
| 255 for (var b = 1; b < ImageEditor.Mode.Histogram.BUCKET_WIDTH; b++) |
| 256 barHeight = Math.max(barHeight, channel[i + b]); |
| 257 barHeight = Math.min(barHeight, height); |
| 258 for (var j = 0; j != ImageEditor.Mode.Histogram.BAR_WIDTH; j++) { |
| 259 context.moveTo(offsetX, offsetY); |
| 260 context.lineTo(offsetX, offsetY - barHeight); |
| 261 offsetX++; |
| 262 } |
| 263 offsetX += 2 * ImageEditor.Mode.Histogram.BAR_WIDTH; |
82 } | 264 } |
83 } | 265 context.closePath(); |
84 }; | 266 context.stroke(); |
85 | 267 } |
| 268 |
| 269 var offsetX = rect.left + 0.5 + ImageEditor.Mode.Histogram.BAR_WIDTH; |
| 270 var offsetY = rect.top + rect.height; |
| 271 |
| 272 drawChannel(this.data_.r, '#F00000', offsetX, offsetY); |
| 273 offsetX += ImageEditor.Mode.Histogram.BAR_WIDTH; |
| 274 drawChannel(this.data_.g, '#00F000', offsetX, offsetY); |
| 275 offsetX += ImageEditor.Mode.Histogram.BAR_WIDTH; |
| 276 drawChannel(this.data_.b, '#0000F0', offsetX, offsetY); |
| 277 }; |
| 278 |
| 279 /** |
| 280 * Exposure/contrast filter. |
| 281 * @constructor |
| 282 */ |
| 283 ImageEditor.Mode.Exposure = function() { |
| 284 ImageEditor.Mode.ColorFilter.call(this, 'Exposure'); |
| 285 }; |
| 286 |
| 287 ImageEditor.Mode.Exposure.prototype = |
| 288 {__proto__: ImageEditor.Mode.ColorFilter.prototype}; |
| 289 |
| 290 ImageEditor.Mode.register(ImageEditor.Mode.Exposure); |
| 291 |
| 292 ImageEditor.Mode.Exposure.prototype.createTools = function(toolbar) { |
| 293 toolbar.addRange('brightness', -1, 0, 1, 100); |
| 294 toolbar.addRange('contrast', -1, 0, 1, 100); |
| 295 }; |
| 296 |
| 297 /** |
| 298 * Autofix. |
| 299 * @constructor |
| 300 */ |
| 301 ImageEditor.Mode.Autofix = function() { |
| 302 ImageEditor.Mode.ColorFilter.call(this, 'Autofix'); |
| 303 }; |
| 304 |
| 305 ImageEditor.Mode.Autofix.prototype = |
| 306 {__proto__: ImageEditor.Mode.ColorFilter.prototype}; |
| 307 |
| 308 ImageEditor.Mode.register(ImageEditor.Mode.Autofix); |
| 309 |
| 310 ImageEditor.Mode.Autofix.prototype.createTools = function(toolbar) { |
| 311 var self = this; |
| 312 toolbar.addButton('Apply', function() { |
| 313 self.update({histogram: self.histogram_.getData()}); |
| 314 }); |
| 315 }; |
| 316 |
| 317 /** |
| 318 * Blur filter. |
| 319 * @constructor |
| 320 */ |
| 321 ImageEditor.Mode.Blur = function() { |
| 322 ImageEditor.Mode.Adjust.call(this, 'Blur'); |
| 323 }; |
| 324 |
| 325 ImageEditor.Mode.Blur.prototype = |
| 326 {__proto__: ImageEditor.Mode.Adjust.prototype}; |
| 327 |
| 328 ImageEditor.Mode.register(ImageEditor.Mode.Blur); |
| 329 |
| 330 ImageEditor.Mode.Blur.prototype.createTools = function(toolbar) { |
| 331 toolbar.addRange('strength', 0, 0, 1, 100); |
| 332 toolbar.addRange('radius', 1, 1, 3); |
| 333 }; |
| 334 |
| 335 /** |
| 336 * Sharpen filter. |
| 337 * @constructor |
| 338 */ |
| 339 ImageEditor.Mode.Sharpen = function() { |
| 340 ImageEditor.Mode.Adjust.call(this, 'Sharpen'); |
| 341 }; |
| 342 |
| 343 ImageEditor.Mode.Sharpen.prototype = |
| 344 {__proto__: ImageEditor.Mode.Adjust.prototype}; |
| 345 |
| 346 ImageEditor.Mode.register(ImageEditor.Mode.Sharpen); |
| 347 |
| 348 ImageEditor.Mode.Sharpen.prototype.createTools = function(toolbar) { |
| 349 toolbar.addRange('strength', 0, 0, 1, 100); |
| 350 toolbar.addRange('radius', 1, 1, 3); |
| 351 }; |
OLD | NEW |