@@ -40,6 +40,16 @@ ImageType? detectImageType(Uint8List data) {
4040 return ImageType .animatedWebp;
4141 }
4242 }
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+ }
4353 return format.imageType;
4454 }
4555
@@ -217,28 +227,22 @@ class _WebpHeaderReader {
217227 /// [expectedHeader] .
218228 bool _readChunkHeader (String expectedHeader) {
219229 final String chunkFourCC = _readFourCC ();
220- // Read chunk size.
221- _readUint32 () ;
230+ // Skip reading chunk size.
231+ _position += 4 ;
222232 return chunkFourCC == expectedHeader;
223233 }
224234
225235 /// Reads the WebP header. Returns [false] if this is not a valid WebP header.
226236 bool _readWebpHeader () {
227237 final String riffBytes = _readFourCC ();
228238
229- // Read file size byte .
230- _readUint32 () ;
239+ // Skip reading file size bytes .
240+ _position += 4 ;
231241
232242 final String webpBytes = _readFourCC ();
233243 return riffBytes == 'RIFF' && webpBytes == 'WEBP' ;
234244 }
235245
236- int _readUint32 () {
237- final int result = bytes.getUint32 (_position, Endian .little);
238- _position += 4 ;
239- return result;
240- }
241-
242246 int _readUint8 () {
243247 final int result = bytes.getUint8 (_position);
244248 _position += 1 ;
@@ -258,3 +262,240 @@ class _WebpHeaderReader {
258262 return String .fromCharCodes (chars);
259263 }
260264}
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