Skip to content

Instantly share code, notes, and snippets.

@zakirt
Last active October 6, 2023 08:33
Show Gist options
  • Star 8 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save zakirt/faa4a58cec5a7505b10e3686a226f285 to your computer and use it in GitHub Desktop.
Save zakirt/faa4a58cec5a7505b10e3686a226f285 to your computer and use it in GitHub Desktop.
Detecting if GIF file is animated using JavaScript
/**
* @author Zakir Tariverdiev
* @class animatedGifDetect
* @description
* GIF file reader that checks whether GIF image is animated, or not.
* Uses information gathered from the website below:
* http://www.matthewflickinger.com/lab/whatsinagif/bits_and_bytes.asp
*/
(function(window, undefined) {
'use strict';
// Prevent redefinition of animatedGifDetect
if (typeof window.animatedGifDetect !== 'undefined') {
return;
}
var HEADER_LEN = 6; // offset bytes for the header section
var LOGICAL_SCREEN_DESC_LEN = 7; // offset bytes for logical screen description section
var fileReader = new FileReader(); // we'll re-use this object
var callbackConfig = {
animatedCb: null,
nonAnimatedCb: null,
context: window
};
fileReader.addEventListener('load', _checkForAnimation, false);
window.animatedGifDetect = {
process: process
};
/**
* @method process
* @memberof animatedGifDetect
* @description Converts the provided GIF file into array buffer and checks whether it is
* animated, or not.
* @param {File} gifFile - GIF file to check for animation
* @param {function} animatedCb - callback to fire if GIF is animated
* @param {function} [nonAnimatedCb] - callback to fire if GIF is not animated
* @param {object} [context] - context for the callbacks. Defaults to window
*/
function process(gifFile, animatedCb, nonAnimatedCb, context) {
if (gifFile) {
callbackConfig.animatedCb = animatedCb;
callbackConfig.nonAnimatedCb = nonAnimatedCb;
callbackConfig.context = context;
fileReader.readAsArrayBuffer(gifFile);
}
}
/**
* @method _checkForAnimation
* @private
* @memberof animatedGifDetect
* @description Looks for the "delay" bytes in the GIF file. These bytes will be set to non 0 value
* if the GIF file is animated.
*/
function _checkForAnimation() {
var buffer = fileReader.result;
// Start from last 4 bytes of the Logical Screen Descriptor
var dv = new DataView(buffer, HEADER_LEN + LOGICAL_SCREEN_DESC_LEN - 3);
var offset = 0;
var globalColorTable = dv.getUint8(0); // aka packet byte
var bgColorIndex = dv.getUint8(1);
var pixelAspectRatio = dv.getUint8(2);
var globalColorTableSize = 0;
// check first bit, if 0, then we don't have a Global Color Table
if (globalColorTable & 0x80) {
// grab the last 3 bits, to calculate the global color table size -> RGB * 2^(N+1)
// N is the value in the last 3 bits.
globalColorTableSize = 3 * Math.pow(2, (globalColorTable & 0x7) + 1);
}
// move on to the Graphics Control Extension
offset = 3 + globalColorTableSize;
var extensionIntroducer = dv.getUint8(offset);
var graphicsConrolLabel = dv.getUint8(offset + 1);
var delayTime = 0;
// Graphics Control Extension section is where GIF animation data is stored
// First 2 bytes must be 0x21 and 0xF9
if ((extensionIntroducer & 0x21) && (graphicsConrolLabel & 0xF9)) {
// skip to the 2 bytes with the delay time
delayTime = dv.getUint16(offset + 4);
}
if (delayTime && typeof callbackConfig.animatedCb === 'function') {
callbackConfig.animatedCb.apply(callbackConfig.context || window);
}
else if (!delayTime && typeof callbackConfig.nonAnimatedCb === 'function') {
callbackConfig.nonAnimatedCb.apply(callbackConfig.context || window);
}
}
})(window);
@Inveth
Copy link

Inveth commented Oct 28, 2019

Great one - mind if I refactor it and use it?

Thanks!

@rickylall
Copy link

Thanks! This was a big help...

@madc0w
Copy link

madc0w commented May 9, 2021

refactored to work on imageData:

	isAnimatedGif(imageData) {
		const base64 = imageData.substr(imageData.indexOf(',') + 1);
		const binaryString = window.atob(base64);
		const len = binaryString.length;
		const bytes = new Uint8Array(len);
		for (let i = 0; i < len; i++) {
			bytes[i] = binaryString.charCodeAt(i);
		}
		const buffer = bytes.buffer;

		const HEADER_LEN = 6;                 // offset bytes for the header section
		const LOGICAL_SCREEN_DESC_LEN = 7;    // offset bytes for logical screen description section

		// Start from last 4 bytes of the Logical Screen Descriptor
		const dv = new DataView(buffer, HEADER_LEN + LOGICAL_SCREEN_DESC_LEN - 3);
		let offset = 0;
		const globalColorTable = dv.getUint8(0);	// aka packet byte
		let globalColorTableSize = 0;

		// check first bit, if 0, then we don't have a Global Color Table
		if (globalColorTable & 0x80) {
			// grab the last 3 bits, to calculate the global color table size -> RGB * 2^(N+1)
			// N is the value in the last 3 bits.
			globalColorTableSize = 3 * (2 ** ((globalColorTable & 0x7) + 1));
		}

		// move on to the Graphics Control Extension
		offset = 3 + globalColorTableSize;

		const extensionIntroducer = dv.getUint8(offset);
		const graphicsConrolLabel = dv.getUint8(offset + 1);
		let delayTime = 0;

		// Graphics Control Extension section is where GIF animation data is stored
		// First 2 bytes must be 0x21 and 0xF9
		if ((extensionIntroducer & 0x21) && (graphicsConrolLabel & 0xF9)) {
			// skip to the 2 bytes with the delay time
			delayTime = dv.getUint16(offset + 4);
		}

		return delayTime > 0;
	},

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment