mirror of
https://github.com/mozilla-firefox/firefox
synced 2026-08-12 12:35:33 +00:00
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 <noreply@anthropic.com> Differential Revision: https://phabricator.services.mozilla.com/D298735
This commit is contained in:
committed by
aosmond@mozilla.com
co-authored by
Claude Opus 4.6
parent
1444d4d26d
commit
06e6579ee6
+12
-6
@@ -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;
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 103 B |
@@ -0,0 +1,48 @@
|
||||
// META: global=window,dedicatedworker
|
||||
|
||||
// Bug: An animated GIF whose first frame has delay_time=0 is incorrectly
|
||||
// detected as non-animated by the metadata decoder. This causes ImageDecoder
|
||||
// to report frameCount=1 and reject decode requests for subsequent 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.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');
|
||||
Reference in New Issue
Block a user