From 3e41f8b65e99176fc64d4669f753f42fefc84add Mon Sep 17 00:00:00 2001 From: Lovell Fuller Date: Fri, 21 Mar 2025 09:36:25 +0000 Subject: [PATCH] Non-animated GIF output defaults to no-loop #3394 --- docs/src/content/docs/changelog.md | 3 +++ lib/constructor.js | 2 ++ src/pipeline.cc | 9 ++------- src/pipeline.h | 2 +- test/unit/gif.js | 14 ++++++++++++++ 5 files changed, 22 insertions(+), 8 deletions(-) diff --git a/docs/src/content/docs/changelog.md b/docs/src/content/docs/changelog.md index e66f042f..9e30b970 100644 --- a/docs/src/content/docs/changelog.md +++ b/docs/src/content/docs/changelog.md @@ -14,6 +14,9 @@ Requires libvips v8.16.1 * Breaking: Ensure `removeAlpha` removes all alpha channels. [#2266](https://github.com/lovell/sharp/issues/2266) +* Breaking: Non-animated GIF output defaults to no-loop instead of loop-forever. + [#3394](https://github.com/lovell/sharp/issues/3394) + * Breaking: Support `info.size` on wide-character systems via upgrade to C++17. [#3943](https://github.com/lovell/sharp/issues/3943) diff --git a/lib/constructor.js b/lib/constructor.js index 7290d368..32a80ede 100644 --- a/lib/constructor.js +++ b/lib/constructor.js @@ -296,6 +296,8 @@ const Sharp = function (input, options) { withExif: {}, withExifMerge: true, resolveWithObject: false, + loop: 1, + delay: [], // output format jpegQuality: 80, jpegProgressive: false, diff --git a/src/pipeline.cc b/src/pipeline.cc index 69715aa7..770c2bbd 100644 --- a/src/pipeline.cc +++ b/src/pipeline.cc @@ -1705,6 +1705,8 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) { } baton->withExifMerge = sharp::AttrAsBool(options, "withExifMerge"); baton->timeoutSeconds = sharp::AttrAsUint32(options, "timeoutSeconds"); + baton->loop = sharp::AttrAsUint32(options, "loop"); + baton->delay = sharp::AttrAsInt32Vector(options, "delay"); // Format-specific baton->jpegQuality = sharp::AttrAsUint32(options, "jpegQuality"); baton->jpegProgressive = sharp::AttrAsBool(options, "jpegProgressive"); @@ -1774,13 +1776,6 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) { baton->jxlEffort = sharp::AttrAsUint32(options, "jxlEffort"); baton->jxlLossless = sharp::AttrAsBool(options, "jxlLossless"); baton->rawDepth = sharp::AttrAsEnum(options, "rawDepth", VIPS_TYPE_BAND_FORMAT); - // Animated output properties - if (sharp::HasAttr(options, "loop")) { - baton->loop = sharp::AttrAsUint32(options, "loop"); - } - if (sharp::HasAttr(options, "delay")) { - baton->delay = sharp::AttrAsInt32Vector(options, "delay"); - } baton->tileSize = sharp::AttrAsUint32(options, "tileSize"); baton->tileOverlap = sharp::AttrAsUint32(options, "tileOverlap"); baton->tileAngle = sharp::AttrAsInt32(options, "tileAngle"); diff --git a/src/pipeline.h b/src/pipeline.h index be53d6ad..90d6f85c 100644 --- a/src/pipeline.h +++ b/src/pipeline.h @@ -380,7 +380,7 @@ struct PipelineBaton { ensureAlpha(-1.0), colourspacePipeline(VIPS_INTERPRETATION_LAST), colourspace(VIPS_INTERPRETATION_LAST), - loop(-1), + loop(1), tileSize(256), tileOverlap(0), tileContainer(VIPS_FOREIGN_DZ_CONTAINER_FS), diff --git a/test/unit/gif.js b/test/unit/gif.js index f067dcc1..3b041b69 100644 --- a/test/unit/gif.js +++ b/test/unit/gif.js @@ -224,4 +224,18 @@ describe('GIF input', () => { const after = await input.gif({ interPaletteMaxError: 100 }).toBuffer(); assert.strict(before.length > after.length); }); + + it('non-animated input defaults to no-loop', async () => { + for (const input of [fixtures.inputGif, fixtures.inputPng]) { + const data = await sharp(input) + .resize(8) + .gif({ effort: 1 }) + .toBuffer(); + + const { format, pages, loop } = await sharp(data).metadata(); + assert.strictEqual('gif', format); + assert.strictEqual(1, pages); + assert.strictEqual(1, loop); + } + }); });