{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE ScopedTypeVariables #-}
module Reanimate.LaTeX
( latexCfg,
TexEngine (..),
TexConfig (..),
latex,
latexWithHeaders,
latexChunks,
xelatex,
xelatexWithHeaders,
ctex,
ctexWithHeaders,
latexAlign,
chalkduster,
calligra,
noto,
helvet,
libertine,
)
where
import Control.Lens
import qualified Data.ByteString as B
import Data.Hashable
import Data.Monoid
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
import GHC.Generics (Generic)
import Graphics.SvgTree
( Tree,
clipPathRef,
clipRule,
mapTree,
parseSvgFile,
strokeColor,
pattern ClipPathTree,
pattern None,
)
import Reanimate.Animation (SVG)
import Reanimate.Cache
import Reanimate.External
import Reanimate.Misc
import Reanimate.Parameters
import Reanimate.Svg
import System.FilePath
( replaceExtension,
takeFileName,
(</>),
)
import System.IO.Unsafe (unsafePerformIO)
data TexEngine = LaTeX | XeLaTeX | LuaLaTeX
deriving (Generic, Hashable, Eq, Ord, Read, Show)
data TexConfig = TexConfig
{ texConfigEngine :: TexEngine,
texConfigHeaders :: [T.Text],
texConfigPostScript :: [T.Text]
}
deriving (Generic, Hashable, Read, Show, Eq, Ord)
latexCfg :: TexConfig -> T.Text -> SVG
latexCfg (TexConfig engine headers postscript) =
gen headers postscript
where
gen =
case engine of
LaTeX -> someTexWithHeaders engine "latex" "dvi" []
XeLaTeX -> someTexWithHeaders engine "xelatex" "xdv" ["-no-pdf"]
LuaLaTeX -> someTexWithHeaders engine "lualatex" "pdf" []
latex :: T.Text -> Tree
latex = latexWithHeaders []
latexWithHeaders :: [T.Text] -> T.Text -> Tree
latexWithHeaders = someTexWithHeaders LaTeX "latex" "dvi" [] []
someTexWithHeaders ::
TexEngine ->
String ->
String ->
[String] ->
[T.Text] ->
[T.Text] ->
T.Text ->
Tree
someTexWithHeaders _engine _exec _dvi _args _headers _postscript tex
| pNoExternals = mkText tex
someTexWithHeaders engine exec dvi args headers postscript tex =
(unsafePerformIO . (cacheMem . cacheDiskSvg) (latexToSVG engine dvi exec args))
script
where
script = mkTexScript exec args headers (T.unlines (postscript ++ [tex]))
latexChunks :: [T.Text] -> [Tree]
latexChunks chunks | pNoExternals = map mkText chunks
latexChunks chunks = worker (svgGlyphs $ latex $ T.concat chunks) chunks
where
merge lst = mkGroup [fmt svg | (fmt, _, svg) <- lst]
worker [] [] = []
worker _ [] = error "latex chunk mismatch"
worker everything (x : xs) =
let width = length $ svgGlyphs (latex x)
in merge (take width everything) : worker (drop width everything) xs
xelatex :: Text -> Tree
xelatex = xelatexWithHeaders []
xelatexWithHeaders :: [T.Text] -> T.Text -> Tree
xelatexWithHeaders = someTexWithHeaders XeLaTeX "xelatex" "xdv" [] ["-no-pdf"]
ctex :: T.Text -> Tree
ctex = ctexWithHeaders []
ctexWithHeaders :: [T.Text] -> T.Text -> Tree
ctexWithHeaders headers = xelatexWithHeaders ("\\usepackage[UTF8]{ctex}" : headers)
latexAlign :: Text -> Tree
latexAlign tex = latex $ T.unlines ["\\begin{align*}", tex, "\\end{align*}"]
postprocess :: Tree -> Tree
postprocess =
simplify
. lowerTransformations
. scaleXY 0.1 (-0.1)
. removeClipPaths
. lowerIds
. mapTree clearDrawAttr
where
clearDrawAttr t = t & strokeColor .~ Last Nothing
enginePostprocess :: TexEngine -> Tree -> Tree
enginePostprocess LuaLaTeX svg = translate 0 (svgHeight svg) svg
enginePostprocess _ svg = svg
removeClipPaths :: SVG -> SVG
removeClipPaths = mapTree worker
where
worker ClipPathTree {} = None
worker t = t & clipRule .~ Last Nothing & clipPathRef .~ Last Nothing
latexToSVG :: TexEngine -> String -> String -> [String] -> Text -> IO Tree
latexToSVG engine dviExt latexExec latexArgs tex = do
latexBin <- requireExecutable latexExec
withTempDir $ \tmp_dir -> withTempFile "tex" $ \tex_file ->
withTempFile "svg" $ \svg_file -> do
let dvi_file =
tmp_dir </> replaceExtension (takeFileName tex_file) dviExt
B.writeFile tex_file (T.encodeUtf8 tex)
runCmd
latexBin
( latexArgs
++ [ "-interaction=nonstopmode",
"-halt-on-error",
"-output-directory=" ++ tmp_dir,
tex_file
]
)
if dviExt == "pdf"
then do
pdf2svg <- requireExecutable "pdf2svg"
runCmd
pdf2svg
[dvi_file, svg_file]
else do
dvisvgm <- requireExecutable "dvisvgm"
runCmd
dvisvgm
[ dvi_file,
"--precision=5",
"--exact",
"--no-fonts",
"--verbosity=0",
"-o",
svg_file
]
svg_data <- B.readFile svg_file
case parseSvgFile svg_file svg_data of
Nothing -> error "Malformed svg"
Just svg ->
return $
enginePostprocess engine $
postprocess $ unbox $ replaceUses svg
mkTexScript :: String -> [String] -> [Text] -> Text -> Text
mkTexScript latexExec latexArgs texHeaders tex =
T.unlines $
[ "% " <> T.pack (unwords (latexExec : latexArgs)),
"\\documentclass[preview]{standalone}",
"\\usepackage{amsmath}",
"\\usepackage{gensymb}"
]
++ texHeaders
++ [ "\\usepackage[english]{babel}",
"\\linespread{1}",
"\\begin{document}",
tex,
"\\end{document}"
]
chalkduster :: TexConfig
chalkduster =
TexConfig
{ texConfigEngine = XeLaTeX,
texConfigHeaders =
[ "\\usepackage[no-math]{fontspec}",
"\\setmainfont[Mapping=tex-text,Path={" <> chalkdusterFont <> "/},Extension=.ttf]{Chalkduster}",
"\\usepackage[defaultmathsizes]{mathastext}"
],
texConfigPostScript = []
}
where
chalkdusterFont =
T.pack $
zipArchive
"https://www.ffonts.net/Chalkduster.font.zip"
"Wplv4RjuFiI0hDQnAM5MVHl2evrZqWstRLdVAfBomCM="
calligra :: TexConfig
calligra =
TexConfig
{ texConfigEngine = LaTeX,
texConfigHeaders = ["\\usepackage{calligra}"],
texConfigPostScript = ["\\calligra"]
}
noto :: TexConfig
noto =
TexConfig
{ texConfigEngine = LaTeX,
texConfigHeaders = ["\\usepackage{noto}"],
texConfigPostScript = []
}
helvet :: TexConfig
helvet =
TexConfig
{ texConfigEngine = LaTeX,
texConfigHeaders = ["\\usepackage{helvet}"],
texConfigPostScript = []
}
libertine :: TexConfig
libertine =
TexConfig
{ texConfigEngine = LaTeX,
texConfigHeaders = ["\\usepackage{libertine}"],
texConfigPostScript = []
}