Creating Animated GIFs with ImageMagick & ffmpeg

07 February 2013

I’ll start off this blog with something lightweight and relatively useless except for creating amusing internet gimmicks and perhaps the occasional animated diagram. Surprisingly, there are few good tools for making animated GIFs on a Mac or Linux desktop. On Windows, Paint Shop/Animation Shop used to be the go-to suite for doing this—now, the closest thing for Mac folks might be GIFBrewery, which has far less features and some mixed reviews. I haven’t plunked down $6 for that yet, so I’ve still been trekking about on the command line, which is certainly more tedious, but you can control every step of the process and get exactly the results you want.

Prerequisites

You won’t be able to jump into this without a little comfort on the command line, and the installation of ffmpeg (if you are working from a video source) and ImageMagick.

If you’re on a Mac, and you already use MacPorts, it should be a simple sudo port install ImageMagick ffmpeg and a lot of thumb twiddling. If you don’t, try installing HomeBrew which is a little more lightweight, and run a brew install imagemagick ffmpeg.

For Linuxes, I’ll leave you to figure out how to get those two packages from your package manager. Depending on how idealistic your distro is, it might require adding some non-free repositories or even compiling ffmpeg from scratch. You only really need ffmpeg if you have a video source file, though; everything else is done with ImageMagick.

The full workflow

First, you want to start with your source video or images. If it’s video, you can crop/trim it down to the relevant clip with Quicktime X or any more advanced video-editing suite. Then, it’s time to extract the images with ffmpeg. Open a terminal and change to the directory with your movie file, and assuming whatever.mov is the filename:

$ mkdir decode
$ ffmpeg -i whatever.mov decode/%d.png

Now you’ll have a sequence of images in the decode folder that correspond to each frame of your movie. At this point, it’s a good idea to check out this folder and preview the sequence of images. They are likely to be far too big to be appropriate for a GIF, so we have to resize and crop them. We may also want to refine the range of frames that will be used in the gif.

Bash can expand a sequence of numbers using the {$begin..$end} notation, which functions as a shell glob. We can use this to select the frames we want, resize and crop them with ImageMagick, and put these into a new folder. In this example, we’ll take the first 12 frames, crop them to 405x720 starting 437 pixels from the left side, and resize them by 30%.

$ mkdir resized
$ convert decode/{1..12}.png -crop 405x720+437+0 -resize 30% resized/%d.png

Time to check the output images again; if they aren’t how you like, you might try fiddling with the parameters in the previous command until it looks better.

A typical thing you might need to do at this point is label some of the frames. For that, we can use the -annotate feature of ImageMagick.

Quick aside: It might so happen that you don’t have any civilised fonts (e.g. Impact) available for use by ImageMagick, which requires them to be in TTF format and specified by an XML configuration file. To fix that, you can follow this guide; the short version is, convert the font into TTF format, put it somewhere sane like /usr/local/share/fonts and then write up an ~/.magick/type.xml that points to it:

<?xml version="1.0"?>
<typemap>

  <type
     format="ttf"
     name="Impact"
     fullname="Impact"
     family="Impact"
     glyphs="/usr/local/share/fonts/Impact.ttf"
  />

</typemap>

Back to text annotation. Note that we set the text and Y coordinate (from the bottom of the image) at the start of this command to avoid repetition, since the text must be painted twice for best results (once for the outline, and once for the fill). To print it near the top of the image instead, change -gravity south to -gravity north. We can copy over any images that we don’t want labeled.

$ mkdir annotated 
$ TEXT="CAPTION" && YDIST="10" && convert resized/{5..8}.png -pointsize 24 \
  -font Impact -strokewidth 2 -stroke black -fill white -gravity south -annotate \
  "+0+$YDIST" "$TEXT" -stroke none -annotate "+0+$YDIST" "$TEXT" annotated/%d.png
$ cp {0..4}.png {9..11}.png annotated

One last thing we may want to do is play the frames in reverse at the end of the GIF, so it appears to play seamlessly when looped. For that, a simple cp inside a for loop will do, but note that we omit copying the first and last frames to avoid playing them twice. Set LAST to the number of the last image in your animation.

$ LAST=11 && for i in `seq $[LAST-1]`; do cp annotated/$i.png \
  annotated/$[LAST*2-i].png; done

Finally, it’s time to assemble this bad boy into a GIFfy little bundle of web-friendly glory.

$ convert -delay 6 -loop 0 annotated/{0..21}.png out.gif

out.gif will contain your masterpiece. Preview it in a few web browsers to get a sense for its flavor. N.B. with that argument to -delay: Using any value lower than 6 is liable to produce strange results in Internet Explorer. This has to do with legacy-compatible interpretations of what the maximum reasonable frame rate for a web browser animation is, explained in great detail elsewhere.