diff --git a/gruff-examples.cabal b/gruff-examples.cabal
--- a/gruff-examples.cabal
+++ b/gruff-examples.cabal
@@ -1,8 +1,9 @@
 Name:                gruff-examples
-Version:             0.3
+Version:             0.3.1
 Synopsis:            Mandelbrot Set examples using ruff and gruff
 Description:
-  Some example animation scripts.
+  Some example scripts, including a converter from old versions of gruff
+  file formats to the current file format.
 
 License:             GPL-2
 License-file:        LICENSE
@@ -17,6 +18,21 @@
   description: use 'hmpfr' for higher precision floating point
   default: False
 
+Executable gruff-convert
+  Hs-source-dirs:     src
+  Main-is:            gruff-convert.hs
+  Other-modules:      Convert.Common
+                      Convert.Gruff
+                      Convert.Gruff1
+                      Convert.Gruff2a
+                      Convert.Gruff2
+  Build-depends:      base >= 4 && < 5,
+                      filepath,
+                      ruff >= 0.3 && < 0.4,
+                      gruff >= 0.3 && < 0.4
+  GHC-options:        -Wall -threaded -rtsopts
+  GHC-Prof-Options:   -prof -auto-all -caf-all
+
 Executable gruff-labels
   Hs-source-dirs:     src
   Main-is:            gruff-labels.hs
@@ -104,4 +120,4 @@
 source-repository this
   type:     git
   location: git://gitorious.org/ruff/gruff-examples.git
