From f6fd45cc901c2110e1c0970c79b00f5a7ea393b8 Mon Sep 17 00:00:00 2001 From: Lovell Fuller Date: Tue, 14 Apr 2015 11:33:25 +0100 Subject: [PATCH] Expose libjpeg extension param features Trellis quantisation, overshoot deringing and scan optimisation --- README.md | 26 ++++++++- index.js | 47 +++++++++++++++++ src/resize.cc | 19 +++++++ test/unit/io.js | 138 ++++++++++++++++++++++++++++++++++++++++-------- 4 files changed, 206 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 7cd9f012..aead5951 100755 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ This module is powered by the blazingly fast [libvips](https://github.com/jcupit * Node.js v0.10+ or io.js * [libvips](https://github.com/jcupitt/libvips) v7.40.0+ (7.42.0+ recommended) -* C++11 compatible compiler such as gcc 4.6+ or clang 3.0+ +* C++11 compatible compiler such as gcc 4.6+, clang 3.0+ or MSVC 2013 To install the most suitable version of libvips on the following Operating Systems: @@ -555,6 +555,30 @@ but usually increases output file size and typically reduces performance by 25%. The default behaviour is to use chroma subsampling (4:2:0). +#### trellisQuantisation() / trellisQuantization() + +_Requires libvips 8.0.0+ compiled against mozjpeg 3.0+_ + +An advanced setting to apply the use of +[trellis quantisation](http://en.wikipedia.org/wiki/Trellis_quantization) with JPEG output. +Reduces file size and slightly increases relative quality at the cost of increased compression time. + +#### overshootDeringing() + +_Requires libvips 8.0.0+ compiled against mozjpeg 3.0+_ + +An advanced setting to reduce the effects of +[ringing](http://en.wikipedia.org/wiki/Ringing_%28signal%29) in JPEG output, +in particular where black text appears on a white background (or vice versa). + +#### optimiseScans() / optimizeScans() + +_Requires libvips 8.0.0+ compiled against mozjpeg 3.0+_ + +An advanced setting for progressive (interlace) JPEG output. +Calculates which spectrum of DCT coefficients uses the fewest bits. +Usually reduces file size at the cost of increased compression time. + #### 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 c4fc9fdf..924976a1 100755 --- a/index.js +++ b/index.js @@ -67,6 +67,9 @@ var Sharp = function(input) { compressionLevel: 6, withoutAdaptiveFiltering: false, withoutChromaSubsampling: false, + trellisQuantisation: false, + overshootDeringing: false, + optimiseScans: false, streamOut: false, withMetadata: false, tileSize: 256, @@ -403,6 +406,50 @@ Sharp.prototype.withoutChromaSubsampling = function(withoutChromaSubsampling) { return this; }; +/* + Apply trellis quantisation to JPEG output - requires libvips 8.0.0+ compiled against mozjpeg 3.0+ +*/ +Sharp.prototype.trellisQuantisation = function(trellisQuantisation) { + if (semver.gte(libvipsVersion, '8.0.0')) { + this.options.trellisQuantisation = (typeof trellisQuantisation === 'boolean') ? trellisQuantisation : true; + } else { + console.error('trellisQuantisation requires libvips 8.0.0+'); + } + return this; +}; +Sharp.prototype.trellisQuantization = Sharp.prototype.trellisQuantisation; + +/* + Apply overshoot deringing to JPEG output - requires libvips 8.0.0+ compiled against mozjpeg 3.0+ +*/ +Sharp.prototype.overshootDeringing = function(overshootDeringing) { + if (semver.gte(libvipsVersion, '8.0.0')) { + this.options.overshootDeringing = (typeof overshootDeringing === 'boolean') ? overshootDeringing : true; + } else { + console.error('overshootDeringing requires libvips 8.0.0+'); + } + return this; +}; + +/* + Optimise scans in progressive JPEG output - requires libvips 8.0.0+ compiled against mozjpeg 3.0+ +*/ +Sharp.prototype.optimiseScans = function(optimiseScans) { + if (semver.gte(libvipsVersion, '8.0.0')) { + this.options.optimiseScans = (typeof optimiseScans === 'boolean') ? optimiseScans : true; + if (this.options.optimiseScans) { + this.progressive(); + } + } else { + console.error('optimiseScans requires libvips 8.0.0+'); + } + return this; +}; +Sharp.prototype.optimizeScans = Sharp.prototype.optimiseScans; + +/* + Include all metadata (EXIF, XMP, IPTC) from the input image in the output image +*/ Sharp.prototype.withMetadata = function(withMetadata) { this.options.withMetadata = (typeof withMetadata === 'boolean') ? withMetadata : true; return this; diff --git a/src/resize.cc b/src/resize.cc index afad0564..2a5e3c08 100755 --- a/src/resize.cc +++ b/src/resize.cc @@ -95,6 +95,9 @@ struct ResizeBaton { int compressionLevel; bool withoutAdaptiveFiltering; bool withoutChromaSubsampling; + bool trellisQuantisation; + bool overshootDeringing; + bool optimiseScans; std::string err; bool withMetadata; int tileSize; @@ -126,6 +129,9 @@ struct ResizeBaton { compressionLevel(6), withoutAdaptiveFiltering(false), withoutChromaSubsampling(false), + trellisQuantisation(false), + overshootDeringing(false), + optimiseScans(false), withMetadata(false), tileSize(256), tileOverlap(0) { @@ -813,6 +819,11 @@ class ResizeWorker : public NanAsyncWorker { // Write JPEG to buffer if (vips_jpegsave_buffer(image, &baton->bufferOut, &baton->bufferOutLength, "strip", !baton->withMetadata, "Q", baton->quality, "optimize_coding", TRUE, "no_subsample", baton->withoutChromaSubsampling, +#if (VIPS_MAJOR_VERSION >= 8) + "trellis_quant", baton->trellisQuantisation, + "overshoot_deringing", baton->overshootDeringing, + "optimize_scans", baton->optimiseScans, +#endif "interlace", baton->progressive, NULL)) { return Error(); } @@ -881,6 +892,11 @@ class ResizeWorker : public NanAsyncWorker { // Write JPEG to file if (vips_jpegsave(image, baton->output.c_str(), "strip", !baton->withMetadata, "Q", baton->quality, "optimize_coding", TRUE, "no_subsample", baton->withoutChromaSubsampling, +#if (VIPS_MAJOR_VERSION >= 8) + "trellis_quant", baton->trellisQuantisation, + "overshoot_deringing", baton->overshootDeringing, + "optimize_scans", baton->optimiseScans, +#endif "interlace", baton->progressive, NULL)) { return Error(); } @@ -1175,6 +1191,9 @@ NAN_METHOD(resize) { baton->compressionLevel = options->Get(NanNew("compressionLevel"))->Int32Value(); baton->withoutAdaptiveFiltering = options->Get(NanNew("withoutAdaptiveFiltering"))->BooleanValue(); baton->withoutChromaSubsampling = options->Get(NanNew("withoutChromaSubsampling"))->BooleanValue(); + baton->trellisQuantisation = options->Get(NanNew("trellisQuantisation"))->BooleanValue(); + baton->overshootDeringing = options->Get(NanNew("overshootDeringing"))->BooleanValue(); + baton->optimiseScans = options->Get(NanNew("optimiseScans"))->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/unit/io.js b/test/unit/io.js index 71ccbf3e..3976a32e 100755 --- a/test/unit/io.js +++ b/test/unit/io.js @@ -317,19 +317,21 @@ describe('Input/output', function() { }); }); - it('WebP output', function(done) { - sharp(fixtures.inputJpg) - .resize(320, 240) - .toFormat(sharp.format.webp) - .toBuffer(function(err, data, info) { - if (err) throw err; - assert.strictEqual(true, data.length > 0); - assert.strictEqual('webp', info.format); - assert.strictEqual(320, info.width); - assert.strictEqual(240, info.height); - done(); - }); - }); + if (sharp.format.webp.output.buffer) { + it('WebP output', function(done) { + sharp(fixtures.inputJpg) + .resize(320, 240) + .toFormat(sharp.format.webp) + .toBuffer(function(err, data, info) { + if (err) throw err; + assert.strictEqual(true, data.length > 0); + assert.strictEqual('webp', info.format); + assert.strictEqual(320, info.width); + assert.strictEqual(240, info.height); + done(); + }); + }); + } it('Invalid output format', function(done) { var isValid = false; @@ -394,17 +396,19 @@ describe('Input/output', function() { }); }); - it('WebP', function(done) { - sharp(fixtures.inputWebP).resize(320, 80).toFile(fixtures.outputZoinks, function(err, info) { - if (err) throw err; - assert.strictEqual(true, info.size > 0); - assert.strictEqual('webp', info.format); - assert.strictEqual(320, info.width); - assert.strictEqual(80, info.height); - fs.unlinkSync(fixtures.outputZoinks); - done(); + if (sharp.format.webp.input.file) { + it('WebP', function(done) { + sharp(fixtures.inputWebP).resize(320, 80).toFile(fixtures.outputZoinks, function(err, info) { + if (err) throw err; + assert.strictEqual(true, info.size > 0); + assert.strictEqual('webp', info.format); + assert.strictEqual(320, info.width); + assert.strictEqual(80, info.height); + fs.unlinkSync(fixtures.outputZoinks); + done(); + }); }); - }); + } it('TIFF', function(done) { sharp(fixtures.inputTiff).resize(320, 80).toFile(fixtures.outputZoinks, function(err, info) { @@ -511,6 +515,94 @@ describe('Input/output', function() { }); }); + if (semver.gte(sharp.libvipsVersion(), '8.0.0')) { + it('Trellis quantisation [libvips ' + sharp.libvipsVersion() + '>=8.0.0]', function(done) { + // First generate without + sharp(fixtures.inputJpg) + .resize(320, 240) + .trellisQuantisation(false) + .toBuffer(function(err, withoutData, withoutInfo) { + if (err) throw err; + assert.strictEqual(true, withoutData.length > 0); + assert.strictEqual(withoutData.length, withoutInfo.size); + assert.strictEqual('jpeg', withoutInfo.format); + assert.strictEqual(320, withoutInfo.width); + assert.strictEqual(240, withoutInfo.height); + // Then generate with + sharp(fixtures.inputJpg) + .resize(320, 240) + .trellisQuantization() + .toBuffer(function(err, withData, withInfo) { + if (err) throw err; + assert.strictEqual(true, withData.length > 0); + assert.strictEqual(withData.length, withInfo.size); + assert.strictEqual('jpeg', withInfo.format); + assert.strictEqual(320, withInfo.width); + assert.strictEqual(240, withInfo.height); + // Verify image is same (as mozjpeg may not be present) size or less + assert.strictEqual(true, withData.length <= withoutData.length); + done(); + }); + }); + }); + it('Overshoot deringing [libvips ' + sharp.libvipsVersion() + '>=8.0.0]', function(done) { + // First generate without + sharp(fixtures.inputJpg) + .resize(320, 240) + .overshootDeringing(false) + .toBuffer(function(err, withoutData, withoutInfo) { + if (err) throw err; + assert.strictEqual(true, withoutData.length > 0); + assert.strictEqual(withoutData.length, withoutInfo.size); + assert.strictEqual('jpeg', withoutInfo.format); + assert.strictEqual(320, withoutInfo.width); + assert.strictEqual(240, withoutInfo.height); + // Then generate with + sharp(fixtures.inputJpg) + .resize(320, 240) + .overshootDeringing() + .toBuffer(function(err, withData, withInfo) { + if (err) throw err; + assert.strictEqual(true, withData.length > 0); + assert.strictEqual(withData.length, withInfo.size); + assert.strictEqual('jpeg', withInfo.format); + assert.strictEqual(320, withInfo.width); + assert.strictEqual(240, withInfo.height); + done(); + }); + }); + }); + it('Optimise scans [libvips ' + sharp.libvipsVersion() + '>=8.0.0]', function(done) { + // First generate without + sharp(fixtures.inputJpg) + .resize(320, 240) + .optimiseScans(false) + .toBuffer(function(err, withoutData, withoutInfo) { + if (err) throw err; + assert.strictEqual(true, withoutData.length > 0); + assert.strictEqual(withoutData.length, withoutInfo.size); + assert.strictEqual('jpeg', withoutInfo.format); + assert.strictEqual(320, withoutInfo.width); + assert.strictEqual(240, withoutInfo.height); + // Then generate with + sharp(fixtures.inputJpg) + .resize(320, 240) + .optimizeScans() + .toBuffer(function(err, withData, withInfo) { + if (err) throw err; + assert.strictEqual(true, withData.length > 0); + assert.strictEqual(withData.length, withInfo.size); + assert.strictEqual('jpeg', withInfo.format); + assert.strictEqual(320, withInfo.width); + assert.strictEqual(240, withInfo.height); + // Verify image is of a different size (progressive output even without mozjpeg) + assert.strictEqual(true, withData.length != withoutData.length); + done(); + }); + }); + }); + } + if (sharp.format.magick.input.file) { it('Convert SVG, if supported, to PNG', function(done) { sharp(fixtures.inputSvg)