Upstream version 7.35.144.0
[platform/framework/web/crosswalk.git] / src / ui / file_manager / file_manager / foreground / js / image_editor / filter.js
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.
4
5 'use strict';
6
7 /**
8  * A namespace for image filter utilities.
9  */
10 var filter = {};
11
12 /**
13  * Create a filter from name and options.
14  *
15  * @param {string} name Maps to a filter method name.
16  * @param {Object} options A map of filter-specific options.
17  * @return {function(ImageData,ImageData,number,number)} created function.
18  */
19 filter.create = function(name, options) {
20   var filterFunc = filter[name](options);
21   return function() {
22     var time = Date.now();
23     filterFunc.apply(null, arguments);
24     var dst = arguments[0];
25     var mPixPerSec = dst.width * dst.height / 1000 / (Date.now() - time);
26     ImageUtil.trace.report(name, Math.round(mPixPerSec * 10) / 10 + 'Mps');
27   }
28 };
29
30 /**
31  * Apply a filter to a image by splitting it into strips.
32  *
33  * To be used with large images to avoid freezing up the UI.
34  *
35  * @param {HTMLCanvasElement} dstCanvas Destination canvas.
36  * @param {HTMLCanvasElement} srcCanvas Source canvas.
37  * @param {function(ImageData,ImageData,number,number)} filterFunc Filter.
38  * @param {function(number, number)} progressCallback Progress callback.
39  * @param {number} maxPixelsPerStrip Pixel number to process at once.
40  */
41 filter.applyByStrips = function(
42     dstCanvas, srcCanvas, filterFunc, progressCallback, maxPixelsPerStrip) {
43   var dstContext = dstCanvas.getContext('2d');
44   var srcContext = srcCanvas.getContext('2d');
45   var source = srcContext.getImageData(0, 0, srcCanvas.width, srcCanvas.height);
46
47   var stripCount = Math.ceil(srcCanvas.width * srcCanvas.height /
48       (maxPixelsPerStrip || 1000000));  // 1 Mpix is a reasonable default.
49
50   var strip = srcContext.getImageData(0, 0,
51       srcCanvas.width, Math.ceil(srcCanvas.height / stripCount));
52
53   var offset = 0;
54
55   function filterStrip() {
56     // If the strip overlaps the bottom of the source image we cannot shrink it
57     // and we cannot fill it partially (since canvas.putImageData always draws
58     // the entire buffer).
59     // Instead we move the strip up several lines (converting those lines
60     // twice is a small price to pay).
61     if (offset > source.height - strip.height) {
62       offset = source.height - strip.height;
63     }
64
65     filterFunc(strip, source, 0, offset);
66     dstContext.putImageData(strip, 0, offset);
67
68     offset += strip.height;
69
70     if (offset < source.height) {
71       setTimeout(filterStrip, 0);
72     } else {
73       ImageUtil.trace.reportTimer('filter-commit');
74     }
75
76     progressCallback(offset, source.height);
77   }
78
79   ImageUtil.trace.resetTimer('filter-commit');
80   filterStrip();
81 };
82
83 /**
84  * Return a color histogram for an image.
85  *
86  * @param {HTMLCanvasElement|ImageData} source Image data to analyze.
87  * @return {{r: Array.<number>, g: Array.<number>, b: Array.<number>}}
88  *     histogram.
89  */
90 filter.getHistogram = function(source) {
91   var imageData;
92   if (source.constructor.name == 'HTMLCanvasElement') {
93     imageData = source.getContext('2d').
94         getImageData(0, 0, source.width, source.height);
95   } else {
96     imageData = source;
97   }
98
99   var r = [];
100   var g = [];
101   var b = [];
102
103   for (var i = 0; i != 256; i++) {
104     r.push(0);
105     g.push(0);
106     b.push(0);
107   }
108
109   var data = imageData.data;
110   var maxIndex = 4 * imageData.width * imageData.height;
111   for (var index = 0; index != maxIndex;) {
112     r[data[index++]]++;
113     g[data[index++]]++;
114     b[data[index++]]++;
115     index++;
116   }
117
118   return { r: r, g: g, b: b };
119 };
120
121 /**
122  * Compute the function for every integer value from 0 up to maxArg.
123  *
124  * Rounds and clips the results to fit the [0..255] range.
125  * Useful to speed up pixel manipulations.
126  *
127  * @param {number} maxArg Maximum argument value (inclusive).
128  * @param {function(number): number} func Function to precompute.
129  * @return {Uint8Array} Computed results.
130  */
131 filter.precompute = function(maxArg, func) {
132   var results = new Uint8Array(maxArg + 1);
133   for (var arg = 0; arg <= maxArg; arg++) {
134     results[arg] = Math.max(0, Math.min(0xFF, Math.round(func(arg))));
135   }
136   return results;
137 };
138
139 /**
140  * Convert pixels by applying conversion tables to each channel individually.
141  *
142  * @param {Array.<number>} rMap Red channel conversion table.
143  * @param {Array.<number>} gMap Green channel conversion table.
144  * @param {Array.<number>} bMap Blue channel conversion table.
145  * @param {ImageData} dst Destination image data. Can be smaller than the
146  *                        source, must completely fit inside the source.
147  * @param {ImageData} src Source image data.
148  * @param {number} offsetX Horizontal offset of dst relative to src.
149  * @param {number} offsetY Vertical offset of dst relative to src.
150  */
151 filter.mapPixels = function(rMap, gMap, bMap, dst, src, offsetX, offsetY) {
152   var dstData = dst.data;
153   var dstWidth = dst.width;
154   var dstHeight = dst.height;
155
156   var srcData = src.data;
157   var srcWidth = src.width;
158   var srcHeight = src.height;
159
160   if (offsetX < 0 || offsetX + dstWidth > srcWidth ||
161       offsetY < 0 || offsetY + dstHeight > srcHeight)
162       throw new Error('Invalid offset');
163
164   var dstIndex = 0;
165   for (var y = 0; y != dstHeight; y++) {
166     var srcIndex = (offsetX + (offsetY + y) * srcWidth) * 4;
167     for (var x = 0; x != dstWidth; x++) {
168       dstData[dstIndex++] = rMap[srcData[srcIndex++]];
169       dstData[dstIndex++] = gMap[srcData[srcIndex++]];
170       dstData[dstIndex++] = bMap[srcData[srcIndex++]];
171       dstIndex++;
172       srcIndex++;
173     }
174   }
175 };
176
177 /**
178  * Number of digits after period(in binary form) to preserve.
179  * @type {number}
180  */
181 filter.FIXED_POINT_SHIFT = 16;
182
183 /**
184  * Maximum value that can be represented in fixed point without overflow.
185  * @type {number}
186  */
187 filter.MAX_FLOAT_VALUE = 0x7FFFFFFF >> filter.FIXED_POINT_SHIFT;
188
189 /**
190  * Converts floating point to fixed.
191  * @param {number} x Number to convert.
192  * @return {number} Converted number.
193  */
194 filter.floatToFixedPoint = function(x) {
195   // Math.round on negative arguments causes V8 to deoptimize the calling
196   // function, so we are using >> 0 instead.
197   return (x * (1 << filter.FIXED_POINT_SHIFT)) >> 0;
198 };
199
200 /**
201  * Perform an image convolution with a symmetrical 5x5 matrix:
202  *
203  *  0  0 w3  0  0
204  *  0 w2 w1 w2  0
205  * w3 w1 w0 w1 w3
206  *  0 w2 w1 w2  0
207  *  0  0 w3  0  0
208  *
209  * @param {Array.<number>} weights See the picture above.
210  * @param {ImageData} dst Destination image data. Can be smaller than the
211  *                        source, must completely fit inside the source.
212  * @param {ImageData} src Source image data.
213  * @param {number} offsetX Horizontal offset of dst relative to src.
214  * @param {number} offsetY Vertical offset of dst relative to src.
215  */
216 filter.convolve5x5 = function(weights, dst, src, offsetX, offsetY) {
217   var w0 = filter.floatToFixedPoint(weights[0]);
218   var w1 = filter.floatToFixedPoint(weights[1]);
219   var w2 = filter.floatToFixedPoint(weights[2]);
220   var w3 = filter.floatToFixedPoint(weights[3]);
221
222   var dstData = dst.data;
223   var dstWidth = dst.width;
224   var dstHeight = dst.height;
225   var dstStride = dstWidth * 4;
226
227   var srcData = src.data;
228   var srcWidth = src.width;
229   var srcHeight = src.height;
230   var srcStride = srcWidth * 4;
231   var srcStride2 = srcStride * 2;
232
233   if (offsetX < 0 || offsetX + dstWidth > srcWidth ||
234       offsetY < 0 || offsetY + dstHeight > srcHeight)
235     throw new Error('Invalid offset');
236
237   // Javascript is not very good at inlining constants.
238   // We inline manually and assert that the constant is equal to the variable.
239   if (filter.FIXED_POINT_SHIFT != 16)
240     throw new Error('Wrong fixed point shift');
241
242   var margin = 2;
243
244   var startX = Math.max(0, margin - offsetX);
245   var endX = Math.min(dstWidth, srcWidth - margin - offsetX);
246
247   var startY = Math.max(0, margin - offsetY);
248   var endY = Math.min(dstHeight, srcHeight - margin - offsetY);
249
250   for (var y = startY; y != endY; y++) {
251     var dstIndex = y * dstStride + startX * 4;
252     var srcIndex = (y + offsetY) * srcStride + (startX + offsetX) * 4;
253
254     for (var x = startX; x != endX; x++) {
255       for (var c = 0; c != 3; c++) {
256         var sum = w0 * srcData[srcIndex] +
257                   w1 * (srcData[srcIndex - 4] +
258                         srcData[srcIndex + 4] +
259                         srcData[srcIndex - srcStride] +
260                         srcData[srcIndex + srcStride]) +
261                   w2 * (srcData[srcIndex - srcStride - 4] +
262                         srcData[srcIndex + srcStride - 4] +
263                         srcData[srcIndex - srcStride + 4] +
264                         srcData[srcIndex + srcStride + 4]) +
265                   w3 * (srcData[srcIndex - 8] +
266                         srcData[srcIndex + 8] +
267                         srcData[srcIndex - srcStride2] +
268                         srcData[srcIndex + srcStride2]);
269         if (sum < 0)
270           dstData[dstIndex++] = 0;
271         else if (sum > 0xFF0000)
272           dstData[dstIndex++] = 0xFF;
273         else
274           dstData[dstIndex++] = sum >> 16;
275         srcIndex++;
276       }
277       srcIndex++;
278       dstIndex++;
279     }
280   }
281 };
282
283 /**
284  * Compute the average color for the image.
285  *
286  * @param {ImageData} imageData Image data to analyze.
287  * @return {{r: number, g: number, b: number}} average color.
288  */
289 filter.getAverageColor = function(imageData) {
290   var data = imageData.data;
291   var width = imageData.width;
292   var height = imageData.height;
293
294   var total = 0;
295   var r = 0;
296   var g = 0;
297   var b = 0;
298
299   var maxIndex = 4 * width * height;
300   for (var i = 0; i != maxIndex;) {
301     total++;
302     r += data[i++];
303     g += data[i++];
304     b += data[i++];
305     i++;
306   }
307   if (total == 0) return { r: 0, g: 0, b: 0 };
308   return { r: r / total, g: g / total, b: b / total };
309 };
310
311 /**
312  * Compute the average color with more weight given to pixes at the center.
313  *
314  * @param {ImageData} imageData Image data to analyze.
315  * @return {{r: number, g: number, b: number}} weighted average color.
316  */
317 filter.getWeightedAverageColor = function(imageData) {
318   var data = imageData.data;
319   var width = imageData.width;
320   var height = imageData.height;
321
322   var total = 0;
323   var r = 0;
324   var g = 0;
325   var b = 0;
326
327   var center = Math.floor(width / 2);
328   var maxDist = center * Math.sqrt(2);
329   maxDist *= 2; // Weaken the effect of distance
330
331   var i = 0;
332   for (var x = 0; x != width; x++) {
333     for (var y = 0; y != height; y++) {
334       var dist = Math.sqrt(
335           (x - center) * (x - center) + (y - center) * (y - center));
336       var weight = (maxDist - dist) / maxDist;
337
338       total += weight;
339       r += data[i++] * weight;
340       g += data[i++] * weight;
341       b += data[i++] * weight;
342       i++;
343     }
344   }
345   if (total == 0) return { r: 0, g: 0, b: 0 };
346   return { r: r / total, g: g / total, b: b / total };
347 };
348
349 /**
350  * Copy part of src image to dst, applying matrix color filter on-the-fly.
351  *
352  * The copied part of src should completely fit into dst (there is no clipping
353  * on either side).
354  *
355  * @param {Array.<number>} matrix 3x3 color matrix.
356  * @param {ImageData} dst Destination image data.
357  * @param {ImageData} src Source image data.
358  * @param {number} offsetX X offset in source to start processing.
359  * @param {number} offsetY Y offset in source to start processing.
360  */
361 filter.colorMatrix3x3 = function(matrix, dst, src, offsetX, offsetY) {
362   var c11 = filter.floatToFixedPoint(matrix[0]);
363   var c12 = filter.floatToFixedPoint(matrix[1]);
364   var c13 = filter.floatToFixedPoint(matrix[2]);
365   var c21 = filter.floatToFixedPoint(matrix[3]);
366   var c22 = filter.floatToFixedPoint(matrix[4]);
367   var c23 = filter.floatToFixedPoint(matrix[5]);
368   var c31 = filter.floatToFixedPoint(matrix[6]);
369   var c32 = filter.floatToFixedPoint(matrix[7]);
370   var c33 = filter.floatToFixedPoint(matrix[8]);
371
372   var dstData = dst.data;
373   var dstWidth = dst.width;
374   var dstHeight = dst.height;
375
376   var srcData = src.data;
377   var srcWidth = src.width;
378   var srcHeight = src.height;
379
380   if (offsetX < 0 || offsetX + dstWidth > srcWidth ||
381       offsetY < 0 || offsetY + dstHeight > srcHeight)
382       throw new Error('Invalid offset');
383
384   // Javascript is not very good at inlining constants.
385   // We inline manually and assert that the constant is equal to the variable.
386   if (filter.FIXED_POINT_SHIFT != 16)
387     throw new Error('Wrong fixed point shift');
388
389   var dstIndex = 0;
390   for (var y = 0; y != dstHeight; y++) {
391     var srcIndex = (offsetX + (offsetY + y) * srcWidth) * 4;
392     for (var x = 0; x != dstWidth; x++) {
393       var r = srcData[srcIndex++];
394       var g = srcData[srcIndex++];
395       var b = srcData[srcIndex++];
396       srcIndex++;
397
398       var rNew = r * c11 + g * c12 + b * c13;
399       var gNew = r * c21 + g * c22 + b * c23;
400       var bNew = r * c31 + g * c32 + b * c33;
401
402       if (rNew < 0) {
403         dstData[dstIndex++] = 0;
404       } else if (rNew > 0xFF0000) {
405         dstData[dstIndex++] = 0xFF;
406       } else {
407         dstData[dstIndex++] = rNew >> 16;
408       }
409
410       if (gNew < 0) {
411         dstData[dstIndex++] = 0;
412       } else if (gNew > 0xFF0000) {
413         dstData[dstIndex++] = 0xFF;
414       } else {
415         dstData[dstIndex++] = gNew >> 16;
416       }
417
418       if (bNew < 0) {
419         dstData[dstIndex++] = 0;
420       } else if (bNew > 0xFF0000) {
421         dstData[dstIndex++] = 0xFF;
422       } else {
423         dstData[dstIndex++] = bNew >> 16;
424       }
425
426       dstIndex++;
427     }
428   }
429 };
430
431 /**
432  * Return a convolution filter function bound to specific weights.
433  *
434  * @param {Array.<number>} weights Weights for the convolution matrix
435  *                                 (not normalized).
436  * @return {function(ImageData,ImageData,number,number)} Convolution filter.
437  */
438 filter.createConvolutionFilter = function(weights) {
439   // Normalize the weights to sum to 1.
440   var total = 0;
441   for (var i = 0; i != weights.length; i++) {
442     total += weights[i] * (i ? 4 : 1);
443   }
444
445   var normalized = [];
446   for (i = 0; i != weights.length; i++) {
447     normalized.push(weights[i] / total);
448   }
449   for (; i < 4; i++) {
450     normalized.push(0);
451   }
452
453   var maxWeightedSum = 0xFF *
454       Math.abs(normalized[0]) +
455       Math.abs(normalized[1]) * 4 +
456       Math.abs(normalized[2]) * 4 +
457       Math.abs(normalized[3]) * 4;
458   if (maxWeightedSum > filter.MAX_FLOAT_VALUE)
459     throw new Error('convolve5x5 cannot convert the weights to fixed point');
460
461   return filter.convolve5x5.bind(null, normalized);
462 };
463
464 /**
465  * Creates matrix filter.
466  * @param {Array.<number>} matrix Color transformation matrix.
467  * @return {function(ImageData,ImageData,number,number)} Matrix filter.
468  */
469 filter.createColorMatrixFilter = function(matrix) {
470   for (var r = 0; r != 3; r++) {
471     var maxRowSum = 0;
472     for (var c = 0; c != 3; c++) {
473       maxRowSum += 0xFF * Math.abs(matrix[r * 3 + c]);
474     }
475     if (maxRowSum > filter.MAX_FLOAT_VALUE)
476       throw new Error(
477           'colorMatrix3x3 cannot convert the matrix to fixed point');
478   }
479   return filter.colorMatrix3x3.bind(null, matrix);
480 };
481
482 /**
483  * Return a blur filter.
484  * @param {Object} options Blur options.
485  * @return {function(ImageData,ImageData,number,number)} Blur filter.
486  */
487 filter.blur = function(options) {
488   if (options.radius == 1)
489     return filter.createConvolutionFilter(
490         [1, options.strength]);
491   else if (options.radius == 2)
492     return filter.createConvolutionFilter(
493         [1, options.strength, options.strength]);
494   else
495     return filter.createConvolutionFilter(
496         [1, options.strength, options.strength, options.strength]);
497 };
498
499 /**
500  * Return a sharpen filter.
501  * @param {Object} options Sharpen options.
502  * @return {function(ImageData,ImageData,number,number)} Sharpen filter.
503  */
504 filter.sharpen = function(options) {
505   if (options.radius == 1)
506     return filter.createConvolutionFilter(
507         [5, -options.strength]);
508   else if (options.radius == 2)
509     return filter.createConvolutionFilter(
510         [10, -options.strength, -options.strength]);
511   else
512     return filter.createConvolutionFilter(
513         [15, -options.strength, -options.strength, -options.strength]);
514 };
515
516 /**
517  * Return an exposure filter.
518  * @param {Object} options exposure options.
519  * @return {function(ImageData,ImageData,number,number)} Exposure filter.
520  */
521 filter.exposure = function(options) {
522   var pixelMap = filter.precompute(
523     255,
524     function(value) {
525      if (options.brightness > 0) {
526        value *= (1 + options.brightness);
527      } else {
528        value += (0xFF - value) * options.brightness;
529      }
530      return 0x80 +
531          (value - 0x80) * Math.tan((options.contrast + 1) * Math.PI / 4);
532     });
533
534   return filter.mapPixels.bind(null, pixelMap, pixelMap, pixelMap);
535 };
536
537 /**
538  * Return a color autofix filter.
539  * @param {Object} options Histogram for autofix.
540  * @return {function(ImageData,ImageData,number,number)} Autofix filter.
541  */
542 filter.autofix = function(options) {
543   return filter.mapPixels.bind(null,
544       filter.autofix.stretchColors(options.histogram.r),
545       filter.autofix.stretchColors(options.histogram.g),
546       filter.autofix.stretchColors(options.histogram.b));
547 };
548
549 /**
550  * Return a conversion table that stretches the range of colors used
551  * in the image to 0..255.
552  * @param {Array.<number>} channelHistogram Histogram to calculate range.
553  * @return {Uint8Array} Color mapping array.
554  */
555 filter.autofix.stretchColors = function(channelHistogram) {
556   var range = filter.autofix.getRange(channelHistogram);
557   return filter.precompute(
558       255,
559       function(x) {
560         return (x - range.first) / (range.last - range.first) * 255;
561       }
562   );
563 };
564
565 /**
566  * Return a range that encloses non-zero elements values in a histogram array.
567  * @param {Array.<number>} channelHistogram Histogram to analyze.
568  * @return {{first: number, last: number}} Channel range in histogram.
569  */
570 filter.autofix.getRange = function(channelHistogram) {
571   var first = 0;
572   while (first < channelHistogram.length && channelHistogram[first] == 0)
573     first++;
574
575   var last = channelHistogram.length - 1;
576   while (last >= 0 && channelHistogram[last] == 0)
577     last--;
578
579   if (first >= last) // Stretching does not make sense
580     return {first: 0, last: channelHistogram.length - 1};
581   else
582     return {first: first, last: last};
583 };
584
585 /**
586  * Minimum channel offset that makes visual difference. If autofix calculated
587  * offset is less than SENSITIVITY, probably autofix is not needed.
588  * Reasonable empirical value.
589  * @type {number}
590  */
591 filter.autofix.SENSITIVITY = 8;
592
593 /**
594  * @param {Array.<number>} channelHistogram Histogram to analyze.
595  * @return {boolean} True if stretching this range to 0..255 would make
596  *                   a visible difference.
597  */
598 filter.autofix.needsStretching = function(channelHistogram) {
599   var range = filter.autofix.getRange(channelHistogram);
600   return (range.first >= filter.autofix.SENSITIVITY ||
601           range.last <= 255 - filter.autofix.SENSITIVITY);
602 };
603
604 /**
605  * @param {{r: Array.<number>, g: Array.<number>, b: Array.<number>}} histogram
606  * @return {boolean} True if the autofix would make a visible difference.
607  */
608 filter.autofix.isApplicable = function(histogram) {
609   return filter.autofix.needsStretching(histogram.r) ||
610          filter.autofix.needsStretching(histogram.g) ||
611          filter.autofix.needsStretching(histogram.b);
612 };