From 86681100b7bf7a7ca0b2c645fd4d9518fc3b2e15 Mon Sep 17 00:00:00 2001 From: Lovell Fuller Date: Mon, 10 Nov 2014 16:20:04 +0000 Subject: [PATCH] Control level of sharpening via radius/flat/jagged #108 --- README.md | 10 ++++++-- index.js | 45 +++++++++++++++++++++++++----------- package.json | 4 ++-- src/resize.cc | 40 +++++++++++++++++++++----------- test/bench/perf.js | 14 +++++++++++- test/unit/sharpen.js | 54 ++++++++++++++++++++++++++++++++++++++++++-- 6 files changed, 134 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index 2093545d..80399e1d 100755 --- a/README.md +++ b/README.md @@ -319,9 +319,15 @@ 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". -#### sharpen() +#### sharpen([radius], [flat], [jagged]) -Perform a mild sharpen of the output image. This typically reduces performance by 10%. +When used without parameters, perform 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%. + +* `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. +* `jagged`, if present, is a Number representing the level of sharpening to apply to "jagged" areas, defaulting to a value of 2.0. #### interpolateWith(interpolator) diff --git a/index.js b/index.js index f30e13fc..2af5ecd6 100755 --- a/index.js +++ b/index.js @@ -43,7 +43,9 @@ var Sharp = function(input) { // operations background: [0, 0, 0, 255], flatten: false, - sharpen: false, + sharpenRadius: 0, + sharpenFlat: 1, + sharpenJagged: 2, gamma: 0, greyscale: false, // output options @@ -137,16 +139,6 @@ Sharp.prototype.extract = function(topOffset, leftOffset, width, height) { return this; }; -/* - Deprecated embed* methods, to be removed in v0.8.0 -*/ -Sharp.prototype.embedWhite = util.deprecate(function() { - return this.background('white').embed(); -}, "embedWhite() is deprecated, use background('white').embed() instead"); -Sharp.prototype.embedBlack = util.deprecate(function() { - return this.background('black').embed(); -}, "embedBlack() is deprecated, use background('black').embed() instead"); - /* Set the background colour for embed and flatten operations. Delegates to the 'Color' module, which can throw an Error @@ -215,8 +207,35 @@ Sharp.prototype.withoutEnlargement = function(withoutEnlargement) { return this; }; -Sharp.prototype.sharpen = function(sharpen) { - this.options.sharpen = (typeof sharpen === 'boolean') ? sharpen : true; +/* + Sharpen the output image. + Call without a radius to use a fast, mild sharpen. + Call with a radius to use a slow, accurate sharpen using the L of LAB colour space. + radius - size of mask in pixels, must be integer + flat - level of "flat" area sharpen, default 1 + jagged - level of "jagged" area sharpen, default 2 +*/ +Sharp.prototype.sharpen = function(radius, flat, jagged) { + if (typeof radius === 'undefined') { + // No arguments: default to mild sharpen + this.options.sharpenRadius = -1; + } else if (typeof radius === 'boolean') { + // Boolean argument: apply mild sharpen? + this.options.sharpenRadius = radius ? -1 : 0; + } else if (typeof radius === 'number' && !Number.isNaN(radius) && (radius % 1 === 0)) { + // Numeric argument: specific radius + this.options.sharpenRadius = radius; + if (typeof flat === 'number' && !Number.isNaN(flat)) { + // Control over flat areas + this.options.sharpenFlat = flat; + } + if (typeof jagged === 'number' && !Number.isNaN(jagged)) { + // Control over jagged areas + this.options.sharpenJagged = jagged; + } + } else { + throw new Error('Invalid integral sharpen radius ' + radius); + } return this; }; diff --git a/package.json b/package.json index fe782d01..37e90936 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sharp", - "version": "0.7.2", + "version": "0.8.0", "author": "Lovell Fuller ", "contributors": [ "Pierre Inglebert ", @@ -43,7 +43,7 @@ "dependencies": { "bluebird": "^2.3.11", "color": "^0.7.1", - "nan": "^1.4.0", + "nan": "^1.4.1", "semver": "^4.1.0" }, "devDependencies": { diff --git a/src/resize.cc b/src/resize.cc index 8f872b3d..5cce6033 100755 --- a/src/resize.cc +++ b/src/resize.cc @@ -50,7 +50,9 @@ struct ResizeBaton { std::string interpolator; double background[4]; bool flatten; - bool sharpen; + int sharpenRadius; + double sharpenFlat; + double sharpenJagged; double gamma; bool greyscale; int angle; @@ -74,7 +76,9 @@ struct ResizeBaton { canvas(CROP), gravity(0), flatten(false), - sharpen(false), + sharpenRadius(0), + sharpenFlat(1.0), + sharpenJagged(2.0), gamma(0.0), greyscale(false), flip(false), @@ -506,18 +510,26 @@ class ResizeWorker : public NanAsyncWorker { image = extractedPost; } - // Mild sharpen - if (baton->sharpen) { + // Sharpen + if (baton->sharpenRadius != 0) { VipsImage *sharpened = vips_image_new(); vips_object_local(hook, sharpened); - VipsImage *sharpen = vips_image_new_matrixv(3, 3, - -1.0, -1.0, -1.0, - -1.0, 32.0, -1.0, - -1.0, -1.0, -1.0); - vips_image_set_double(sharpen, "scale", 24); - vips_object_local(hook, sharpen); - if (vips_conv(image, &sharpened, sharpen, NULL)) { - return Error(baton, hook); + if (baton->sharpenRadius == -1) { + // Fast, mild sharpen + VipsImage *sharpen = vips_image_new_matrixv(3, 3, + -1.0, -1.0, -1.0, + -1.0, 32.0, -1.0, + -1.0, -1.0, -1.0); + vips_image_set_double(sharpen, "scale", 24); + vips_object_local(hook, sharpen); + if (vips_conv(image, &sharpened, sharpen, NULL)) { + return Error(baton, hook); + } + } else { + // Slow, accurate sharpen in LAB colour space, with control over flat vs jagged areas + if (vips_sharpen(image, &sharpened, "radius", baton->sharpenRadius, "m1", baton->sharpenFlat, "m2", baton->sharpenJagged, NULL)) { + return Error(baton, hook); + } } g_object_unref(image); image = sharpened; @@ -838,7 +850,9 @@ NAN_METHOD(resize) { baton->interpolator = *String::Utf8Value(options->Get(NanNew("interpolator"))->ToString()); // Operators baton->flatten = options->Get(NanNew("flatten"))->BooleanValue(); - baton->sharpen = options->Get(NanNew("sharpen"))->BooleanValue(); + baton->sharpenRadius = options->Get(NanNew("sharpenRadius"))->Int32Value(); + baton->sharpenFlat = options->Get(NanNew("sharpenFlat"))->NumberValue(); + baton->sharpenJagged = options->Get(NanNew("sharpenJagged"))->NumberValue(); baton->gamma = options->Get(NanNew("gamma"))->NumberValue(); baton->greyscale = options->Get(NanNew("greyscale"))->BooleanValue(); baton->angle = options->Get(NanNew("angle"))->Int32Value(); diff --git a/test/bench/perf.js b/test/bench/perf.js index 6d6f91d1..8afdf935 100755 --- a/test/bench/perf.js +++ b/test/bench/perf.js @@ -163,7 +163,7 @@ async.series({ deferred.resolve(); }); } - }).add('sharp-sharpen', { + }).add('sharp-sharpen-mild', { defer: true, fn: function(deferred) { sharp(inputJpgBuffer).resize(width, height).sharpen().toBuffer(function(err, buffer) { @@ -175,6 +175,18 @@ async.series({ } }); } + }).add('sharp-sharpen-radius', { + defer: true, + fn: function(deferred) { + sharp(inputJpgBuffer).resize(width, height).sharpen(3, 1, 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/sharpen.js b/test/unit/sharpen.js index c2e6c931..b8853853 100755 --- a/test/unit/sharpen.js +++ b/test/unit/sharpen.js @@ -7,7 +7,57 @@ var fixtures = require('../fixtures'); describe('Sharpen', function() { - it('sharpen image is larger than non-sharpen', function(done) { + it('specific radius and levels 0.5, 2.5', function(done) { + sharp(fixtures.inputJpg) + .resize(320, 240) + .sharpen(3, 0.5, 2.5) + .toFile(fixtures.path('output.sharpen-3-0.5-2.5.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('specific radius 3 and levels 2, 4', function(done) { + sharp(fixtures.inputJpg) + .resize(320, 240) + .sharpen(5, 2, 4) + .toFile(fixtures.path('output.sharpen-5-2-4.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 sharpen', function(done) { + sharp(fixtures.inputJpg) + .resize(320, 240) + .sharpen() + .toFile(fixtures.path('output.sharpen-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).sharpen(1.5); + } catch (err) { + isValid = false; + } + assert.strictEqual(false, isValid); + done(); + }); + + it('sharpened image is larger than non-sharpened', function(done) { sharp(fixtures.inputJpg) .resize(320, 240) .sharpen(false) @@ -19,7 +69,7 @@ describe('Sharpen', function() { assert.strictEqual(240, info.height); sharp(fixtures.inputJpg) .resize(320, 240) - .sharpen() + .sharpen(true) .toBuffer(function(err, sharpened, info) { if (err) throw err; assert.strictEqual(true, sharpened.length > 0);