diff --git a/docs/api-output.md b/docs/api-output.md index b132ba96..66c013e8 100644 --- a/docs/api-output.md +++ b/docs/api-output.md @@ -243,6 +243,7 @@ Set `palette` to `true` for slower, indexed PNG output. * `options.adaptiveFiltering` **[boolean][7]** use adaptive row filtering (optional, default `false`) * `options.palette` **[boolean][7]** quantise to a palette-based image with alpha transparency support (optional, default `false`) * `options.quality` **[number][9]** use the lowest number of colours needed to achieve given quality, sets `palette` to `true` (optional, default `100`) + * `options.effort` **[number][9]** CPU effort, between 1 (fastest) and 10 (slowest), sets `palette` to `true` (optional, default `7`) * `options.colours` **[number][9]** maximum number of palette entries, sets `palette` to `true` (optional, default `256`) * `options.colors` **[number][9]** alternative spelling of `options.colours`, sets `palette` to `true` (optional, default `256`) * `options.dither` **[number][9]** level of Floyd-Steinberg error diffusion, sets `palette` to `true` (optional, default `1.0`) diff --git a/docs/changelog.md b/docs/changelog.md index 924f3f29..d39be72d 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -10,6 +10,9 @@ Requires libvips v8.12.0 * Reduce minimum Linux ARM64v8 glibc requirement to 2.17. +* Expose control over CPU effort for palette-based PNG output. + [#2541](https://github.com/lovell/sharp/issues/2541) + * Ensure 16-bit PNG output uses correct bitdepth. [#2958](https://github.com/lovell/sharp/pull/2958) [@gforge](https://github.com/gforge) diff --git a/lib/constructor.js b/lib/constructor.js index 6e726624..21bd68e7 100644 --- a/lib/constructor.js +++ b/lib/constructor.js @@ -234,6 +234,7 @@ const Sharp = function (input, options) { pngAdaptiveFiltering: false, pngPalette: false, pngQuality: 100, + pngEffort: 7, pngBitdepth: 8, pngDither: 1, jp2Quality: 80, diff --git a/lib/output.js b/lib/output.js index d0f7a8e9..e141a60d 100644 --- a/lib/output.js +++ b/lib/output.js @@ -373,6 +373,7 @@ function jpeg (options) { * @param {boolean} [options.adaptiveFiltering=false] - use adaptive row filtering * @param {boolean} [options.palette=false] - quantise to a palette-based image with alpha transparency support * @param {number} [options.quality=100] - use the lowest number of colours needed to achieve given quality, sets `palette` to `true` + * @param {number} [options.effort=7] - CPU effort, between 1 (fastest) and 10 (slowest), sets `palette` to `true` * @param {number} [options.colours=256] - maximum number of palette entries, sets `palette` to `true` * @param {number} [options.colors=256] - alternative spelling of `options.colours`, sets `palette` to `true` * @param {number} [options.dither=1.0] - level of Floyd-Steinberg error diffusion, sets `palette` to `true` @@ -397,7 +398,7 @@ function png (options) { } if (is.defined(options.palette)) { this._setBooleanOption('pngPalette', options.palette); - } else if (is.defined(options.quality) || is.defined(options.colours || options.colors) || is.defined(options.dither)) { + } else if ([options.quality, options.effort, options.colours, options.colors, options.dither].some(is.defined)) { this._setBooleanOption('pngPalette', true); } if (this.options.pngPalette) { @@ -408,6 +409,13 @@ function png (options) { throw is.invalidParameterError('quality', 'integer between 0 and 100', options.quality); } } + if (is.defined(options.effort)) { + if (is.integer(options.effort) && is.inRange(options.effort, 1, 10)) { + this.options.pngEffort = options.effort; + } else { + throw is.invalidParameterError('effort', 'integer between 1 and 10', options.effort); + } + } const colours = options.colours || options.colors; if (is.defined(colours)) { if (is.integer(colours) && is.inRange(colours, 2, 256)) { diff --git a/src/pipeline.cc b/src/pipeline.cc index 0cb161f5..343fea87 100644 --- a/src/pipeline.cc +++ b/src/pipeline.cc @@ -824,6 +824,7 @@ class PipelineWorker : public Napi::AsyncWorker { ->set("filter", baton->pngAdaptiveFiltering ? VIPS_FOREIGN_PNG_FILTER_ALL : VIPS_FOREIGN_PNG_FILTER_NONE) ->set("palette", baton->pngPalette) ->set("Q", baton->pngQuality) + ->set("effort", baton->pngEffort) ->set("bitdepth", sharp::Is16Bit(image.interpretation()) ? 16 : baton->pngBitdepth) ->set("dither", baton->pngDither))); baton->bufferOut = static_cast(area->data); @@ -994,6 +995,7 @@ class PipelineWorker : public Napi::AsyncWorker { ->set("palette", baton->pngPalette) ->set("Q", baton->pngQuality) ->set("bitdepth", sharp::Is16Bit(image.interpretation()) ? 16 : baton->pngBitdepth) + ->set("effort", baton->pngEffort) ->set("dither", baton->pngDither)); baton->formatOut = "png"; } else if (baton->formatOut == "webp" || (mightMatchInput && isWebp) || @@ -1470,6 +1472,7 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) { baton->pngAdaptiveFiltering = sharp::AttrAsBool(options, "pngAdaptiveFiltering"); baton->pngPalette = sharp::AttrAsBool(options, "pngPalette"); baton->pngQuality = sharp::AttrAsUint32(options, "pngQuality"); + baton->pngEffort = sharp::AttrAsUint32(options, "pngEffort"); baton->pngBitdepth = sharp::AttrAsUint32(options, "pngBitdepth"); baton->pngDither = sharp::AttrAsDouble(options, "pngDither"); baton->jp2Quality = sharp::AttrAsUint32(options, "jp2Quality"); diff --git a/src/pipeline.h b/src/pipeline.h index db1e7873..336ea7bd 100644 --- a/src/pipeline.h +++ b/src/pipeline.h @@ -147,6 +147,7 @@ struct PipelineBaton { bool pngAdaptiveFiltering; bool pngPalette; int pngQuality; + int pngEffort; int pngBitdepth; double pngDither; int jp2Quality; @@ -287,6 +288,7 @@ struct PipelineBaton { pngAdaptiveFiltering(false), pngPalette(false), pngQuality(100), + pngEffort(7), pngBitdepth(8), pngDither(1.0), jp2Quality(80), diff --git a/test/unit/png.js b/test/unit/png.js index 92feb25e..3a8d940d 100644 --- a/test/unit/png.js +++ b/test/unit/png.js @@ -143,8 +143,8 @@ describe('PNG', function () { it('Valid PNG libimagequant quality value produces image of same size or smaller', function () { const inputPngBuffer = fs.readFileSync(fixtures.inputPng); return Promise.all([ - sharp(inputPngBuffer).resize(10).png({ quality: 80 }).toBuffer(), - sharp(inputPngBuffer).resize(10).png({ quality: 100 }).toBuffer() + sharp(inputPngBuffer).resize(10).png({ effort: 1, quality: 80 }).toBuffer(), + sharp(inputPngBuffer).resize(10).png({ effort: 1, quality: 100 }).toBuffer() ]).then(function (data) { assert.strictEqual(true, data[0].length <= data[1].length); }); @@ -156,6 +156,12 @@ describe('PNG', function () { }); }); + it('Invalid effort value throws error', () => { + assert.throws(() => { + sharp().png({ effort: 0.1 }); + }); + }); + it('Valid PNG libimagequant colours value produces image of same size or smaller', function () { const inputPngBuffer = fs.readFileSync(fixtures.inputPng); return Promise.all([