{-# LANGUAGE LambdaCase #-}
module Image.LaTeX.Render.Pandoc
( -- * Data URIs
convertFormulaDataURI
, convertAllFormulaeDataURI
-- * Separate Files
, convertFormulaFiles
, convertAllFormulaeFiles
-- ** Name Supplies
, NameSupply
, newNameSupply
-- * Options
, PandocFormulaOptions(..)
, ShrinkSize
, defaultPandocFormulaOptions
-- ** Error display functions
, hideError
, displayError
-- * Generalised versions
-- ** Data URIs
, convertFormulaDataURIWith
, convertAllFormulaeDataURIWith
-- ** Files
, convertFormulaFilesWith
, convertAllFormulaeFilesWith
) where
import Text.Pandoc.Definition
import Text.Pandoc.Walk
import Image.LaTeX.Render
import Codec.Picture
import Control.Applicative
import Data.IORef
import System.FilePath
import qualified Data.ByteString.Base64.Lazy as B64
import qualified Data.ByteString.Lazy.Char8 as BS
dims :: Image a -> (Int,Int)
dims = liftA2 (,) imageWidth imageHeight
dimensions :: DynamicImage -> (Int,Int)
dimensions (ImageRGB8 i) = dims i
dimensions (ImageRGBA8 i) = dims i
dimensions (ImageRGB16 i) = dims i
dimensions (ImageRGBA16 i) = dims i
dimensions (ImageY8 i) = dims i
dimensions (ImageY16 i) = dims i
dimensions (ImageYA8 i) = dims i
dimensions (ImageYA16 i) = dims i
dimensions _ = error "Unsupported image format somehow!"
-- | All options pertaining to the actual display of formulae.
data PandocFormulaOptions = PandocFormulaOptions
{ shrinkBy :: ShrinkSize
-- ^ Denominator for all dimensions. Useful for displaying high DPI images in small sizes, for retina displays. Otherwise set to 1.
, errorDisplay :: RenderError -> Inline
-- ^ How to display various errors (such as LaTeX errors). Usually this can just be @displayError@ but you may wish @hideError@
-- to avoid putting potentially secure information into the output page.
, formulaOptions :: MathType -> FormulaOptions
-- ^ LaTeX environment settings, including the preamble, for each equation type (display and inline)
}
-- | A set of sensible defaults for formula options.
defaultPandocFormulaOptions :: PandocFormulaOptions
defaultPandocFormulaOptions = PandocFormulaOptions
{ shrinkBy = 2
, errorDisplay = displayError
, formulaOptions = \case DisplayMath -> displaymath; _ -> math
}
-- | Denominator for various dimensions. For high DPI displays, it can be useful to use values of 2 or 4, so that the dimensions
-- of the image are a fraction of the actual image size, and the image appears more crisp. Otherwise, a value of 1 will always
-- produce sensible, if somewhat pixelated results.
type ShrinkSize = Int
-- | Convert a formula in a pandoc document to an image, embedding the image into the HTML using Data URIs.
convertFormulaDataURI
:: EnvironmentOptions -- ^ System environment settings
-> PandocFormulaOptions -- ^ Formula display settings
-> Inline -> IO Inline
convertFormulaDataURI = convertFormulaDataURIWith . imageForFormula
-- | Convert all formulae in a pandoc document to images, embedding the images into the HTML using Data URIs.
convertAllFormulaeDataURI
:: EnvironmentOptions -- ^ System environment settings
-> PandocFormulaOptions -- ^ Formula display settings
-> Pandoc -> IO Pandoc
convertAllFormulaeDataURI e = walkM . convertFormulaDataURI e
-- | A generalisation of 'convertFormulaDataURI' which allows the actual image rendering
-- function to be customised, so that (e.g) caching can be added or other image processing.
convertFormulaDataURIWith
:: (FormulaOptions -> Formula -> IO (Either RenderError (Baseline, DynamicImage)))
-- ^ Function that renders a formula, such as @imageForFormula defaultEnv@
-> PandocFormulaOptions -- ^ Formula display settings
-> Inline -> IO Inline
convertFormulaDataURIWith f o (Math t s) = f (formulaOptions o t) s >>= \case
Left e -> return $ errorDisplay o e
Right (b,i) -> let
Right bs = encodeDynamicPng i
dataUri = "data:image/png;base64," ++ BS.unpack (B64.encode bs)
(ow,oh) = dimensions i
(w,h) = (ow `div` shrinkBy o, oh `div` shrinkBy o)
in return $ RawInline (Format "html") $
" "display-math") ++
" style=\"margin:0; vertical-align:-" ++ show (b `div` shrinkBy o) ++ "px;\"/>"
where processAltString = (>>= \case
'<' -> "<"
'>' -> ">"
'&' -> "&"
'"' -> """
'\'' -> "&39;"
'\n' -> " "
'\r' -> " "
'\t' -> " "
x -> [x])
convertFormulaDataURIWith _ _ x = return x
-- | A generalisation of 'convertAllFormulaeDataURI' which allows the actual image rendering
-- function to be customised, so that (e.g) caching can be added or other image processing.
convertAllFormulaeDataURIWith
:: (FormulaOptions -> Formula -> IO (Either RenderError (Baseline, DynamicImage)))
-- ^ Function that renders a formula, such as @imageForFormula defaultEnv@
-> PandocFormulaOptions -- ^ Formula display settings
-> Pandoc -> IO Pandoc
convertAllFormulaeDataURIWith f = walkM . convertFormulaDataURIWith f
-- | If we use files for the images, we need some way of naming the image files we produce
-- A NameSupply provides us with a source of unique names via an ever-increasing integer.
-- It's important that any invocation of 'convertFormulaFiles' or 'convertAllFormulaeFiles'
-- that shares the same image storage directory will also use the same name supply, or they
-- will overwrite each others images.
type NameSupply = IORef Int
-- | Create a new name supply.
newNameSupply :: IO NameSupply
newNameSupply = newIORef 0
-- | A generalisation of 'convertFormulaFiles' which allows the actual image rendering
-- function to be customised, so that (e.g) caching can be added or other image processing.
convertFormulaFilesWith
:: (FormulaOptions -> Formula -> IO (Either RenderError (Baseline, DynamicImage)))
-- ^ Function that renders a formula, such as @imageForFormula defaultEnv@
-> NameSupply -- ^ Unique file name supply. Reuse this for every invocation that shares the same image directory.
-> FilePath -- ^ Name of image directory where images will be stored
-> PandocFormulaOptions -- ^ Formula display settings
-> Inline -> IO Inline
convertFormulaFilesWith f ns bn o (Math t s) = f (formulaOptions o t) s >>= \case
Left e -> return $ errorDisplay o e
Right (b,i) -> do
fn <- readIORef ns
modifyIORef ns (+1)
let uri = bn > show fn <.> "png"
(ow,oh) = dimensions i
(w,h) = (ow `div` shrinkBy o, oh `div` shrinkBy o)
_ <- writeDynamicPng uri i
return $ RawInline (Format "html") $
" "display-math") ++
" style=\"margin:0; vertical-align:-" ++ show (b `div` shrinkBy o) ++ "px;\"/>"
convertFormulaFilesWith _ _ _ _ x = return x
-- | Convert a formula in a pandoc document to an image, storing the images in a separate directory.
convertFormulaFiles
:: EnvironmentOptions -- ^ System environment settings
-> NameSupply -- ^ Unique file name supply. Reuse this for every invocation that shares the same image directory.
-> FilePath -- ^ Name of image directory where images will be stored
-> PandocFormulaOptions -- ^ Formula display settings
-> Inline -> IO Inline
convertFormulaFiles = convertFormulaFilesWith . imageForFormula
-- | Convert every formula in a pandoc document to an image, storing the images in a separate directory.
convertAllFormulaeFiles
:: EnvironmentOptions -- ^ System environment settings
-> NameSupply -- ^ Unique file name supply. Reuse this for every invocation that shares the same image directory.
-> FilePath -- ^ Name of image directory where images will be stored
-> PandocFormulaOptions -- ^ Formula display settings
-> Pandoc -> IO Pandoc
convertAllFormulaeFiles eo ns fp = walkM . convertFormulaFiles eo ns fp
-- | A generalisation of 'convertAllFormulaeFiles' which allows the actual image rendering
-- function to be customised, so that (e.g) caching can be added or other image processing.
convertAllFormulaeFilesWith
:: (FormulaOptions -> Formula -> IO (Either RenderError (Baseline, DynamicImage)))
-- ^ Function that renders a formula, such as @imageForFormula defaultEnv@
-> NameSupply -- ^ Unique file name supply. Reuse this for every invocation that shares the same image directory.
-> FilePath -- ^ Name of image directory where images will be stored
-> PandocFormulaOptions -- ^ Formula display settings
-> Pandoc -> IO Pandoc
convertAllFormulaeFilesWith x y a = walkM . convertFormulaFilesWith x y a
-- | Render all errors simply as "Error"
hideError :: RenderError -> Inline
hideError = const $ Str blank
where
blank = "Error"
-- | Render errors nicely, in order to show any problems clearly, with all information intact.
displayError :: RenderError -> Inline
displayError ImageIsEmpty = pandocError [Str "The rendered image was empty"]
displayError CannotDetectBaseline = pandocError [Str "Cannot detect baseline in rendered image"]
displayError (LaTeXFailure str) = pandocError [Str "LaTeX failed:", LineBreak, Code nullAttr str]
displayError (DVIPSFailure str) = pandocError [Str "DVIPS failed:", LineBreak, Code nullAttr str]
displayError (IMConvertFailure str) = pandocError [Str "convert failed:", LineBreak, Code nullAttr str]
displayError (ImageReadError str) = pandocError [Str "Error reading image:", LineBreak, Code nullAttr str]
displayError (IOException e) = pandocError [Str "IO Exception:", LineBreak, Code nullAttr $ show e]
pandocError :: [Inline] -> Inline
pandocError = Strong . (Emph [Str "Error:"] :)