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.

Feel free to if you found this useful.

Care to Comment?

Or if you'd prefer to get in touch privately, please send me an email.

Name

Email (optional)

Blog (optional)