@@ -40,6 +40,16 @@ ImageType? detectImageType(Uint8List data) {
40
40
return ImageType .animatedWebp;
41
41
}
42
42
}
43
+
44
+ // We conservatively detected an animated GIF. Check if the GIF is actually
45
+ // animated by reading the bytes.
46
+ if (format.imageType == ImageType .animatedGif) {
47
+ if (_GifHeaderReader (data.buffer.asByteData ()).isAnimated ()) {
48
+ return ImageType .animatedGif;
49
+ } else {
50
+ return ImageType .gif;
51
+ }
52
+ }
43
53
return format.imageType;
44
54
}
45
55
@@ -217,28 +227,22 @@ class _WebpHeaderReader {
217
227
/// [expectedHeader] .
218
228
bool _readChunkHeader (String expectedHeader) {
219
229
final String chunkFourCC = _readFourCC ();
220
- // Read chunk size.
221
- _readUint32 () ;
230
+ // Skip reading chunk size.
231
+ _position += 4 ;
222
232
return chunkFourCC == expectedHeader;
223
233
}
224
234
225
235
/// Reads the WebP header. Returns [false] if this is not a valid WebP header.
226
236
bool _readWebpHeader () {
227
237
final String riffBytes = _readFourCC ();
228
238
229
- // Read file size byte .
230
- _readUint32 () ;
239
+ // Skip reading file size bytes .
240
+ _position += 4 ;
231
241
232
242
final String webpBytes = _readFourCC ();
233
243
return riffBytes == 'RIFF' && webpBytes == 'WEBP' ;
234
244
}
235
245
236
- int _readUint32 () {
237
- final int result = bytes.getUint32 (_position, Endian .little);
238
- _position += 4 ;
239
- return result;
240
- }
241
-
242
246
int _readUint8 () {
243
247
final int result = bytes.getUint8 (_position);
244
248
_position += 1 ;
@@ -258,3 +262,240 @@ class _WebpHeaderReader {
258
262
return String .fromCharCodes (chars);
259
263
}
260
264
}
265
+
266
+ /// Reads the header of a GIF file to determine if it is animated or not.
267
+ ///
268
+ /// See https://www.w3.org/Graphics/GIF/spec-gif89a.txt
269
+ class _GifHeaderReader {
270
+ _GifHeaderReader (this .bytes);
271
+
272
+ final ByteData bytes;
273
+
274
+ /// The current position we are reading from in bytes.
275
+ int _position = 0 ;
276
+
277
+ /// Returns [true] if this GIF is animated.
278
+ ///
279
+ /// We say a GIF is animated if it has more than one image frame.
280
+ bool isAnimated () {
281
+ final bool isGif = _readGifHeader ();
282
+ if (! isGif) {
283
+ return false ;
284
+ }
285
+
286
+ // Read the logical screen descriptor block.
287
+
288
+ // Advance 4 bytes to skip over the screen width and height.
289
+ _position += 4 ;
290
+
291
+ final int logicalScreenDescriptorFields = _readUint8 ();
292
+ const int globalColorTableFlagMask = 1 << 7 ;
293
+ final bool hasGlobalColorTable =
294
+ logicalScreenDescriptorFields & globalColorTableFlagMask != 0 ;
295
+
296
+ // Skip over the background color index and pixel aspect ratio.
297
+ _position += 2 ;
298
+
299
+ if (hasGlobalColorTable) {
300
+ // Skip past the global color table.
301
+ const int globalColorTableSizeMask = 1 << 2 | 1 << 1 | 1 ;
302
+ final int globalColorTableSize =
303
+ logicalScreenDescriptorFields & globalColorTableSizeMask;
304
+ // This is 3 * 2^(Global Color Table Size + 1).
305
+ final int globalColorTableSizeInBytes =
306
+ 3 * (1 << (globalColorTableSize + 1 ));
307
+ _position += globalColorTableSizeInBytes;
308
+ }
309
+
310
+ int framesFound = 0 ;
311
+ // Read the GIF until we either find 2 frames or reach the end of the GIF.
312
+ while (true ) {
313
+ final bool isTrailer = _checkForTrailer ();
314
+ if (isTrailer) {
315
+ return framesFound > 1 ;
316
+ }
317
+
318
+ // If we haven't reached the end, then the next block must either be a
319
+ // graphic block or a special-purpose block (comment extension or
320
+ // application extension).
321
+ final bool isSpecialPurposeBlock = _checkForSpecialPurposeBlock ();
322
+ if (isSpecialPurposeBlock) {
323
+ _skipSpecialPurposeBlock ();
324
+ continue ;
325
+ }
326
+
327
+ // If the next block isn't a special-purpose block, it must be a graphic
328
+ // block. Increase the frame count, skip the graphic block, and keep
329
+ // looking for more.
330
+ if (framesFound >= 1 ) {
331
+ // We've found multiple frames, this is an animated GIF.
332
+ return true ;
333
+ }
334
+ _skipGraphicBlock ();
335
+ framesFound++ ;
336
+ }
337
+ }
338
+
339
+ /// Reads the GIF header. Returns [false] if this is not a valid GIF header.
340
+ bool _readGifHeader () {
341
+ final String signature = _readCharCode ();
342
+ final String version = _readCharCode ();
343
+
344
+ return signature == 'GIF' && (version == '89a' || version == '87a' );
345
+ }
346
+
347
+ /// Returns [true] if the next block is a trailer.
348
+ bool _checkForTrailer () {
349
+ final int nextByte = bytes.getUint8 (_position);
350
+ return nextByte == 0x3b ;
351
+ }
352
+
353
+ /// Returns [true] if the next block is a Special-Purpose Block (either a
354
+ /// Comment Extension or an Application Extension).
355
+ bool _checkForSpecialPurposeBlock () {
356
+ final int extensionIntroducer = bytes.getUint8 (_position);
357
+ if (extensionIntroducer != 0x21 ) {
358
+ return false ;
359
+ }
360
+
361
+ final int extensionLabel = bytes.getUint8 (_position + 1 );
362
+
363
+ // The Comment Extension label is 0xFE, the Application Extension Label is
364
+ // 0xFF.
365
+ return extensionLabel == 0xfe || extensionLabel == 0xff ;
366
+ }
367
+
368
+ /// Skips past the current control block.
369
+ void _skipSpecialPurposeBlock () {
370
+ assert (_checkForSpecialPurposeBlock ());
371
+
372
+ // Skip the extension introducer.
373
+ _position += 1 ;
374
+
375
+ // Read the extension label to determine if this is a comment block or
376
+ // application block.
377
+ final int extensionLabel = _readUint8 ();
378
+ if (extensionLabel == 0xfe ) {
379
+ // This is a Comment Extension. Just skip past data sub-blocks.
380
+ _skipDataBlocks ();
381
+ } else {
382
+ assert (extensionLabel == 0xff );
383
+ // This is an Application Extension. Skip past the application identifier
384
+ // bytes and then skip past the data sub-blocks.
385
+
386
+ // Skip the application identifier.
387
+ _position += 12 ;
388
+
389
+ _skipDataBlocks ();
390
+ }
391
+ }
392
+
393
+ /// Skip past the graphic block.
394
+ void _skipGraphicBlock () {
395
+ // Check for the optional Graphic Control Extension.
396
+ if (_checkForGraphicControlExtension ()) {
397
+ _skipGraphicControlExtension ();
398
+ }
399
+
400
+ // Check if the Graphic Block is a Plain Text Extension.
401
+ if (_checkForPlainTextExtension ()) {
402
+ _skipPlainTextExtension ();
403
+ return ;
404
+ }
405
+
406
+ // This is a Table-Based Image block.
407
+ assert (bytes.getUint8 (_position) == 0x2c );
408
+
409
+ // Skip to the packed fields to check if there is a local color table.
410
+ _position += 9 ;
411
+
412
+ final int packedImageDescriptorFields = _readUint8 ();
413
+ const int localColorTableFlagMask = 1 << 7 ;
414
+ final bool hasLocalColorTable =
415
+ packedImageDescriptorFields & localColorTableFlagMask != 0 ;
416
+ if (hasLocalColorTable) {
417
+ // Skip past the local color table.
418
+ const int localColorTableSizeMask = 1 << 2 | 1 << 1 | 1 ;
419
+ final int localColorTableSize =
420
+ packedImageDescriptorFields & localColorTableSizeMask;
421
+ // This is 3 * 2^(Local Color Table Size + 1).
422
+ final int localColorTableSizeInBytes =
423
+ 3 * (1 << (localColorTableSize + 1 ));
424
+ _position += localColorTableSizeInBytes;
425
+ }
426
+ // Skip LZW minimum code size byte.
427
+ _position += 1 ;
428
+ _skipDataBlocks ();
429
+ }
430
+
431
+ /// Returns [true] if the next block is a Graphic Control Extension block.
432
+ bool _checkForGraphicControlExtension () {
433
+ final int nextByte = bytes.getUint8 (_position);
434
+ if (nextByte != 0x21 ) {
435
+ // This is not an extension block.
436
+ return false ;
437
+ }
438
+
439
+ final int extensionLabel = bytes.getUint8 (_position + 1 );
440
+ // The Graphic Control Extension label is 0xF9.
441
+ return extensionLabel == 0xf9 ;
442
+ }
443
+
444
+ /// Skip past the Graphic Control Extension block.
445
+ void _skipGraphicControlExtension () {
446
+ assert (_checkForGraphicControlExtension ());
447
+ // The Graphic Control Extension block is 8 bytes.
448
+ _position += 8 ;
449
+ }
450
+
451
+ /// Check if the next block is a Plain Text Extension block.
452
+ bool _checkForPlainTextExtension () {
453
+ final int nextByte = bytes.getUint8 (_position);
454
+ if (nextByte != 0x21 ) {
455
+ // This is not an extension block.
456
+ return false ;
457
+ }
458
+
459
+ final int extensionLabel = bytes.getUint8 (_position + 1 );
460
+ // The Plain Text Extension label is 0x01.
461
+ return extensionLabel == 0x01 ;
462
+ }
463
+
464
+ /// Skip the Plain Text Extension block.
465
+ void _skipPlainTextExtension () {
466
+ assert (_checkForPlainTextExtension ());
467
+ // Skip the 15 bytes before the data sub-blocks.
468
+ _position += 15 ;
469
+
470
+ _skipDataBlocks ();
471
+ }
472
+
473
+ /// Skip past any data sub-blocks and the block terminator.
474
+ void _skipDataBlocks () {
475
+ while (true ) {
476
+ final int blockSize = _readUint8 ();
477
+ if (blockSize == 0 ) {
478
+ // This is a block terminator.
479
+ return ;
480
+ }
481
+ _position += blockSize;
482
+ }
483
+ }
484
+
485
+ /// Read a 3 digit character code.
486
+ String _readCharCode () {
487
+ final List <int > chars = < int > [
488
+ bytes.getUint8 (_position),
489
+ bytes.getUint8 (_position + 1 ),
490
+ bytes.getUint8 (_position + 2 ),
491
+ ];
492
+ _position += 3 ;
493
+ return String .fromCharCodes (chars);
494
+ }
495
+
496
+ int _readUint8 () {
497
+ final int result = bytes.getUint8 (_position);
498
+ _position += 1 ;
499
+ return result;
500
+ }
501
+ }
0 commit comments