{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TemplateHaskell #-}
module Text.Pandoc.Filter.Pyplot.FigureSpec
( FigureSpec(..)
, SaveFormat(..)
, toImage
, sourceCodePath
, figurePath
, addPlotCapture
, parseFigureSpec
, extension
) where
import Control.Monad (join)
import Control.Monad.IO.Class (liftIO)
import Control.Monad.Reader (ask)
import Data.Default.Class (def)
import Data.Hashable (hash)
import Data.List (intersperse)
import qualified Data.Map.Strict as Map
import Data.Maybe (fromMaybe)
import Data.Monoid ((<>))
import Data.String (fromString)
import Data.Text (Text, pack, unpack)
import qualified Data.Text.IO as TIO
import Data.Version (showVersion)
import Paths_pandoc_pyplot (version)
import System.FilePath (FilePath, addExtension,
makeValid, normalise,
replaceExtension, (</>))
import Text.Pandoc.Builder (fromList, imageWith, link,
para, toList)
import Text.Pandoc.Definition (Block (..), Inline,
Pandoc (..))
import Text.Shakespeare.Text (st)
import Text.Pandoc.Class (runPure)
import Text.Pandoc.Extensions (Extension (..),
extensionsFromList)
import Text.Pandoc.Options (ReaderOptions (..))
import Text.Pandoc.Readers (readMarkdown)
import Text.Pandoc.Filter.Pyplot.Types
parseFigureSpec :: Block -> PyplotM (Maybe FigureSpec)
parseFigureSpec (CodeBlock (id', cls, attrs) content)
| "pyplot" `elem` cls = Just <$> figureSpec Matplotlib
| "plotly" `elem` cls = Just <$> figureSpec Plotly
| otherwise = return Nothing
where
attrs' = Map.fromList attrs
filteredAttrs = filter (\(k, _) -> k `notElem` inclusionKeys) attrs
includePath = unpack <$> Map.lookup includePathKey attrs'
header = "# Generated by pandoc-pyplot " <> ((pack . showVersion) version)
figureSpec :: RenderingLibrary -> PyplotM FigureSpec
figureSpec lib = do
config <- ask
includeScript <- fromMaybe
(return $ defaultIncludeScript config)
((liftIO . TIO.readFile) <$> includePath)
return $
FigureSpec
{ caption = Map.findWithDefault mempty captionKey attrs'
, withLinks = fromMaybe (defaultWithLinks config) $ readBool <$> Map.lookup withLinksKey attrs'
, script = mconcat $ intersperse "\n" [header, includeScript, content]
, saveFormat = fromMaybe (defaultSaveFormat config) $ (fromString . unpack) <$> Map.lookup saveFormatKey attrs'
, directory = makeValid $ unpack $ Map.findWithDefault (pack $ defaultDirectory config) directoryKey attrs'
, dpi = fromMaybe (defaultDPI config) $ (read . unpack) <$> Map.lookup dpiKey attrs'
, renderingLib = lib
, tightBbox = isTightBbox config
, transparent = isTransparent config
, blockAttrs = (id', filter (\c -> c `notElem` ["pyplot", "plotly"]) cls, filteredAttrs)
}
parseFigureSpec _ = return Nothing
toImage :: FigureSpec -> Block
toImage spec = head . toList $ para $ imageWith attrs' (pack target') "fig:" caption'
where
attrs' = blockAttrs spec
target' = figurePath spec
withLinks' = withLinks spec
srcLink = link (pack $ replaceExtension target' ".txt") mempty "Source code"
hiresLink = link (pack $ hiresFigurePath spec) mempty "high res."
captionText = fromList $ fromMaybe mempty (captionReader $ caption spec)
captionLinks = mconcat [" (", srcLink, ", ", hiresLink, ")"]
caption' = if withLinks' then captionText <> captionLinks else captionText
figurePath :: FigureSpec -> FilePath
figurePath spec = normalise $ directory spec </> stem spec
where
stem = flip addExtension ext . show . hash
ext = extension . saveFormat $ spec
sourceCodePath :: FigureSpec -> FilePath
sourceCodePath = normalise . flip replaceExtension ".txt" . figurePath
hiresFigurePath :: FigureSpec -> FilePath
hiresFigurePath spec = normalise $ flip replaceExtension (".hires" <> ext) . figurePath $ spec
where
ext = extension . saveFormat $ spec
addPlotCapture :: FigureSpec
-> PythonScript
addPlotCapture spec = mconcat
[ script spec <> "\n"
, plotCapture (renderingLib spec) (figurePath spec) (dpi spec) (transparent spec) (tight')
, plotCapture (renderingLib spec) (hiresFigurePath spec) (minimum [200, 2 * dpi spec]) False (tight')
]
where
tight' = if tightBbox spec then ("'tight'" :: Text) else ("None" :: Text)
plotCapture Matplotlib = captureMatplotlib
plotCapture Plotly = capturePlotly
type Tight = Text
type DPI = Int
type IsTransparent = Bool
type RenderingFunc = (FilePath -> DPI -> IsTransparent -> Tight -> PythonScript)
captureMatplotlib :: RenderingFunc
captureMatplotlib fname' dpi' transparent' tight' = [st|
import matplotlib.pyplot as plt
plt.savefig(r"#{fname'}", dpi=#{dpi'}, transparent=#{transparent''}, bbox_inches=#{tight'})
|]
where
transparent'' :: Text
transparent'' = if transparent' then "True" else "False"
capturePlotly :: RenderingFunc
capturePlotly fname' _ _ _ = [st|
import plotly.graph_objects as go
__current_plotly_figure = next(obj for obj in globals().values() if type(obj) == go.Figure)
__current_plotly_figure.write_image("#{fname'}")
|]
readerOptions :: ReaderOptions
readerOptions = def
{readerExtensions =
extensionsFromList
[ Ext_tex_math_dollars
, Ext_superscript
, Ext_subscript
, Ext_raw_tex
]
}
captionReader :: Text -> Maybe [Inline]
captionReader t = either (const Nothing) (Just . extractFromBlocks) $ runPure $ readMarkdown' t
where
readMarkdown' = readMarkdown readerOptions
extractFromBlocks (Pandoc _ blocks) = mconcat $ extractInlines <$> blocks
extractInlines (Plain inlines) = inlines
extractInlines (Para inlines) = inlines
extractInlines (LineBlock multiinlines) = join multiinlines
extractInlines _ = []
readBool :: Text -> Bool
readBool s | s `elem` ["True", "true", "'True'", "'true'", "1"] = True
| s `elem` ["False", "false", "'False'", "'false'", "0"] = False
| otherwise = error $ unpack $ mconcat ["Could not parse '", s, "' into a boolean. Please use 'True' or 'False'"]