Files
sousa-gecko/testing/web-platform/tests/jpegxl/imagedecoder-animation.https.html
T
Timothy Nikkel 9b48332245 Bug 2033688. Finite length animated jxl images should loop the correct number of times. r=saschanaz
On the ImageDecoder interface repetitionCount being 0 mean one total play through, see https://github.com/w3c/webcodecs/issues/932

On the jpeg xl side, num_loops being 0 means to loop infinitely https://github.com/libjxl/libjxl/blob/6553831b39a542d6450e76ca5afb64520649d838/lib/jxl/headers.h#L87 and then I confirmed with Jon Sneyers that num_loops being 1 means to have one total play through. (If num_loops being 1 meant two total play throughs then there would be no way to specify one total play through, so that is good.)

There doesn't appear to be any tools that can make finitely looping animated jxl files at this time, so a c file using libjxl was created to create them.

Differential Revision: https://phabricator.services.mozilla.com/D295458
2026-04-28 04:27:16 +00:00

71 lines
2.7 KiB
HTML

<!DOCTYPE html>
<title>JPEG XL: animation via ImageDecoder</title>
<link rel="help" href="https://github.com/web-platform-tests/interop-jpegxl">
<meta name="assert" content="ImageDecoder reports animation track, decodes multiple frames, and reflects repetition count.">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="resources/helpers.js"></script>
<script>
async function test_animated_jxl(filename, { repetitionCount, frameCount, cycleDurationUs }) {
assert_implements('ImageDecoder' in self, 'ImageDecoder should exist');
const supported = await ImageDecoder.isTypeSupported('image/jxl');
assert_true(supported, 'image/jxl should be supported');
const response = await fetch(filename);
const decoder = new ImageDecoder({data: response.body, type: 'image/jxl'});
await decoder.tracks.ready;
const track = decoder.tracks.selectedTrack;
assert_true(track.animated, 'track must be animated');
if (repetitionCount !== undefined) {
assert_equals(track.repetitionCount, repetitionCount,
'repetitionCount must match');
}
await decoder.completed;
const frame_count = track.frameCount;
assert_equals(frame_count, frameCount,
`must have exactly ${frameCount} frames`);
let cycle_duration_us = 0;
let prev_timestamp = null;
for (let i = 0; i < frame_count; i++) {
const result = await decoder.decode({frameIndex: i});
cycle_duration_us += result.image.duration;
if (prev_timestamp !== null) {
assert_less_than(prev_timestamp, result.image.timestamp,
'timestamps should increase');
}
prev_timestamp = result.image.timestamp;
result.image.close();
}
decoder.close();
assert_equals(cycle_duration_us, cycleDurationUs, 'cycle duration must match');
}
promise_test(async () => {
await test_animated_jxl('resources/conformance_animation_spline.jxl',
{ repetitionCount: Infinity, frameCount: 60, cycleDurationUs: 1200000 });
}, 'Animated JPEG XL reports multiple frames and decodes frame sequence.');
promise_test(async t => {
await test_animated_jxl('resources/animation-1loop.jxl',
{ repetitionCount: 0, frameCount: 4, cycleDurationUs: 2000000 });
}, 'Animated JXL with repetition count 0 plays exactly once.');
promise_test(async t => {
await test_animated_jxl('resources/animation-2loops.jxl',
{ repetitionCount: 1, frameCount: 4, cycleDurationUs: 2000000 });
}, 'Animated JXL with repetition count 1 plays exactly twice.');
promise_test(async t => {
await test_animated_jxl('resources/animation-infiniteloop.jxl',
{ repetitionCount: Infinity, frameCount: 4, cycleDurationUs: 2000000 });
}, 'Animated JXL with infinite repetition count repeats indefinitely.');
</script>