Chromium Code Reviews| Index: chrome/browser/resources/file_manager/js/image_editor/filter.js |
| diff --git a/chrome/browser/resources/file_manager/js/image_editor/filter.js b/chrome/browser/resources/file_manager/js/image_editor/filter.js |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..da27f54fb58c7855e8ee8ccf94e4f80ff80df829 |
| --- /dev/null |
| +++ b/chrome/browser/resources/file_manager/js/image_editor/filter.js |
| @@ -0,0 +1,357 @@ |
| +// Copyright (c) 2011 The Chromium Authors. All rights reserved. |
| +// Use of this source code is governed by a BSD-style license that can be |
| +// found in the LICENSE file. |
| + |
| +/** |
| + * A namespace class for image filter utilities. |
| + */ |
| +function Filter() {} |
| + |
| +/** |
| + * Create a filter from name and options. |
| + * |
| + * @param {String} name Maps to a Filter method name. |
|
SeRya
2011/08/10 13:00:20
Shouldn't it be "@param {string}" ?
Vladislav Kaznacheev
2011/08/10 14:03:25
Done.
|
| + * @param {Object} options A map of filter-specific options. |
| + * @return {Function} A closure to be called with 4 parameters: |
|
SeRya
2011/08/10 13:00:20
@return {function(ImageData,ImageData,umber,number
Vladislav Kaznacheev
2011/08/10 14:03:25
Done.
|
| + * filter(dstImageData, srcImageData, offsetX, offsetY) |
| + */ |
| +Filter.create = function(name, options) { |
| + var filter = Filter[name](options); |
| + return function() { |
| + var time = Date.now(); |
| + filter.apply(null, arguments); |
| + var dst = arguments[0]; |
| + var mPixPerSec = dst.width * dst.height / 1000 / (Date.now() - time); |
| + ImageUtil.trace.report(name, Math.round(mPixPerSec * 10) / 10 + 'Mps'); |
| + } |
| +}; |
| + |
| +/** |
| + * Apply a filter to a image by splitting it into strips. |
| + * |
| + * To be used with large images to avoid freezing up the UI. |
| + * |
| + * @param {CanvasRenderingContext2D} context |
| + * @param {Function} filterFunc |
| + * @param {Function} progressCallback Called with 2 parameters (current, total). |
| + * @param {Number} maxPixelsPerStrip |
|
SeRya
2011/08/10 13:00:20
{number}
Vladislav Kaznacheev
2011/08/10 14:03:25
Done.
|
| + */ |
| +Filter.applyByStrips = function( |
| + context, filterFunc, progressCallback, maxPixelsPerStrip) { |
| + var source = context.getImageData( |
| + 0, 0, context.canvas.width, context.canvas.height); |
| + |
| + var stripCount = Math.ceil (source.width * source.height / |
| + (maxPixelsPerStrip || 1000000)); // 1 Mpix is a reasonable default. |
| + |
| + var strip = context.getImageData(0, 0, |
| + source.width, Math.ceil (source.height / stripCount)); |
| + |
| + var offset = 0; |
| + |
| + function filterStrip() { |
| + // If the strip overlaps the bottom of the source image we cannot shrink it |
| + // and we cannot fill it partially (since canvas.putImageData always draws |
| + // the entire buffer). |
| + // Instead we move the strip up several lines (converting those lines |
| + // twice is a small price to pay). |
| + if (offset > source.height - strip.height) { |
| + offset = source.height - strip.height; |
| + } |
| + |
| + filterFunc(strip, source, 0, offset); |
| + context.putImageData(strip, 0, offset); |
| + |
| + offset += strip.height; |
| + progressCallback(offset, source.height); |
| + |
| + if (offset < source.height) { |
| + setTimeout(filterStrip, 0); |
| + } |
| + } |
| + |
| + filterStrip(); |
| +}; |
| + |
| +/** |
| + * Return a color histogram for an image. |
| + * |
| + * @param {ImageData} imageData |
| + */ |
| +Filter.getHistogram = function(imageData) { |
| + var r = []; |
| + var g = []; |
| + var b = []; |
| + |
| + for (var i = 0; i != 256; i++) { |
| + r.push(0); |
| + g.push(0); |
| + b.push(0); |
| + } |
| + |
| + var data = imageData.data; |
| + var maxIndex = 4 * imageData.width * imageData.height; |
| + for (var index = 0; index != maxIndex;) { |
| + r[data[index++]]++; |
| + g[data[index++]]++; |
| + b[data[index++]]++; |
| + index++; |
| + } |
| + |
| + return { r: r, g: g, b: b }; |
| +}; |
| + |
| +/** |
| + * Compute the function for every integer value from 0 up to maxArg. |
| + * |
| + * Rounds and clips the results to fit the [0..255] range. |
| + * Useful to speed up pixel manipulations. |
| + * |
| + * @param {Number} maxArg Maximum argument value (inclusive). |
| + * @param {Function} func Function returning a number. |
| + * @return {Array<Number>} Computed results |
| + */ |
| +Filter.precompute = function(maxArg, func) { |
| + var results = []; |
| + for (var arg = 0; arg <= maxArg; arg ++) { |
| + results.push(Math.max(0, Math.min(0xFF, Math.round(func(arg))))); |
| + } |
| + return results; |
| +}; |
| + |
| +/** |
| + * Convert pixels by applying conversion tables to each channel individually. |
| + * |
| + * @param {Array<Number>} rMap Red channel conversion table. |
| + * @param {Array<Number>} gMap Green channel conversion table. |
| + * @param {Array<Number>} bMap Blue channel conversion table. |
| + * @param {ImageData} dst Destination image data. Can be smaller than the |
| + * source, must completely fit inside the source. |
| + * @param {ImageData} src Source image data. |
| + * @param {Number} offsetX Horizontal offset of dst relative to src. |
| + * @param {Number} offsetY Vertical offset of dst relative to src. |
| + */ |
| +Filter.mapPixels = function(rMap, gMap, bMap, dst, src, offsetX, offsetY) { |
| + var dstData = dst.data; |
| + var dstWidth = dst.width; |
| + var dstHeight = dst.height; |
| + |
| + var srcData = src.data; |
| + var srcWidth = src.width; |
| + var srcHeight = src.height; |
| + |
| + if (offsetX < 0 || offsetX + dstWidth > srcWidth || |
| + offsetY < 0 || offsetY + dstHeight > srcHeight) |
| + throw new Error('Invalid offset'); |
| + |
| + var dstIndex = 0; |
| + for (var y = 0; y != dstHeight; y++) { |
| + var srcIndex = (offsetX + (offsetY + y)* srcWidth)* 4; |
| + for (var x = 0; x != dstWidth; x++ ) { |
| + dstData[dstIndex++] = rMap[srcData[srcIndex++]]; |
| + dstData[dstIndex++] = gMap[srcData[srcIndex++]]; |
| + dstData[dstIndex++] = bMap[srcData[srcIndex++]]; |
| + dstIndex++; |
| + srcIndex++; |
| + } |
| + } |
| +}; |
| + |
| +Filter.FIXED_POINT_SHIFT = 16; |
| + |
| +Filter.floatToFixedPoint = function(x) { |
| + return Math.round(x * (1<< Filter.FIXED_POINT_SHIFT)); |
| +}; |
| + |
| +/** |
| + * Perform an image convolution with a symmetrical 5x5 matrix: |
| + * |
| + * 0 0 w3 0 0 |
| + * 0 w2 w1 w2 0 |
| + * w3 w1 w0 w1 w3 |
| + * 0 w2 w1 w2 0 |
| + * 0 0 w3 0 0 |
| + * |
| + * For performance reasons the weights are in fixed point format |
| + * (left shifted by Filter.FIXED_POINT_SHIFT). |
| + * |
| + * @param {Number} w0 See the picture above |
| + * @param {Number} w1 See the picture above |
| + * @param {Number} w2 See the picture above |
| + * @param {Number} w3 See the picture above |
| + * @param {ImageData} dst Destination image data. Can be smaller than the |
| + * source, must completely fit inside the source. |
| + * @param {ImageData} src Source image data. |
| + * @param {Number} offsetX Horizontal offset of dst relative to src. |
| + * @param {Number} offsetY Vertical offset of dst relative to src. |
| + */ |
| +Filter.convolve5x5 = function(w0, w1, w2, w3, dst, src, offsetX, offsetY) { |
| + var dstData = dst.data; |
| + var dstWidth = dst.width; |
| + var dstHeight = dst.height; |
| + var dstStride = dstWidth * 4; |
| + |
| + var srcData = src.data; |
| + var srcWidth = src.width; |
| + var srcHeight = src.height; |
| + var srcStride = srcWidth * 4; |
| + var srcStride2 = srcStride * 2; |
| + |
| + if (offsetX < 0 || offsetX + dstWidth > srcWidth || |
| + offsetY < 0 || offsetY + dstHeight > srcHeight) |
| + throw new Error('Invalid offset'); |
| + |
| + w0 = w0 || 0; |
| + w1 = w1 || 0; |
| + w2 = w2 || 0; |
| + w3 = w3 || 0; |
| + |
| + var margin = 2; |
| + |
| + var startX = Math.max(0, margin - offsetX); |
| + var endX = Math.min(dstWidth, srcWidth - margin - offsetX); |
| + |
| + var startY = Math.max(0, margin - offsetY); |
| + var endY = Math.min(dstHeight, srcHeight - margin - offsetY); |
| + |
| + for (var y = startY; y != endY; y++) { |
| + var dstIndex = y * dstStride + startX * 4; |
| + var srcIndex = (y + offsetY) * srcStride + (startX + offsetX) * 4; |
| + |
| + for (var x = startX; x != endX; x++ ) { |
| + for (var c = 0; c != 3; c++) { |
| + var sum = w0 * srcData[srcIndex] + |
| + w1 * (srcData[srcIndex - 4] + |
| + srcData[srcIndex + 4] + |
| + srcData[srcIndex - srcStride] + |
| + srcData[srcIndex + srcStride]) + |
| + w2 * (srcData[srcIndex - srcStride - 4] + |
| + srcData[srcIndex + srcStride - 4] + |
| + srcData[srcIndex - srcStride + 4] + |
| + srcData[srcIndex + srcStride + 4]) + |
| + w3 * (srcData[srcIndex - 8] + |
| + srcData[srcIndex + 8] + |
| + srcData[srcIndex - srcStride2] + |
| + srcData[srcIndex + srcStride2]); |
| + if (sum < 0) |
| + dstData[dstIndex++] = 0; |
| + else if (sum > (0xFF << Filter.FIXED_POINT_SHIFT)) |
| + dstData[dstIndex++] = 0xFF; |
| + else |
| + dstData[dstIndex++] = sum >> Filter.FIXED_POINT_SHIFT; |
| + srcIndex++; |
| + } |
| + srcIndex++; |
| + dstIndex++; |
| + } |
| + } |
| +}; |
| + |
| +/** |
| + * Return a convolution filter function bound to specific weights. |
| + * |
| + * @param {Array<Number>} weights Weights for the convolution matrix. |
| + * @return {Function} |
| + */ |
| +Filter.convolution5x5 = function(weights) { |
| + var total = 0; |
| + for (var i = 0; i != weights.length; i++) |
| + total += weights[i] * (i ? 4 : 1); |
| + |
| + for (i = 0; i != weights.length; i++) { |
| + weights[i] = Filter.floatToFixedPoint(weights[i] / total); |
| + } |
| + |
| + return Filter.convolve5x5.bind( |
| + null, weights[0], weights[1], weights[2], weights[3]); |
| +}; |
| + |
| +/** |
| + * Return a blur filter. |
| + * @param {Object} options |
| + * @return {Function} |
| + */ |
| +Filter.blur = function(options) { |
| + if (options.radius == 1) |
| + return Filter.convolution5x5( |
| + [1, options.strength]); |
| + else if (options.radius == 2) |
| + return Filter.convolution5x5( |
| + [1, options.strength, options.strength]); |
| + else |
| + return Filter.convolution5x5( |
| + [1, options.strength, options.strength, options.strength]); |
| +}; |
| + |
| +/** |
| + * Return a sharpen filter. |
| + * @param {Object} options |
| + * @return {Function} |
| + */ |
| +Filter.sharpen = function(options) { |
| + if (options.radius == 1) |
| + return Filter.convolution5x5( |
| + [5, -options.strength]); |
| + else if (options.radius == 2) |
| + return Filter.convolution5x5( |
| + [10, -options.strength, -options.strength]); |
| + else |
| + return Filter.convolution5x5( |
| + [15, -options.strength, -options.strength, -options.strength]); |
| +}; |
| + |
| +/** |
| + * Return an exposure filter. |
| + * @param {Object} options |
| + * @return {Function} |
| + */ |
| +Filter.exposure = function(options) { |
| + var pixelMap = Filter.precompute( |
| + 255, |
| + function(value) { |
| + if (options.brightness > 0) { |
| + value *= (1 + options.brightness); |
| + } else { |
| + value += (0xFF - value) * options.brightness; |
| + } |
| + return 0x80 + |
| + (value - 0x80) * Math.tan((options.contrast + 1) * Math.PI / 4); |
| + }); |
| + |
| + return Filter.mapPixels.bind(null, pixelMap, pixelMap, pixelMap); |
| +}; |
| + |
| +/** |
| + * Return a color autofix filter. |
| + * @param {Object} options |
| + * @return {Function} |
| + */ |
| +Filter.autofix = function(options) { |
| + return Filter.mapPixels.bind(null, |
| + Filter.autofix.stretchColors(options.histogram.r), |
| + Filter.autofix.stretchColors(options.histogram.g), |
| + Filter.autofix.stretchColors(options.histogram.b)); |
| +}; |
| + |
| +/** |
| + * Return a conversion table that stretches the range of colors used |
| + * in the image to 0..255. |
| + * @param {Array<Number>} channelHistogram |
| + * @return {Array<Number>} |
| + */ |
| +Filter.autofix.stretchColors = function(channelHistogram) { |
| + var first = 0; |
| + while (first <= 255 && channelHistogram[first] == 0) |
| + first++; |
| + |
| + var last = 255; |
| + while (last > first && channelHistogram[last] == 0) |
| + last--; |
| + |
| + return Filter.precompute( |
| + 255, |
| + last == first ? |
| + function(x) { return x } : |
| + function(x) { return (x - first) / (last - first) * 255 } |
| + ); |
| +}; |