-  tag:      v0.3
+  tag:      v0.3.1
diff --git a/src/Convert/Common.hs b/src/Convert/Common.hs
new file mode 100644
--- /dev/null
+++ b/src/Convert/Common.hs
@@ -0,0 +1,33 @@
+module Convert.Common (readMay, image, image', Colour(Colour), Image()) where
+import Data.List (sortBy)
+import Data.Ord (comparing)
+import Fractal.GRUFF
+import Fractal.RUFF.Types.Complex(Complex((:+)))
+
+image :: Rational -> Rational -> Double -> Image
+image = image' 512 288 4 (red, black, white)
+  where
+    red   = Colour 1 0 0
+    black = Colour 0 0 0
+    white = Colour 1 1 1
+
+image' :: Int -> Int -> Double -> (Colour, Colour, Colour) -> Rational -> Rational -> Double -> Image
+image' w h ss (ci, cb, ce) re im sz = Image
+  { imageWindow = Window{ width = w, height = h, supersamples = ss }
+  , imageViewport = Viewport{ aspect = fromIntegral w / fromIntegral h, orient = 0 }
+  , imageLocation = Location{ center = re :+ im, radius = sz }
+  , imageColours = Colours{ colourInterior = ci, colourBoundary = cb, colourExterior = ce }
+  , imageLabels = []
+  , imageLines = []
+  }
+
+readMay :: Read a => String -> Maybe a
+readMay s = case sortBy (comparing (length . snd)) . filter (all whiteSpace . snd) . reads $ s of
+  (a, _):_ -> Just a
+  _ -> Nothing
+  where
+    whiteSpace ' '  = True
+    whiteSpace '\t' = True
+    whiteSpace '\n' = True
+    whiteSpace '\r' = True
+    whiteSpace  _   = False
diff --git a/src/Convert/Gruff.hs b/src/Convert/Gruff.hs
new file mode 100644
--- /dev/null
+++ b/src/Convert/Gruff.hs
@@ -0,0 +1,34 @@
+module Convert.Gruff (gruff) where
+import Convert.Common (readMay, image, Image)
+import Numeric (readSigned, readFloat)
+
+data AngledInternalAddress
+  = Unangled Integer
+  | Angled Integer Angle AngledInternalAddress
+  deriving Read
+
+type Angle = Rational
+
+newtype R = R Rational
+
+instance Read R where
+  readsPrec _ = map (\(x, s) -> (R x, s)) . readParen False (readSigned readFloat)
+
+data Gruff = Gruff
+  { gAddress :: Maybe AngledInternalAddress
+  , gIsland :: Maybe AngledInternalAddress
+  , gChild :: Maybe [Angle]
+  , gLowerAngle :: Maybe Angle
+  , gUpperAngle :: Maybe Angle
+  , gReal :: Maybe R
+  , gImag :: Maybe R
+  , gSize :: Maybe R
+  }
+  deriving Read
+
+gruff :: String -> Maybe Image
+gruff s = convert =<< readMay s
+
+convert :: Gruff -> Maybe Image
+convert Gruff{ gReal = Just (R re), gImag = Just (R im), gSize = Just (R sz) } = Just $ image re im (fromRational sz)
+convert _ = Nothing
diff --git a/src/Convert/Gruff1.hs b/src/Convert/Gruff1.hs
new file mode 100644
--- /dev/null
+++ b/src/Convert/Gruff1.hs
@@ -0,0 +1,36 @@
+module Convert.Gruff1 (gruff1) where
+import Convert.Common (readMay, image, Image)
+import Numeric (readSigned, readFloat)
+
+data AngledInternalAddress
+  = Unangled Integer
+  | Angled Integer Angle AngledInternalAddress
+  deriving Read
+
+type Angle = Rational
+
+newtype R = R Rational
+
+instance Read R where
+  readsPrec _ = map (\(x, s) -> (R x, s)) . readParen False (readSigned readFloat)
+
+data Gruff1 = Gruff1
+  { gAddress :: Maybe AngledInternalAddress
+  , gIsland :: Maybe AngledInternalAddress
+  , gChild :: Maybe [Angle]
+  , gLowerAngle :: Maybe Angle
+  , gUpperAngle :: Maybe Angle
+  , gReal :: Maybe R
+  , gImag :: Maybe R
+  , gSize :: Maybe R
+  , gHueShift :: Maybe R
+  , gHueScale :: Maybe R
+  }
+  deriving Read
+
+gruff1 :: String -> Maybe Image
+gruff1 s = convert =<< readMay s
+
+convert :: Gruff1 -> Maybe Image
+convert Gruff1{ gReal = Just (R re), gImag = Just (R im), gSize = Just (R sz) } = Just $ image re im (fromRational sz)
+convert _ = Nothing
diff --git a/src/Convert/Gruff2.hs b/src/Convert/Gruff2.hs
new file mode 100644
--- /dev/null
+++ b/src/Convert/Gruff2.hs
@@ -0,0 +1,50 @@
+module Convert.Gruff2 (gruff2) where
+import Convert.Common (readMay, image', Colour(Colour), Image())
+import Numeric (readSigned, readFloat)
+
+data AngledInternalAddress
+  = Unangled Integer
+  | Angled Integer Angle AngledInternalAddress
+  deriving Read
+
+type Angle = Rational
+
+newtype R = R Rational
+
+instance Read R where
+  readsPrec _ = map (\(x, s) -> (R x, s)) . readParen False (readSigned readFloat)
+
+data Color = Color Int Int Int
+  deriving Read
+
+c :: Color -> Colour
+c (Color r g b) = Colour (fromIntegral r / m) (fromIntegral g / m) (fromIntegral b / m) where m = 65535
+
+data Window = Window{ width :: Int, height :: Int, supersamples :: R }
+  deriving Read
+
+data Viewport = Viewport{ aspect :: R, orient :: R }
+  deriving Read
+
+data Gruff2 = Gruff2
+  { gAddress :: Maybe AngledInternalAddress
+  , gReal :: Maybe R
+  , gImag :: Maybe R
+  , gSize :: Maybe R
+  , gRota :: Maybe Double
+  , gColours :: (Color, Color, Color)
+  , gWindow :: Window
+  , gViewport :: Viewport
+  }
+  deriving Read
+
+gruff2 :: String -> Maybe Image
+gruff2 s = convert =<< readMay s
+
+convert :: Gruff2 -> Maybe Image -- FIXME handle rota, orient, aspect?
+convert Gruff2
+  { gReal = Just (R re), gImag = Just (R im), gSize = Just (R sz)
+  , gColours = (ci, cb, ce)
+  , gWindow = Window{ width = w, height = h, supersamples = R ss }
+  } = Just $ image' w h (fromRational ss) (c ci, c cb, c ce) re im (fromRational sz)
+convert _ = Nothing
diff --git a/src/Convert/Gruff2a.hs b/src/Convert/Gruff2a.hs
new file mode 100644
--- /dev/null
+++ b/src/Convert/Gruff2a.hs
@@ -0,0 +1,41 @@
+module Convert.Gruff2a (gruff2a) where
+import Convert.Common (readMay, image', Colour(Colour), Image())
+import Numeric (readSigned, readFloat)
+
+data AngledInternalAddress
+  = Unangled Integer
+  | Angled Integer Angle AngledInternalAddress
+  deriving Read
+
+type Angle = Rational
+
+newtype R = R Rational
+
+instance Read R where
+  readsPrec _ = map (\(x, s) -> (R x, s)) . readParen False (readSigned readFloat)
+
+data Color = Color Int Int Int
+  deriving Read
+
+c :: Color -> Colour
+c (Color r g b) = Colour (fromIntegral r / m) (fromIntegral g / m) (fromIntegral b / m) where m = 65535
+
+data Gruff2a = Gruff2
+  { gAddress :: Maybe AngledInternalAddress
+  , gReal :: Maybe R
+  , gImag :: Maybe R
+  , gSize :: Maybe R
+  , gRota :: Maybe Double
+  , gColours :: (Color, Color, Color)
+  }
+  deriving Read
+
+gruff2a :: String -> Maybe Image
+gruff2a s = convert =<< readMay s
+
+convert :: Gruff2a -> Maybe Image -- FIXME handle rota?
+convert Gruff2
+  { gReal = Just (R re), gImag = Just (R im), gSize = Just (R sz)
+  , gColours = (ci, cb, ce)
+  } = Just $ image' 512 288 4 (c ci, c cb, c ce) re im (fromRational sz)
+convert _ = Nothing
diff --git a/src/gruff-convert.hs b/src/gruff-convert.hs
new file mode 100644
--- /dev/null
+++ b/src/gruff-convert.hs
@@ -0,0 +1,27 @@
+import Data.Maybe (mapMaybe)
+import System.Environment (getArgs)
+import System.FilePath ((</>), takeFileName)
+
+import Convert.Common (Image)
+import Convert.Gruff  (gruff)
+import Convert.Gruff1 (gruff1)
+import Convert.Gruff2a(gruff2a)
+import Convert.Gruff2 (gruff2)
+
+parsers :: [String -> Maybe Image]
+parsers = [gruff2, gruff2a, gruff1, gruff]
+
+main :: IO ()
+main = do
+  args <- getArgs
+  case args of
+    odir:files@(_:_) -> mapM_ (main1 odir) files
+    _ -> putStrLn "usage: gruff-convert outdir/ *.oldformat.gruff"
+
+main1 :: FilePath -> FilePath -> IO ()
+main1 odir file = do
+  old <- readFile file
+  case mapMaybe ($ old) parsers of
+    []    -> putStrLn $ "Error: `" ++ file ++ "' unrecognised"
+    [new] -> writeFile (odir </> takeFileName file) (show new)
+    _     -> putStrLn $ "Error: `" ++ file ++ "' ambiguous"
diff --git a/src/gruff-octopus.hs b/src/gruff-octopus.hs
--- a/src/gruff-octopus.hs
+++ b/src/gruff-octopus.hs
@@ -9,7 +9,7 @@
 import Fractal.RUFF.Types.Complex (Complex((:+)))
 
 main :: IO ()
-main = print animation
+main = defaultMain animation
 
 animation :: [(Image, FilePath)]
 animation = mapMaybe scene (score `zip` [0..])
diff --git a/src/gruff-patterns.hs b/src/gruff-patterns.hs
--- a/src/gruff-patterns.hs
+++ b/src/gruff-patterns.hs
@@ -13,8 +13,11 @@
 
 main :: IO ()
 main = do
-  [num, den, depth] <- map read `fmap` getArgs
-  defaultMain (animation (num % den) (fromIntegral depth))
+  args <- getArgs
+  case map reads args of
+    [[(num, "")], [(den, "")], [(depth, "")]] ->
+      defaultMain (animation (num % den) (fromIntegral depth))
+    _ -> putStrLn "usage: gruff-patterns num den depth | gruff"
 
 animation :: Rational -> Int -> [(Image, FilePath)]
 animation r d = mapMaybe scene (score r d)
diff --git a/src/gruff-randoms.hs b/src/gruff-randoms.hs
--- a/src/gruff-randoms.hs
+++ b/src/gruff-randoms.hs
@@ -20,13 +20,20 @@
 
 main :: IO ()
 main = do
-  n <- (read . head) `fmap` getArgs
-  gs <- unfoldr (Just . split) `fmap` newStdGen
-  ch <- newChan
-  forM_ ([0..] `zip` take numCapabilities gs) $ forkIO . worker ch
-  let unique = nubBy ((==) `on` snd)
-      f ((i, _), a) = (i, toFileName (prettyAngledInternalAddress a))
-  defaultMain . take n . map f . unique =<< getChanContents ch
+  args <- getArgs
+  case args of
+    [ns] -> case reads ns of
+      [(n, "")] -> do
+        gs <- unfoldr (Just . split) `fmap` newStdGen
+        ch <- newChan
+        forM_ ([0..] `zip` take numCapabilities gs) $ forkIO . worker ch
+        let unique = nubBy ((==) `on` snd)
+            f ((i, _), a) = (i, toFileName (prettyAngledInternalAddress a))
+        defaultMain . take n . map f . unique =<< getChanContents ch
+      _ -> usage
+    _ -> usage
+  where
+    usage = putStrLn "usage: gruff-randoms count | gruff"
 
 toFileName :: String -> String
 toFileName = (++ ".ppm") . map toFileChar
diff --git a/src/gruff-whn.hs b/src/gruff-whn.hs
--- a/src/gruff-whn.hs
+++ b/src/gruff-whn.hs
@@ -32,7 +32,7 @@
               [(q'', "")] -> q''
               _ -> Preview
             _ -> Preview
-  print (animation q)
+  defaultMain (animation q)
 
 window :: Quality -> Window
 window Preview = Window{ width =  512, height =  288, supersamples =  1    }
