Regarding the problem selecting the right image type, you have enough information in your header to select which type of image to create.

      case header of
        Nothing -> putStrLn $ source ++ ": Bad image"
        Just (header,_) -> case (imageEncoding header) of
          ASCII -> putStrLn $ source ++ ": Already an ASCII image"
          Binary -> let image = decode content :: Maybe PPMImage
                    in convertImage image dest

Becomes

      image <- case header of
        Nothing -> putStrLn $ source ++ ": Bad image"
        Just (Header _ ASCII _ _) ->
          putStrLn $ source ++ ": Already an ASCII image"
        Just (Header PPM _ _ _) -> decode content :: Maybe PPMImage
        Just (Header PGM _ _ _) -> decode content :: Maybe PGMImage
      convertImage image dest

Besides, this is a check that should really be happening inside your call to decode. Ask yourself what would happen if your user called `decode content :: Maybe PGMImage` with PPM encoded content.

Perhaps consider leaving the image format at the value level?

    newtype Image pix = MkImg (Array Coord pix) deriving Show

    decode PPM content :: Image p

-------------

Following up on cdk's answer: if you are worried about speed, you should take care how you represent your data.

    data ColorPixel = ColorPixel Int Int Int deriving Show

while this looks like it's cheap, beware that those `Int`s are boxed values. This means that they can potentially hold thunks, or pointers to ints. It would be very rare that you would ever need just one color component, try using strictness annotations here:

    {-# LANGUAGE BangPatterns #-}
    data ColorPixel = ColorPixel !Int !Int !Int deriving Show

You should also do this for all performance sensitive single field datatypes that you don't convert into newtypes.