GIF animation duration calculation
At Federated Media, any individual can buy ads on any of our 80+ sites. When they submit their creative, it’s very likely it’ll be animated. There’s probably no doubt that animated ads are more effective at attracting attention than their static counterparts, but that’s also what makes them so annoying.
To strike a balance—respecting our authors and their audiences above all else—we require that any animation lasts no longer than 15 seconds.
The only problem is, we had no way to enforce this. So Andre, who first built FM’s platform, would watch every banner ad for at least 15 seconds to make sure it stopped animating. A nefarious ad designer could have successfully gotten past the Andre-check by animating the banner for 12 seconds, pausing it for 20 seconds, and then repeating continuously. What usually happens though is that someone submits a banner with animation that lasts for about 2-3 seconds, pauses for 3, and then repeats continuously.
Whenever an ad like this was submitted, we’d have to email the person who submitted it, ask if they could resubmit an ad with animation that stops after 15 seconds, etc etc etc. It was a really manual process.
So earlier this week I set out to see if there was an easy way to probe a GIF file programmatically to determine whether or not it was configured to loop continuously. PHP doesn’t do this natively, and I couldn’t find anything on Google that even came close to analyzing a GIF’s animation metadata—in any language.
So, I decided to write my own.
With a hex editor in one hand and the GIF 89a standard in the other, I set about understanding the image format and the Netscape 2.0 Application Extension (below) that added the option to loop the animation continuously (or for a specific number of iterations):
byte 1 : 33 (hex 0x21) GIF Extension code byte 2 : 255 (hex 0xFF) Application Extension Label byte 3 : 11 (hex (0x0B) Length of Application Block (eleven bytes of data to follow) bytes 4 to 11 : "NETSCAPE" bytes 12 to 14 : "2.0" byte 15 : 3 (hex 0x03) Length of Data Sub-Block (three bytes of data to follow) byte 16 : 1 (hex 0x01) bytes 17 to 18 : 0 to 65535, an unsigned integer in lo-hi byte format. This indicate the number of iterations the loop should be executed. bytes 19 : 0 (hex 0x00) a Data Sub-block Terminator.
In the end, I didn’t write a complete GIF parser, but opted for a simple regular expression probe of the GIF file. Bonus: figuring out how to convert little-endian unsigned ints from hexadecimal to decimal.
So far I’ve tested the code on some pretty long GIF animations, and it seems accurate. So if you need to find the duration of a GIF animation and/or the number of times it loops, here’s the code to do so in PHP—which should be pretty easily translated into just about any other language.
Consider this code licensed under the WTFPL.
<?php // returns true if animation runs less than 15 seconds // returns false if animation loops continuously or for longer than 15 seconds function checkGIFAnimation($image_filename) { $max_animation_duration = 15; // in seconds // if set to loop continuously, return false $iterations = getGIFIterations($image_filename); if ($iterations == 0) { return false; } $delay = getGIFDuration($image_filename); if ($delay * $iterations > $max_animation_duration) { return false; } else { return true; } } function getGIFIterations($image_filename) { // see http://www.w3.org/Graphics/GIF/spec-gif89a.txt and http://www.let.rug.nl/~kleiweg/gif/netscape.html for more info $gif_netscape_application_extension = "/21ff0b4e45545343415045322e300301([0-9a-f]{4})00/"; $file = file_get_contents($image_filename); $file = bin2hex($file); // get looping iterations $iterations = 1; if (preg_match($gif_netscape_application_extension, $file, $matches)) { // convert little-endian hex unsigned int to decimal $iterations = hexdec(substr($matches[1],-2) . substr($matches[1], 0, 2)); // the presence of the header with a nonzero number of iterations // should be interpreted as "additional" iterations, // hence a specifed iteration of 1 means loop twice. // zero iterations means loop continuously if ($iterations > 0) $iterations++; } // a return value of zero means gif will loop continuously return $iterations; } // returns length of time to display a gif image/animation once // must be multipled by getGIFIterations to determine total duration function getGIFDuration($image_filename) { // see http://www.w3.org/Graphics/GIF/spec-gif89a.txt for more info $gif_graphic_control_extension = "/21f904[0-9a-f]{2}([0-9a-f]{4})[0-9a-f]{2}00/"; $file = file_get_contents($image_filename); $file = bin2hex($file); // sum all frame delays $total_delay = 0; preg_match_all($gif_graphic_control_extension, $file, $matches); foreach ($matches[1] as $match) { // convert little-endian hex unsigned ints to decimals $delay = hexdec(substr($match,-2) . substr($match, 0, 2)); if ($delay == 0) $delay = 1; $total_delay += $delay; } // delays are stored as hundredths of a second, lets convert to seconds $total_delay /= 100; return $total_delay; } ?>
Happy hacking.