{-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE PatternSynonyms #-} {-# LANGUAGE ScopedTypeVariables #-} -- | -- Copyright : Written by David Himmelstrup -- License : Unlicense -- Maintainer : lemmih@gmail.com -- Stability : experimental -- Portability : POSIX module Reanimate.LaTeX ( latexCfg, TexEngine (..), TexConfig (..), latex, latexWithHeaders, latexChunks, xelatex, xelatexWithHeaders, ctex, ctexWithHeaders, latexAlign, -- * Font configurations 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) -- | TeX backends. They have different features and capabilities. data TexEngine = LaTeX | XeLaTeX | LuaLaTeX deriving (Generic, Hashable, Eq, Ord, Read, Show) -- | TeX configurations can load packages and set up environments for tex scripts. data TexConfig = TexConfig { texConfigEngine :: TexEngine, texConfigHeaders :: [T.Text], texConfigPostScript :: [T.Text] } deriving (Generic, Hashable, Read, Show, Eq, Ord) -- | Render TeX script using a given configuration. 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" [] -- | Invoke latex and import the result as an SVG object. SVG objects are -- cached to improve performance. -- -- Example: -- -- > latex "$e^{i\\pi}+1=0$" -- -- <> latex :: T.Text -> Tree latex = latexWithHeaders [] -- | Invoke latex with extra script headers. 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])) -- | Invoke latex and separate results. 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 -- | Invoke xelatex and import the result as an SVG object. SVG objects are -- cached to improve performance. Xelatex has support for non-western scripts. xelatex :: Text -> Tree xelatex = xelatexWithHeaders [] -- | Invoke xelatex with extra script headers. xelatexWithHeaders :: [T.Text] -> T.Text -> Tree xelatexWithHeaders = someTexWithHeaders XeLaTeX "xelatex" "xdv" [] ["-no-pdf"] -- | Invoke xelatex with "\usepackage[UTF8]{ctex}" and import the result as an -- SVG object. SVG objects are cached to improve performance. Xelatex has -- support for non-western scripts. -- -- Example: -- -- > ctex "中文" -- -- <> ctex :: T.Text -> Tree ctex = ctexWithHeaders [] -- | Invoke xelatex with extra script headers + ctex headers. ctexWithHeaders :: [T.Text] -> T.Text -> Tree ctexWithHeaders headers = xelatexWithHeaders ("\\usepackage[UTF8]{ctex}" : headers) -- | Invoke latex and import the result as an SVG object. SVG objects are -- cached to improve performance. This wraps the TeX code in an 'align*' -- context. -- -- Example: -- -- > latexAlign "R = \\frac{{\\Delta x}}{{kA}}" -- -- <> 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 -- executable, arguments, header, tex 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", -- better bboxes. "--no-fonts", -- use glyphs instead of 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}" ] {- Packages used by manim. \\\usepackage{amsmath}\n\ \\\usepackage{amssymb}\n\ \\\usepackage{dsfont}\n\ \\\usepackage{setspace}\n\ \\\usepackage{relsize}\n\ \\\usepackage{textcomp}\n\ \\\usepackage{mathrsfs}\n\ \\\usepackage{calligra}\n\ \\\usepackage{wasysym}\n\ \\\usepackage{ragged2e}\n\ \\\usepackage{physics}\n\ \\\usepackage{xcolor}\n\ \\\usepackage{textcomp}\n\ \\\usepackage{xfrac}\n\ \\\usepackage{microtype}\n\ -} -- | Chalkduster configuration. Depends on lualatex. -- Font files are automatically downloaded. -- -- Example: -- -- @ -- `latexCfg` `chalkduster` "chalkduster" -- @ -- -- <> 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 configuration. -- -- Example: -- -- @ -- `latexCfg` `calligra` "calligra" -- @ -- -- <> calligra :: TexConfig calligra = TexConfig { texConfigEngine = LaTeX, texConfigHeaders = ["\\usepackage{calligra}"], texConfigPostScript = ["\\calligra"] } -- | Noto configuration. -- -- Example: -- -- @ -- `latexCfg` `noto` "noto" -- @ -- -- <> noto :: TexConfig noto = TexConfig { texConfigEngine = LaTeX, texConfigHeaders = ["\\usepackage{noto}"], texConfigPostScript = [] } -- | Helvetica configuration. -- -- Example: -- -- @ -- `latexCfg` `helvet` "helvet" -- @ -- -- <> helvet :: TexConfig helvet = TexConfig { texConfigEngine = LaTeX, texConfigHeaders = ["\\usepackage{helvet}"], texConfigPostScript = [] } -- | Libertine configuration. -- -- Example: -- -- @ -- `latexCfg` `libertine` "libertine" -- @ -- -- <> libertine :: TexConfig libertine = TexConfig { texConfigEngine = LaTeX, texConfigHeaders = ["\\usepackage{libertine}"], texConfigPostScript = [] }