{-# LANGUAGE NoMonomorphismRestriction #-} {-# LANGUAGE DoAndIfThenElse #-} -- The Secret Santa assigner -- Reads a list of friends from the standard input (or a file) and renders to a -- specified output file (or "output.pdf" if not specified) all assignments of -- gift givers and their corresponding receivers encoded in a QR-Code. module Main where import Control.Monad (liftM) import Data.List (intersperse, isSuffixOf) import Data.Map (Map, singleton, elems, (!), insert) import Data.Maybe ( fromMaybe ) import Data.QRCode import Data.Word (Word8) import Diagrams.Backend.Cairo.Internal import Diagrams.Prelude import System.Console.GetOpt hiding (Option) import qualified System.Console.GetOpt as GetOpt import System.Environment (getArgs, getProgName) import System.Random -- The QR-Code is a matrix of 0/1 bytes type QRCodeMatrix = [[Word8]] -- Return a random permutation from the list. Implementation based on -- http://okmij.org/ftp/Haskell/perfect-shuffle.txt shuffle :: RandomGen g => g -> [a] -> ([a], g) shuffle gen [] = ([], gen) shuffle gen (k:ks) = toElems $ foldl step (initial k gen) $ zip [1..] ks where toElems (x, y) = (elems x, y) initial x g = (singleton 0 x, g) step :: RandomGen g => (Map Int a, g) -> (Int, a) -> (Map Int a, g) step (m, g) (i, x) = ((insert j x . insert i (m ! j)) m, g') where (j, g') = randomR (0, i) g -- From a list of friends names, return a random assignment of gift giver and -- receiver, as tuples, given that the receiver name will be encoded as a -- QR-Code. The assignment is such that all giver-receiver are in a single -- loop. assign :: RandomGen g => g -> [String] -> IO ([(String, QRCodeMatrix)], g) assign g friends@(_:_:_) = do qrCodes <- mapM qrEncode $ tail shuffled ++ shuffled return $ shuffle g' $ zip shuffled qrCodes where (shuffled, g') = shuffle g friends qrEncode s = liftM toMatrix $ encodeString s Nothing QR_ECLEVEL_L QR_MODE_EIGHT True assign _ _ = error "Please provide at least two names" -- Partition a list in intervals, returning a grid toGrid :: Int -> [a] -> [[a]] toGrid _ [] = [[]] toGrid r l = a:toGrid r b where (a, b) = splitAt r l -- Renders a list of tuples of giver-name and qr-encoded-receiver-name into a -- diagram layout as a grid friendsDiagram :: Int -> [(String, QRCodeMatrix)] -> Diagram Cairo R2 friendsDiagram rowSize l = vcat $ intersperse hSep $ map formatRow $ toGrid rowSize $ map cell l where formatRow row = centerX $ hcat $ intersperse vSep row hSep = hrule (80 * fromIntegral rowSize) # dashing [5] 0 # lc gray vSep = vrule 80 # dashing [5] 0 # lc gray cell (n, q) = centerY (qrcDiagram q === txtD) <> square 80 # lw 0 where txtD = baselineText n # scale 10 # translate t # clipBy (rect 70 20) <> rect 80 20 # lw 0 t = (-35) & (-5) qrcDiagram code = centerX $ vcat $ map (hcat . map qrTile) code where qrTile x = square 2 # fc (if x == 0 then white else black) # lw 0 -- Command line options type data UserOptions = UserOptions {optOutput :: Maybe FilePath, optInput :: Maybe FilePath, optHelp :: Bool} deriving Show -- The default command line options defaultOptions :: UserOptions defaultOptions = UserOptions {optOutput = Nothing, optInput = Nothing, optHelp = False} -- Command line options definition options :: [OptDescr (UserOptions -> UserOptions)] options = [GetOpt.Option "o" ["output"] (ReqArg (\f opts -> opts {optOutput = Just f}) "FILE") "output FILE", GetOpt.Option "i" ["input"] (ReqArg (\f opts -> opts {optInput = Just f}) "FILE") "input FILE", GetOpt.Option "h" ["help", "usage"] (NoArg (\opts -> opts {optHelp = True})) "This help message"] -- Parse command line options into an options type getOptions :: IO UserOptions getOptions = do argv <- getArgs name <- getProgName case getOpt Permute options argv of (o,_,[]) -> return $ foldl (flip id) defaultOptions o (_,_,er) -> ioError $ userError $ concat er ++ usageInfo header options where header = "Usage: " ++ name ++ " [OPTION...]" -- Get input from standard input or from a file, depending on the options getInputContents :: UserOptions -> IO String getInputContents UserOptions {optInput = Nothing} = getContents getInputContents UserOptions {optInput = Just f} = readFile f -- Determine the Cairo output type based on the file path's extension getFileType :: FilePath -> OutputType getFileType filePath | ".png" `isSuffixOf` filePath = PNG | ".ps" `isSuffixOf` filePath = PS | ".pdf" `isSuffixOf` filePath = PDF | ".svg" `isSuffixOf` filePath = SVG | otherwise = error "Unknown file type" -- The program: parse command line options, read input and render graphics main :: IO () main = do opts <- getOptions if optHelp opts then do name <- getProgName let header = "Usage: " ++ name ++ " [OPTION...]" in putStr $ usageInfo header options else do contents <- getInputContents opts g <- getStdGen (codes, _) <- assign g $ lines contents let path = fromMaybe "output.pdf" $ optOutput opts outTy = getFileType path in fst $ renderDia Cairo (CairoOptions path (Width 800) outTy False) $ friendsDiagram 8 codes