Creating GIF Videos Using Python
Sep 10, 2016

Since I occasionally do work related to computer animation, it’s useful to have an easy way of displaying these animations in a webpage. The best way of actually doing this depends on what the user’s browser actually supports. Using an animated GIF is the most universally supported option, but restricts the video to a palette of 256 colors and can have a large file size. Using HTML video is a better option when it’s supported.

To help make this easier, I’m using a combination of a Python script to help in creating the needed video files, and a bit of HTML to embed them in the page. I’ve tried to write things so that the videos will work in a variety of situations including if JavaScript is unsupported/disabled, on mobile, and if HTML video isn’t supported. That said, I probably missed many cases, so don’t trust this as a foolproof solution (and it’s known to fail on mobile if JavaScript is also unsupported).

The model I’m using for this approach is a simplified form of the GIFV “format” used by imgur. The idea is to first try to load the video as an HTML video object, and fall back to an animated GIF if that fails. I don’t take Imgur’s approach exactly, but I do take some inspiration from them. Also, a word of warning: I do almost no web development, so it’s entirely possible that what follows is riddles with issues that I’m blissfully unaware of. But it seems to work well enough for me.

Creating the Video

This code is written with the assumption that your initial animation is represented as a sequence of images. If that’s not true in your case, it may need some modification. The required steps to convert this image sequence into a usable GIFV-style video are as follows:

  1. load the images in the animation
  2. optionally process each frame (for example cropping or resizing)
  3. save the first frame as a PNG image to display before the video loads
  4. save the video in both MP4 and GIF formats

All the heavy lifting for loading and saving the images/videos is done by the imageio library. Since imageio doesn’t currently support the WebM video format, we’ll just save the video in MP4 (H.264) and GIF. The basic imageio loop to do this is pretty simple, although we’ll complicate it a bit later on:

def create_gifv(input_glob, output_basename, fps):
	output_extensions = ["gif", "mp4"]
	input_filenames = glob.glob(input_glob)

	poster_writer = imageio.get_writer("{}.png".format(output_base_name), mode='i')
	video_writers = [
		imageio.get_writer("{}.{}".format(output_base_name, ext), mode='I', fps=fps)
		for ext in output_extensions]

	is_first = True
	for filename in input_filenames:
		img = imageio.imread(filename)

		# ... processing to crop, rescale, etc. goes here

		for writer in video_writers:
			writer.append_data(img)
		if is_first:
			poster_writer.append_data(img)

		is_first = False

	for writer in video_writers + [poster_writer]:
		writer.close()

There are a few things in this code which are not totally great Python style (such as the lack of good exception handling), but I think it serves its relatively simple purposes well enough. Also be forewarned that the GIF output of imageio doesn’t do a very good job of selecting a color palette, so you may see some artifacts in the GIF.

Since the return type of imageio.imread is a subclass of numpy.ndarray and imageio writers can take numpy arrays as input, there are a wide range of options available in order to preform whatever image processing steps you want on each frame. For what it’s worth, here’s what I’m using to crop the image:

def crop(img, top=0, bottom=0, left=0, right=0):
	"""Removes the specified number of rows/columns from the sides of img"""
	return img[top : img.shape[0]-bottom, left : img.shape[1]-right, :]

The code to rescale it is slightly more complex, since the simpler methods I tried led to aliasing artifacts. Here I’m using scikit-image to do most of the work. For increasing the resolution things are simple, but for shrinking the image I use pyramid-based rescaling followed by bicubic interpolation, which seems to give nice results:

def rescale(img, factor):
	"""Returns a version of img scaled in size by the given factor
	A value of factor=1.0 gives no change in image size."""
	if factor == 1:
		return img
	elif factor > 1:
		return skimage.transform.rescale(img, factor, order=3)
	else:
		inv_factor = 1.0 / factor
		pow2_factor = 2**int(numpy.floor(numpy.log2(inv_factor)))
		if pow2_factor != 1:
			img = skimage.transform.pyramid_reduce(img, pow2_factor)
		remaining_factor = factor * pow2_factor
		return skimage.transform.rescale(img, remaining_factor, order=3)

