{-# 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,
biolinum,
droidSerif,
droidSans,
)
where
import Control.Lens ((&), (.~))
import qualified Data.ByteString as B
import Data.Hashable (Hashable)
import Data.Monoid (Last (Last))
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
import qualified Data.Text.IO as T
import GHC.Generics (Generic)
import Graphics.SvgTree (pattern ClipPathTree, pattern None, Tree, clipPathRef,
clipRule, mapTree, parseSvgFile, strokeColor)
import Reanimate.Animation (SVG)
import Reanimate.Cache (cacheDiskSvg, cacheMem)
import Reanimate.External (zipArchive)
import Reanimate.Misc (requireExecutable, runCmd, withTempDir, withTempFile)
import Reanimate.Parameters (pNoExternals)
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]))
latexCfgChunks :: TexConfig -> [T.Text] -> [Tree]
latexCfgChunks _cfg chunks | pNoExternals = map mkText chunks
latexCfgChunks cfg chunks = worker chunks $ svgGlyphs $ tex $ T.concat chunks
where
tex = latexCfg cfg
merge lst = mkGroup [fmt svg | (fmt, _, svg) <- lst]
worker [] [] = []
worker [] _ = error "latex chunk mismatch"
worker (x : xs) everything =
let width = length $ svgGlyphs (tex x)
(first, rest) = splitAt width everything
in merge first : worker xs rest
latexChunks :: [T.Text] -> [Tree]
latexChunks = latexCfgChunks (TexConfig LaTeX [] [])
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 <- T.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 = []
}
biolinum :: TexConfig
biolinum =
TexConfig
{ texConfigEngine = LaTeX,
texConfigHeaders =
["\\usepackage{libertine}"
,"\\renewcommand{\\familydefault}{\\sfdefault}"],
texConfigPostScript = []
}
droidSerif :: TexConfig
droidSerif =
TexConfig
{ texConfigEngine = LaTeX,
texConfigHeaders =
["\\usepackage[default]{droidserif}"
,"\\let\\varepsilon\\epsilon"],
texConfigPostScript = []
}
droidSans :: TexConfig
droidSans =
TexConfig
{ texConfigEngine = LaTeX,
texConfigHeaders =
["\\usepackage[default]{droidsans}"
,"\\let\\varepsilon\\epsilon"],
texConfigPostScript = []
}