packages feed

qr-0.2.0.0: cqr/cqr.hs

{-# LANGUAGE DeriveFunctor, DeriveFoldable, DeriveTraversable #-}
module Main where

import Control.Monad
import Control.Monad.IO.Class
import Control.Monad.Trans.Class
import Control.Monad.Trans.Maybe
import qualified Data.Array as A
import Data.Foldable (Foldable, asum)
import Data.Maybe
import Data.Monoid ((<>))
import Data.Traversable (Traversable, traverse)
import Data.IORef
import qualified Graphics.Rendering.Cairo as Cairo
import Graphics.UI.Gtk hiding (Display, Target)
import Options.Applicative
import System.Exit
import System.IO

import Data.QR.Encode
import Data.QR.Layout
import Data.QR.Grouping
import Data.QR.Types

data Opts s = Opts
  { optVersion :: Maybe Version
  , optLevel :: Level
  , optMode :: Mode
  , optMDisplay :: Maybe Display
  , optOutput :: Maybe FilePath
  , optSize :: Maybe (Int, Int)
  , optSource :: s }
  deriving (Eq, Ord, Read, Show, Functor, Foldable, Traversable)

optDisplay :: Opts s -> Display
optDisplay args = case optMDisplay args of
  Just d -> d
  Nothing -> case optOutput args *> optSize args of
    Just _ -> Image
    Nothing -> Cairo

data Display = Cairo | Console | Image
  deriving (Eq, Ord, Read, Show)

data Source = Text String
            | File FilePath
            | StdIn
  deriving (Eq, Ord, Read, Show)

opts :: Parser (Opts Source)
opts = Opts
  <$> (optional . option auto)
             ( long "symversion"
            <> short 'V'
            <> metavar "NUMBER"
            <> help "Version of the QR code: 1 to 40 (default: auto)" )
  <*> option auto
             ( long "level"
            <> short 'l'
            <> metavar "LEVEL"
            <> help "Error correction: L, M, Q (default) or H"
            <> value Q )
  <*> option auto
             ( long "mode"
            <> short 'm'
            <> metavar "MODE"
            <> help "Encoding mode: Numeric, Alpha or Byte (default)"
            <> value Byte )
  <*> (optional . option displayReader)
             ( long "display"
            <> short 'd'
            <> metavar "[cairo|console|image]"
            <> help "Display mode" )
  <*> (optional . option str)
             ( long "output"
            <> short 'o'
            <> metavar "FILE"
            <> help "Output file (implies image display)" )
  <*> (optional . option sizeReader)
             ( long "size"
            <> short 's'
            <> metavar "WIDTH,HEIGHT"
            <> help (concat [ "Image width and height in pixels, "
                            , "comma separated "
                            , "(implies image display)" ]) )
  <*> src

displayReader :: ReadM Display
displayReader = eitherReader $ \s -> do
  case s of
    "cairo" -> return Cairo
    "console" -> return Console
    "image" -> return Image
    _ -> Left "Invalid display mode. Possible choices: cairo, console, image."

sizeReader :: ReadM (Int, Int)
sizeReader = eitherReader $ \s -> do
  case break (== ',') s of
    (x, ',':y) -> (,) <$> sread "width" x <*> sread "height" y
    _ -> Left "Invalid size."
  where
    sread w x = case reads x of
      [(v, "")] -> Right v
      _ -> Left ("Invalid " ++ w ++ ".")

fileSource :: FilePath -> Source
fileSource "-" = StdIn
fileSource s = File s

src :: Parser Source
src = asum
  [ option r ( long "file"
          <> short 'f'
          <> metavar "FILENAME"
          <> help ( "Filename containing the data to encode "
                 ++ "(use '-' for standard input)" ) )
  , Text <$> argument str ( metavar "TEXT" )
  , pure StdIn ]
  where
    r = fmap fileSource str

extractText :: Source -> IO String
extractText (Text t) = pure t
extractText StdIn = getContents
extractText (File f) = readFile f

matrix :: Opts String -> Maybe Matrix
matrix (Opts mv l m _ _ _ txt) = do
  v <- mv <|> minimumVersion l m (length txt)
  return $ layout v l (message v l m txt)

main :: IO ()
main = do
  args <- execParser $ info (opts <**> helper)
    ( progDesc "Show a QR code" )
  targs <- traverse extractText args
  case matrix targs of
    Nothing -> hPutStrLn stderr "Message too large for a QR code"
            >> exitWith (ExitFailure 1)
    Just m -> runGUI args (optDisplay args) m

runGUI :: Opts s -> Display -> Matrix -> IO ()
runGUI _ Console m = do
  let (_, (xsize, ysize)) = A.bounds m
  let white = putStr "\ESC[47m  \ESC[0m"
  let black = putStr "\ESC[40m  \ESC[0m"
  replicateM_ (xsize + 3) white
  putChar '\n'
  forM_ [0 .. ysize] $ \y -> do
    white
    forM_ [0 .. xsize] $ \x -> do
      let c = m A.! (x, y)
      if c == Dark then black else white
    white
    putChar '\n'
  replicateM_ (xsize + 3) white
  putChar '\n'
runGUI _ Cairo m = do
  _ <- initGUI
  window <- windowNew

  _ <- window `on` exposeEvent $
    drawWindow window m
  _ <- handleResize window

  _ <- onDestroy window mainQuit
  widgetShowAll window
  mainGUI
runGUI args Image m = do
  let (w, h) = fromMaybe (300, 300) (optSize args)
      fmt = Cairo.FormatARGB32
  Cairo.withImageSurface fmt w h $ \surface -> do
    Cairo.renderWith surface (drawMatrix w h m)
    Cairo.surfaceWriteToPNG surface (fromMaybe "out.png" (optOutput args))

handleResize :: WindowClass w
             => w -> IO (ConnectId w)
handleResize window = do
  current <- windowGetSize window >>= newIORef
  (window `on` configureEvent) . fmap (const False) . runMaybeT $ do
    old_sz <- liftIO $ readIORef current
    sz <- lift eventSize
    guard $ sz /= old_sz
    liftIO $ do
      widgetQueueDraw window
      writeIORef current sz

drawMatrix :: Int -> Int -> Matrix -> Cairo.Render ()
drawMatrix wxi wyi m = do
  let (_, (xsize, ysize)) = A.bounds m
      multx = wxi `div` (xsize + 9)
      multy = wyi `div` (ysize + 9)
      mult = min multx multy
      offsetx = (wxi - mult * (xsize + 1)) `div` 2
      offsety = (wyi - mult * (ysize + 1)) `div` 2
      setColor Light = Cairo.setSourceRGB 1 1 1
      setColor Dark = Cairo.setSourceRGB 0 0 0
      setColor Reserved = Cairo.setSourceRGB 0.0 0.0 0.8
      setColor Empty = Cairo.setSourceRGB 0.8 0.8 0.8
  let drawTile md (x, y) = do
        Cairo.rectangle
          (fromIntegral (offsetx + x * mult))
          (fromIntegral (offsety + y * mult))
          (fromIntegral mult)
          (fromIntegral mult)
        setColor md
        Cairo.fill

  -- background
  setColor Light
  Cairo.rectangle 0 0 (fromIntegral wxi) (fromIntegral wyi)
  Cairo.fill

  Cairo.setLineWidth 0.1
  forM_ (A.assocs m) $ \(p, t) -> drawTile t p

drawWindow :: (MonadIO m, WidgetClass w)
           => w -> Matrix -> m Bool
drawWindow window m = liftIO $ do
  (wxi, wyi) <- liftIO $ widgetGetSize window
  cr <- widgetGetDrawWindow window
  renderWithDrawable cr (drawMatrix wxi wyi m)

  return True