@@ -0,0 +1,167 @@
1+{-# LANGUAGE OverloadedStrings #-}
2+{-
3+Copyright (C) 2012 John MacFarlane <jgm@berkeley.edu>
4+
5+This program is free software; you can redistribute it and/or modify
6+it under the terms of the GNU General Public License as published by
7+the Free Software Foundation; either version 2 of the License, or
8+(at your option) any later version.
9+
10+This program is distributed in the hope that it will be useful,
11+but WITHOUT ANY WARRANTY; without even the implied warranty of
12+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+GNU General Public License for more details.
14+
15+You should have received a copy of the GNU General Public License
16+along with this program; if not, write to the Free Software
17+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18+-}
19+
20+{- |
21+ Module : Text.Pandoc.PDF
22+ Copyright : Copyright (C) 2012 John MacFarlane
23+ License : GNU GPL, version 2 or above
24+
25+ Maintainer : John MacFarlane <jgm@berkeley.edu>
26+ Stability : alpha
27+ Portability : portable
28+
29+Conversion of LaTeX documents to PDF.
30+-}
31+module Text.Pandoc.PDF ( tex2pdf ) where
32+
33+import Data.Char (isDigit)
34+import System.IO.Temp
35+import Data.Maybe (catMaybes)
36+import Data.Char (toLower)
37+import Data.ByteString.Lazy (ByteString)
38+import qualified Data.ByteString.Lazy as B
39+import qualified Data.ByteString.Lazy.Char8 as BC
40+import qualified Data.ByteString as BS
41+import System.Exit (ExitCode (..), exitWith)
42+import System.FilePath
43+import System.Directory
44+import System.Process
45+import Control.Exception (tryJust, evaluate)
46+import Control.Monad.Trans (liftIO)
47+import Control.Monad (guard)
48+import System.IO.Error (isAlreadyExistsError)
49+import System.IO (hClose, stderr, stdout)
50+import Control.Concurrent (putMVar, takeMVar, newEmptyMVar, forkIO)
51+import Text.Pandoc.UTF8 as UTF8
52+
53+tex2pdf :: TeXProgram
54+ -> String -- ^ latex source
55+ -> IO (Either TeXError ByteString)
56+tex2pdf program source = withSystemTempDirectory "tex2pdf" $ \tmpdir ->
57+ tex2pdf' 1 tmpdir program source
58+
59+tex2pdf' :: Int -- ^ number of run
60+ -> FilePath -- ^ temp directory for output
61+ -> TeXProgram
62+ -> String -- ^ tex source
63+ -> IO (Either TeXError ByteString)
64+tex2pdf' run tmpDir program source = do
65+ (exit, log, mbPdf) <- runTeXProgram program 3 tmpDir source
66+ case (exit, mbPdf) of
67+ (ExitFailure ec, _) -> return $ Left $ extractTeXError ec log
68+ (ExitSuccess, Nothing) -> error "tex2pdf: ExitSuccess but no PDF created!"
69+ (ExitSuccess, Just pdf) -> return $ Right pdf
70+
71+data TeXProgram = PDFLaTeX
72+ | XeLaTeX
73+ | LuaLaTeX
74+ | XeTeX
75+ | LuaTeX
76+ | PDFTeX
77+ deriving (Show, Read)
78+
79+data TeXError = TeXError { exitCode :: Int
80+ , message :: ByteString
81+ , fullLog :: ByteString
82+ } deriving (Show, Read)
83+
84+-- parsing output
85+
86+extractTeXError :: Int -> ByteString -> TeXError
87+extractTeXError ec log = TeXError { exitCode = ec
88+ , message = extractMsg log
89+ , fullLog = log }
90+
91+extractMsg :: ByteString -> ByteString
92+extractMsg log = do
93+ let msg' = dropWhile (not . ("!" `BC.isPrefixOf`)) $ BC.lines log
94+ let (msg'',rest) = break ("l." `BC.isPrefixOf`) msg'
95+ let lineno = take 1 rest
96+ if not (null msg')
97+ then BC.unlines (msg'' ++ lineno)
98+ else "Unknown errorR"
99+
100+parseLine :: ByteString -> Bool
101+parseLine ln =
102+ if "LaTeX Warning:" `BC.isPrefixOf` ln
103+ then if "Rerun" `elem` ws
104+ then True
105+ else False
106+ else False
107+ where ws = BC.words ln
108+
109+hasUndefinedRefs :: ByteString -> Bool
110+hasUndefinedRefs = or . map parseLine . BC.lines
111+
112+-- running tex programs
113+
114+-- Run a TeX program on an input bytestring and return (exit code,
115+-- contents of stdout, contents of produced PDF if any). Rerun
116+-- latex as needed to resolve references, but don't run bibtex/biber.
117+runTeXProgram :: TeXProgram -> Int -> FilePath -> String
118+ -> IO (ExitCode, ByteString, Maybe ByteString)
119+runTeXProgram program runsLeft tmpDir source = do
120+ let programName = map toLower (show program)
121+ withTempFile tmpDir "tex2pdf" $ \file h -> do
122+ UTF8.hPutStr h source
123+ hClose h
124+ let programArgs = ["-halt-on-error", "-interaction", "nonstopmode",
125+ "-output-directory", tmpDir, file]
126+ (exit, out, _err) <- readCommand programName programArgs
127+ removeFile file
128+ let pdfFile = replaceDirectory (replaceExtension file ".pdf") tmpDir
129+ pdfExists <- doesFileExist pdfFile
130+ pdf <- if pdfExists
131+ then Just `fmap` B.readFile pdfFile
132+ else return Nothing
133+ if hasUndefinedRefs out && runsLeft > 0
134+ then runTeXProgram program (runsLeft - 1) tmpDir source
135+ else return (exit, out, pdf)
136+
137+-- utility functions
138+
139+-- Run a command and return exitcode, contents of stdout, and
140+-- contents of stderr. (Based on
141+-- 'readProcessWithExitCode' from 'System.Process'.)
142+readCommand :: FilePath -- ^ command to run
143+ -> [String] -- ^ any arguments
144+ -> IO (ExitCode,ByteString,ByteString) -- ^ exitcode, stdout, stderr
145+
146+readCommand cmd args = do
147+ (Just inh, Just outh, Just errh, pid) <-
148+ createProcess (proc cmd args){ std_in = CreatePipe,
149+ std_out = CreatePipe,
150+ std_err = CreatePipe }
151+ outMVar <- newEmptyMVar
152+ -- fork off a thread to start consuming stdout
153+ out <- B.hGetContents outh
154+ _ <- forkIO $ evaluate (B.length out) >> putMVar outMVar ()
155+ -- fork off a thread to start consuming stderr
156+ err <- B.hGetContents errh
157+ _ <- forkIO $ evaluate (B.length err) >> putMVar outMVar ()
158+ -- now write and flush any input
159+ hClose inh -- done with stdin
160+ -- wait on the output
161+ takeMVar outMVar
162+ takeMVar outMVar
163+ hClose outh
164+ -- wait on the process
165+ ex <- waitForProcess pid
166+ return (ex, out, err)
167+