tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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');