packages feed

svgutils-0.1: Data/SVG/TileMain.hs

-- SVGutils
-- Copyright (c) 2010, Neil Brown
--
-- All rights reserved.
--
-- Redistribution and use in source and binary forms, with or without
-- modification, are permitted provided that the following conditions are met:
--
--     * Redistributions of source code must retain the above copyright
--       notice, this list of conditions and the following disclaimer.
--
--     * Redistributions in binary form must reproduce the above
--       copyright notice, this list of conditions and the following
--       disclaimer in the documentation and/or other materials provided
--       with the distribution.
--
--     * Neither the name of Neil Brown nor the names of other
--       contributors may be used to endorse or promote products derived
--       from this software without specific prior written permission.
--
-- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-- "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-- LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
-- A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
-- OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-- SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-- LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
-- DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
-- THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-- (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-- OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

import Control.Applicative ((<$>), (<*>))
import Control.Exception (IOException, try)
import Control.Monad (zipWithM_)
import Data.Either (lefts, rights)
import Data.List (minimumBy, sortBy)
import Data.Ord (comparing)
import Data.SVG.Internal.Fail
import Data.SVG.Paper
import Data.SVG.SVG
import Data.SVG.Tile
import System.Console.GetOpt
import System.Environment (getArgs)
import System.FilePath ((<.>), takeBaseName)
import System.IO (hPutStrLn, stderr)
import System.Exit (exitWith, ExitCode(..))

data Raw a b = Raw { r :: a }
data Processed a b = Processed { p :: b }

data Settings f = Settings
  { tryPaperRotate :: Bool
  , trySortingBySize :: Bool
  , help :: Bool
  , ignoreConflictingNamespaces :: Bool
  , dpi :: f String DPI
  , paperSize :: f String Size
  , margin :: f String MM
  , gap :: f String MM
  }

processSettings :: Settings Raw -> Either String (Settings Processed)
processSettings (Settings rt s h i d ps m g)
  = runFail $ do
      d' <- maybeFail ("Could not parse DPI: " ++ r d) (DPI <$> maybeRead (r d)) 
      Settings rt s h i (Processed d')
       <$> proc ("Unknown paper size: " ++ r ps) (parsePaperSize (r ps))
       <*> proc ("Could not parse margin: " ++ r m) (parseCoord d' (r m))
       <*> proc ("Could not parse gap: " ++ r g) (parseCoord d' (r g))
  where
    proc u x = Processed <$> maybeFail u x

defaultSettings :: Settings Raw
defaultSettings = Settings False False False False (Raw "90") (Raw "a4") (Raw "20mm") (Raw "0mm")

options :: [OptDescr (Settings Raw -> Settings Raw)]
options =
 [Option "h" ["help"] (NoArg $ \s -> s {help = True}) "This help"
 ,Option "d" ["dpi"] (ReqArg (\d s -> s {dpi = Raw d}) "DPI")
   "Sets the DPI (Dots-Per-Inch) assumed; default is 90"
 ,Option "r" ["rotate"] (NoArg $ \s -> s {tryPaperRotate = True})
   "Try rotating the paper to see if less pages are needed"
 ,Option "s" ["sort"] (NoArg $ \s -> s {trySortingBySize = True})
   "Try sorting the items by height to see if less pages are needed"
 ,Option "z" ["size"] (ReqArg (\z s -> s {paperSize = Raw z}) "SIZE")
   "Paper size (e.g. \"a4\", \"letter\", \"10cm*10cm\"); default is a4"
 ,Option "m" ["margin"] (ReqArg (\m s -> s {margin = Raw m}) "MARGIN")
   "Margin (e.g. 3cm), default is 20mm"
 ,Option "g" ["gap"] (ReqArg (\g s -> s {gap = Raw g}) "GAP")
   "Gap between tiled items (e.g. 1cm), default is 0cm"
 ,Option "i" ["ignore-ns-conflicts"] (NoArg $ \s -> s {ignoreConflictingNamespaces = True})
   "Ignore conflicting namespaces in SVG files"
 ]

processArgs :: IO (Settings Processed, [String])
processArgs = do (fs, files, errs) <- getOpt Permute options <$> getArgs
                 if null errs
                   then let s = foldr (.) id fs $ defaultSettings
                        in if help s || null files
                             then do putStrLn $ usageInfo "SVGtile <file1.svg> <file2.svg> ... <fileN.svg>" options
                                     exitWith (if help s then ExitSuccess else ExitFailure 1)
                             else case processSettings s of
                               Left err -> quitWith err
                               Right s' -> return (s', files)
                   else quitWith (unlines errs)
                   
makeParams :: Settings Processed -> [(String, SVG, Maybe SVG)] -> [(TileSettings, [TileItem])]
makeParams s svgs
  = [ (TileSettings paper (p $ margin s) (p $ gap s) (ignoreConflictingNamespaces s), map (uncurry3 TileItem) items)
    | paper <- (p $ paperSize s) : [rotate (p $ paperSize s) | tryPaperRotate s]
    , items <- svgs : [map snd $ sortBy cmp svgsWithSize
                      | cmp <- [comparing height, flip (comparing height)]
                      , trySortingBySize s]
    ]
  where
    rotate (Size w h) = Size h w

    uncurry3 f (x, y, z) = f x y z

    svgsWithSize :: [(Maybe Size, (String, SVG, Maybe SVG))]
    svgsWithSize = [(getSVGSize (p $ dpi s) front, (l, front, back)) | (l, front, back) <- svgs]

    height :: (Maybe Size, a) -> Maybe MM
    height = fmap mmHeight . fst

output :: [(SVG, Maybe SVG)] -> IO ()
output = zipWithM_ write [1..]
  where
    write :: Int -> (SVG, Maybe SVG) -> IO ()
    write n (front, mback)
      = do writeFile ("TiledFront-" ++ show n ++ ".svg") (show front)
           case mback of
             Nothing -> return ()
             Just back -> writeFile ("TiledBack-" ++ show n ++ ".svg") (show back)

loadSVG :: FilePath -> IO (String, SVG, Maybe SVG)
loadSVG fullName
  = do mfront <- parseSVG <$> readFile fullName
       case mfront of
         Nothing -> quitWith ("Could not parse SVG file: " ++ fullName)
         Just front -> do mend <- either ioNothing parseSVG <$> try (readFile backName)
                          return (fullName, front, mend)
  where
    ioNothing :: IOException -> Maybe a
    ioNothing _ = Nothing

    backName = (takeBaseName fullName ++ "-front") <.> "svg"

quitWith :: String -> IO a
quitWith msg = hPutStrLn stderr msg >> exitWith (ExitFailure 1)

main :: IO ()
main = do (s, fileNames) <- processArgs
          svgs <- mapM loadSVG fileNames
          let ps = makeParams s svgs
          let rs = map (uncurry $ tileSVGs (p $ dpi s)) ps
          case rights rs of
            [] -> quitWith $ head (lefts rs)
            rrs -> output $ minimumBy (comparing length) rrs