latex-svg-image (empty) → 0.1
raw patch · 4 files changed
+414/−0 lines, 4 filesdep +basedep +base64-bytestringdep +bytestringsetup-changed
Dependencies added: base, base64-bytestring, bytestring, cryptohash-sha256, deepseq, directory, filepath, parsec, process, temporary, transformers
Files
- LICENSE +30/−0
- Setup.hs +2/−0
- latex-svg-image.cabal +55/−0
- src/Image/LaTeX/Render.hs +327/−0
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright (c) 2020 Oleg Grenrus, 2015 Liam O'Connor++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-svg-image.cabal view
@@ -0,0 +1,55 @@+cabal-version: 2.2+name: latex-svg-image+version: 0.1+synopsis:+ A library for rendering LaTeX formulae as SVG using an actual LaTeX++description:+ This library provides the basic infrastructure necessary to convert LaTeX+ formulae into SVG 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@, @dvisvgm@ to be present in the system.+ .+ The companion library to this, @latex-svg-pandoc@, provides useful tools to+ integrate this library with pandoc, when generating HTML documents.+ .+ This is a fork of https://github.com/liamoc/latex-formulae++homepage: http://github.com/phadej/latex-svg#readme+license: BSD-3-Clause+license-file: LICENSE+author: Oleg Grenrus, Liam O'Connor+maintainer: Oleg Grenrus <oleg.grenrus@iki.fi>+copyright: 2020 Oleg Grenrus, 2015-2019 Liam O'Connor+category: Image+build-type: Simple+tested-with: GHC ==8.2.2 || ==8.4.4 || ==8.6.5 || ==8.8.3++source-repository head+ type: git+ location: https://github.com/phadej/latex-svg+ subdir: latex-svg-image++library+ default-language: Haskell2010+ hs-source-dirs: src+ ghc-options: -Wall+ exposed-modules: Image.LaTeX.Render+ build-depends:+ , base >=4.10 && <4.14+ , deepseq ^>=1.4.3.0+ , directory ^>=1.3.0.1+ , filepath ^>=1.4.1.2+ , parsec ^>=3.1.13.0+ , process ^>=1.6.1.0+ , temporary ^>=1.3+ , transformers ^>=0.5.2.0++ -- used for caching+ build-depends:+ , base64-bytestring ^>=1.0.0.3+ , bytestring ^>=0.10.8.2+ , cryptohash-sha256 ^>=0.11.101.0
+ src/Image/LaTeX/Render.hs view
@@ -0,0 +1,327 @@+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE RecordWildCards #-}+module Image.LaTeX.Render (+ -- * Rendering Formulas+ imageForFormula,+ Formula, SVG,+ -- * BaseLine+ BaseLine, getBaseline, alterForHTML,+ -- * Errors+ RenderError (..),+ -- * Options+ -- ** Environment Options+ EnvironmentOptions (..),+ defaultEnv,+ TempDirectoryHandling (..),+ -- ** Formula Options+ FormulaOptions (..),+ displaymath,+ math,+ ) where++import Control.Applicative (some, (<|>))+import Control.DeepSeq (NFData (..), ($!!))+import Control.Monad (when)+import Control.Monad.IO.Class (MonadIO (..))+import Control.Monad.Trans.Except (ExceptT (..), runExceptT, throwE, withExceptT)+import Data.Char (isSpace)+import Data.List (foldl', isPrefixOf)+import Numeric (showFFloat)+import System.Exit (ExitCode (..))+import System.FilePath ((<.>), (</>))+import System.IO.Temp (withSystemTempDirectory, withTempDirectory)++-- import System.IO++import qualified Control.Exception as E+import qualified Crypto.Hash.SHA256 as SHA256+import qualified Data.ByteString.Base64.URL as Base64+import qualified Data.ByteString.Builder as B+import qualified Data.ByteString.Char8 as BS8+import qualified System.Directory as Dir+import qualified System.Process as Proc+import qualified Text.Parsec as P+import qualified Text.Parsec.String as P++-------------------------------------------------------------------------------+-- Types+-------------------------------------------------------------------------------++-- | 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 points (@pt@) from the bottom of the image to the typesetting baseline. Useful for setting your formulae inline with text+type BaseLine = Double++-- | A source of 'SVG' image.+type SVG = String++-- | 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+ = LaTeXFailure String -- ^ @latex@ returned a nonzero error code+ | DVISVGMFailure String -- ^ @dvisvgm@ returned a nonzero error code+ | IOException E.IOException -- ^ An 'IOException' occurred while managing the temporary files used to convert the equation+ deriving (Show, Eq)++data TempDirectoryHandling+ = UseSystemTempDir String -- ^ A temporary directory with a name based on the given template will be created in the system temporary files location+ | UseCurrentDir 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@+ , dvisvgmCommand :: String -- ^ Command to use for @dvisvgm@, default is @dvisvgm@+ , latexArgs :: [String] -- ^ Any additional arguments for @latex@+ , dvisvgmArgs :: [String] -- ^ Any additional arguments for @dvisvgm@+ , tempDir :: TempDirectoryHandling -- ^ How to handle temporary files+ , tempFileBaseName :: String -- ^ The base name to use for the temporary files.+ , globalCache :: Bool -- ^ Cache outputs globally in @XDG_CACHE/latex-svg@+ }+ 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@+ }+ deriving (Eq, Show, Read, Ord)++-------------------------------------------------------------------------------+-- Defaults+-------------------------------------------------------------------------------++-- | Use the @amsmath@ package, the @displaymath@ environment.+displaymath :: FormulaOptions+displaymath = FormulaOptions "\\usepackage{amsmath}\\usepackage{amsfonts}\\usepackage{stmaryrd}" "displaymath"++-- | Use the @amsmath@ package, the @math@ environment.+math :: FormulaOptions+math = displaymath { environment = "math" }++-- | Sensible defaults for system environments. Works if @dvisvgm@ and @latex@ are recent enough and in your @$PATH@.+defaultEnv :: EnvironmentOptions+defaultEnv = EnvironmentOptions+ { latexCommand = "latex"+ , dvisvgmCommand = "dvisvgm"+ , latexArgs = []+ , dvisvgmArgs = ["--no-fonts=1", "--clipjoin"] -- "--exact-bbox" is good idea if you have recent dvisvgm+ , tempDir = UseSystemTempDir "latex-eqn-temp"+ , tempFileBaseName = "working"+ , globalCache = False+ }++-------------------------------------------------------------------------------+-- Image for formula+-------------------------------------------------------------------------------++-- | Convert a formula into a SVG image.+imageForFormula :: EnvironmentOptions -> FormulaOptions -> Formula -> IO (Either RenderError SVG)+imageForFormula EnvironmentOptions {..} FormulaOptions {..} eqn =+ withTemp $ \temp -> runExceptT $ do+ let doc :: String+ doc = unlines $+ [ "% " ++ latexCommand ++ " " ++ show latexArgs+ , "% " ++ dvisvgmCommand ++ " " ++ show dvisvgmArgs+ , "\\nonstopmode"+ , "\\documentclass[12pt]{article}"+ , "\\pagestyle{empty}"+ , "\\usepackage[active,tightpage]{preview}"+ , "\\usepackage{amsmath}"+ , "\\usepackage{xcolor}"+ , preamble+ , "\\begin{document}"+ , "\\begin{preview}"+ , "\\begin{" ++ environment ++ "}"+ ] ++ filter (not . all isSpace) (lines eqn) +++ [ "\\end{" ++ environment ++ "}"+ , "\\end{preview}"+ , "\\end{document}"+ ]++ cached doc $ do+ -- io $ hPutStrLn stderr doc+ io $ writeFile (temp </> tempFileBaseName <.> "tex") doc++ (c,o,e) <- io $ readProcessWithCWD temp latexCommand $ latexArgs ++ [tempFileBaseName <.> "tex"]+ when (c /= ExitSuccess) $ throwE $ LaTeXFailure (o ++ "\n" ++ e)++ (c',o',e') <- io $ readProcessWithCWD temp dvisvgmCommand $ dvisvgmArgs ++ ["-o", tempFileBaseName <.> "svg", tempFileBaseName <.> "dvi"]+ when (c' /= ExitSuccess) $ throwE $ DVISVGMFailure (o' ++ "\n" ++ e')++ svg <- io $ readFile (temp </> tempFileBaseName <.> "svg")++ return $ addTitle eqn svg+ where+ io :: NFData a => IO a -> ExceptT RenderError IO a+ io = withExceptT IOException . tryIO++ withTemp a = case tempDir of+ UseSystemTempDir f -> withSystemTempDirectory f a+ UseCurrentDir f -> withTempDirectory "." f a++ cached :: String -> ExceptT RenderError IO String -> ExceptT RenderError IO String+ cached doc action+ | not globalCache = action+ | otherwise = do+ let key :: String+ key = filter (/= '=')+ $ BS8.unpack+ $ Base64.encode+ $ SHA256.hashlazy+ $ B.toLazyByteString+ $ B.stringUtf8 doc++ -- cache directory+ xdgCache <- io $ Dir.getXdgDirectory Dir.XdgCache "latex-svg"+ io $ Dir.createDirectoryIfMissing True xdgCache+ let path = xdgCache </> key <.> "svg"++ readFile path `orElse` do+ result <- action+ io $ writeFile path result+ return result++ orElse :: IO a -> ExceptT e IO a -> ExceptT e IO a+ orElse lft rgt = ExceptT $ fmap Right lft `E.catch` handler rgt++ handler :: ExceptT e IO a -> E.IOException -> IO (Either e a)+ handler rgt _ = runExceptT rgt++-------------------------------------------------------------------------------+-- Baseline and other postprocessing+-------------------------------------------------------------------------------++addTitle :: Formula -> String -> String+addTitle eqn svg =+ let (x0,x1) = spanL "<svg" svg+ (y1,y2) = spanR '>' x1+ in x0 ++ y1 ++ "\n<title>" ++ processAltString eqn ++ "</title>" ++ y2++spanL :: Eq a => [a] -> [a] -> ([a], [a])+spanL sep = go where+ go str@[] = (str, str)+ go str@(c:sfx)+ | sep `isPrefixOf` str = ([], str)+ | otherwise = (c:xs , ys)+ where+ ~(xs,ys) = go sfx++spanR :: Eq a => a -> [a] -> ([a], [a])+spanR sep = go where+ go str@[] = (str, str)+ go _str@(c:sfx)+ | sep == c = ([c], sfx)+ | otherwise = (c:xs , ys)+ where+ ~(xs,ys) = go sfx++processAltString :: String -> String+processAltString = concatMap $ \c -> case c of+ '<' -> "<"+ '>' -> ">"+ '&' -> "&"+ '"' -> """+ '\'' -> "&39;"+ '\n' -> " "+ '\r' -> " "+ '\t' -> " "+ x -> [x]++getBaseline :: SVG -> Double+getBaseline str = getBaseline' sfx+ where+ (_pfx, sfx) = spanL viewboxMarker str++-- | Alter 'SVG' image to be embeddable in HTML page, i.e.align baseline.+--+-- * Add @style="vertical-align: baseline-correction"@+--+alterForHTML :: SVG -> SVG+alterForHTML xml =+ pfx ++ " style='vertical-align: " ++ showFFloat (Just 6) baseline "" ++ "pt'" ++ sfx+ where+ (_, svg) = spanL "<svg" xml+ (pfx, sfx) = spanL viewboxMarker svg+ baseline = getBaseline' sfx++viewboxMarker :: String+viewboxMarker = " viewBox='"++getBaseline' :: String -> Double+getBaseline' sfx = case P.parse parser "<input>" sfx of+ Left err -> error $ show (err, sfx)+ Right x -> negate x+ where+ parser :: P.Parser Double+ parser = do+ _ <- P.string viewboxMarker+ _ <- lexeme double+ _ <- P.spaces+ x <- lexeme double+ _ <- lexeme double+ y <- lexeme double+ return (y + x)++ double :: P.Parser Double+ double = sign <*> (float1 <|> float2)++ float1 :: P.Parser Double+ float1 = do+ d <- decimal+ f <- P.option 0 (P.char '.' *> fraction)+ return (d + f)++ float2 :: P.Parser Double+ float2 = P.char '.' *> fraction++ decimal :: P.Parser Double+ decimal = foldl' (\x d -> 10*x + digitToInt d) 0+ <$> digits1++ fraction :: P.Parser Double+ fraction = uncurry (/) . foldl' (\(x,n) d -> (10*x + digitToInt d,n*10)) (0,1)+ <$> digits1++ digits1 = some P.digit++ digitToInt '0' = 0+ digitToInt '1' = 1+ digitToInt '2' = 2+ digitToInt '3' = 3+ digitToInt '4' = 4+ digitToInt '5' = 5+ digitToInt '6' = 6+ digitToInt '7' = 7+ digitToInt '8' = 8+ digitToInt '9' = 9+ digitToInt _ = 0++ sign :: P.Parser (Double -> Double)+ sign = P.option id (negate <$ P.char '-')++ lexeme :: P.Parser a -> P.Parser a+ lexeme p = p <* P.spaces++-------------------------------------------------------------------------------+-- Utilities+-------------------------------------------------------------------------------++readProcessWithCWD+ :: FilePath -- ^ working directory+ -> FilePath -- ^ Filename of the executable (see 'RawCommand' for details)+ -> [String] -- ^ any arguments+ -> IO (ExitCode,String,String) -- ^ exitcode, stdout, stderr+readProcessWithCWD cwd cmd args = Proc.readCreateProcessWithExitCode+ ((Proc.proc cmd args) { Proc.cwd = Just cwd })+ ""++-- | Catch 'IOException's and convert them to the 'ExceptT' monad+tryIO :: (MonadIO m, NFData a) => IO a -> ExceptT E.IOException m a+tryIO action = ExceptT $ liftIO $ E.try $ evaluateDeep action++-- | Internal helper function+evaluateDeep :: NFData a => IO a -> IO a+evaluateDeep action = do+ res <- action+ E.evaluate $!! res