packages feed

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 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+    '<'  -> "&lt;"+    '>'  -> "&gt;"+    '&'  -> "&amp;"+    '"'  -> "&quot;"+    '\'' -> "&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