summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/webcodecs/image-decoder.https.any.js
diff options
context:
space:
mode:
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.js502
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');