content: {-# LANGUAGE FlexibleContexts, RecordWildCards #-}
  module Image.LaTeX.Render
         ( -- * Rendering Formulas
           imageForFormula
         , Formula, Baseline
           -- * Errors
         , RenderError (..)
           -- * Options
           -- ** Environment Options
         , EnvironmentOptions (..)
         , defaultEnv
         , TempDirectoryHandling (..)
           -- ** Formula Options
         , FormulaOptions (..)
         , displaymath
         , math
         )
         where
  
  import Codec.Picture
  import Data.Maybe
  import Control.Error.Util
  import Data.List
  import System.IO.Temp
  import System.FilePath
  import System.Process
  import System.Directory
  import Control.Monad.Trans.Except
  import Control.Monad
  import System.Exit
  import Control.Exception
  import Control.Arrow(second)
  import Control.Applicative
  import Data.Monoid
  import Prelude
  
  -- | This type contains all possible errors than can happen while rendering an equation.
  --   It includes all IO errors that can happen as well as more specific errors.
  data RenderError = ImageIsEmpty -- ^ The equation produced an empty image
                   | CannotDetectBaseline -- ^ The baseline marker could not be found
                   | LaTeXFailure String -- ^ @latex@ returned a nonzero error code
                   | DVIPSFailure String -- ^ @dvips@ returned a nonzero error code
                   | IMConvertFailure String -- ^ @convert@ returned a nonzero error code
                   | IOException IOException -- ^ An 'IOException' occurred while managing the temporary files used to convert the equation
                   | ImageReadError String -- ^ The PNG image from ImageMagick could not be read by JuicyPixels.
                   deriving (Show, Eq)
  
  
  data TempDirectoryHandling = UseSystemTempDir { nameTemplate :: String }
                             -- ^ A temporary directory with a name based on the given template will be created in the system temporary files location
                             | UseCurrentDir    { nameTemplate :: String }
                             -- ^ A temporary directory with a name based on the given template will be created in the current directory
         deriving (Eq, Show, Read, Ord)
  
  data EnvironmentOptions
       = EnvironmentOptions { latexCommand :: String -- ^ Command to use for @latex@, default is @latex@
                            , dvipsCommand :: String -- ^ Command to use for @dvips@, default is @dvips@
                            , imageMagickCommand :: String -- ^ Command to use for ImageMagick's @convert@, default is @convert@
                            , latexArgs :: [String] -- ^ Any additional arguments for @latex@
                            , dvipsArgs :: [String] -- ^ Any additional arguments for @dvips@
                            , imageMagickArgs :: [String] -- ^ Any additional arguments for @convert@
                            , tempDir :: TempDirectoryHandling -- ^ How to handle temporary files
                            , tempFileBaseName :: String -- ^ The base name to use for the temporary files.
                            }
         deriving (Eq, Show, Read, Ord)
  
  data FormulaOptions
       = FormulaOptions { preamble :: String -- ^ LaTeX preamble to use. Put your @\usepackage@ commands here.@ commands here.
                        , environment :: String -- ^ LaTeX environment in which the equation will be typeset, usually @math@ or @displaymath@
                        , dpi :: Int -- ^ DPI for the image to be rendered at. ~200 is good for retina displays, ~100 works OK for non-retina displays.
                        }
         deriving (Eq, Show, Read, Ord)
  
  -- | Use the @amsmath@ package, the @displaymath@ environment, and 200dpi.
  displaymath :: FormulaOptions
  displaymath = FormulaOptions "\\usepackage{amsmath}" "displaymath" 200
  
  -- | Use the @amsmath@ package, the @math@ environment, and 200dpi.
  math :: FormulaOptions
  math = FormulaOptions "\\usepackage{amsmath}\\usepackage{amsfonts}\\usepackage{stmaryrd}" "math" 200
  
  -- | Sensible defaults for system environments. Works if @dvips@, @convert@, and @latex@ are recent enough and in your @$PATH@.
  defaultEnv :: EnvironmentOptions
  defaultEnv = EnvironmentOptions "latex" "dvips" "convert" [] [] [] (UseSystemTempDir "latex-eqn-temp") "working"
  
  -- | A LaTeX formula, e.g @x=\frac{-b\pm\sqrt{b^2-4ac}}{2a}@ for the quadratic formula. Do not include any @$@s to denote the environment, just
  --   specify the environment in the 'FormulaOptions' instead.
  type Formula = String
  
  -- | Number of pixels from the bottom of the image to the typesetting baseline. Useful for setting your formulae inline with text.
  type Baseline = Int
  
  
  
  -- | Convert a formula into a JuicyPixels 'DynamicImage', also detecting where the typesetting baseline of the image is.
  imageForFormula :: EnvironmentOptions -> FormulaOptions -> Formula -> IO (Either RenderError (Baseline, DynamicImage))
  imageForFormula (EnvironmentOptions {..}) (FormulaOptions {..}) eqn =
      bracket getCurrentDirectory setCurrentDirectory $ const $ withTemp $ \temp -> runExceptT $ do
        let doc = mconcat ["\\nonstopmode\n",
                   "\\documentclass[12pt]{article}\n",
                   "\\pagestyle{empty}\n", preamble,
                   "\\begin{document}\n",
                   "\\begin{", environment, "}\n",
                   ".",eqn,
                   "\\end{", environment, "}\n",
                   "\\end{document}\n"]
        io $ writeFile (temp </> tempFileBaseName <.> "tex") doc
        io $ setCurrentDirectory temp
        (c,o,e) <- io $ flip (readProcessWithExitCode latexCommand) "" $ latexArgs ++ [tempFileBaseName <.> "tex"]
        io $ removeFile (tempFileBaseName <.> "tex") 
        io $ removeFile (tempFileBaseName <.> "aux")
        when (c /= ExitSuccess) $ do
          io $ removeFile (tempFileBaseName <.> "dvi")
          throwE $ LaTeXFailure (o ++ "\n" ++ e)
        (c',o',e') <- io $ flip (readProcessWithExitCode dvipsCommand) "" $ dvipsArgs ++ ["-q", "-E", "-o", tempFileBaseName <.> "ps", tempFileBaseName <.> "dvi"]
        io $ removeFile (tempFileBaseName <.> "dvi")
        when (c' /= ExitSuccess) $ throwE $ DVIPSFailure (o' ++ "\n" ++ e')
        (c'', o'', e'') <- io $ flip (readProcessWithExitCode imageMagickCommand) "" $
                                  [ "-density", show dpi
                                  , "-bordercolor", "none"
                                  , "-border", "1x1"
                                  , "-trim"
                                  , "-background", "none"
                                  , "-splice","1x0"
                                  ] ++ imageMagickArgs ++
                                  [ tempFileBaseName <.> "ps", tempFileBaseName <.> "png" ]
        io $ removeFile (tempFileBaseName <.> "ps")
        when (c'' /= ExitSuccess) $ throwE $ IMConvertFailure (o'' ++ "\n" ++ e'')
        imgM <- io $ readImage (tempFileBaseName <.> "png")
        img <- withExceptT ImageReadError $ hoistEither imgM
        io $ removeFile $ tempFileBaseName <.> "png"
        hoistEither $ postprocess img
    where
      io = withExceptT IOException . tryIO
      withTemp a = case tempDir of
        UseSystemTempDir f -> withSystemTempDirectory f a
        UseCurrentDir f -> withTempDirectory "." f a
  
  postprocess :: DynamicImage -> Either RenderError (Int, DynamicImage)
  postprocess (ImageY8 i)     = second ImageY8     <$> postprocess' i (pixelAt i 0 0)
  postprocess (ImageY16 i)    = second ImageY16    <$> postprocess' i (pixelAt i 0 0)
  postprocess (ImageYF i)     = second ImageYF     <$> postprocess' i (pixelAt i 0 0)
  postprocess (ImageYA8 i)    = second ImageYA8    <$> postprocess' i (pixelAt i 0 0)
  postprocess (ImageYA16 i)   = second ImageYA16   <$> postprocess' i (pixelAt i 0 0)
  postprocess (ImageRGB8 i)   = second ImageRGB8   <$> postprocess' i (pixelAt i 0 0)
  postprocess (ImageRGB16 i)  = second ImageRGB16  <$> postprocess' i (pixelAt i 0 0)
  postprocess (ImageRGBF i)   = second ImageRGBF   <$> postprocess' i (pixelAt i 0 0)
  postprocess (ImageRGBA8 i)  = second ImageRGBA8  <$> postprocess' i (pixelAt i 0 0)
  postprocess (ImageRGBA16 i) = second ImageRGBA16 <$> postprocess' i (pixelAt i 0 0)
  postprocess (ImageYCbCr8 i) = second ImageYCbCr8 <$> postprocess' i (pixelAt i 0 0)
  postprocess (ImageCMYK8 i)  = second ImageCMYK8  <$> postprocess' i (pixelAt i 0 0)
  postprocess (ImageCMYK16 i) = second ImageCMYK16 <$> postprocess' i (pixelAt i 0 0)
  
  
  postprocess' :: (Eq a, Pixel a) => Image a -> a -> Either RenderError (Int, Image a)
  postprocess' img bg
    = do startX <- note ImageIsEmpty $ listToMaybe $ dropWhile isEmptyCol [0.. imageWidth img - 1]
         let (dotXs, postXs) = break isEmptyCol [startX .. imageWidth img]
         postX <- note CannotDetectBaseline $ listToMaybe postXs
         let postY = (+ 2) $ average $ dotXs >>= (\x -> takeWhile (not . isEmpty x) (dropWhile (isEmpty x) [0..imageHeight img - 1]))
             average = uncurry div . foldl' (\(s,c) e -> (e+s,c+1)) (0,0)
             newHeight = imageHeight img
             newWidth  = imageWidth img - postX + 3
             baseline  = imageHeight img - postY
         let image = generateImage (pixelAt' . (+ postX)) newWidth newHeight
         return (baseline, image)
    where
      isEmptyCol x = all (isEmpty x) [0.. imageHeight img - 1]
      isEmpty x = (== bg) . pixelAt img x
      pixelAt' x y | x < imageWidth img && y < imageHeight img = pixelAt img x y
                   | otherwise = bg
  
  