image-decoder.https.any.js (20903B)
1 // META: global=window,dedicatedworker 2 // META: script=/webcodecs/image-decoder-utils.js 3 4 async function testFourColorsDecode(filename, mimeType, options = {}) { 5 return ImageDecoder.isTypeSupported(mimeType).then(support => { 6 assert_implements_optional( 7 support, 'Optional codec ' + mimeType + ' not supported.'); 8 return fetch(filename).then(response => { 9 return testFourColorsDecodeBuffer(response.body, mimeType, options); 10 }); 11 }); 12 } 13 14 // Note: Requiring all data to do YUV decoding is a Chromium limitation, other 15 // implementations may support YUV decode with partial ReadableStream data. 16 async function testFourColorsYuvDecode(filename, mimeType, options = {}) { 17 return ImageDecoder.isTypeSupported(mimeType).then(support => { 18 assert_implements_optional( 19 support, 'Optional codec ' + mimeType + ' not supported.'); 20 return fetch(filename).then(response => { 21 return response.arrayBuffer().then(buffer => { 22 return testFourColorsDecodeBuffer(buffer, mimeType, options); 23 }); 24 }); 25 }); 26 } 27 28 promise_test(t => { 29 return testFourColorsDecode('four-colors.jpg', 'image/jpeg'); 30 }, 'Test JPEG image decoding.'); 31 32 promise_test(t => { 33 return testFourColorDecodeWithExifOrientation(1); 34 }, 'Test JPEG w/ EXIF orientation top-left.'); 35 36 promise_test(t => { 37 return testFourColorDecodeWithExifOrientation(2); 38 }, 'Test JPEG w/ EXIF orientation top-right.'); 39 40 promise_test(t => { 41 return testFourColorDecodeWithExifOrientation(3); 42 }, 'Test JPEG w/ EXIF orientation bottom-right.'); 43 44 promise_test(t => { 45 return testFourColorDecodeWithExifOrientation(4); 46 }, 'Test JPEG w/ EXIF orientation bottom-left.'); 47 48 promise_test(t => { 49 return testFourColorDecodeWithExifOrientation(5); 50 }, 'Test JPEG w/ EXIF orientation left-top.'); 51 52 promise_test(t => { 53 return testFourColorDecodeWithExifOrientation(6); 54 }, 'Test JPEG w/ EXIF orientation right-top.'); 55 56 promise_test(t => { 57 return testFourColorDecodeWithExifOrientation(7); 58 }, 'Test JPEG w/ EXIF orientation right-bottom.'); 59 60 promise_test(t => { 61 return testFourColorDecodeWithExifOrientation(8); 62 }, 'Test JPEG w/ EXIF orientation left-bottom.'); 63 64 promise_test(t => { 65 return testFourColorDecodeWithExifOrientation(1, null, /*useYuv=*/ true); 66 }, 'Test 4:2:0 JPEG w/ EXIF orientation top-left.'); 67 68 promise_test(t => { 69 return testFourColorDecodeWithExifOrientation(2, null, /*useYuv=*/ true); 70 }, 'Test 4:2:0 JPEG w/ EXIF orientation top-right.'); 71 72 promise_test(t => { 73 return testFourColorDecodeWithExifOrientation(3, null, /*useYuv=*/ true); 74 }, 'Test 4:2:0 JPEG w/ EXIF orientation bottom-right.'); 75 76 promise_test(t => { 77 return testFourColorDecodeWithExifOrientation(4, null, /*useYuv=*/ true); 78 }, 'Test 4:2:0 JPEG w/ EXIF orientation bottom-left.'); 79 80 promise_test(t => { 81 return testFourColorDecodeWithExifOrientation(5, null, /*useYuv=*/ true); 82 }, 'Test 4:2:0 JPEG w/ EXIF orientation left-top.'); 83 84 promise_test(t => { 85 return testFourColorDecodeWithExifOrientation(6, null, /*useYuv=*/ true); 86 }, 'Test 4:2:0 JPEG w/ EXIF orientation right-top.'); 87 88 promise_test(t => { 89 return testFourColorDecodeWithExifOrientation(7, null, /*useYuv=*/ true); 90 }, 'Test 4:2:0 JPEG w/ EXIF orientation right-bottom.'); 91 92 promise_test(t => { 93 return testFourColorDecodeWithExifOrientation(8, null, /*useYuv=*/ true); 94 }, 'Test 4:2:0 JPEG w/ EXIF orientation left-bottom.'); 95 96 promise_test(t => { 97 return testFourColorsDecode('four-colors.png', 'image/png'); 98 }, 'Test PNG image decoding.'); 99 100 promise_test(t => { 101 return testFourColorsDecode('four-colors.avif', 'image/avif'); 102 }, 'Test AVIF image decoding.'); 103 104 promise_test(t => { 105 return testFourColorsDecode( 106 'four-colors-full-range-bt2020-pq-444-10bpc.avif', 'image/avif', 107 { tolerance: 3 }); 108 }, 'Test high bit depth HDR AVIF image decoding.'); 109 110 promise_test(t => { 111 return testFourColorsDecode( 112 'four-colors-flip.avif', 'image/avif', {preferAnimation: false}); 113 }, 'Test multi-track AVIF image decoding w/ preferAnimation=false.'); 114 115 promise_test(t => { 116 return testFourColorsDecode( 117 'four-colors-flip.avif', 'image/avif', {preferAnimation: true}); 118 }, 'Test multi-track AVIF image decoding w/ preferAnimation=true.'); 119 120 promise_test(t => { 121 return testFourColorsDecode('four-colors.webp', 'image/webp'); 122 }, 'Test WEBP image decoding.'); 123 124 promise_test(t => { 125 return testFourColorsDecode('four-colors.gif', 'image/gif'); 126 }, 'Test GIF image decoding.'); 127 128 promise_test(t => { 129 return testFourColorsYuvDecode( 130 'four-colors-limited-range-420-8bpc.jpg', 'image/jpeg', 131 {yuvFormat: 'I420', tolerance: 3}); 132 }, 'Test JPEG image YUV 4:2:0 decoding.'); 133 134 promise_test(t => { 135 return testFourColorsYuvDecode( 136 'four-colors-limited-range-420-8bpc.avif', 'image/avif', 137 {yuvFormat: 'I420', tolerance: 3}); 138 }, 'Test AVIF image YUV 4:2:0 decoding.'); 139 140 promise_test(t => { 141 return testFourColorsYuvDecode( 142 'four-colors-limited-range-422-8bpc.avif', 'image/avif', 143 {yuvFormat: 'I422', tolerance: 3}); 144 }, 'Test AVIF image YUV 4:2:2 decoding.'); 145 146 promise_test(t => { 147 return testFourColorsYuvDecode( 148 'four-colors-limited-range-444-8bpc.avif', 'image/avif', 149 {yuvFormat: 'I444', tolerance: 3}); 150 }, 'Test AVIF image YUV 4:4:4 decoding.'); 151 152 promise_test(t => { 153 return testFourColorsYuvDecode( 154 'four-colors-limited-range-420-8bpc.webp', 'image/webp', 155 {yuvFormat: 'I420', tolerance: 3}); 156 }, 'Test WEBP image YUV 4:2:0 decoding.'); 157 158 const FOUR_COLORS_AVIF_HLG_COLOR_SPACE = new VideoColorSpace({ 159 primaries: 'bt2020', 160 transfer: 'hlg', 161 matrix: 'bt2020-ncl', 162 fullRange: true, 163 }); 164 promise_test(t => { 165 return testFourColorsYuvDecode( 166 'four-colors-full-range-hlg-420-10bpc.avif', 'image/avif', { 167 yuvFormat: 'I420P10', 168 colorSpace: FOUR_COLORS_AVIF_HLG_COLOR_SPACE, 169 tolerance: 3 170 }); 171 }, 'Test AVIF image HDR YUV 10-bit 4:2:0 decoding.'); 172 173 promise_test(t => { 174 return testFourColorsYuvDecode( 175 'four-colors-full-range-hlg-422-10bpc.avif', 'image/avif', { 176 yuvFormat: 'I422P10', 177 colorSpace: FOUR_COLORS_AVIF_HLG_COLOR_SPACE, 178 tolerance: 3 179 }); 180 }, 'Test AVIF image HDR YUV 10-bit 4:2:2 decoding.'); 181 182 promise_test(t => { 183 return testFourColorsYuvDecode( 184 'four-colors-full-range-hlg-444-10bpc.avif', 'image/avif', { 185 yuvFormat: 'I444P10', 186 colorSpace: FOUR_COLORS_AVIF_HLG_COLOR_SPACE, 187 tolerance: 3 188 }); 189 }, 'Test AVIF image HDR YUV 10-bit 4:4:4 decoding.'); 190 191 promise_test(t => { 192 return testFourColorsYuvDecode( 193 'four-colors-full-range-hlg-420-12bpc.avif', 'image/avif', { 194 yuvFormat: 'I420P12', 195 colorSpace: FOUR_COLORS_AVIF_HLG_COLOR_SPACE, 196 tolerance: 3 197 }); 198 }, 'Test AVIF image HDR YUV 12-bit 4:2:0 decoding.'); 199 200 promise_test(t => { 201 return testFourColorsYuvDecode( 202 'four-colors-full-range-hlg-422-12bpc.avif', 'image/avif', { 203 yuvFormat: 'I422P12', 204 colorSpace: FOUR_COLORS_AVIF_HLG_COLOR_SPACE, 205 tolerance: 3 206 }); 207 }, 'Test AVIF image HDR YUV 12-bit 4:2:2 decoding.'); 208 209 promise_test(t => { 210 return testFourColorsYuvDecode( 211 'four-colors-full-range-hlg-444-12bpc.avif', 'image/avif', { 212 yuvFormat: 'I444P12', 213 colorSpace: FOUR_COLORS_AVIF_HLG_COLOR_SPACE, 214 tolerance: 3 215 }); 216 }, 'Test AVIF image HDR YUV 12-bit 4:4:4 decoding.'); 217 218 promise_test(t => { 219 return fetch('four-colors.png').then(response => { 220 let decoder = new ImageDecoder({data: response.body, type: 'junk/type'}); 221 return promise_rejects_dom(t, 'NotSupportedError', decoder.decode()); 222 }); 223 }, 'Test invalid mime type rejects decode() requests'); 224 225 promise_test(t => { 226 return fetch('four-colors.png').then(response => { 227 let decoder = new ImageDecoder({data: response.body, type: 'junk/type'}); 228 return promise_rejects_dom(t, 'NotSupportedError', decoder.tracks.ready); 229 }); 230 }, 'Test invalid mime type rejects decodeMetadata() requests'); 231 232 promise_test(t => { 233 return ImageDecoder.isTypeSupported('image/png').then(support => { 234 assert_implements_optional( 235 support, 'Optional codec image/png not supported.'); 236 return fetch('four-colors.png') 237 .then(response => { 238 return response.arrayBuffer(); 239 }) 240 .then(buffer => { 241 let decoder = new ImageDecoder({data: buffer, type: 'image/png'}); 242 return promise_rejects_js( 243 t, RangeError, decoder.decode({frameIndex: 1})); 244 }); 245 }); 246 }, 'Test out of range index returns RangeError'); 247 248 promise_test(t => { 249 var decoder; 250 var p1; 251 return ImageDecoder.isTypeSupported('image/png').then(support => { 252 assert_implements_optional( 253 support, 'Optional codec image/png not supported.'); 254 return fetch('four-colors.png') 255 .then(response => { 256 return response.arrayBuffer(); 257 }) 258 .then(buffer => { 259 // IDAT chunk starts at byte 83 (0x53). 260 decoder = 261 new ImageDecoder({data: buffer.slice(0, 100), type: 'image/png'}); 262 return decoder.tracks.ready; 263 }) 264 .then(_ => { 265 // Queue two decodes to ensure index verification and decoding are 266 // properly ordered. 267 p1 = decoder.decode({frameIndex: 0}); 268 return promise_rejects_dom( 269 // Requesting to decode frame #1 would normally be expected to 270 // return RangeError (see 'Test out of range index returns 271 // RangeError' above). Here the decoder fails earlier because 272 // `p1` is requesting to decode frame #0 and the PNG has been 273 // truncated to the first 100 bytes. This is why here we expect 274 // EncodingError instead. 275 // 276 // Also note that in this test the data source is an ArrayBuffer. 277 // Therefore the decoder can see that there is no more data coming 278 // - this means that the decoder can declare a fatal error, rather 279 // than assuming an incomplete input stream. 280 t, 'EncodingError', decoder.decode({frameIndex: 1})); 281 }) 282 .then(_ => { 283 // Requesting to decode frame #0 (the `p1` Promise) throws 284 // EncodingError, because the PNG has been truncated to the first 100 285 // bytes. 286 return promise_rejects_dom(t, 'EncodingError', p1); 287 }) 288 }); 289 }, 'Test decoding a partial ArrayBuffer results in EncodingError'); 290 291 promise_test(t => { 292 var decoder; 293 var p1; 294 return ImageDecoder.isTypeSupported('image/png').then(support => { 295 assert_implements_optional( 296 support, 'Optional codec image/png not supported.'); 297 return fetch('four-colors.png') 298 .then(response => { 299 return response.arrayBuffer(); 300 }) 301 .then(buffer => { 302 decoder = 303 new ImageDecoder({data: buffer.slice(0, 100), type: 'image/png'}); 304 return decoder.completed; 305 }) 306 }); 307 }, 'Test completed property on fully buffered decode'); 308 309 promise_test(t => { 310 var decoder = null; 311 312 return ImageDecoder.isTypeSupported('image/png').then(support => { 313 assert_implements_optional( 314 support, 'Optional codec image/png not supported.'); 315 return fetch('four-colors.png') 316 .then(response => { 317 decoder = new ImageDecoder({data: response.body, type: 'image/png'}); 318 return decoder.tracks.ready; 319 }) 320 .then(_ => { 321 decoder.tracks.selectedTrack.selected = false; 322 assert_equals(decoder.tracks.selectedIndex, -1); 323 assert_equals(decoder.tracks.selectedTrack, null); 324 return decoder.tracks.ready; 325 }) 326 .then(_ => { 327 return promise_rejects_dom(t, 'InvalidStateError', decoder.decode()); 328 }) 329 .then(_ => { 330 decoder.tracks[0].selected = true; 331 assert_equals(decoder.tracks.selectedIndex, 0); 332 assert_not_equals(decoder.tracks.selected, null); 333 return decoder.decode(); 334 }) 335 .then(result => { 336 assert_equals(result.image.displayWidth, 320); 337 assert_equals(result.image.displayHeight, 240); 338 }); 339 }); 340 }, 'Test decode, decodeMetadata after no track selected.'); 341 342 promise_test(t => { 343 var decoder = null; 344 345 return ImageDecoder.isTypeSupported('image/avif').then(support => { 346 assert_implements_optional( 347 support, 'Optional codec image/avif not supported.'); 348 return fetch('four-colors-flip.avif') 349 .then(response => { 350 decoder = new ImageDecoder({ 351 data: response.body, 352 type: 'image/avif', 353 preferAnimation: false 354 }); 355 return decoder.tracks.ready; 356 }) 357 .then(_ => { 358 assert_equals(decoder.tracks.length, 2); 359 assert_false(decoder.tracks[decoder.tracks.selectedIndex].animated) 360 assert_false(decoder.tracks.selectedTrack.animated); 361 assert_equals(decoder.tracks.selectedTrack.frameCount, 1); 362 assert_equals(decoder.tracks.selectedTrack.repetitionCount, 0); 363 return decoder.decode(); 364 }) 365 .then(result => { 366 assert_equals(result.image.displayWidth, 320); 367 assert_equals(result.image.displayHeight, 240); 368 assert_equals(result.image.timestamp, 0); 369 370 // Swap to the other track. 371 let newIndex = (decoder.tracks.selectedIndex + 1) % 2; 372 decoder.tracks[newIndex].selected = true; 373 return decoder.decode() 374 }) 375 .then(result => { 376 assert_equals(result.image.displayWidth, 320); 377 assert_equals(result.image.displayHeight, 240); 378 assert_equals(result.image.timestamp, 0); 379 assert_equals(result.image.duration, 10000); 380 381 assert_equals(decoder.tracks.length, 2); 382 assert_true(decoder.tracks[decoder.tracks.selectedIndex].animated) 383 assert_true(decoder.tracks.selectedTrack.animated); 384 assert_equals(decoder.tracks.selectedTrack.frameCount, 7); 385 assert_equals(decoder.tracks.selectedTrack.repetitionCount, Infinity); 386 return decoder.decode({frameIndex: 1}); 387 }) 388 .then(result => { 389 assert_equals(result.image.timestamp, 10000); 390 assert_equals(result.image.duration, 10000); 391 }); 392 }); 393 }, 'Test track selection in multi track image.'); 394 395 class InfiniteGifSource { 396 async load(repetitionCount) { 397 let response = await fetch('four-colors-flip.gif'); 398 let buffer = await response.arrayBuffer(); 399 400 // Strip GIF trailer (0x3B) so we can continue to append frames. 401 this.baseImage = new Uint8Array(buffer.slice(0, buffer.byteLength - 1)); 402 this.baseImage[0x23] = repetitionCount; 403 this.counter = 0; 404 } 405 406 start(controller) { 407 this.controller = controller; 408 this.controller.enqueue(this.baseImage); 409 } 410 411 close() { 412 this.controller.enqueue(new Uint8Array([0x3B])); 413 this.controller.close(); 414 } 415 416 addFrame() { 417 const FRAME1_START = 0x26; 418 const FRAME2_START = 0x553; 419 420 if (this.counter++ % 2 == 0) 421 this.controller.enqueue(this.baseImage.slice(FRAME1_START, FRAME2_START)); 422 else 423 this.controller.enqueue(this.baseImage.slice(FRAME2_START)); 424 } 425 } 426 427 promise_test(async t => { 428 let support = await ImageDecoder.isTypeSupported('image/gif'); 429 assert_implements_optional( 430 support, 'Optional codec image/gif not supported.'); 431 432 let source = new InfiniteGifSource(); 433 await source.load(5); 434 435 let stream = new ReadableStream(source, {type: 'bytes'}); 436 let decoder = new ImageDecoder({data: stream, type: 'image/gif'}); 437 return decoder.tracks.ready 438 .then(_ => { 439 assert_equals(decoder.tracks.selectedTrack.frameCount, 2); 440 assert_equals(decoder.tracks.selectedTrack.repetitionCount, 5); 441 442 source.addFrame(); 443 return decoder.decode({frameIndex: 2}); 444 }) 445 .then(result => { 446 assert_equals(decoder.tracks.selectedTrack.frameCount, 3); 447 assert_equals(result.image.displayWidth, 320); 448 assert_equals(result.image.displayHeight, 240); 449 450 // Note: The stream has an alternating duration of 30ms, 40ms per frame. 451 assert_equals(result.image.timestamp, 70000, "timestamp frame 2"); 452 assert_equals(result.image.duration, 30000, "duration frame 2"); 453 source.addFrame(); 454 return decoder.decode({frameIndex: 3}); 455 }) 456 .then(result => { 457 assert_equals(decoder.tracks.selectedTrack.frameCount, 4); 458 assert_equals(result.image.displayWidth, 320); 459 assert_equals(result.image.displayHeight, 240); 460 assert_equals(result.image.timestamp, 100000, "timestamp frame 3"); 461 assert_equals(result.image.duration, 40000, "duration frame 3"); 462 463 // Decode frame not yet available then reset before it comes in. 464 let p = decoder.decode({frameIndex: 5}); 465 decoder.reset(); 466 return promise_rejects_dom(t, 'AbortError', p); 467 }) 468 .then(_ => { 469 // Ensure we can still decode earlier frames. 470 assert_equals(decoder.tracks.selectedTrack.frameCount, 4); 471 return decoder.decode({frameIndex: 3}); 472 }) 473 .then(result => { 474 assert_equals(decoder.tracks.selectedTrack.frameCount, 4); 475 assert_equals(result.image.displayWidth, 320); 476 assert_equals(result.image.displayHeight, 240); 477 assert_equals(result.image.timestamp, 100000, "timestamp frame 3"); 478 assert_equals(result.image.duration, 40000, "duration frame 3"); 479 480 // Decode frame not yet available then close before it comes in. 481 let p = decoder.decode({frameIndex: 5}); 482 let tracks = decoder.tracks; 483 let track = decoder.tracks.selectedTrack; 484 decoder.close(); 485 486 assert_equals(decoder.type, ''); 487 assert_equals(decoder.tracks.length, 0); 488 assert_equals(tracks.length, 0); 489 track.selected = true; // Should do nothing. 490 491 // Previous decode should be aborted. 492 return promise_rejects_dom(t, 'AbortError', p); 493 }) 494 .then(_ => { 495 // Ensure feeding the source after closing doesn't crash. 496 assert_throws_js(TypeError, () => { 497 source.addFrame(); 498 }); 499 }); 500 }, 'Test ReadableStream of gif'); 501 502 promise_test(async t => { 503 let support = await ImageDecoder.isTypeSupported('image/gif'); 504 assert_implements_optional( 505 support, 'Optional codec image/gif not supported.'); 506 507 let source = new InfiniteGifSource(); 508 await source.load(5); 509 510 let stream = new ReadableStream(source, {type: 'bytes'}); 511 let decoder = new ImageDecoder({data: stream, type: 'image/gif'}); 512 return decoder.tracks.ready.then(_ => { 513 assert_equals(decoder.tracks.selectedTrack.frameCount, 2); 514 assert_equals(decoder.tracks.selectedTrack.repetitionCount, 5); 515 516 decoder.decode({frameIndex: 2}).then(t.unreached_func()); 517 decoder.decode({frameIndex: 1}).then(t.unreached_func()); 518 return decoder.tracks.ready; 519 }); 520 }, 'Test that decode requests are serialized.'); 521 522 promise_test(async t => { 523 let support = await ImageDecoder.isTypeSupported('image/gif'); 524 assert_implements_optional( 525 support, 'Optional codec image/gif not supported.'); 526 527 let source = new InfiniteGifSource(); 528 await source.load(5); 529 530 let stream = new ReadableStream(source, {type: 'bytes'}); 531 let decoder = new ImageDecoder({data: stream, type: 'image/gif'}); 532 return decoder.tracks.ready.then(_ => { 533 assert_equals(decoder.tracks.selectedTrack.frameCount, 2); 534 assert_equals(decoder.tracks.selectedTrack.repetitionCount, 5); 535 536 // Decode frame not yet available then change tracks before it comes in. 537 let p = decoder.decode({frameIndex: 5}); 538 decoder.tracks.selectedTrack.selected = false; 539 return promise_rejects_dom(t, 'AbortError', p); 540 }); 541 }, 'Test ReadableStream aborts promises on track change'); 542 543 promise_test(async t => { 544 let support = await ImageDecoder.isTypeSupported('image/gif'); 545 assert_implements_optional( 546 support, 'Optional codec image/gif not supported.'); 547 548 let source = new InfiniteGifSource(); 549 await source.load(5); 550 551 let stream = new ReadableStream(source, {type: 'bytes'}); 552 let decoder = new ImageDecoder({data: stream, type: 'image/gif'}); 553 return decoder.tracks.ready.then(_ => { 554 let p = decoder.completed; 555 decoder.close(); 556 return promise_rejects_dom(t, 'AbortError', p); 557 }); 558 }, 'Test ReadableStream aborts completed on close'); 559 560 promise_test(async t => { 561 let support = await ImageDecoder.isTypeSupported('image/gif'); 562 assert_implements_optional( 563 support, 'Optional codec image/gif not supported.'); 564 565 let source = new InfiniteGifSource(); 566 await source.load(5); 567 568 let stream = new ReadableStream(source, {type: 'bytes'}); 569 let decoder = new ImageDecoder({data: stream, type: 'image/gif'}); 570 return decoder.tracks.ready.then(_ => { 571 source.close(); 572 return decoder.completed; 573 }); 574 }, 'Test ReadableStream resolves completed');