diff --git a/README.md b/README.md index ff1b9720..87150180 100755 --- a/README.md +++ b/README.md @@ -283,7 +283,7 @@ This is equivalent to GraphicsMagick's `>` geometry option: "change the dimensio #### sharpen() -Perform a mild sharpen of the resultant image. This typically reduces performance by 30%. +Perform a mild sharpen of the output image. This typically reduces performance by 10%. #### interpolateWith(interpolator) @@ -308,6 +308,14 @@ This can improve the perceived brightness of a resized image in non-linear colou JPEG input images will not take advantage of the shrink-on-load performance optimisation when applying a gamma correction. +#### grayscale() / greyscale() + +Convert to 8-bit greyscale; 256 shades of grey. + +This is a linear operation. If the input image is in a non-linear colourspace such as sRGB, use `gamma()` with `greyscale()` for the best results. + +The output image will still be web-friendly sRGB and contain three (identical) channels. + ### Output options #### jpeg() @@ -332,7 +340,7 @@ The output quality to use for lossy JPEG, WebP and TIFF output formats. The defa Use progressive (interlace) scan for JPEG and PNG output. This typically reduces compression performance by 30% but results in an image that can be rendered sooner when decompressed. -#### withMetadata([boolean]) +#### withMetadata() Include all metadata (ICC, EXIF, XMP) from the input image in the output image. The default behaviour is to strip all metadata. diff --git a/index.js b/index.js index 89d9f33d..97db7209 100755 --- a/index.js +++ b/index.js @@ -21,6 +21,7 @@ var Sharp = function(input) { sharpen: false, interpolator: 'bilinear', gamma: 0, + greyscale: false, progressive: false, sequentialRead: false, quality: 80, @@ -181,6 +182,15 @@ Sharp.prototype.gamma = function(gamma) { return this; }; +/* + Convert to greyscale +*/ +Sharp.prototype.greyscale = function(greyscale) { + this.options.greyscale = (typeof greyscale === 'boolean') ? greyscale : true; + return this; +}; +Sharp.prototype.grayscale = Sharp.prototype.greyscale; + 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 c3d6a5fe..f0950c01 100755 --- a/src/sharp.cc +++ b/src/sharp.cc @@ -28,6 +28,7 @@ struct resize_baton { bool sharpen; std::string interpolator; double gamma; + bool greyscale; bool progressive; bool without_enlargement; VipsAccess access_method; @@ -483,6 +484,16 @@ class ResizeWorker : public NanAsyncWorker { shrunk_on_load = gamma_encoded; } + // Convert to greyscale (linear, therefore after gamma encoding, if any) + if (baton->greyscale) { + VipsImage *greyscale = vips_image_new(); + if (vips_colourspace(shrunk_on_load, &greyscale, VIPS_INTERPRETATION_B_W, NULL)) { + return resize_error(baton, shrunk_on_load); + } + g_object_unref(shrunk_on_load); + shrunk_on_load = greyscale; + } + VipsImage *shrunk = vips_image_new(); if (shrink > 1) { // Use vips_shrink with the integral reduction @@ -750,6 +761,7 @@ NAN_METHOD(resize) { baton->sharpen = options->Get(NanNew("sharpen"))->BooleanValue(); baton->interpolator = *String::Utf8Value(options->Get(NanNew("interpolator"))->ToString()); baton->gamma = options->Get(NanNew("gamma"))->NumberValue(); + baton->greyscale = options->Get(NanNew("greyscale"))->BooleanValue(); 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/perf.js b/tests/perf.js index b3177d93..1314c160 100755 --- a/tests/perf.js +++ b/tests/perf.js @@ -257,6 +257,30 @@ async.series({ } }); } + }).add("sharp-file-buffer-greyscale", { + defer: true, + fn: function(deferred) { + sharp(inputJpg).resize(width, height).greyscale().toBuffer(function(err, buffer) { + if (err) { + throw err; + } else { + assert.notStrictEqual(null, buffer); + deferred.resolve(); + } + }); + } + }).add("sharp-file-buffer-greyscale-gamma", { + defer: true, + fn: function(deferred) { + sharp(inputJpg).resize(width, height).gamma().greyscale().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 e07195e4..4c6123ee 100755 --- a/tests/unit.js +++ b/tests/unit.js @@ -24,6 +24,8 @@ var inputPng = path.join(fixturesPath, "50020484-00001.png"); // http://c.searsp 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 +var outputZoinks = path.join(fixturesPath, 'output.zoinks'); // an "unknown" file extension + // Ensure cache limits can be set sharp.cache(0); // Disable sharp.cache(50, 500); // 50MB, 500 items @@ -425,6 +427,7 @@ async.series([ anErrorWasEmitted = !!err; }).on('end', function() { assert(anErrorWasEmitted); + fs.unlinkSync(outputJpg); done(); }); var readableButNotAnImage = fs.createReadStream(__filename); @@ -439,6 +442,7 @@ async.series([ anErrorWasEmitted = !!err; }).on('end', function() { assert(anErrorWasEmitted); + fs.unlinkSync(outputJpg); done(); }); var writable = fs.createWriteStream(outputJpg); @@ -522,44 +526,49 @@ async.series([ }, // Output filename without extension should mirror input format function(done) { - sharp(inputJpg).resize(320, 80).toFile(path.join(fixturesPath, 'output.zoinks'), function(err, info) { + sharp(inputJpg).resize(320, 80).toFile(outputZoinks, function(err, info) { if (err) throw err; assert.strictEqual('jpeg', info.format); assert.strictEqual(320, info.width); assert.strictEqual(80, info.height); + fs.unlinkSync(outputZoinks); done(); }); }, function(done) { - sharp(inputPng).resize(320, 80).toFile(path.join(fixturesPath, 'output.zoinks'), function(err, info) { + sharp(inputPng).resize(320, 80).toFile(outputZoinks, function(err, info) { if (err) throw err; assert.strictEqual('png', info.format); assert.strictEqual(320, info.width); assert.strictEqual(80, info.height); + fs.unlinkSync(outputZoinks); done(); }); }, function(done) { - sharp(inputWebP).resize(320, 80).toFile(path.join(fixturesPath, 'output.zoinks'), function(err, info) { + sharp(inputWebP).resize(320, 80).toFile(outputZoinks, function(err, info) { if (err) throw err; assert.strictEqual('webp', info.format); assert.strictEqual(320, info.width); assert.strictEqual(80, info.height); + fs.unlinkSync(outputZoinks); done(); }); }, function(done) { - sharp(inputTiff).resize(320, 80).toFile(path.join(fixturesPath, 'output.zoinks'), function(err, info) { + sharp(inputTiff).resize(320, 80).toFile(outputZoinks, function(err, info) { if (err) throw err; assert.strictEqual('tiff', info.format); assert.strictEqual(320, info.width); assert.strictEqual(80, info.height); + fs.unlinkSync(outputZoinks); done(); }); }, function(done) { - sharp(inputGif).resize(320, 80).toFile(path.join(fixturesPath, 'output.zoinks'), function(err, info) { + sharp(inputGif).resize(320, 80).toFile(outputZoinks, function(err, info) { assert(!!err); + done(); }); }, // Metadata - JPEG @@ -681,7 +690,7 @@ async.series([ }, // Gamma correction function(done) { - sharp(inputJpgWithGammaHoliness).resize(129, 111).toFile(path.join(fixturesPath, 'output.gamma-0.0.jpg'), function(err) { + sharp(inputJpgWithGammaHoliness).resize(129, 111).toFile(path.join(fixturesPath, 'output.gamma-0.0.jpg'), function(err, info) { if (err) throw err; done(); }); @@ -698,6 +707,19 @@ async.series([ done(); }); }, + // Greyscale conversion + function(done) { + sharp(inputJpg).resize(320, 240).greyscale().toFile(path.join(fixturesPath, 'output.greyscale-gamma-0.0.jpg'), function(err, info) { + if (err) throw err; + done(); + }); + }, + function(done) { + sharp(inputJpg).resize(320, 240).gamma().greyscale().toFile(path.join(fixturesPath, 'output.greyscale-gamma-2.2.jpg'), function(err) { + if (err) throw err; + done(); + }); + }, // Verify internal counters function(done) { var counters = sharp.counters();