From 36e8a3da8838c788640741c1e337c60b1f98b9e9 Mon Sep 17 00:00:00 2001 From: Lovell Fuller Date: Sun, 14 Jul 2019 22:52:38 +0100 Subject: [PATCH] Expose libwebp smartSubsample and reductionEffort #1545 --- docs/api-output.md | 2 ++ docs/changelog.md | 3 +++ lib/constructor.js | 3 +++ lib/output.js | 16 +++++++++++++-- src/pipeline.cc | 10 +++++++++- src/pipeline.h | 7 +++++++ test/unit/webp.js | 50 ++++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 88 insertions(+), 3 deletions(-) diff --git a/docs/api-output.md b/docs/api-output.md index d4004a48..3f9f8ee8 100644 --- a/docs/api-output.md +++ b/docs/api-output.md @@ -185,6 +185,8 @@ Use these WebP options for output image. - `options.alphaQuality` **[Number][8]** quality of alpha layer, integer 0-100 (optional, default `100`) - `options.lossless` **[Boolean][6]** use lossless compression mode (optional, default `false`) - `options.nearLossless` **[Boolean][6]** use near_lossless compression mode (optional, default `false`) + - `options.smartSubsample` **[Boolean][6]** use high quality chroma subsampling (optional, default `false`) + - `options.reductionEffort` **[Number][8]** level of CPU effort to reduce file size, integer 0-6 (optional, default `4`) - `options.force` **[Boolean][6]** force WebP output, otherwise attempt to use input format (optional, default `true`) ### Examples diff --git a/docs/changelog.md b/docs/changelog.md index caf14831..a01992d9 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -11,6 +11,9 @@ Requires libvips v8.8.0. * Add experimental support for HEIF images. Requires libvips compiled with libheif. [#1105](https://github.com/lovell/sharp/issues/1105) +* Expose libwebp `smartSubsample` and `reductionEffort` options. + [#1545](https://github.com/lovell/sharp/issues/1545) + * Add experimental support for Worker Threads. [#1558](https://github.com/lovell/sharp/issues/1558) diff --git a/lib/constructor.js b/lib/constructor.js index e51debe2..253030cb 100644 --- a/lib/constructor.js +++ b/lib/constructor.js @@ -9,6 +9,7 @@ const is = require('./is'); require('./libvips').hasVendoredLibvips(); let sharp; +/* istanbul ignore next */ try { sharp = require('../build/Release/sharp.node'); } catch (err) { @@ -197,6 +198,8 @@ const Sharp = function (input, options) { webpAlphaQuality: 100, webpLossless: false, webpNearLossless: false, + webpSmartSubsample: false, + webpReductionEffort: 4, tiffQuality: 80, tiffCompression: 'jpeg', tiffPredictor: 'horizontal', diff --git a/lib/output.js b/lib/output.js index a7000cd0..57adde4e 100644 --- a/lib/output.js +++ b/lib/output.js @@ -290,6 +290,8 @@ function png (options) { * @param {Number} [options.alphaQuality=100] - quality of alpha layer, integer 0-100 * @param {Boolean} [options.lossless=false] - use lossless compression mode * @param {Boolean} [options.nearLossless=false] - use near_lossless compression mode + * @param {Boolean} [options.smartSubsample=false] - use high quality chroma subsampling + * @param {Number} [options.reductionEffort=4] - level of CPU effort to reduce file size, integer 0-6 * @param {Boolean} [options.force=true] - force WebP output, otherwise attempt to use input format * @returns {Sharp} * @throws {Error} Invalid options @@ -299,14 +301,14 @@ function webp (options) { if (is.integer(options.quality) && is.inRange(options.quality, 1, 100)) { this.options.webpQuality = options.quality; } else { - throw new Error('Invalid quality (integer, 1-100) ' + options.quality); + throw is.invalidParameterError('quality', 'integer between 1 and 100', options.quality); } } if (is.object(options) && is.defined(options.alphaQuality)) { if (is.integer(options.alphaQuality) && is.inRange(options.alphaQuality, 0, 100)) { this.options.webpAlphaQuality = options.alphaQuality; } else { - throw new Error('Invalid webp alpha quality (integer, 0-100) ' + options.alphaQuality); + throw is.invalidParameterError('alphaQuality', 'integer between 0 and 100', options.alphaQuality); } } if (is.object(options) && is.defined(options.lossless)) { @@ -315,6 +317,16 @@ function webp (options) { if (is.object(options) && is.defined(options.nearLossless)) { this._setBooleanOption('webpNearLossless', options.nearLossless); } + if (is.object(options) && is.defined(options.smartSubsample)) { + this._setBooleanOption('webpSmartSubsample', options.smartSubsample); + } + if (is.object(options) && is.defined(options.reductionEffort)) { + if (is.integer(options.reductionEffort) && is.inRange(options.reductionEffort, 0, 6)) { + this.options.webpReductionEffort = options.reductionEffort; + } else { + throw is.invalidParameterError('reductionEffort', 'integer between 0 and 6', options.reductionEffort); + } + } return this._updateFormatOut('webp', options); } diff --git a/src/pipeline.cc b/src/pipeline.cc index 16d753e9..c166bd06 100644 --- a/src/pipeline.cc +++ b/src/pipeline.cc @@ -760,6 +760,8 @@ class PipelineWorker : public Nan::AsyncWorker { ->set("Q", baton->webpQuality) ->set("lossless", baton->webpLossless) ->set("near_lossless", baton->webpNearLossless) + ->set("smart_subsample", baton->webpSmartSubsample) + ->set("reduction_effort", baton->webpReductionEffort) ->set("alpha_q", baton->webpAlphaQuality))); baton->bufferOut = static_cast(area->data); baton->bufferOutLength = area->length; @@ -884,6 +886,8 @@ class PipelineWorker : public Nan::AsyncWorker { ->set("Q", baton->webpQuality) ->set("lossless", baton->webpLossless) ->set("near_lossless", baton->webpNearLossless) + ->set("smart_subsample", baton->webpSmartSubsample) + ->set("reduction_effort", baton->webpReductionEffort) ->set("alpha_q", baton->webpAlphaQuality)); baton->formatOut = "webp"; } else if (baton->formatOut == "tiff" || (mightMatchInput && isTiff) || @@ -938,7 +942,9 @@ class PipelineWorker : public Nan::AsyncWorker { {"Q", std::to_string(baton->webpQuality)}, {"alpha_q", std::to_string(baton->webpAlphaQuality)}, {"lossless", baton->webpLossless ? "TRUE" : "FALSE"}, - {"near_lossless", baton->webpNearLossless ? "TRUE" : "FALSE"} + {"near_lossless", baton->webpNearLossless ? "TRUE" : "FALSE"}, + {"smart_subsample", baton->webpSmartSubsample ? "TRUE" : "FALSE"}, + {"reduction_effort", std::to_string(baton->webpReductionEffort)} }; suffix = AssembleSuffixString(".webp", options); } else { @@ -1345,6 +1351,8 @@ NAN_METHOD(pipeline) { baton->webpAlphaQuality = AttrTo(options, "webpAlphaQuality"); baton->webpLossless = AttrTo(options, "webpLossless"); baton->webpNearLossless = AttrTo(options, "webpNearLossless"); + baton->webpSmartSubsample = AttrTo(options, "webpSmartSubsample"); + baton->webpReductionEffort = AttrTo(options, "webpReductionEffort"); baton->tiffQuality = AttrTo(options, "tiffQuality"); baton->tiffPyramid = AttrTo(options, "tiffPyramid"); baton->tiffSquash = AttrTo(options, "tiffSquash"); diff --git a/src/pipeline.h b/src/pipeline.h index 0d0481d0..f3813531 100644 --- a/src/pipeline.h +++ b/src/pipeline.h @@ -138,6 +138,8 @@ struct PipelineBaton { int webpAlphaQuality; bool webpNearLossless; bool webpLossless; + bool webpSmartSubsample; + int webpReductionEffort; int tiffQuality; VipsForeignTiffCompression tiffCompression; VipsForeignTiffPredictor tiffPredictor; @@ -241,6 +243,11 @@ struct PipelineBaton { pngColours(256), pngDither(1.0), webpQuality(80), + webpAlphaQuality(100), + webpNearLossless(false), + webpLossless(false), + webpSmartSubsample(false), + webpReductionEffort(4), tiffQuality(80), tiffCompression(VIPS_FOREIGN_TIFF_COMPRESSION_JPEG), tiffPredictor(VIPS_FOREIGN_TIFF_PREDICTOR_HORIZONTAL), diff --git a/test/unit/webp.js b/test/unit/webp.js index e6705956..6d46566b 100644 --- a/test/unit/webp.js +++ b/test/unit/webp.js @@ -75,4 +75,54 @@ describe('WebP', function () { fixtures.assertSimilar(fixtures.expected('webp-near-lossless-50.webp'), data50, done); }); }); + + it('should produce a larger file size using smartSubsample', () => + sharp(fixtures.inputJpg) + .resize(320, 240) + .webp({ smartSubsample: false }) + .toBuffer() + .then(withoutSmartSubsample => { + sharp(fixtures.inputJpg) + .resize(320, 240) + .webp({ smartSubsample: true }) + .toBuffer() + .then(withSmartSubsample => { + assert.strictEqual(true, withSmartSubsample.length > withoutSmartSubsample.length); + }); + }) + ); + + it('invalid smartSubsample throws', () => { + assert.throws(() => { + sharp().webp({ smartSubsample: 1 }); + }); + }); + + it('should produce a smaller file size with increased reductionEffort', () => + sharp(fixtures.inputJpg) + .resize(320, 240) + .webp() + .toBuffer() + .then(reductionEffort4 => { + sharp(fixtures.inputJpg) + .resize(320, 240) + .webp({ reductionEffort: 6 }) + .toBuffer() + .then(reductionEffort6 => { + assert.strictEqual(true, reductionEffort4.length > reductionEffort6.length); + }); + }) + ); + + it('invalid reductionEffort throws', () => { + assert.throws(() => { + sharp().webp({ reductionEffort: true }); + }); + }); + + it('out of range reductionEffort throws', () => { + assert.throws(() => { + sharp().webp({ reductionEffort: -1 }); + }); + }); });