Embedding the Video

The final step is to embed the GIF video in a webpage. Something like this:

This would appear to be simple, but it’s more difficult than it looks because HTML5 video has varying support levels and restrictions on different platforms/browsers (particularly on mobile). I’ve taken an approach where the code attempts to show the MP4 video by default, but includes a couple of fallback cases depending on what features are available on the user’s browser.

In the following code, assume that you want to show an animation created using the above process and stored in the files animations/anim.{png,gif,mp4}. The first step is to embed the animation in HTML, which is pretty simple:

<div class="gifv-container" id="gifv-container-XXXXXX">
	<!-- the default video -->
	<video poster="animations/anim.png" preload="auto" autoplay="autoplay"
	muted="muted" loop="loop" webkit-playsinline>
		<source src="animations/anim.mp4" type="video/mp4"/>
		Your browser doesn't seem to support HTML5 video.  Couldn't display
		an animated GIF instead, possibly because you've disabled JavaScript.
	</video>

	<!-- handle cases where some features are incompatible
	     with the user's browser -->
	<script type="text/javascript">
		var video_info = {
			path: "animations",
			basename: "anim",
			hash: "XXXXXX"
		};
		process_gifv_container(video_info);
	</script>
</div>

The first part of this is straightforward, and just involves setting up an HTML5 video element to play animations/anim.mp4. This animation is set up to mimic how an animated GIF behaves in that it starts playing automatically and doesn’t show any playback controls.

The second section of the above code is some JavaScript which tries to fix things up if the user’s platform is likely to have problems with displaying the video. To do this, the JavaScript needs a reference to which video element it’s operating on. This is provided by the video_info variable defined above. The XXXXXX is a stand-in for a hash which uniquely identifies the gifv-container div in which the video element to be operated on resides (note the use of this hash in setting the id of the div). In your own code you’ll need to set this hash to some unique string for each use of a gifv-container div.

Anyway, there are two cases where things won’t work as intended:

  1. The browser can play the video, but doesn’t support the autoplay attribute. This is common on mobile platforms.
  2. The browser can’t play the video at all.

Before addressing these two cases, it’s necessary to detect when there’s likely to be a problem in the first place. For this I use Modernizr, which is a JavaScript library designed for exactly this sort of purpose. For this example, it’s sufficient to go to the Modernizr website and create a build supporting the “HTML5 video”, “Video Autoplay”, and “Video Loop” detectors. You can put the resulting JavaScript in the header of the page, or wherever else is convenient to make it available to the process_gifv_container function (defined below):

function process_gifv_container(gifv_info) {
	var container = document.getElementById("gifv-container-" + gifv_info.hash);

	Modernizr.on('video', function(has_video) {
		if(has_video && Modernizr.video.h264 == "probably") {
			Modernizr.on('videoautoplay', function(has_autoplay) {
			Modernizr.on('videoloop', function(has_loop){
				if(!has_autoplay || !has_loop) {
					// add controls to the video
					video_elems = container.getElementsByTagName("video");
					for (var i = 0; i < video_elems.length; i++) {
						video_elems[i].setAttribute("controls", "controls");
					}
				}
			}) });
		} else {
			// replace video with an animated GIF
			var pathbasename = gifv_info.path + "/" + gifv_info.basename;
			var gif_elem = document.createElement("img");
			gif_elem.setAttribute("src", pathbasename + ".gif");
			container.innerHTML = "";
			container.appendChild(gif_elem);
		}
	});
};

This function first detects if the browser can play the video. This is done with the Modernizr.on function instead of a simple if statement to account for the fact that Modernizr detects video support asynchronously, so the test may not be complete when this function is called. If video is not supported, the function modifies the DOM to remove the video element and inserts an animated GIF in its place. Assuming the browser can play the video, it’s still possible that some needed features will be unsupported, in particular the autoplay or loop attributes. In this case the JavaScript just modifies the video element to show playback controls for the animation. It’s not a perfect solution, but it works well enough, and I prefer it to resorting to an animated GIF.

That’s it! A bit more involved than I would have liked, but all in all it’s not too much code, and it gives results nice enough that I feel it’s well worth it.