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