From 1f7e80e581f9eefca8215904ee8b7b3897e2f475 Mon Sep 17 00:00:00 2001 From: Lovell Fuller Date: Fri, 13 Feb 2015 09:41:42 +0000 Subject: [PATCH] Add chroma subsampling options for JPEG output --- README.md | 9 +++++++++ index.js | 9 +++++++++ package.json | 4 ++-- src/resize.cc | 9 +++++++-- test/bench/package.json | 2 +- test/bench/perf.js | 12 ++++++++++++ test/unit/io.js | 29 +++++++++++++++++++++++++++++ 7 files changed, 69 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 0f4d99b9..f4147b1f 100755 --- a/README.md +++ b/README.md @@ -438,6 +438,15 @@ Include all metadata (EXIF, XMP, IPTC) from the input image in the output image. The default behaviour is to strip all metadata and convert to the device-independent sRGB colour space. +#### withoutChromaSubsampling() + +Disable the use of [chroma subsampling](http://en.wikipedia.org/wiki/Chroma_subsampling) with JPEG output (4:4:4). + +This can improve colour representation at higher quality settings (90+), +but usually increases output file size and typically reduces performance by 25%. + +The default behaviour is to use chroma subsampling (4:2:0). + #### compressionLevel(compressionLevel) An advanced setting for the _zlib_ compression level of the lossless PNG output format. The default level is `6`. diff --git a/index.js b/index.js index 89f2a4fe..025b8ddf 100755 --- a/index.js +++ b/index.js @@ -64,6 +64,7 @@ var Sharp = function(input) { quality: 80, compressionLevel: 6, withoutAdaptiveFiltering: false, + withoutChromaSubsampling: false, streamOut: false, withMetadata: false }; @@ -368,6 +369,14 @@ Sharp.prototype.withoutAdaptiveFiltering = function(withoutAdaptiveFiltering) { return this; }; +/* + Disable the use of chroma subsampling for JPEG output +*/ +Sharp.prototype.withoutChromaSubsampling = function(withoutChromaSubsampling) { + this.options.withoutChromaSubsampling = (typeof withoutChromaSubsampling === 'boolean') ? withoutChromaSubsampling : true; + return this; +}; + Sharp.prototype.withMetadata = function(withMetadata) { this.options.withMetadata = (typeof withMetadata === 'boolean') ? withMetadata : true; return this; diff --git a/package.json b/package.json index 18438746..7073395c 100644 --- a/package.json +++ b/package.json @@ -34,10 +34,10 @@ "vips" ], "dependencies": { - "bluebird": "^2.9.8", + "bluebird": "^2.9.9", "color": "^0.7.3", "nan": "^1.6.2", - "semver": "^4.2.2" + "semver": "^4.3.0" }, "devDependencies": { "mocha": "^2.1.0", diff --git a/src/resize.cc b/src/resize.cc index 384b2480..175b136e 100755 --- a/src/resize.cc +++ b/src/resize.cc @@ -90,6 +90,7 @@ struct ResizeBaton { int quality; int compressionLevel; bool withoutAdaptiveFiltering; + bool withoutChromaSubsampling; std::string err; bool withMetadata; @@ -117,6 +118,7 @@ struct ResizeBaton { quality(80), compressionLevel(6), withoutAdaptiveFiltering(false), + withoutChromaSubsampling(false), withMetadata(false) { background[0] = 0.0; background[1] = 0.0; @@ -675,7 +677,8 @@ class ResizeWorker : public NanAsyncWorker { if (baton->output == "__jpeg" || (baton->output == "__input" && inputImageType == ImageType::JPEG)) { // Write JPEG to buffer if (vips_jpegsave_buffer(image, &baton->bufferOut, &baton->bufferOutLength, "strip", !baton->withMetadata, - "Q", baton->quality, "optimize_coding", TRUE, "interlace", baton->progressive, NULL)) { + "Q", baton->quality, "optimize_coding", TRUE, "no_subsample", baton->withoutChromaSubsampling, + "interlace", baton->progressive, NULL)) { return Error(baton, hook); } baton->outputFormat = "jpeg"; @@ -741,7 +744,8 @@ class ResizeWorker : public NanAsyncWorker { if (outputJpeg || (matchInput && inputImageType == ImageType::JPEG)) { // Write JPEG to file if (vips_jpegsave(image, baton->output.c_str(), "strip", !baton->withMetadata, - "Q", baton->quality, "optimize_coding", TRUE, "interlace", baton->progressive, NULL)) { + "Q", baton->quality, "optimize_coding", TRUE, "no_subsample", baton->withoutChromaSubsampling, + "interlace", baton->progressive, NULL)) { return Error(baton, hook); } baton->outputFormat = "jpeg"; @@ -992,6 +996,7 @@ NAN_METHOD(resize) { baton->quality = options->Get(NanNew("quality"))->Int32Value(); baton->compressionLevel = options->Get(NanNew("compressionLevel"))->Int32Value(); baton->withoutAdaptiveFiltering = options->Get(NanNew("withoutAdaptiveFiltering"))->BooleanValue(); + baton->withoutChromaSubsampling = options->Get(NanNew("withoutChromaSubsampling"))->BooleanValue(); baton->withMetadata = options->Get(NanNew("withMetadata"))->BooleanValue(); // Output filename or __format for Buffer baton->output = *String::Utf8Value(options->Get(NanNew("output"))->ToString()); diff --git a/test/bench/package.json b/test/bench/package.json index 0f1993ef..98db94ba 100755 --- a/test/bench/package.json +++ b/test/bench/package.json @@ -12,7 +12,7 @@ "imagemagick-native": "^1.7.0", "gm": "^1.17.0", "async": "^0.9.0", - "semver": "^4.2.0", + "semver": "^4.3.0", "benchmark": "^1.0.0" }, "license": "Apache 2.0", diff --git a/test/bench/perf.js b/test/bench/perf.js index b08b4864..c80a162e 100755 --- a/test/bench/perf.js +++ b/test/bench/perf.js @@ -347,6 +347,18 @@ async.series({ } }); } + }).add('sharp-without-chroma-subsampling', { + defer: true, + fn: function(deferred) { + sharp(inputJpgBuffer).resize(width, height).withoutChromaSubsampling().toBuffer(function(err, buffer) { + if (err) { + throw err; + } else { + assert.notStrictEqual(null, buffer); + deferred.resolve(); + } + }); + } }).add('sharp-rotate', { defer: true, fn: function(deferred) { diff --git a/test/unit/io.js b/test/unit/io.js index 498da21f..1501b6f3 100755 --- a/test/unit/io.js +++ b/test/unit/io.js @@ -459,6 +459,35 @@ describe('Input/output', function() { }); + it('Without chroma subsampling generates larger file', function(done) { + // First generate with chroma subsampling (default) + sharp(fixtures.inputJpg) + .resize(320, 240) + .withoutChromaSubsampling(false) + .toBuffer(function(err, withChromaSubsamplingData, withChromaSubsamplingInfo) { + if (err) throw err; + assert.strictEqual(true, withChromaSubsamplingData.length > 0); + assert.strictEqual(withChromaSubsamplingData.length, withChromaSubsamplingInfo.size); + assert.strictEqual('jpeg', withChromaSubsamplingInfo.format); + assert.strictEqual(320, withChromaSubsamplingInfo.width); + assert.strictEqual(240, withChromaSubsamplingInfo.height); + // Then generate without + sharp(fixtures.inputJpg) + .resize(320, 240) + .withoutChromaSubsampling() + .toBuffer(function(err, withoutChromaSubsamplingData, withoutChromaSubsamplingInfo) { + if (err) throw err; + assert.strictEqual(true, withoutChromaSubsamplingData.length > 0); + assert.strictEqual(withoutChromaSubsamplingData.length, withoutChromaSubsamplingInfo.size); + assert.strictEqual('jpeg', withoutChromaSubsamplingInfo.format); + assert.strictEqual(320, withoutChromaSubsamplingInfo.width); + assert.strictEqual(240, withoutChromaSubsamplingInfo.height); + assert.strictEqual(true, withChromaSubsamplingData.length < withoutChromaSubsamplingData.length); + done(); + }); + }); + }); + it('Convert SVG, if supported, to PNG', function(done) { sharp(fixtures.inputSvg) .resize(100, 100)