diff --git a/CHANGELOG.md b/CHANGELOG.md index d764a096f..dd0bb8e51 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ - Remove unused `getTimetableImage` function in `Export/GetImages.hs` - Refactored various backend text functions and tests to avoid `String` data in favour of `Text` when feasible - Removed unused files +- Refactored `returnPDF`, `exportTimetablePDFResponse`, and `graphImageResponse` to use stdin instead of temporary `.tex` and `.svg` files - Refactored controllers to retrieve graph info data using `lookText'` instead of `look` - Removed `Location` datatype in favour of `Building` diff --git a/app/Controllers/Graph.hs b/app/Controllers/Graph.hs index cf241b0e0..51b4b31f5 100644 --- a/app/Controllers/Graph.hs +++ b/app/Controllers/Graph.hs @@ -3,12 +3,12 @@ module Controllers.Graph (graphResponse, index, getGraphJSON, graphImageResponse import Control.Monad.IO.Class (liftIO) import Data.Aeson (decode, object, (.=)) import Data.Maybe (fromMaybe) -import Export.ImageConversion (createImageFile) +import Export.ImageConversion (withImageFile) import Happstack.Server (Response, ServerPart, lookBS, lookText', ok, toResponse) import MasterTemplate (masterTemplate) import Scripts (graphScripts) -import System.IO (hClose) -import System.IO.Temp (withSystemTempFile) +import System.FilePath (()) +import System.IO.Temp (withSystemTempDirectory) import Text.Blaze ((!)) import qualified Text.Blaze.Html5 as H import qualified Text.Blaze.Html5.Attributes as A @@ -54,13 +54,10 @@ getGraphJSON = do graphImageResponse :: ServerPart Response graphImageResponse = do graphInfo <- lookText' "JsonLocalStorageObj" - liftIO $ withSystemTempFile "graph.svg" $ \svgPath svgHandle -> do - withSystemTempFile "graph.png" $ \pngPath pngHandle -> do - hClose pngHandle - writeActiveGraphImage graphInfo svgHandle - hClose svgHandle - createImageFile svgPath pngPath - readImageData pngPath + liftIO $ withSystemTempDirectory "graph-image" $ \tempDir -> do + let pngPath = tempDir "graph.png" + withImageFile pngPath (writeActiveGraphImage graphInfo) + readImageData pngPath -- | Inserts SVG graph data into Texts, Shapes, and Paths tables saveGraphJSON :: ServerPart Response diff --git a/app/Controllers/Timetable.hs b/app/Controllers/Timetable.hs index 7c56fec49..9763698f4 100644 --- a/app/Controllers/Timetable.hs +++ b/app/Controllers/Timetable.hs @@ -15,7 +15,7 @@ import Data.Time.Calendar.OrdinalDate (fromMondayStartWeek, mondayStartWeek) import Database.Persist.Sqlite (entityKey, entityVal, selectList, (==.)) import Database.Tables import Export.GetImages (getActiveTimetable, writeActiveGraphImage) -import Export.ImageConversion (createImageFile) +import Export.ImageConversion (withImageFile) import Export.LatexGenerator import Export.PdfGenerator import Happstack.Server @@ -23,7 +23,6 @@ import MasterTemplate import Models.Meeting (returnMeeting) import Scripts import System.FilePath (()) -import System.IO (IOMode (WriteMode), withFile) import System.IO.Temp (withSystemTempDirectory) import Text.Blaze ((!)) import qualified Text.Blaze.Html5 as H @@ -60,12 +59,8 @@ exportTimetablePDFResponse = do graphInfo <- lookText' "JsonLocalStorageObj" liftIO $ withSystemTempDirectory "timetable-pdf" $ \tempDir -> do - let graphSvgPath = tempDir "graph.svg" - graphPngPath = tempDir "graph.png" - - withFile graphSvgPath WriteMode $ \graphSvgHandle -> - writeActiveGraphImage graphInfo graphSvgHandle - createImageFile graphSvgPath graphPngPath + let graphPngPath = tempDir "graph.png" + withImageFile graphPngPath (writeActiveGraphImage graphInfo) fallPngPath <- getActiveTimetable selectedCourses "Fall" tempDir springPngPath <- getActiveTimetable selectedCourses "Spring" tempDir pdfName <- returnPDF graphPngPath fallPngPath springPngPath tempDir @@ -81,10 +76,10 @@ returnPdfBS pdfFilename = do -- and springTimetableImg generated in tempDir. returnPDF :: String -> String -> String -> FilePath -> IO String returnPDF graphImg fallTimetableImg springTimetableImg tempDir = do - let texName = tempDir "timetable.tex" - pdfName = tempDir "timetable.pdf" - generateTex [graphImg, fallTimetableImg, springTimetableImg] texName -- generate a temporary TEX file - createPDF texName -- create PDF using TEX + let timetableName = "timetable" + pdfName = tempDir (timetableName ++ ".pdf") + texText = generateTex [graphImg, fallTimetableImg, springTimetableImg] + createPDF texText tempDir timetableName -- create PDF using TEX return pdfName diff --git a/app/Export/ImageConversion.hs b/app/Export/ImageConversion.hs index 580b82209..21d4a8496 100644 --- a/app/Export/ImageConversion.hs +++ b/app/Export/ImageConversion.hs @@ -3,10 +3,21 @@ Description : Includes functions for converting SVG files to PNG. -} module Export.ImageConversion - (createImageFile) where + (createImageFile, withImageFile) where import GHC.IO.Handle.Types -import System.Process (ProcessHandle, createProcess, shell, waitForProcess) +import System.IO (hClose) +import System.Process (ProcessHandle, StdStream (CreatePipe), createProcess, proc, shell, std_in, + waitForProcess) + +-- | Opens a new process to convert an SVG read from stdin to a PNG (outName) +withImageFile :: String -> (Handle -> IO ()) -> IO () +withImageFile outName writeAction = do + (Just hin, _, _, pid) <- createProcess (proc "magick" ["svg:-", outName]) { std_in = CreatePipe } + writeAction hin + hClose hin + _ <- waitForProcess pid + return () -- | Opens a new process to convert an SVG (inName) to a PNG (outName) -- Note: hGetContents can be used to read Handles. Useful when trying to read from @@ -14,9 +25,8 @@ import System.Process (ProcessHandle, createProcess, shell, waitForProcess) createImageFile :: String -> String -> IO () createImageFile inName outName = do (_, _, _, pid) <- convertToImage inName outName - putStrLn "Waiting for process..." _ <- waitForProcess pid - putStrLn "Process Complete" + return () -- | Converts an SVG file to a PNG file. Note that ImageMagick's 'magick' command -- can take in file descriptors. diff --git a/app/Export/LatexGenerator.hs b/app/Export/LatexGenerator.hs index 80f573e0e..035b1841a 100644 --- a/app/Export/LatexGenerator.hs +++ b/app/Export/LatexGenerator.hs @@ -10,10 +10,9 @@ import Text.LaTeX.Packages.Fancyhdr import Text.LaTeX.Packages.Geometry import Text.LaTeX.Packages.Graphicx --- | Create a TEX file named texName that includes all of the images in --- imageNames -generateTex :: [String] -> String -> IO () -generateTex imageNames texName = execLaTeXT (buildTex imageNames) >>= renderFile texName +-- | Generates a TEX text that includes all of the images in imageNames +generateTex :: [String] -> Text +generateTex imageNames = render $ execLaTeXM (buildTex imageNames) -- | Combine the preamble and the document text into a single block of latex -- code. The document text contains code to insert all of the images in diff --git a/app/Export/PdfGenerator.hs b/app/Export/PdfGenerator.hs index e1741fcfb..238859436 100644 --- a/app/Export/PdfGenerator.hs +++ b/app/Export/PdfGenerator.hs @@ -5,30 +5,33 @@ module Export.PdfGenerator (createPDF) where -import Data.List.Utils (replace) +import Data.Text (Text) +import qualified Data.Text.IO as TIO import GHC.IO.Handle.Types -import System.Directory (removeFile) -import System.Process (ProcessHandle, createProcess, shell, waitForProcess) +import System.IO (hClose) +import System.Process (ProcessHandle, StdStream (CreatePipe), createProcess, proc, std_in, + waitForProcess) --- | Opens a new process to create a PDF from a TEX (texName) and deletes --- the tex file and extra files created by pdflatex -createPDF :: String -> IO () -createPDF texName = do - (_, _, _, pid) <- convertTexToPDF texName - putStrLn "Waiting for a process..." - _ <- waitForProcess pid - let auxFile = replace ".tex" ".aux" texName - logFile = replace ".tex" ".log" texName - mapM_ removeFile [auxFile, logFile, texName] - putStrLn "Process Complete" +-- | Opens a new process to create a PDF from a TEX +createPDF :: Text -> FilePath -> String -> IO () +createPDF texText outDir jobName = do + (Just hin, _, _, pid) <- convertTexToPDF outDir jobName + TIO.hPutStr hin texText + hClose hin + _ <- waitForProcess pid + return () -- | Create a process to use the pdflatex program to create a PDF from a TEX --- file (texName). The process is run in nonstop mode and so it will not block --- if an error occurs. The resulting PDF will have the same filename as texName. -convertTexToPDF :: String -> IO - (Maybe Handle, - Maybe Handle, - Maybe Handle, - ProcessHandle) -convertTexToPDF texName = - createProcess $ shell $ "pdflatex -interaction=nonstopmode " ++ texName +-- read from stdin. The process is run in nonstop mode and so it will not block +-- if an error occurs. The resulting PDF will have the same filename as jobName. +convertTexToPDF :: FilePath -> String -> IO + (Maybe Handle, + Maybe Handle, + Maybe Handle, + ProcessHandle) +convertTexToPDF outDir jobName = + createProcess (proc "pdflatex" + [ "-interaction=nonstopmode" + , "-output-directory=" ++ outDir + , "-jobname=" ++ jobName + ]) { std_in = CreatePipe } diff --git a/backend-test/Controllers/CourseControllerTests.hs b/backend-test/Controllers/CourseControllerTests.hs index ed8b002f4..806d0447e 100644 --- a/backend-test/Controllers/CourseControllerTests.hs +++ b/backend-test/Controllers/CourseControllerTests.hs @@ -13,12 +13,13 @@ import Config (runDb) import Control.Monad (unless) import Controllers.Course (courseInfo, index, retrieveCourse) import qualified Data.ByteString.Lazy.Char8 as BL +import Data.List (nub) import qualified Data.Map as Map import Data.Maybe (fromMaybe, mapMaybe) -import Data.List (nub) import qualified Data.Text as T -import Database.Persist.Sqlite (SqlPersistM, insert, insert_, insertMany_) -import Database.Tables (Building (..), Courses (..), MeetTime (..), Meeting (..), Time' (..), buildTimes) +import Database.Persist.Sqlite (SqlPersistM, insert, insertMany_, insert_) +import Database.Tables (Building (..), Courses (..), MeetTime (..), Meeting (..), Time' (..), + buildTimes) import Happstack.Server (rsBody, rsCode) import Test.Tasty (TestTree) import Test.Tasty.HUnit (assertEqual, testCase)