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