packages feed

drawille-0.1.0.6: examples/Image2Term.hs

import Control.Monad (forM_, liftM, unless, when)
import Data.Maybe (isJust)
import qualified Data.Vector.Storable as VS
import Vision.Image (Grey, GreyPixel, InterpolMethod(..), convert, load,
                     manifestVector, resize, shape)
import Vision.Primitive ((:.)(..), ix2, Z(..))
import System.Console.Terminal.Size (size, height, width, Window(..))
import System.Console.GetOpt (ArgDescr(..), ArgOrder(..), getOpt,
                              OptDescr(..), usageInfo)
import System.Environment (getArgs, getProgName)
import System.Exit (ExitCode(..), exitWith, exitSuccess)
import System.IO (hPutStr, hPutStrLn, stderr)

import qualified System.Drawille as D

data Options = Options { optRatio     :: Maybe Double
                       , optThreshold :: Int
                       , optInvert    :: Bool
                       , optHelp      :: Bool
                       }
  deriving(Eq)

defaultOpts :: Options
defaultOpts = Options { optRatio     = Nothing
                      , optThreshold = 128
                      , optInvert    = False
                      , optHelp      = False
                      }


options :: [ OptDescr (Options -> Options) ]
options = [ Option "r" ["ratio"]
                   (ReqArg (\arg opt -> opt { optRatio = Just $ read arg })
                           "ratio")
                   "Image resize ratio"
          , Option "t" ["threshold"]
                   (ReqArg (\arg opt -> opt { optThreshold = read arg })
                           "threshold")
                   "Pixel printing threshold"
          , Option "i" ["invert"]
                   (NoArg (\opt -> opt { optInvert = True}))
                   "Invert colors"
          , Option "h" ["help"]
                   (NoArg (\opt -> opt { optHelp = True }))
                   "Display this message"
          ]

main :: IO ()
main = do
    (genOpts, files, errs) <- liftM (getOpt Permute options) getArgs
    let opts = foldr ($) defaultOpts genOpts

    when (not (null errs) || null files) $
        hPutStr stderr "\n" >>
        mapM_ (hPutStrLn stderr) errs >>
        printUsage >>
        exitWith (ExitFailure 1)

    when (optHelp opts) $
        hPutStr stderr "\n" >>
        printUsage >>
        exitSuccess

    maybeWindow <- size
    unless (isJust maybeWindow || isJust (optRatio opts)) $
        hPutStrLn stderr "Couldn't get window size and no ratio was provided" >>
        exitWith (ExitFailure 2)

    forM_ files $ \fp -> do
        errOrImage  <- load Nothing fp
        case errOrImage of
            Left err ->
                hPutStr stderr (show err) >>
                exitWith (ExitFailure 3)
            Right image ->
                let grey = convert image :: Grey
                    nwindow = getNWindow grey (optRatio opts) maybeWindow in
                    putStr $ D.frame $ fromImage nwindow
                                                 grey
                                                 (optThreshold opts)
                                                 (optInvert opts)

printUsage :: IO ()
printUsage = do
    name <- getProgName
    hPutStrLn stderr $ usageInfo ("Usage: " ++ name ++ " [options] files...")
                                 options

-- |
-- Gets a normalized Window given an image, a Maybe ratio and a Maybe
-- window.
getNWindow :: Grey -> Maybe Double -> Maybe (Window Int) -> Window Int
-- This is the base case. We'll want to resize the window to fit the
-- terminal's width and have its height scaled relative to that.
getNWindow image Nothing (Just window) = window { width = wwid
                                                , height = round whei
                                                }
  where Z :. h :. w = shape image
        wwid = 2 * width window
        whei = fromIntegral h * (fromIntegral wwid / fromIntegral w) :: Double
-- This is the ratio case, in which a factor to scale the image by is
-- given.
getNWindow image (Just ratio) _  =
    Window { height = round $ ratio * fromIntegral h
           , width  = round $ ratio * fromIntegral w
           }
  where Z :. h :. w = shape image
-- Because we check this is main, this pattern shouldn't ever run. It's
-- expected that one value or the other is provided - I think it'd be more
-- idiomatic to do this in two separate functions.
getNWindow _ Nothing Nothing = error "This shouldn't have happened"

-- |
-- Creates a canvas from a greyscale image.
fromImage :: Window Int -> Grey -> Int -> Bool -> D.Canvas
fromImage window greyImage threshold invert = VS.ifoldr accCanvas' D.empty imageVector
  where resizedImage = resizeToFitWindow window greyImage
        imageVector = manifestVector resizedImage
        {-# INLINEABLE accCanvas' #-}
        accCanvas' = accCanvas threshold (width window) invert

-- |
-- Gets a canvas coordinate given a canvas' width and a index to the flat
-- vector representation of an image's pixel matrix.
coord :: Int -> Int -> (Int, Int)
coord w i = (i `rem` w, w - i `div` w)
{-# INLINEABLE coord #-}

-- |
-- Accumulates pixels into a canvas.
accCanvas :: Int -> Int -> Bool -> Int -> GreyPixel -> D.Canvas -> D.Canvas
accCanvas threshold winW invert idx pix m =
    let withinThreshold = pix < fromIntegral threshold
        shouldSet = if invert then not withinThreshold else withinThreshold
      in if shouldSet
             then D.set m (coord winW idx)
             else m
{-# INLINEABLE accCanvas #-}

-- |
-- Resizes an image to fit a window.
resizeToFitWindow :: Window Int -> Grey -> Grey
resizeToFitWindow win = resize TruncateInteger (ix2 h w)
  where h = height win
        w = width win