Expose control over CPU effort for palette PNG #2541

This commit is contained in:
Lovell Fuller 2021-11-24 17:12:53 +00:00
parent 140eeebb3d
commit 72025051c5
7 changed files with 27 additions and 3 deletions

View File

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

View File

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

View File

@ -234,6 +234,7 @@ const Sharp = function (input, options) {
pngAdaptiveFiltering: false,
pngPalette: false,
pngQuality: 100,
pngEffort: 7,
pngBitdepth: 8,
pngDither: 1,
jp2Quality: 80,

View File

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

View File

@ -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<char*>(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");

View File

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

View File

@ -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([