From 32d9bc204a415050f49be1520ef10b8ef96f4188 Mon Sep 17 00:00:00 2001 From: Lovell Fuller Date: Mon, 10 Nov 2014 22:38:13 +0000 Subject: [PATCH] Add 'fast' blur and Gaussian blur feature #108 --- README.md | 12 ++++++-- index.js | 22 ++++++++++++++ src/resize.cc | 28 ++++++++++++++++++ test/bench/perf.js | 24 ++++++++++++++++ test/unit/blur.js | 72 ++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 156 insertions(+), 2 deletions(-) create mode 100755 test/unit/blur.js diff --git a/README.md b/README.md index 80399e1d..5f8eea4a 100755 --- a/README.md +++ b/README.md @@ -319,11 +319,19 @@ Do not enlarge the output image if the input image width *or* height are already This is equivalent to GraphicsMagick's `>` geometry option: "change the dimensions of the image only if its width or height exceeds the geometry specification". +#### blur([radius]) + +When used without parameters, performs a fast, mild blur of the output image. This typically reduces performance by 10%. + +When a `radius` is provided, performs a slower, more accurate Gaussian blur. This typically reduces performance by 30%. + +* `radius`, if present, is an integral Number representing the approximate blur mask radius in pixels. + #### sharpen([radius], [flat], [jagged]) -When used without parameters, perform a fast, mild sharpen of the output image. This typically reduces performance by 10%. +When used without parameters, performs a fast, mild sharpen of the output image. This typically reduces performance by 10%. -When a `radius` is provided, perform a slower, more accurate sharpen of the L channel in the LAB colour space. Separate control over the level of sharpening in "flat" and "jagged" areas is available. This typically reduces performance by 50%. +When a `radius` is provided, performs a slower, more accurate sharpen of the L channel in the LAB colour space. Separate control over the level of sharpening in "flat" and "jagged" areas is available. This typically reduces performance by 50%. * `radius`, if present, is an integral Number representing the sharpen mask radius in pixels. * `flat`, if present, is a Number representing the level of sharpening to apply to "flat" areas, defaulting to a value of 1.0. diff --git a/index.js b/index.js index 2af5ecd6..fe1838b6 100755 --- a/index.js +++ b/index.js @@ -43,6 +43,7 @@ var Sharp = function(input) { // operations background: [0, 0, 0, 255], flatten: false, + blurRadius: 0, sharpenRadius: 0, sharpenFlat: 1, sharpenJagged: 2, @@ -207,6 +208,27 @@ Sharp.prototype.withoutEnlargement = function(withoutEnlargement) { return this; }; +/* + Blur the output image. + Call without a radius to use a fast, mild blur. + Call with a radius to use a slower, more accurate Gaussian blur. +*/ +Sharp.prototype.blur = function(radius) { + if (typeof radius === 'undefined') { + // No arguments: default to mild blur + this.options.blurRadius = -1; + } else if (typeof radius === 'boolean') { + // Boolean argument: apply mild blur? + this.options.blurRadius = radius ? -1 : 0; + } else if (typeof radius === 'number' && !Number.isNaN(radius) && (radius % 1 === 0)) { + // Numeric argument: specific radius + this.options.blurRadius = radius; + } else { + throw new Error('Invalid integral blur radius ' + radius); + } + return this; +}; + /* Sharpen the output image. Call without a radius to use a fast, mild sharpen. diff --git a/src/resize.cc b/src/resize.cc index 5cce6033..ddd015f4 100755 --- a/src/resize.cc +++ b/src/resize.cc @@ -50,6 +50,7 @@ struct ResizeBaton { std::string interpolator; double background[4]; bool flatten; + int blurRadius; int sharpenRadius; double sharpenFlat; double sharpenJagged; @@ -76,6 +77,7 @@ struct ResizeBaton { canvas(CROP), gravity(0), flatten(false), + blurRadius(0), sharpenRadius(0), sharpenFlat(1.0), sharpenJagged(2.0), @@ -510,6 +512,31 @@ class ResizeWorker : public NanAsyncWorker { image = extractedPost; } + // Blur + if (baton->blurRadius != 0) { + VipsImage *blurred = vips_image_new(); + vips_object_local(hook, blurred); + if (baton->blurRadius == -1) { + // Fast, mild blur + VipsImage *blur = vips_image_new_matrixv(3, 3, + 1.0, 1.0, 1.0, + 1.0, 1.0, 1.0, + 1.0, 1.0, 1.0); + vips_image_set_double(blur, "scale", 9); + vips_object_local(hook, blur); + if (vips_conv(image, &blurred, blur, NULL)) { + return Error(baton, hook); + } + } else { + // Slower, accurate Gaussian blur + if (vips_gaussblur(image, &blurred, baton->blurRadius, NULL)) { + return Error(baton, hook); + } + } + g_object_unref(image); + image = blurred; + } + // Sharpen if (baton->sharpenRadius != 0) { VipsImage *sharpened = vips_image_new(); @@ -850,6 +877,7 @@ NAN_METHOD(resize) { baton->interpolator = *String::Utf8Value(options->Get(NanNew("interpolator"))->ToString()); // Operators baton->flatten = options->Get(NanNew("flatten"))->BooleanValue(); + baton->blurRadius = options->Get(NanNew("blurRadius"))->Int32Value(); baton->sharpenRadius = options->Get(NanNew("sharpenRadius"))->Int32Value(); baton->sharpenFlat = options->Get(NanNew("sharpenFlat"))->NumberValue(); baton->sharpenJagged = options->Get(NanNew("sharpenJagged"))->NumberValue(); diff --git a/test/bench/perf.js b/test/bench/perf.js index 8afdf935..9abd14ed 100755 --- a/test/bench/perf.js +++ b/test/bench/perf.js @@ -187,6 +187,30 @@ async.series({ } }); } + }).add('sharp-blur-mild', { + defer: true, + fn: function(deferred) { + sharp(inputJpgBuffer).resize(width, height).blur().toBuffer(function(err, buffer) { + if (err) { + throw err; + } else { + assert.notStrictEqual(null, buffer); + deferred.resolve(); + } + }); + } + }).add('sharp-blur-radius', { + defer: true, + fn: function(deferred) { + sharp(inputJpgBuffer).resize(width, height).blur(3).toBuffer(function(err, buffer) { + if (err) { + throw err; + } else { + assert.notStrictEqual(null, buffer); + deferred.resolve(); + } + }); + } }).add('sharp-nearest-neighbour', { defer: true, fn: function(deferred) { diff --git a/test/unit/blur.js b/test/unit/blur.js new file mode 100755 index 00000000..7faa9115 --- /dev/null +++ b/test/unit/blur.js @@ -0,0 +1,72 @@ +'use strict'; + +var assert = require('assert'); + +var sharp = require('../../index'); +var fixtures = require('../fixtures'); + +describe('Blur', function() { + + it('specific radius', function(done) { + sharp(fixtures.inputJpg) + .resize(320, 240) + .blur(1) + .toFile(fixtures.path('output.blur-1.jpg'), function(err, info) { + if (err) throw err; + assert.strictEqual('jpeg', info.format); + assert.strictEqual(320, info.width); + assert.strictEqual(240, info.height); + done(); + }); + }); + + it('mild blur', function(done) { + sharp(fixtures.inputJpg) + .resize(320, 240) + .blur() + .toFile(fixtures.path('output.blur-mild.jpg'), function(err, info) { + if (err) throw err; + assert.strictEqual('jpeg', info.format); + assert.strictEqual(320, info.width); + assert.strictEqual(240, info.height); + done(); + }); + }); + + it('invalid radius', function(done) { + var isValid = true; + try { + sharp(fixtures.inputJpg).blur(1.5); + } catch (err) { + isValid = false; + } + assert.strictEqual(false, isValid); + done(); + }); + + it('blurred image is smaller than non-blurred', function(done) { + sharp(fixtures.inputJpg) + .resize(320, 240) + .blur(false) + .toBuffer(function(err, notBlurred, info) { + if (err) throw err; + assert.strictEqual(true, notBlurred.length > 0); + assert.strictEqual('jpeg', info.format); + assert.strictEqual(320, info.width); + assert.strictEqual(240, info.height); + sharp(fixtures.inputJpg) + .resize(320, 240) + .blur(true) + .toBuffer(function(err, blurred, info) { + if (err) throw err; + assert.strictEqual(true, blurred.length > 0); + assert.strictEqual(true, blurred.length < notBlurred.length); + assert.strictEqual('jpeg', info.format); + assert.strictEqual(320, info.width); + assert.strictEqual(240, info.height); + done(); + }); + }); + }); + +});