diff options
Diffstat (limited to 'testing/web-platform/tests/webcodecs/image-decoder.https.any.js')
-rw-r--r-- | testing/web-platform/tests/webcodecs/image-decoder.https.any.js | 502 |
1 files changed, 502 insertions, 0 deletions
diff --git a/testing/web-platform/tests/webcodecs/image-decoder.https.any.js b/testing/web-platform/tests/webcodecs/image-decoder.https.any.js new file mode 100644 index 0000000000..78eea763aa --- /dev/null +++ b/testing/web-platform/tests/webcodecs/image-decoder.https.any.js @@ -0,0 +1,502 @@ +// META: global=window,dedicatedworker +// META: script=/webcodecs/image-decoder-utils.js + +function testFourColorsDecode(filename, mimeType, options = {}) { + var decoder = null; + return ImageDecoder.isTypeSupported(mimeType).then(support => { + assert_implements_optional( + support, 'Optional codec ' + mimeType + ' not supported.'); + return fetch(filename).then(response => { + return testFourColorsDecodeBuffer(response.body, mimeType, options); + }); + }); +} + +// Note: Requiring all data to do YUV decoding is a Chromium limitation, other +// implementations may support YUV decode with partial ReadableStream data. +function testFourColorsYuvDecode(filename, mimeType, options = {}) { + var decoder = null; + return ImageDecoder.isTypeSupported(mimeType).then(support => { + assert_implements_optional( + support, 'Optional codec ' + mimeType + ' not supported.'); + return fetch(filename).then(response => { + return response.arrayBuffer().then(buffer => { + return testFourColorsDecodeBuffer(buffer, mimeType, options); + }); + }); + }); +} + +promise_test(t => { + return testFourColorsDecode('four-colors.jpg', 'image/jpeg'); +}, 'Test JPEG image decoding.'); + +promise_test(t => { + return testFourColorDecodeWithExifOrientation(1); +}, 'Test JPEG w/ EXIF orientation top-left.'); + +promise_test(t => { + return testFourColorDecodeWithExifOrientation(2); +}, 'Test JPEG w/ EXIF orientation top-right.'); + +promise_test(t => { + return testFourColorDecodeWithExifOrientation(3); +}, 'Test JPEG w/ EXIF orientation bottom-right.'); + +promise_test(t => { + return testFourColorDecodeWithExifOrientation(4); +}, 'Test JPEG w/ EXIF orientation bottom-left.'); + +promise_test(t => { + return testFourColorDecodeWithExifOrientation(5); +}, 'Test JPEG w/ EXIF orientation left-top.'); + +promise_test(t => { + return testFourColorDecodeWithExifOrientation(6); +}, 'Test JPEG w/ EXIF orientation right-top.'); + +promise_test(t => { + return testFourColorDecodeWithExifOrientation(7); +}, 'Test JPEG w/ EXIF orientation right-bottom.'); + +promise_test(t => { + return testFourColorDecodeWithExifOrientation(8); +}, 'Test JPEG w/ EXIF orientation left-bottom.'); + +promise_test(t => { + return testFourColorDecodeWithExifOrientation(1, null, /*useYuv=*/ true); +}, 'Test 4:2:0 JPEG w/ EXIF orientation top-left.'); + +promise_test(t => { + return testFourColorDecodeWithExifOrientation(2, null, /*useYuv=*/ true); +}, 'Test 4:2:0 JPEG w/ EXIF orientation top-right.'); + +promise_test(t => { + return testFourColorDecodeWithExifOrientation(3, null, /*useYuv=*/ true); +}, 'Test 4:2:0 JPEG w/ EXIF orientation bottom-right.'); + +promise_test(t => { + return testFourColorDecodeWithExifOrientation(4, null, /*useYuv=*/ true); +}, 'Test 4:2:0 JPEG w/ EXIF orientation bottom-left.'); + +promise_test(t => { + return testFourColorDecodeWithExifOrientation(5, null, /*useYuv=*/ true); +}, 'Test 4:2:0 JPEG w/ EXIF orientation left-top.'); + +promise_test(t => { + return testFourColorDecodeWithExifOrientation(6, null, /*useYuv=*/ true); +}, 'Test 4:2:0 JPEG w/ EXIF orientation right-top.'); + +promise_test(t => { + return testFourColorDecodeWithExifOrientation(7, null, /*useYuv=*/ true); +}, 'Test 4:2:0 JPEG w/ EXIF orientation right-bottom.'); + +promise_test(t => { + return testFourColorDecodeWithExifOrientation(8, null, /*useYuv=*/ true); +}, 'Test 4:2:0 JPEG w/ EXIF orientation left-bottom.'); + + +promise_test(t => { + return testFourColorsDecode('four-colors.png', 'image/png'); +}, 'Test PNG image decoding.'); + +promise_test(t => { + return testFourColorsDecode('four-colors.avif', 'image/avif'); +}, 'Test AVIF image decoding.'); + +promise_test(t => { + return testFourColorsDecode( + 'four-colors-full-range-bt2020-pq-444-10bpc.avif', 'image/avif', + { tolerance: 3 }); +}, 'Test high bit depth HDR AVIF image decoding.'); + +promise_test(t => { + return testFourColorsDecode( + 'four-colors-flip.avif', 'image/avif', {preferAnimation: false}); +}, 'Test multi-track AVIF image decoding w/ preferAnimation=false.'); + +promise_test(t => { + return testFourColorsDecode( + 'four-colors-flip.avif', 'image/avif', {preferAnimation: true}); +}, 'Test multi-track AVIF image decoding w/ preferAnimation=true.'); + +promise_test(t => { + return testFourColorsDecode('four-colors.webp', 'image/webp'); +}, 'Test WEBP image decoding.'); + +promise_test(t => { + return testFourColorsDecode('four-colors.gif', 'image/gif'); +}, 'Test GIF image decoding.'); + +promise_test(t => { + return testFourColorsYuvDecode( + 'four-colors-limited-range-420-8bpc.jpg', 'image/jpeg', + {yuvFormat: 'I420', tolerance: 3}); +}, 'Test JPEG image YUV 4:2:0 decoding.'); + +promise_test(t => { + return testFourColorsYuvDecode( + 'four-colors-limited-range-420-8bpc.avif', 'image/avif', + {yuvFormat: 'I420', tolerance: 3}); +}, 'Test AVIF image YUV 4:2:0 decoding.'); + +promise_test(t => { + return testFourColorsYuvDecode( + 'four-colors-limited-range-422-8bpc.avif', 'image/avif', + {yuvFormat: 'I422', tolerance: 3}); +}, 'Test AVIF image YUV 4:2:2 decoding.'); + +promise_test(t => { + return testFourColorsYuvDecode( + 'four-colors-limited-range-444-8bpc.avif', 'image/avif', + {yuvFormat: 'I444', tolerance: 3}); +}, 'Test AVIF image YUV 4:4:4 decoding.'); + +promise_test(t => { + return testFourColorsYuvDecode( + 'four-colors-limited-range-420-8bpc.webp', 'image/webp', + {yuvFormat: 'I420', tolerance: 3}); +}, 'Test WEBP image YUV 4:2:0 decoding.'); + +promise_test(t => { + return fetch('four-colors.png').then(response => { + let decoder = new ImageDecoder({data: response.body, type: 'junk/type'}); + return promise_rejects_dom(t, 'NotSupportedError', decoder.decode()); + }); +}, 'Test invalid mime type rejects decode() requests'); + +promise_test(t => { + return fetch('four-colors.png').then(response => { + let decoder = new ImageDecoder({data: response.body, type: 'junk/type'}); + return promise_rejects_dom(t, 'NotSupportedError', decoder.tracks.ready); + }); +}, 'Test invalid mime type rejects decodeMetadata() requests'); + +promise_test(t => { + return ImageDecoder.isTypeSupported('image/png').then(support => { + assert_implements_optional( + support, 'Optional codec image/png not supported.'); + return fetch('four-colors.png') + .then(response => { + return response.arrayBuffer(); + }) + .then(buffer => { + let decoder = new ImageDecoder({data: buffer, type: 'image/png'}); + return promise_rejects_js( + t, RangeError, decoder.decode({frameIndex: 1})); + }); + }); +}, 'Test out of range index returns RangeError'); + +promise_test(t => { + var decoder; + var p1; + return ImageDecoder.isTypeSupported('image/png').then(support => { + assert_implements_optional( + support, 'Optional codec image/png not supported.'); + return fetch('four-colors.png') + .then(response => { + return response.arrayBuffer(); + }) + .then(buffer => { + decoder = + new ImageDecoder({data: buffer.slice(0, 100), type: 'image/png'}); + return decoder.tracks.ready; + }) + .then(_ => { + // Queue two decodes to ensure index verification and decoding are + // properly ordered. + p1 = decoder.decode({frameIndex: 0}); + return promise_rejects_js( + t, RangeError, decoder.decode({frameIndex: 1})); + }) + .then(_ => { + return promise_rejects_js(t, RangeError, p1); + }) + }); +}, 'Test partial decoding without a frame results in an error'); + +promise_test(t => { + var decoder; + var p1; + return ImageDecoder.isTypeSupported('image/png').then(support => { + assert_implements_optional( + support, 'Optional codec image/png not supported.'); + return fetch('four-colors.png') + .then(response => { + return response.arrayBuffer(); + }) + .then(buffer => { + decoder = + new ImageDecoder({data: buffer.slice(0, 100), type: 'image/png'}); + return decoder.completed; + }) + }); +}, 'Test completed property on fully buffered decode'); + +promise_test(t => { + var decoder = null; + + return ImageDecoder.isTypeSupported('image/png').then(support => { + assert_implements_optional( + support, 'Optional codec image/png not supported.'); + return fetch('four-colors.png') + .then(response => { + decoder = new ImageDecoder({data: response.body, type: 'image/png'}); + return decoder.tracks.ready; + }) + .then(_ => { + decoder.tracks.selectedTrack.selected = false; + assert_equals(decoder.tracks.selectedIndex, -1); + assert_equals(decoder.tracks.selectedTrack, null); + return decoder.tracks.ready; + }) + .then(_ => { + return promise_rejects_dom(t, 'InvalidStateError', decoder.decode()); + }) + .then(_ => { + decoder.tracks[0].selected = true; + assert_equals(decoder.tracks.selectedIndex, 0); + assert_not_equals(decoder.tracks.selected, null); + return decoder.decode(); + }) + .then(result => { + assert_equals(result.image.displayWidth, 320); + assert_equals(result.image.displayHeight, 240); + }); + }); +}, 'Test decode, decodeMetadata after no track selected.'); + +promise_test(t => { + var decoder = null; + + return ImageDecoder.isTypeSupported('image/avif').then(support => { + assert_implements_optional( + support, 'Optional codec image/avif not supported.'); + return fetch('four-colors-flip.avif') + .then(response => { + decoder = new ImageDecoder({ + data: response.body, + type: 'image/avif', + preferAnimation: false + }); + return decoder.tracks.ready; + }) + .then(_ => { + assert_equals(decoder.tracks.length, 2); + assert_false(decoder.tracks[decoder.tracks.selectedIndex].animated) + assert_false(decoder.tracks.selectedTrack.animated); + assert_equals(decoder.tracks.selectedTrack.frameCount, 1); + assert_equals(decoder.tracks.selectedTrack.repetitionCount, 0); + return decoder.decode(); + }) + .then(result => { + assert_equals(result.image.displayWidth, 320); + assert_equals(result.image.displayHeight, 240); + assert_equals(result.image.timestamp, 0); + + // Swap to the the other track. + let newIndex = (decoder.tracks.selectedIndex + 1) % 2; + decoder.tracks[newIndex].selected = true; + return decoder.decode() + }) + .then(result => { + assert_equals(result.image.displayWidth, 320); + assert_equals(result.image.displayHeight, 240); + assert_equals(result.image.timestamp, 0); + assert_equals(result.image.duration, 10000); + + assert_equals(decoder.tracks.length, 2); + assert_true(decoder.tracks[decoder.tracks.selectedIndex].animated) + assert_true(decoder.tracks.selectedTrack.animated); + assert_equals(decoder.tracks.selectedTrack.frameCount, 7); + assert_equals(decoder.tracks.selectedTrack.repetitionCount, Infinity); + return decoder.decode({frameIndex: 1}); + }) + .then(result => { + assert_equals(result.image.timestamp, 10000); + assert_equals(result.image.duration, 10000); + }); + }); +}, 'Test track selection in multi track image.'); + +class InfiniteGifSource { + async load(repetitionCount) { + let response = await fetch('four-colors-flip.gif'); + let buffer = await response.arrayBuffer(); + + // Strip GIF trailer (0x3B) so we can continue to append frames. + this.baseImage = new Uint8Array(buffer.slice(0, buffer.byteLength - 1)); + this.baseImage[0x23] = repetitionCount; + this.counter = 0; + } + + start(controller) { + this.controller = controller; + this.controller.enqueue(this.baseImage); + } + + close() { + this.controller.enqueue(new Uint8Array([0x3B])); + this.controller.close(); + } + + addFrame() { + const FRAME1_START = 0x26; + const FRAME2_START = 0x553; + + if (this.counter++ % 2 == 0) + this.controller.enqueue(this.baseImage.slice(FRAME1_START, FRAME2_START)); + else + this.controller.enqueue(this.baseImage.slice(FRAME2_START)); + } +} + +promise_test(async t => { + let support = await ImageDecoder.isTypeSupported('image/gif'); + assert_implements_optional( + support, 'Optional codec image/gif not supported.'); + + let source = new InfiniteGifSource(); + await source.load(5); + + let stream = new ReadableStream(source, {type: 'bytes'}); + let decoder = new ImageDecoder({data: stream, type: 'image/gif'}); + return decoder.tracks.ready + .then(_ => { + assert_equals(decoder.tracks.selectedTrack.frameCount, 2); + assert_equals(decoder.tracks.selectedTrack.repetitionCount, 5); + + source.addFrame(); + return decoder.decode({frameIndex: 2}); + }) + .then(result => { + assert_equals(decoder.tracks.selectedTrack.frameCount, 3); + assert_equals(result.image.displayWidth, 320); + assert_equals(result.image.displayHeight, 240); + + // Note: The stream has an alternating duration of 30ms, 40ms per frame. + assert_equals(result.image.timestamp, 70000, "timestamp frame 2"); + assert_equals(result.image.duration, 30000, "duration frame 2"); + source.addFrame(); + return decoder.decode({frameIndex: 3}); + }) + .then(result => { + assert_equals(decoder.tracks.selectedTrack.frameCount, 4); + assert_equals(result.image.displayWidth, 320); + assert_equals(result.image.displayHeight, 240); + assert_equals(result.image.timestamp, 100000, "timestamp frame 3"); + assert_equals(result.image.duration, 40000, "duration frame 3"); + + // Decode frame not yet available then reset before it comes in. + let p = decoder.decode({frameIndex: 5}); + decoder.reset(); + return promise_rejects_dom(t, 'AbortError', p); + }) + .then(_ => { + // Ensure we can still decode earlier frames. + assert_equals(decoder.tracks.selectedTrack.frameCount, 4); + return decoder.decode({frameIndex: 3}); + }) + .then(result => { + assert_equals(decoder.tracks.selectedTrack.frameCount, 4); + assert_equals(result.image.displayWidth, 320); + assert_equals(result.image.displayHeight, 240); + assert_equals(result.image.timestamp, 100000, "timestamp frame 3"); + assert_equals(result.image.duration, 40000, "duration frame 3"); + + // Decode frame not yet available then close before it comes in. + let p = decoder.decode({frameIndex: 5}); + let tracks = decoder.tracks; + let track = decoder.tracks.selectedTrack; + decoder.close(); + + assert_equals(decoder.type, ''); + assert_equals(decoder.tracks.length, 0); + assert_equals(tracks.length, 0); + track.selected = true; // Should do nothing. + + // Previous decode should be aborted. + return promise_rejects_dom(t, 'AbortError', p); + }) + .then(_ => { + // Ensure feeding the source after closing doesn't crash. + assert_throws_js(TypeError, () => { + source.addFrame(); + }); + }); +}, 'Test ReadableStream of gif'); + +promise_test(async t => { + let support = await ImageDecoder.isTypeSupported('image/gif'); + assert_implements_optional( + support, 'Optional codec image/gif not supported.'); + + let source = new InfiniteGifSource(); + await source.load(5); + + let stream = new ReadableStream(source, {type: 'bytes'}); + let decoder = new ImageDecoder({data: stream, type: 'image/gif'}); + return decoder.tracks.ready.then(_ => { + assert_equals(decoder.tracks.selectedTrack.frameCount, 2); + assert_equals(decoder.tracks.selectedTrack.repetitionCount, 5); + + decoder.decode({frameIndex: 2}).then(t.unreached_func()); + decoder.decode({frameIndex: 1}).then(t.unreached_func()); + return decoder.tracks.ready; + }); +}, 'Test that decode requests are serialized.'); + +promise_test(async t => { + let support = await ImageDecoder.isTypeSupported('image/gif'); + assert_implements_optional( + support, 'Optional codec image/gif not supported.'); + + let source = new InfiniteGifSource(); + await source.load(5); + + let stream = new ReadableStream(source, {type: 'bytes'}); + let decoder = new ImageDecoder({data: stream, type: 'image/gif'}); + return decoder.tracks.ready.then(_ => { + assert_equals(decoder.tracks.selectedTrack.frameCount, 2); + assert_equals(decoder.tracks.selectedTrack.repetitionCount, 5); + + // Decode frame not yet available then change tracks before it comes in. + let p = decoder.decode({frameIndex: 5}); + decoder.tracks.selectedTrack.selected = false; + return promise_rejects_dom(t, 'AbortError', p); + }); +}, 'Test ReadableStream aborts promises on track change'); + +promise_test(async t => { + let support = await ImageDecoder.isTypeSupported('image/gif'); + assert_implements_optional( + support, 'Optional codec image/gif not supported.'); + + let source = new InfiniteGifSource(); + await source.load(5); + + let stream = new ReadableStream(source, {type: 'bytes'}); + let decoder = new ImageDecoder({data: stream, type: 'image/gif'}); + return decoder.tracks.ready.then(_ => { + let p = decoder.completed; + decoder.close(); + return promise_rejects_dom(t, 'AbortError', p); + }); +}, 'Test ReadableStream aborts completed on close'); + +promise_test(async t => { + let support = await ImageDecoder.isTypeSupported('image/gif'); + assert_implements_optional( + support, 'Optional codec image/gif not supported.'); + + let source = new InfiniteGifSource(); + await source.load(5); + + let stream = new ReadableStream(source, {type: 'bytes'}); + let decoder = new ImageDecoder({data: stream, type: 'image/gif'}); + return decoder.tracks.ready.then(_ => { + source.close(); + return decoder.completed; + }); +}, 'Test ReadableStream resolves completed'); |