Expose libjpeg extension param features

Trellis quantisation, overshoot deringing and scan optimisation
This commit is contained in:
Lovell Fuller 2015-04-14 11:33:25 +01:00
parent be39297f3b
commit f6fd45cc90
4 changed files with 206 additions and 24 deletions

View File

@ -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`.

View File

@ -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;

View File

@ -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<String>("compressionLevel"))->Int32Value();
baton->withoutAdaptiveFiltering = options->Get(NanNew<String>("withoutAdaptiveFiltering"))->BooleanValue();
baton->withoutChromaSubsampling = options->Get(NanNew<String>("withoutChromaSubsampling"))->BooleanValue();
baton->trellisQuantisation = options->Get(NanNew<String>("trellisQuantisation"))->BooleanValue();
baton->overshootDeringing = options->Get(NanNew<String>("overshootDeringing"))->BooleanValue();
baton->optimiseScans = options->Get(NanNew<String>("optimiseScans"))->BooleanValue();
baton->withMetadata = options->Get(NanNew<String>("withMetadata"))->BooleanValue();
// Output filename or __format for Buffer
baton->output = *String::Utf8Value(options->Get(NanNew<String>("output"))->ToString());

View File

@ -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)