From 06e6579ee6cb532ce90be2ae006bfbca124887ff Mon Sep 17 00:00:00 2001 From: Xidorn Quan Date: Wed, 20 May 2026 17:28:44 +0000 Subject: [PATCH] Bug 2036986 - Fix ImageDecoder failing to decode animated GIFs with zero first-frame delay. r=aosmond Some animated GIFs have a first-frame delay of zero (or no Graphic Control Extension for the first frame). The metadata decoder terminates after parsing the first frame's image descriptor without ever encountering a second frame, so it reports the image as non-animated. Previously, `AnonymousDecoderImpl` would trust this result, set `frameCount=1`/`frameCountComplete=true`, and destroy the frame count task. This caused `ImageDecoder` to return zero for frame count making code running it consider the GIF to be non-animated, differing from the rendering result when such a GIF presents in DOM. The fix always runs the frame count task after metadata completes, even when the metadata decoder reported non-animated. The frame count task does a full parse (with `COUNT_FRAMES` flag) and will discover additional frames. If it finds more than one frame, we update the animated flag before resolving the metadata promise to `ImageDecoder`. Co-Authored-By: Claude Opus 4.6 Differential Revision: https://phabricator.services.mozilla.com/D298735 --- image/ImageUtils.cpp | 18 ++++--- .../tests/webcodecs/animated-zero-delay.gif | Bin 0 -> 103 bytes ...e-decoder-animated-zero-delay.https.any.js | 48 ++++++++++++++++++ 3 files changed, 60 insertions(+), 6 deletions(-) create mode 100644 testing/web-platform/tests/webcodecs/animated-zero-delay.gif create mode 100644 testing/web-platform/tests/webcodecs/image-decoder-animated-zero-delay.https.any.js diff --git a/image/ImageUtils.cpp b/image/ImageUtils.cpp index 003de135d141..86a7e8d431ef 100644 --- a/image/ImageUtils.cpp +++ b/image/ImageUtils.cpp @@ -346,12 +346,7 @@ class AnonymousDecoderImpl final : public AnonymousDecoder { } } - if (!mMetadataResult.mAnimated) { - mMetadataResult.mFrameCount = 1; - mMetadataResult.mFrameCountComplete = true; - mMetadataTask = nullptr; - mFrameCountTask = nullptr; - } else if (mFrameCountTask && !mFrameCountTaskRunning) { + if (mFrameCountTask && !mFrameCountTaskRunning) { MOZ_LOG( sLog, LogLevel::Debug, ("[%p] AnonymousDecoderImpl::OnMetadata -- start frame count task", @@ -391,6 +386,17 @@ class AnonymousDecoderImpl final : public AnonymousDecoder { resolve = true; } + // If the frame count task discovered more than one frame, the image is + // actually animated even if the metadata decoder missed it (e.g. a GIF + // whose first frame has delay_time=0). + if (mFrameCount > 1 && !mMetadataResult.mAnimated) { + MOZ_LOG(sLog, LogLevel::Debug, + ("[%p] AnonymousDecoderImpl::OnFrameCount -- discovered " + "animation, frameCount %u", + this, mFrameCount)); + mMetadataResult.mAnimated = true; + } + // If metadata completing is waiting on an updated frame count, resolve it. mMetadataResult.mFrameCount = mFrameCount; mMetadataResult.mFrameCountComplete = aComplete; diff --git a/testing/web-platform/tests/webcodecs/animated-zero-delay.gif b/testing/web-platform/tests/webcodecs/animated-zero-delay.gif new file mode 100644 index 0000000000000000000000000000000000000000..1cbc4158db0c75b456fe323aaeb4dda5d4a9d4fa GIT binary patch literal 103 zcmZ?wbhEHbWMW`qXk=jc&wv1m|GE8KLxP { + let support = await ImageDecoder.isTypeSupported('image/gif'); + assert_implements_optional(support, 'Optional codec image/gif not supported.'); + + let response = await fetch('animated-zero-delay.gif'); + let buffer = await response.arrayBuffer(); + let decoder = new ImageDecoder({data: buffer, type: 'image/gif'}); + + await decoder.tracks.ready; + assert_equals(decoder.tracks.length, 1, 'Should have one track'); + + let track = decoder.tracks.selectedTrack; + assert_true(track.animated, 'Track should be detected as animated'); + assert_equals(track.frameCount, 2, 'Should report 2 frames'); + + let result0 = await decoder.decode({frameIndex: 0}); + assert_true(result0.complete, 'Frame 0 should be complete'); + assert_equals(result0.image.codedWidth, 2); + assert_equals(result0.image.codedHeight, 2); + + let result1 = await decoder.decode({frameIndex: 1}); + assert_true(result1.complete, 'Frame 1 should be complete'); + assert_equals(result1.image.codedWidth, 2); + assert_equals(result1.image.codedHeight, 2); +}, 'Test animated GIF with zero first-frame delay decodes all frames'); + +promise_test(async t => { + let support = await ImageDecoder.isTypeSupported('image/gif'); + assert_implements_optional(support, 'Optional codec image/gif not supported.'); + + let response = await fetch('animated-zero-delay.gif'); + let buffer = await response.arrayBuffer(); + let decoder = new ImageDecoder({data: buffer, type: 'image/gif'}); + + await decoder.completed; + + let track = decoder.tracks.selectedTrack; + assert_true(track.animated, 'Track should be animated after completed'); + assert_equals(track.frameCount, 2, 'Should report 2 frames after completed'); + assert_equals(track.repetitionCount, Infinity, + 'Should report infinite repetitions (loop=0)'); +}, 'Test animated GIF with zero first-frame delay reports correct metadata');