latex-formulae-image (empty) → 0.1.0.0
raw patch · 4 files changed
+253/−0 lines, 4 filesdep +JuicyPixelsdep +basedep +directorysetup-changed
Dependencies added: JuicyPixels, base, directory, errors, filepath, process, temporary, transformers, transformers-compat
Files
- LICENSE +30/−0
- Setup.hs +2/−0
- latex-formulae-image.cabal +52/−0
- src/Image/LaTeX/Render.hs +169/−0
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright Liam O'Connor (c) 2015++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 Liam O'Connor 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.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ latex-formulae-image.cabal view
@@ -0,0 +1,52 @@+name: latex-formulae-image+version: 0.1.0.0+synopsis: A library for rendering LaTeX formulae as images using an actual LaTeX installation+description: This library provides the basic infrastructure necessary to convert LaTeX formulae into images, using a real+ LaTeX installation. This is useful in particular for showing formulae on websites, where using alternatives+ like MathJax is not an option (e.g, when you want to use various LaTeX packages that MathJax doesn't support).+ .+ This library requires @latex@, @dvips@ and ImageMagick's @convert@ to be present in the system. @convert@+ needs to understand PostScript files.+ .+ We also+ use a few tricks to compute where the baseline is in the image, so these images can be effectively typeset inline+ with other text.+ .+ The companion library to this, @latex-formulae-pandoc@, provides useful tools to integrate this library with pandoc,+ when generating HTML documents.+homepage: http://github.com/liamoc/latex-formulae#readme+license: BSD3+license-file: LICENSE+author: Liam O'Connor+maintainer: liamoc@cse.unsw.edu.au+copyright: Liam O'Connor, 2015+category: Image+build-type: Simple+-- extra-source-files:+cabal-version: >=1.10+source-repository head+ type: git+ location: https://github.com/liamoc/latex-formulae+flag old-base+ description: if building with older base versions+ default: False++library+ hs-source-dirs: src+ exposed-modules: Image.LaTeX.Render+ if flag(old-base)+ build-depends: base >= 4.7 && < 4.8+ , transformers-compat >= 0.4 && < 0.5+ else+ build-depends: base >= 4.8 && < 4.9+ build-depends: JuicyPixels >= 3.2 && < 3.3, errors >= 2.0 && < 2.1+ , temporary >= 1.2 && < 1.3+ , transformers >= 0.3 && < 0.5+ , directory >= 1.2 && < 1.3+ , process >= 1.2 && < 1.3+ , filepath >= 1.3 && < 1.5+ default-language: Haskell2010++source-repository head+ type: git+ location: https://github.com/liamoc/latex-formulae-image
+ src/Image/LaTeX/Render.hs view
@@ -0,0 +1,169 @@+{-# 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)+++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++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.+ }++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.+ }++-- | 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+