diff --git a/README.md b/README.md index 270874d4..1661f7ea 100755 --- a/README.md +++ b/README.md @@ -279,6 +279,16 @@ Possible interpolators, in order of performance, are: * `locallyBoundedBicubic`: Use [LBB interpolation](https://github.com/jcupitt/libvips/blob/master/libvips/resample/lbb.cpp#L100), which prevents some "[acutance](http://en.wikipedia.org/wiki/Acutance)" and typically reduces performance by a factor of 2. * `nohalo`: Use [Nohalo interpolation](http://eprints.soton.ac.uk/268086/), which prevents acutance and typically reduces performance by a factor of 3. +#### gamma([gamma]) + +Apply a gamma correction by reducing the encoding (darken) pre-resize at a factor of `1/gamma` then increasing the encoding (brighten) post-resize at a factor of `gamma`. + +`gamma`, if present, is a Number betweem 1 and 3. The default value is `2.2`, a suitable approximation for sRGB images. + +This can improve the perceived brightness of a resized image in non-linear colour spaces. + +JPEG input images will not take advantage of the shrink-on-load performance optimisation when applying a gamma correction. + ### Output options #### jpeg() diff --git a/index.js b/index.js index 0422a60e..df5fba9d 100755 --- a/index.js +++ b/index.js @@ -20,6 +20,7 @@ var Sharp = function(input) { withoutEnlargement: false, sharpen: false, interpolator: 'bilinear', + gamma: 0, progressive: false, sequentialRead: false, quality: 80, @@ -164,6 +165,22 @@ Sharp.prototype.nohaloInterpolation = util.deprecate(function() { return this.interpolateWith(module.exports.interpolator.nohalo); }, 'nohaloInterpolation() is deprecated, use interpolateWith(sharp.interpolator.nohalo) instead'); +/* + Darken image pre-resize (1/gamma) and brighten post-resize (gamma). + Improves brightness of resized image in non-linear colour spaces. +*/ +Sharp.prototype.gamma = function(gamma) { + if (typeof gamma === 'undefined') { + // Default gamma correction of 2.2 (sRGB) + this.options.gamma = 2.2; + } else if (!Number.isNaN(gamma) && gamma >= 1 && gamma <= 3) { + this.options.gamma = gamma; + } else { + throw new Error('Invalid gamma correction (1.0 to 3.0) ' + gamma); + } + return this; +}; + Sharp.prototype.progressive = function(progressive) { this.options.progressive = (typeof progressive === 'boolean') ? progressive : true; return this; diff --git a/src/sharp.cc b/src/sharp.cc index 3db2d21a..8c2fd102 100755 --- a/src/sharp.cc +++ b/src/sharp.cc @@ -26,6 +26,7 @@ struct resize_baton { VipsExtend extend; bool sharpen; std::string interpolator; + double gamma; bool progressive; bool without_enlargement; VipsAccess access_method; @@ -42,6 +43,7 @@ struct resize_baton { gravity(0), max(false), sharpen(false), + gamma(0.0), progressive(false), without_enlargement(false), withMetadata(false) {} @@ -434,9 +436,9 @@ class ResizeWorker : public NanAsyncWorker { } } - // Try to use libjpeg shrink-on-load + // Try to use libjpeg shrink-on-load, but not when applying gamma correction int shrink_on_load = 1; - if (inputImageType == JPEG) { + if (inputImageType == JPEG && baton->gamma == 0) { if (shrink >= 8) { factor = factor / 8; shrink_on_load = 8; @@ -469,6 +471,16 @@ class ResizeWorker : public NanAsyncWorker { } g_object_unref(in); + // Gamma encoding (darken) + if (baton->gamma >= 1 && baton->gamma <= 3) { + VipsImage *gamma_encoded = vips_image_new(); + if (vips_gamma(shrunk_on_load, &gamma_encoded, "exponent", 1.0 / baton->gamma, NULL)) { + return resize_error(baton, shrunk_on_load); + } + g_object_unref(shrunk_on_load); + shrunk_on_load = gamma_encoded; + } + VipsImage *shrunk = vips_image_new(); if (shrink > 1) { // Use vips_shrink with the integral reduction @@ -567,6 +579,16 @@ class ResizeWorker : public NanAsyncWorker { } g_object_unref(canvased); + // Gamma decoding (brighten) + if (baton->gamma >= 1 && baton->gamma <= 3) { + VipsImage *gamma_decoded = vips_image_new(); + if (vips_gamma(sharpened, &gamma_decoded, "exponent", baton->gamma, NULL)) { + return resize_error(baton, sharpened); + } + g_object_unref(sharpened); + sharpened = gamma_decoded; + } + // Always convert to sRGB colour space VipsImage *colourspaced = vips_image_new(); vips_colourspace(sharpened, &colourspaced, VIPS_INTERPRETATION_sRGB, NULL); @@ -702,6 +724,7 @@ NAN_METHOD(resize) { baton->gravity = options->Get(NanNew("gravity"))->Int32Value(); baton->sharpen = options->Get(NanNew("sharpen"))->BooleanValue(); baton->interpolator = *String::Utf8Value(options->Get(NanNew("interpolator"))->ToString()); + baton->gamma = options->Get(NanNew("gamma"))->NumberValue(); baton->progressive = options->Get(NanNew("progressive"))->BooleanValue(); baton->without_enlargement = options->Get(NanNew("withoutEnlargement"))->BooleanValue(); baton->access_method = options->Get(NanNew("sequentialRead"))->BooleanValue() ? VIPS_ACCESS_SEQUENTIAL : VIPS_ACCESS_RANDOM; diff --git a/tests/fixtures/gamma_dalai_lama_gray.jpg b/tests/fixtures/gamma_dalai_lama_gray.jpg new file mode 100644 index 00000000..56cbc337 Binary files /dev/null and b/tests/fixtures/gamma_dalai_lama_gray.jpg differ diff --git a/tests/perf.js b/tests/perf.js index c2b60d96..b3177d93 100755 --- a/tests/perf.js +++ b/tests/perf.js @@ -245,6 +245,18 @@ async.series({ } }); } + }).add("sharp-file-buffer-gamma", { + defer: true, + fn: function(deferred) { + sharp(inputJpg).resize(width, height).gamma().toBuffer(function(err, buffer) { + if (err) { + throw err; + } else { + assert.notStrictEqual(null, buffer); + deferred.resolve(); + } + }); + } }).add("sharp-file-buffer-progressive", { defer: true, fn: function(deferred) { diff --git a/tests/unit.js b/tests/unit.js index 57090aef..525b26f6 100755 --- a/tests/unit.js +++ b/tests/unit.js @@ -18,6 +18,8 @@ var outputTiff = path.join(fixturesPath, "output.tiff"); var inputJpgWithExif = path.join(fixturesPath, "Landscape_8.jpg"); // https://github.com/recurser/exif-orientation-examples/blob/master/Landscape_8.jpg +var inputJpgWithGammaHoliness = path.join(fixturesPath, "gamma_dalai_lama_gray.jpg"); // http://www.4p8.com/eric.brasseur/gamma.html + var inputPng = path.join(fixturesPath, "50020484-00001.png"); // http://c.searspartsdirect.com/lis_png/PLDM/50020484-00001.png var inputWebp = path.join(fixturesPath, "4.webp"); // http://www.gstatic.com/webp/gallery/4.webp var inputGif = path.join(fixturesPath, "Crash_test.gif"); // http://upload.wikimedia.org/wikipedia/commons/e/e3/Crash_test.gif @@ -571,6 +573,25 @@ async.series([ }); }); }, + // Gamma correction + function(done) { + sharp(inputJpgWithGammaHoliness).resize(129, 111).toFile(path.join(fixturesPath, 'output.gamma-0.0.jpg'), function(err) { + if (err) throw err; + done(); + }); + }, + function(done) { + sharp(inputJpgWithGammaHoliness).resize(129, 111).gamma().toFile(path.join(fixturesPath, 'output.gamma-2.2.jpg'), function(err) { + if (err) throw err; + done(); + }); + }, + function(done) { + sharp(inputJpgWithGammaHoliness).resize(129, 111).gamma(3).toFile(path.join(fixturesPath, 'output.gamma-3.0.jpg'), function(err) { + if (err) throw err; + done(); + }); + }, // Verify internal counters function(done) { var counters = sharp.counters();