diff --git a/docs/api-output.md b/docs/api-output.md index 4f34bfd8..c3c5b1ea 100644 --- a/docs/api-output.md +++ b/docs/api-output.md @@ -321,10 +321,14 @@ Use these GIF options for the output image. The first entry in the palette is reserved for transparency. +The palette of the input image will be re-used if possible. + ### Parameters * `options` **[Object][6]?** output options + * `options.reoptimise` **[boolean][10]** always generate new palettes (slow), re-use existing by default (optional, default `false`) + * `options.reoptimize` **[boolean][10]** alternative spelling of `options.reoptimise` (optional, default `false`) * `options.colours` **[number][12]** maximum number of palette entries, including transparency, between 2 and 256 (optional, default `256`) * `options.colors` **[number][12]** alternative spelling of `options.colours` (optional, default `256`) * `options.effort` **[number][12]** CPU effort, between 1 (fastest) and 10 (slowest) (optional, default `7`) diff --git a/docs/changelog.md b/docs/changelog.md index 337a74f5..cfb47c03 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -8,6 +8,8 @@ Requires libvips v8.13.0 * Drop support for Node.js 12, now requires Node.js >= 14.15.0. +* GIF output now re-uses input palette if possible. Use `reoptimise` option to generate a new palette. + * Add WebP `minSize` and `mixed` options for greater control over animation frames. * Use combined bounding box of alpha and non-alpha channels for `trim` operation. diff --git a/lib/constructor.js b/lib/constructor.js index cd6241df..411384e1 100644 --- a/lib/constructor.js +++ b/lib/constructor.js @@ -256,6 +256,7 @@ const Sharp = function (input, options) { gifBitdepth: 8, gifEffort: 7, gifDither: 1, + gifReoptimise: false, tiffQuality: 80, tiffCompression: 'jpeg', tiffPredictor: 'horizontal', diff --git a/lib/output.js b/lib/output.js index f40231b3..a23b1a63 100644 --- a/lib/output.js +++ b/lib/output.js @@ -524,6 +524,8 @@ function webp (options) { * * The first entry in the palette is reserved for transparency. * + * The palette of the input image will be re-used if possible. + * * @since 0.30.0 * * @example @@ -545,6 +547,8 @@ function webp (options) { * .toBuffer(); * * @param {Object} [options] - output options + * @param {boolean} [options.reoptimise=false] - always generate new palettes (slow), re-use existing by default + * @param {boolean} [options.reoptimize=false] - alternative spelling of `options.reoptimise` * @param {number} [options.colours=256] - maximum number of palette entries, including transparency, between 2 and 256 * @param {number} [options.colors=256] - alternative spelling of `options.colours` * @param {number} [options.effort=7] - CPU effort, between 1 (fastest) and 10 (slowest) @@ -557,6 +561,11 @@ function webp (options) { */ function gif (options) { if (is.object(options)) { + if (is.defined(options.reoptimise)) { + this._setBooleanOption('gifReoptimise', options.reoptimise); + } else if (is.defined(options.reoptimize)) { + this._setBooleanOption('gifReoptimise', options.reoptimize); + } 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 70709417..1a858ff6 100644 --- a/src/pipeline.cc +++ b/src/pipeline.cc @@ -888,6 +888,7 @@ class PipelineWorker : public Napi::AsyncWorker { ->set("strip", !baton->withMetadata) ->set("bitdepth", baton->gifBitdepth) ->set("effort", baton->gifEffort) + ->set("reoptimise", baton->gifReoptimise) ->set("dither", baton->gifDither))); baton->bufferOut = static_cast(area->data); baton->bufferOutLength = area->length; @@ -1053,6 +1054,7 @@ class PipelineWorker : public Napi::AsyncWorker { ->set("strip", !baton->withMetadata) ->set("bitdepth", baton->gifBitdepth) ->set("effort", baton->gifEffort) + ->set("reoptimise", baton->gifReoptimise) ->set("dither", baton->gifDither)); baton->formatOut = "gif"; } else if (baton->formatOut == "tiff" || (mightMatchInput && isTiff) || @@ -1537,6 +1539,7 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) { baton->gifBitdepth = sharp::AttrAsUint32(options, "gifBitdepth"); baton->gifEffort = sharp::AttrAsUint32(options, "gifEffort"); baton->gifDither = sharp::AttrAsDouble(options, "gifDither"); + baton->gifReoptimise = sharp::AttrAsBool(options, "gifReoptimise"); baton->tiffQuality = sharp::AttrAsUint32(options, "tiffQuality"); baton->tiffPyramid = sharp::AttrAsBool(options, "tiffPyramid"); baton->tiffBitdepth = sharp::AttrAsUint32(options, "tiffBitdepth"); diff --git a/src/pipeline.h b/src/pipeline.h index 1f082421..3696e857 100644 --- a/src/pipeline.h +++ b/src/pipeline.h @@ -162,6 +162,7 @@ struct PipelineBaton { int gifBitdepth; int gifEffort; double gifDither; + bool gifReoptimise; int tiffQuality; VipsForeignTiffCompression tiffCompression; VipsForeignTiffPredictor tiffPredictor; @@ -306,6 +307,10 @@ struct PipelineBaton { webpEffort(4), webpMinSize(false), webpMixed(false), + gifBitdepth(8), + gifEffort(7), + gifDither(1.0), + gifReoptimise(false), tiffQuality(80), tiffCompression(VIPS_FOREIGN_TIFF_COMPRESSION_JPEG), tiffPredictor(VIPS_FOREIGN_TIFF_PREDICTOR_HORIZONTAL), diff --git a/test/unit/gif.js b/test/unit/gif.js index d44b54f9..89f7901c 100644 --- a/test/unit/gif.js +++ b/test/unit/gif.js @@ -80,6 +80,22 @@ describe('GIF input', () => { assert.strictEqual(true, reduced.length < original.length); }); + it('valid optimise', () => { + assert.doesNotThrow(() => sharp().gif({ reoptimise: true })); + assert.doesNotThrow(() => sharp().gif({ reoptimize: true })); + }); + + it('invalid reoptimise throws', () => { + assert.throws( + () => sharp().gif({ reoptimise: -1 }), + /Expected boolean for gifReoptimise but received -1 of type number/ + ); + assert.throws( + () => sharp().gif({ reoptimize: 'fail' }), + /Expected boolean for gifReoptimise but received fail of type string/ + ); + }); + it('invalid loop throws', () => { assert.throws(() => { sharp().gif({ loop: -1 });