Sunday, March 10, 2013

"top" in an animated GIF

I noticed a lot of animated GIFs showing up in my Google+ feed recently.  Mainly it was cats.

I thought, why not render "top" output as an animated GIF?  It's not the most practical thing, but it seemed like a fun challenge to see if I could do this with minimal python coding.

The Result!


A 770kb GIF showing about 12 seconds of top output.  You can see pretty easily that I have a misbehaving chrome tab, chewing up an entire CPU core.  I added the green pie-chart thingy to give a visual indication of position within the animation, since it loops 10 times and would be confusing.

The Code

#!/usr/bin/python

from PIL import Image, ImageDraw, ImageFont, ImageSequence
from images2gif import writeGif
import os, subprocess, sys, time

FRAMES = 12
FRAME_DELAY = 0.75
WIDTH, HEIGHT = 650, 300
PIE_POS = (WIDTH-50,10, WIDTH-10,50)
FONT = ImageFont.truetype('/usr/share/fonts/liberation/LiberationMono-Regular.ttf', 12)

def make_frame(txt, count, font=FONT):
  image = Image.new("RGBA", (WIDTH, HEIGHT), (255,255,255))
  draw = ImageDraw.Draw(image)
  fontsize = font.getsize('')[1]
  for row, line in enumerate(txt.split('\n')):
    draw.text((5, fontsize * row), line, (0,0,0), font=font)
  draw.pieslice(PIE_POS, 0, 360, (255,255,204))
  draw.pieslice(PIE_POS, 0, int(360.0/FRAMES*(1+count)), (0,128,0))
  return image


frames = []
for count in range(FRAMES):
  txt = subprocess.Popen('top -c -n 1 -b'.split(), stdout=subprocess.PIPE).stdout.read()
  frames.append(make_frame(txt, count))
  time.sleep(FRAME_DELAY)

writeGif("topmovie.gif", frames, duration=FRAME_DELAY, loops=10, dither=0)


The Libraries: PIL and images2gif

I run Fedora (18) which provides the PIL library as an RPM named python-imaging, so you can install that easily:

  sudo yum install python-imaging


The PIL modules provide the basic drawing and canvas processing features, and the 'images2gif.py' module works on top of PIL to implement the animated GIF standard.  


The images2gif.py library is available for download here: http://bit.ly/XMMn5h

The images2gif.py code looked like it could use some cleanup, and I didn't need its support for numpy arrays, so a slightly smaller cleaned up version is also here: http://pastebin.com/1xDHnWFK


How It Works


The code uses subprocess to run 'top' once per frame interval, and builds a new frame by calling the draw.text() method for each line in the output.  I was a little surprised that PIL's text() method doesn't handle newlines, but it was easy to implement using split() and enumerate() on the text, and multiplying the line number by the font height to get the next offset.  All the frames are collected into a list and passed to the writeGif() method from images2gif which outputs the special header and per-frame extension blocks needed to animate the GIF.

The pie slice rendering is done by first drawing a full circle and then a partial pie with the filled-in degrees calculated from the current frame number.

This code can be improved a lot, but as an example to work from, it's small and should be easy to hack on.