git.iko.soy/typeable/pandoc
on 34412cf help sign in
383 lines 15 kB history raw
1{-# LANGUAGE CPP #-}
2{-# LANGUAGE OverloadedStrings #-}
3{-# LANGUAGE ScopedTypeVariables #-}
4{-
5Copyright (C) 2012-2016 John MacFarlane <jgm@berkeley.edu>
7This program is free software; you can redistribute it and/or modify
8it under the terms of the GNU General Public License as published by
9the Free Software Foundation; either version 2 of the License, or
10(at your option) any later version.
12This program is distributed in the hope that it will be useful,
13but WITHOUT ANY WARRANTY; without even the implied warranty of
14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15GNU General Public License for more details.
17You should have received a copy of the GNU General Public License
18along with this program; if not, write to the Free Software
19Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
20-}
22{- |
23 Module : Text.Pandoc.PDF
24 Copyright : Copyright (C) 2012-2016 John MacFarlane
25 License : GNU GPL, version 2 or above
27 Maintainer : John MacFarlane <jgm@berkeley.edu>
28 Stability : alpha
29 Portability : portable
31Conversion of LaTeX documents to PDF.
32-}
33module Text.Pandoc.PDF ( makePDF ) where
35import qualified Codec.Picture as JP
36import qualified Control.Exception as E
37import Control.Monad (unless, when, (<=<))
38import Control.Monad.Trans (MonadIO (..))
39import qualified Data.ByteString as BS
40import Data.ByteString.Lazy (ByteString)
41import qualified Data.ByteString.Lazy as B
42import qualified Data.ByteString.Lazy as BL
43import qualified Data.ByteString.Lazy.Char8 as BC
44import Data.Digest.Pure.SHA (sha1, showDigest)
45import Data.List (isInfixOf)
46import Data.Maybe (fromMaybe)
47import Data.Monoid ((<>))
48import System.Directory
49import System.Environment
50import System.Exit (ExitCode (..))
51import System.FilePath
52import System.IO (stdout)
53import System.IO.Temp (withTempDirectory, withTempFile)
54import Text.Pandoc.Definition
55import Text.Pandoc.MediaBag
56import Text.Pandoc.MIME (extensionFromMimeType, getMimeType)
57import Text.Pandoc.Options (HTMLMathMethod (..), WriterOptions (..))
58import Text.Pandoc.Process (pipeProcess)
59import Text.Pandoc.Shared (inDirectory, stringify, withTempDir)
60import qualified Text.Pandoc.UTF8 as UTF8
61import Text.Pandoc.Walk (walkM)
62import Text.Pandoc.Writers.Shared (getField, metaToJSON)
63#ifdef _WINDOWS
64import Data.List (intercalate)
65#endif
66import Text.Pandoc.Class (PandocIO, fetchItem, report, runIO, runIOorExplode,
67 setMediaBag, setVerbosity)
68import Text.Pandoc.Logging
70#ifdef _WINDOWS
71changePathSeparators :: FilePath -> FilePath
72changePathSeparators = intercalate "/" . splitDirectories
73#endif
75makePDF :: MonadIO m
76 => String -- ^ pdf creator (pdflatex, lualatex,
77 -- xelatex, context, wkhtmltopdf)
78 -> (WriterOptions -> Pandoc -> PandocIO String) -- ^ writer
79 -> WriterOptions -- ^ options
80 -> Verbosity -- ^ verbosity level
81 -> MediaBag -- ^ media
82 -> Pandoc -- ^ document
83 -> m (Either ByteString ByteString)
84makePDF "wkhtmltopdf" writer opts verbosity _ doc@(Pandoc meta _) = liftIO $ do
85 let mathArgs = case writerHTMLMathMethod opts of
86 -- with MathJax, wait til all math is rendered:
87 MathJax _ -> ["--run-script", "MathJax.Hub.Register.StartupHook('End Typeset', function() { window.status = 'mathjax_loaded' });",
88 "--window-status", "mathjax_loaded"]
89 _ -> []
90 meta' <- metaToJSON opts (return . stringify) (return . stringify) meta
91 let toArgs (f, mbd) = maybe [] (\d -> ['-':'-':f, d]) mbd
92 let args = mathArgs ++
93 concatMap toArgs
94 [("page-size", getField "papersize" meta')
95 ,("title", getField "title" meta')
96 ,("margin-bottom", fromMaybe (Just "1.2in")
97 (getField "margin-bottom" meta'))
98 ,("margin-top", fromMaybe (Just "1.25in")
99 (getField "margin-top" meta'))
100 ,("margin-right", fromMaybe (Just "1.25in")
101 (getField "margin-right" meta'))
102 ,("margin-left", fromMaybe (Just "1.25in")
103 (getField "margin-left" meta'))
104 ]
105 source <- runIOorExplode $ do
106 setVerbosity verbosity
107 writer opts doc
108 html2pdf verbosity args source
109makePDF program writer opts verbosity mediabag doc = do
110 let withTemp = if takeBaseName program == "context"
111 then withTempDirectory "."
112 else withTempDir
113 liftIO $ withTemp "tex2pdf." $ \tmpdir -> do
114 doc' <- handleImages verbosity opts mediabag tmpdir doc
115 source <- runIOorExplode $ do
116 setVerbosity verbosity
117 writer opts doc'
118 let args = writerLaTeXArgs opts
119 case takeBaseName program of
120 "context" -> context2pdf verbosity tmpdir source
121 prog | prog `elem` ["pdflatex", "lualatex", "xelatex"]
122 -> tex2pdf' verbosity args tmpdir program source
123 _ -> return $ Left $ UTF8.fromStringLazy $ "Unknown program " ++ program
125handleImages :: Verbosity
126 -> WriterOptions
127 -> MediaBag
128 -> FilePath -- ^ temp dir to store images
129 -> Pandoc -- ^ document
130 -> IO Pandoc
131handleImages verbosity opts mediabag tmpdir =
132 walkM (convertImages verbosity tmpdir) <=<
133 walkM (handleImage' verbosity opts mediabag tmpdir)
135handleImage' :: Verbosity
136 -> WriterOptions
137 -> MediaBag
138 -> FilePath
139 -> Inline
140 -> IO Inline
141handleImage' verbosity opts mediabag tmpdir (Image attr ils (src,tit)) = do
142 exists <- doesFileExist src
143 if exists
144 then return $ Image attr ils (src,tit)
145 else do
146 res <- runIO $ do
147 setVerbosity verbosity
148 setMediaBag mediabag
149 fetchItem (writerSourceURL opts) src
150 case res of
151 Right (contents, Just mime) -> do
152 let ext = fromMaybe (takeExtension src) $
153 extensionFromMimeType mime
154 let basename = showDigest $ sha1 $ BL.fromChunks [contents]
155 let fname = tmpdir </> basename <.> ext
156 BS.writeFile fname contents
157 return $ Image attr ils (fname,tit)
158 _ -> do
159 runIO $ do
160 setVerbosity verbosity
161 report $ CouldNotFetchResource src "skipping..."
162 -- return alt text
163 return $ Emph ils
164handleImage' _ _ _ _ x = return x
166convertImages :: Verbosity -> FilePath -> Inline -> IO Inline
167convertImages verbosity tmpdir (Image attr ils (src, tit)) = do
168 img <- convertImage tmpdir src
169 newPath <-
170 case img of
171 Left e -> do
172 runIO $ do
173 setVerbosity verbosity
174 report $ CouldNotConvertImage src e
175 return src
176 Right fp -> return fp
177 return (Image attr ils (newPath, tit))
178convertImages _ _ x = return x
180-- Convert formats which do not work well in pdf to png
181convertImage :: FilePath -> FilePath -> IO (Either String FilePath)
182convertImage tmpdir fname =
183 case mime of
184 Just "image/png" -> doNothing
185 Just "image/jpeg" -> doNothing
186 Just "application/pdf" -> doNothing
187 _ -> JP.readImage fname >>= \res ->
188 case res of
189 Left e -> return $ Left e
190 Right img ->
191 E.catch (Right fileOut <$ JP.savePngImage fileOut img) $
192 \(e :: E.SomeException) -> return (Left (show e))
193 where
194 fileOut = replaceDirectory (replaceExtension fname ".png") tmpdir
195 mime = getMimeType fname
196 doNothing = return (Right fname)
198tex2pdf' :: Verbosity -- ^ Verbosity level
199 -> [String] -- ^ Arguments to the latex-engine
200 -> FilePath -- ^ temp directory for output
201 -> String -- ^ tex program
202 -> String -- ^ tex source
203 -> IO (Either ByteString ByteString)
204tex2pdf' verbosity args tmpDir program source = do
205 let numruns = if "\\tableofcontents" `isInfixOf` source
206 then 3 -- to get page numbers
207 else 2 -- 1 run won't give you PDF bookmarks
208 (exit, log', mbPdf) <- runTeXProgram verbosity program args 1 numruns tmpDir source
209 case (exit, mbPdf) of
210 (ExitFailure _, _) -> do
211 let logmsg = extractMsg log'
212 let extramsg =
213 case logmsg of
214 x | "! Package inputenc Error" `BC.isPrefixOf` x
215 && program /= "xelatex"
216 -> "\nTry running pandoc with --latex-engine=xelatex."
217 _ -> ""
218 return $ Left $ logmsg <> extramsg
219 (ExitSuccess, Nothing) -> return $ Left ""
220 (ExitSuccess, Just pdf) -> return $ Right pdf
222-- parsing output
224extractMsg :: ByteString -> ByteString
225extractMsg log' = do
226 let msg' = dropWhile (not . ("!" `BC.isPrefixOf`)) $ BC.lines log'
227 let (msg'',rest) = break ("l." `BC.isPrefixOf`) msg'
228 let lineno = take 1 rest
229 if null msg'
230 then log'
231 else BC.unlines (msg'' ++ lineno)
233extractConTeXtMsg :: ByteString -> ByteString
234extractConTeXtMsg log' = do
235 let msg' = take 1 $
236 dropWhile (not . ("tex error" `BC.isPrefixOf`)) $ BC.lines log'
237 if null msg'
238 then log'
239 else BC.unlines msg'
241-- running tex programs
243-- Run a TeX program on an input bytestring and return (exit code,
244-- contents of stdout, contents of produced PDF if any). Rerun
245-- a fixed number of times to resolve references.
246runTeXProgram :: Verbosity -> String -> [String] -> Int -> Int -> FilePath
247 -> String -> IO (ExitCode, ByteString, Maybe ByteString)
248runTeXProgram verbosity program args runNumber numRuns tmpDir source = do
249 let file = tmpDir </> "input.tex"
250 exists <- doesFileExist file
251 unless exists $ UTF8.writeFile file source
252#ifdef _WINDOWS
253 -- note: we want / even on Windows, for TexLive
254 let tmpDir' = changePathSeparators tmpDir
255 let file' = changePathSeparators file
256#else
257 let tmpDir' = tmpDir
258 let file' = file
259#endif
260 let programArgs = ["-halt-on-error", "-interaction", "nonstopmode",
261 "-output-directory", tmpDir'] ++ args ++ [file']
262 env' <- getEnvironment
263 let sep = [searchPathSeparator]
264 let texinputs = maybe (tmpDir' ++ sep) ((tmpDir' ++ sep) ++)
265 $ lookup "TEXINPUTS" env'
266 let env'' = ("TEXINPUTS", texinputs) :
267 [(k,v) | (k,v) <- env', k /= "TEXINPUTS"]
268 when (verbosity >= INFO && runNumber == 1) $ do
269 putStrLn "[makePDF] temp dir:"
270 putStrLn tmpDir'
271 putStrLn "[makePDF] Command line:"
272 putStrLn $ program ++ " " ++ unwords (map show programArgs)
273 putStr "\n"
274 putStrLn "[makePDF] Environment:"
275 mapM_ print env''
276 putStr "\n"
277 putStrLn $ "[makePDF] Contents of " ++ file' ++ ":"
278 B.readFile file' >>= B.putStr
279 putStr "\n"
280 (exit, out) <- pipeProcess (Just env'') program programArgs BL.empty
281 when (verbosity >= INFO) $ do
282 putStrLn $ "[makePDF] Run #" ++ show runNumber
283 B.hPutStr stdout out
284 putStr "\n"
285 if runNumber <= numRuns
286 then runTeXProgram verbosity program args (runNumber + 1) numRuns tmpDir source
287 else do
288 let pdfFile = replaceDirectory (replaceExtension file ".pdf") tmpDir
289 pdfExists <- doesFileExist pdfFile
290 pdf <- if pdfExists
291 -- We read PDF as a strict bytestring to make sure that the
292 -- temp directory is removed on Windows.
293 -- See https://github.com/jgm/pandoc/issues/1192.
294 then (Just . B.fromChunks . (:[])) `fmap` BS.readFile pdfFile
295 else return Nothing
296 return (exit, out, pdf)
298html2pdf :: Verbosity -- ^ Verbosity level
299 -> [String] -- ^ Args to wkhtmltopdf
300 -> String -- ^ HTML5 source
301 -> IO (Either ByteString ByteString)
302html2pdf verbosity args source = do
303 file <- withTempFile "." "html2pdf.html" $ \fp _ -> return fp
304 pdfFile <- withTempFile "." "html2pdf.pdf" $ \fp _ -> return fp
305 UTF8.writeFile file source
306 let programArgs = args ++ [file, pdfFile]
307 env' <- getEnvironment
308 when (verbosity >= INFO) $ do
309 putStrLn "[makePDF] Command line:"
310 putStrLn $ "wkhtmltopdf" ++ " " ++ unwords (map show programArgs)
311 putStr "\n"
312 putStrLn "[makePDF] Environment:"
313 mapM_ print env'
314 putStr "\n"
315 putStrLn $ "[makePDF] Contents of " ++ file ++ ":"
316 B.readFile file >>= B.putStr
317 putStr "\n"
318 (exit, out) <- pipeProcess (Just env') "wkhtmltopdf" programArgs BL.empty
319 removeFile file
320 when (verbosity >= INFO) $ do
321 B.hPutStr stdout out
322 putStr "\n"
323 pdfExists <- doesFileExist pdfFile
324 mbPdf <- if pdfExists
325 -- We read PDF as a strict bytestring to make sure that the
326 -- temp directory is removed on Windows.
327 -- See https://github.com/jgm/pandoc/issues/1192.
328 then do
329 res <- (Just . B.fromChunks . (:[])) `fmap` BS.readFile pdfFile
330 removeFile pdfFile
331 return res
332 else return Nothing
333 return $ case (exit, mbPdf) of
334 (ExitFailure _, _) -> Left out
335 (ExitSuccess, Nothing) -> Left ""
336 (ExitSuccess, Just pdf) -> Right pdf
338context2pdf :: Verbosity -- ^ Verbosity level
339 -> FilePath -- ^ temp directory for output
340 -> String -- ^ ConTeXt source
341 -> IO (Either ByteString ByteString)
342context2pdf verbosity tmpDir source = inDirectory tmpDir $ do
343 let file = "input.tex"
344 UTF8.writeFile file source
345#ifdef _WINDOWS
346 -- note: we want / even on Windows, for TexLive
347 let tmpDir' = changePathSeparators tmpDir
348#else
349 let tmpDir' = tmpDir
350#endif
351 let programArgs = "--batchmode" : [file]
352 env' <- getEnvironment
353 when (verbosity >= INFO) $ do
354 putStrLn "[makePDF] temp dir:"
355 putStrLn tmpDir'
356 putStrLn "[makePDF] Command line:"
357 putStrLn $ "context" ++ " " ++ unwords (map show programArgs)
358 putStr "\n"
359 putStrLn "[makePDF] Environment:"
360 mapM_ print env'
361 putStr "\n"
362 putStrLn $ "[makePDF] Contents of " ++ file ++ ":"
363 B.readFile file >>= B.putStr
364 putStr "\n"
365 (exit, out) <- pipeProcess (Just env') "context" programArgs BL.empty
366 when (verbosity >= INFO) $ do
367 B.hPutStr stdout out
368 putStr "\n"
369 let pdfFile = replaceExtension file ".pdf"
370 pdfExists <- doesFileExist pdfFile
371 mbPdf <- if pdfExists
372 -- We read PDF as a strict bytestring to make sure that the
373 -- temp directory is removed on Windows.
374 -- See https://github.com/jgm/pandoc/issues/1192.
375 then (Just . B.fromChunks . (:[])) `fmap` BS.readFile pdfFile
376 else return Nothing
377 case (exit, mbPdf) of
378 (ExitFailure _, _) -> do
379 let logmsg = extractConTeXtMsg out
380 return $ Left logmsg
381 (ExitSuccess, Nothing) -> return $ Left ""
382 (ExitSuccess, Just pdf) -> return $ Right pdf