never executed always true always false
1 {-# LANGUAGE MultiWayIf #-}
2 {-|
3 Copyright : Written by David Himmelstrup
4 License : Unlicense
5 Maintainer : lemmih@gmail.com
6 Stability : experimental
7 Portability : POSIX
8
9 Internal tools for rastering SVGs and rendering videos. You are unlikely
10 to ever directly use the functions in this module.
11
12 -}
13 module Reanimate.Render
14 ( render
15 , renderSvgs
16 , renderSnippets -- :: Animation -> IO ()
17 , renderLimitedFrames
18 , Format(..)
19 , Raster(..)
20 , Width, Height, FPS
21 , requireRaster -- :: Raster -> IO Raster
22 , selectRaster -- :: Raster -> IO Raster
23 , applyRaster -- :: Raster -> FilePath -> IO ()
24 ) where
25
26 import Control.Concurrent
27 import Control.Exception
28 import Control.Monad (forM_, forever, unless, void, when)
29 import Data.Either
30 import Data.Function
31 import qualified Data.Text as T
32 import qualified Data.Text.IO as T
33 import Data.Time
34 import Graphics.SvgTree (Number (..))
35 import Numeric
36 import Reanimate.Animation
37 import Reanimate.Driver.Check
38 import Reanimate.Driver.Magick
39 import Reanimate.Misc
40 import Reanimate.Parameters
41 import System.Console.ANSI.Codes
42 import System.Exit
43 import System.FileLock (withTryFileLock, SharedExclusive(..), unlockFile)
44 import System.Directory
45 import System.FilePath (replaceExtension, (<.>), (</>))
46 import System.IO
47 import Text.Printf (printf)
48
49 idempotentFile :: FilePath -> IO () -> IO ()
50 idempotentFile path action = do
51 _ <- withTryFileLock lockFile Exclusive $ \lock -> do
52 haveFile <- doesFileExist path
53 unless haveFile action
54 unlockFile lock
55 _ <- try (removeFile lockFile) :: IO (Either SomeException ())
56 return ()
57 return ()
58 where
59 lockFile = path <.> "lock"
60
61 -- | Generate SVGs at 60fps and put them in a folder.
62 renderSvgs :: FilePath -> Int -> Bool -> Animation -> IO ()
63 renderSvgs folder offset _prettyPrint ani = do
64 print frameCount
65 lock <- newMVar ()
66 handle errHandler $ concurrentForM_ (frameOrder rate frameCount) $ \nth' -> do
67 let nth = (nth'+offset) `mod` frameCount
68 now = (duration ani / (fromIntegral frameCount - 1)) * fromIntegral nth
69 frame = frameAt (if frameCount <= 1 then 0 else now) ani
70 path = folder </> show nth <.> "svg"
71 ~svg = renderSvg Nothing Nothing frame
72
73 idempotentFile path $
74 writeFile path svg
75 withMVar lock $ \_ -> do
76 print nth
77 hFlush stdout
78 where
79 rate = 60
80 frameCount = round (duration ani * fromIntegral rate) :: Int
81 errHandler (ErrorCall msg) = do
82 hPutStrLn stderr msg
83 exitWith (ExitFailure 1)
84
85 -- | Render as many frames as possible in 2 seconds. Limited to 20 frames.
86 renderLimitedFrames :: FilePath -> Int -> Bool -> Int -> Animation -> IO ()
87 renderLimitedFrames folder offset _prettyPrint rate ani = do
88 now <- getCurrentTime
89 worker (addUTCTime timeLimit now) frameLimit (frameOrder rate frameCount)
90 where
91 timeLimit = 2
92 frameLimit = 20 :: Int
93 worker _ 0 _ = return ()
94 worker _ _ [] = putStrLn "Done"
95 worker localTimeLimit l (x:xs) = do
96 curTime <- getCurrentTime
97 if curTime > localTimeLimit
98 then return ()
99 else do
100 let nth = (x+offset) `mod` frameCount
101 now = (duration ani / (fromIntegral frameCount - 1)) * fromIntegral nth
102 frame = frameAt (if frameCount <= 1 then 0 else now) ani
103 svg = renderSvg Nothing Nothing frame
104 path = folder </> show nth <.> "svg"
105 tmpPath = path <.> "tmp"
106 haveFile <- doesFileExist path
107 if haveFile
108 then worker localTimeLimit l xs
109 else do
110 writeFile tmpPath svg
111 renameOrCopyFile tmpPath path
112 print nth
113 worker localTimeLimit (l-1) xs
114 frameCount = round (duration ani * fromIntegral rate) :: Int
115
116 -- XXX: Merge with 'renderSvgs'
117 -- | Render 10 frames and print them to stdout. Used for testing.
118 --
119 -- XXX: Not related to the snippets in the playground.
120 renderSnippets :: Animation -> IO ()
121 renderSnippets ani = forM_ [0 .. frameCount - 1] $ \nth -> do
122 let now = (duration ani / (fromIntegral frameCount - 1)) * fromIntegral nth
123 frame = frameAt now ani
124 svg = renderSvg Nothing Nothing frame
125 putStr (show nth)
126 T.putStrLn $ T.concat . T.lines . T.pack $ svg
127 where frameCount = 10 :: Integer
128
129 frameOrder :: Int -> Int -> [Int]
130 frameOrder fps nFrames = worker [] fps
131 where
132 worker _seen 0 = []
133 worker seen nthFrame = filterFrameList seen nthFrame nFrames
134 ++ worker (nthFrame : seen) (nthFrame `div` 2)
135
136 filterFrameList :: [Int] -> Int -> Int -> [Int]
137 filterFrameList seen nthFrame nFrames = filter (not . isSeen)
138 [0, nthFrame .. nFrames - 1]
139 where isSeen x = any (\y -> x `mod` y == 0) seen
140
141 -- | Video formats supported by reanimate.
142 data Format = RenderMp4 | RenderGif | RenderWebm
143 deriving (Show)
144
145 mp4Arguments :: FPS -> FilePath -> FilePath -> FilePath -> [String]
146 mp4Arguments fps progress template target =
147 [ "-r"
148 , show fps
149 , "-i"
150 , template
151 , "-y"
152 , "-c:v"
153 , "libx264"
154 , "-vf"
155 , "fps=" ++ show fps
156 , "-preset"
157 , "slow"
158 , "-crf"
159 , "18"
160 , "-movflags"
161 , "+faststart"
162 , "-progress"
163 , progress
164 , "-pix_fmt"
165 , "yuv420p"
166 , target
167 ]
168
169 -- gifArguments :: FPS -> FilePath -> FilePath -> FilePath -> [String]
170 -- gifArguments fps progress template target =
171
172 -- | Render animation to a video file with given parameters.
173 render
174 :: Animation
175 -> FilePath
176 -> Raster
177 -> Format
178 -> Width
179 -> Height
180 -> FPS
181 -> Bool
182 -> IO ()
183 render ani target raster format width height fps partial = do
184 printf "Starting render of animation: %.1f\n" (duration ani)
185 ffmpeg <- requireExecutable "ffmpeg"
186 generateFrames raster ani width height fps partial $ \template ->
187 withTempFile "txt" $ \progress -> do
188 writeFile progress ""
189 progressH <- openFile progress ReadMode
190 hSetBuffering progressH NoBuffering
191 allFinished <- newEmptyMVar
192 void $ forkIO $ do
193 progressPrinter "rendered" (animationFrameCount ani fps)
194 $ \done -> fix $ \loop -> do
195 eof <- hIsEOF progressH
196 if eof
197 then threadDelay 1000000 >> loop
198 else do
199 l <- try (hGetLine progressH)
200 case l of
201 Left SomeException{} -> return ()
202 Right str ->
203 case take 6 str of
204 "frame=" -> do
205 void $ swapMVar done (read (drop 6 str))
206 loop
207 _ | str == "progress=end" -> return ()
208 _ -> loop
209 putMVar allFinished ()
210 case format of
211 RenderMp4 -> runCmd ffmpeg (mp4Arguments fps progress template target)
212 RenderGif -> withTempFile "png" $ \palette -> do
213 runCmd
214 ffmpeg
215 [ "-i"
216 , template
217 , "-y"
218 , "-vf"
219 , "fps="
220 ++ show fps
221 ++ ",scale="
222 ++ show width
223 ++ ":"
224 ++ show height
225 ++ ":flags=lanczos,palettegen"
226 , "-t"
227 , showFFloat Nothing (duration ani) ""
228 , palette
229 ]
230 runCmd
231 ffmpeg
232 [ "-framerate"
233 , show fps
234 , "-i"
235 , template
236 , "-y"
237 , "-i"
238 , palette
239 , "-progress"
240 , progress
241 , "-filter_complex"
242 , "fps="
243 ++ show fps
244 ++ ",scale="
245 ++ show width
246 ++ ":"
247 ++ show height
248 ++ ":flags=lanczos[x];[x][1:v]paletteuse"
249 , "-t"
250 , showFFloat Nothing (duration ani) ""
251 , target
252 ]
253 RenderWebm -> runCmd
254 ffmpeg
255 [ "-r"
256 , show fps
257 , "-i"
258 , template
259 , "-y"
260 , "-progress"
261 , progress
262 , "-c:v"
263 , "libvpx-vp9"
264 , "-vf"
265 , "fps=" ++ show fps
266 , target
267 ]
268 takeMVar allFinished
269
270 ---------------------------------------------------------------------------------
271 -- Helpers
272
273 progressPrinter :: String -> Int -> (MVar Int -> IO ()) -> IO ()
274 progressPrinter typeName maxCount action = do
275 printf "\rFrames %s: 0/%d" typeName maxCount
276 putStr $ clearFromCursorToLineEndCode ++ "\r"
277 done <- newMVar (0 :: Int)
278 start <- getCurrentTime
279 let bgThread = forever $ do
280 nDone <- readMVar done
281 now <- getCurrentTime
282 let spent = diffUTCTime now start
283 remaining =
284 (spent / (fromIntegral nDone / fromIntegral maxCount)) - spent
285 printf "\rFrames %s: %d/%d" typeName nDone maxCount
286 putStr $ ", time spent: " ++ ppDiff spent
287 unless (nDone == 0) $ do
288 putStr $ ", time remaining: " ++ ppDiff remaining
289 putStr $ ", total time: " ++ ppDiff (remaining + spent)
290 putStr $ clearFromCursorToLineEndCode ++ "\r"
291 hFlush stdout
292 threadDelay 1000000
293 withBackgroundThread bgThread $ action done
294 now <- getCurrentTime
295 let spent = diffUTCTime now start
296 printf "\rFrames %s: %d/%d" typeName maxCount maxCount
297 putStr $ ", time spent: " ++ ppDiff spent
298 putStr $ clearFromCursorToLineEndCode ++ "\n"
299
300 animationFrameCount :: Animation -> FPS -> Int
301 animationFrameCount ani rate = round (duration ani * fromIntegral rate) :: Int
302
303 generateFrames
304 :: Raster -> Animation -> Width -> Height -> FPS -> Bool -> (FilePath -> IO a) -> IO a
305 generateFrames raster ani width_ height_ rate partial action = withTempDir $ \tmp -> do
306 let frameName nth = tmp </> printf nameTemplate nth
307 setRootDirectory tmp
308 progressPrinter "generated" frameCount
309 $ \done -> handle h $ concurrentForM_ frames $ \n -> do
310 writeFile (frameName n) $ renderSvg width height $ nthFrame n
311 modifyMVar_ done $ \nDone -> return (nDone + 1)
312
313 when (isValidRaster raster)
314 $ progressPrinter "rastered" frameCount
315 $ \done -> handle h $ concurrentForM_ frames $ \n -> do
316 applyRaster raster (frameName n)
317 modifyMVar_ done $ \nDone -> return (nDone + 1)
318
319 action (tmp </> rasterTemplate raster)
320 where
321 isValidRaster RasterNone = False
322 isValidRaster RasterAuto = False
323 isValidRaster _ = True
324
325 width = Just $ Px $ fromIntegral width_
326 height = Just $ Px $ fromIntegral height_
327 h UserInterrupt | partial = do
328 hPutStrLn
329 stderr
330 "\nCtrl-C detected. Trying to generate video with available frames. \
331 \Hit ctrl-c again to abort."
332 return ()
333 h other = throwIO other
334 -- frames = [0..frameCount-1]
335 frames = frameOrder rate frameCount
336 nthFrame nth = frameAt (recip (fromIntegral rate) * fromIntegral nth) ani
337 frameCount = animationFrameCount ani rate
338 nameTemplate :: String
339 nameTemplate = "render-%05d.svg"
340
341 withBackgroundThread :: IO () -> IO a -> IO a
342 withBackgroundThread t = bracket (forkIO t) killThread . const
343
344 ppDiff :: NominalDiffTime -> String
345 ppDiff diff | hours == 0 && mins == 0 = show secs ++ "s"
346 | hours == 0 = printf "%.2d:%.2d" mins secs
347 | otherwise = printf "%.2d:%.2d:%.2d" hours mins secs
348 where
349 (osecs, secs) = round diff `divMod` (60 :: Int)
350 (hours, mins) = osecs `divMod` 60
351
352 rasterTemplate :: Raster -> String
353 rasterTemplate RasterNone = "render-%05d.svg"
354 rasterTemplate RasterAuto = "render-%05d.svg"
355 rasterTemplate _ = "render-%05d.png"
356
357 -- | Resolve RasterNone and RasterAuto. If no valid raster can
358 -- be found, exit with an error message.
359 requireRaster :: Raster -> IO Raster
360 requireRaster raster = do
361 raster' <- selectRaster (if raster == RasterNone then RasterAuto else raster)
362 case raster' of
363 RasterNone -> do
364 hPutStrLn
365 stderr
366 "Raster required but none could be found. \
367 \Please install either inkscape, imagemagick, or rsvg-convert."
368 exitWith (ExitFailure 1)
369 _ -> pure raster'
370
371 -- | Resolve RasterNone and RasterAuto. If no valid raster can
372 -- be found, return RasterNone.
373 selectRaster :: Raster -> IO Raster
374 selectRaster RasterAuto = do
375 rsvg <- hasRSvg
376 ink <- hasInkscape
377 magick <- hasMagick
378 if
379 | isRight rsvg -> pure RasterRSvg
380 | isRight ink -> pure RasterInkscape
381 | isRight magick -> pure RasterMagick
382 | otherwise -> pure RasterNone
383 selectRaster r = pure r
384
385 -- | Convert SVG file to a PNG file with selected raster engine. If
386 -- raster engine is RasterAuto or RasterNone, do nothing.
387 applyRaster :: Raster -> FilePath -> IO ()
388 applyRaster RasterNone _ = return ()
389 applyRaster RasterAuto _ = return ()
390 applyRaster RasterInkscape path = runCmd
391 "inkscape"
392 [ "--without-gui"
393 , "--file=" ++ path
394 , "--export-png=" ++ replaceExtension path "png"
395 ]
396 applyRaster RasterRSvg path = runCmd
397 "rsvg-convert"
398 [path, "--unlimited", "--output", replaceExtension path "png"]
399 applyRaster RasterMagick path =
400 runCmd magickCmd [path, replaceExtension path "png"]
401
402 concurrentForM_ :: [a] -> (a -> IO ()) -> IO ()
403 concurrentForM_ lst action = do
404 n <- getNumCapabilities
405 sem <- newQSemN n
406 eVar <- newEmptyMVar
407 forM_ lst $ \elt -> do
408 waitQSemN sem 1
409 emp <- isEmptyMVar eVar
410 if emp
411 then
412 void
413 $ forkIO
414 ( catch (action elt) (void . tryPutMVar eVar)
415 `finally` signalQSemN sem 1
416 )
417 else signalQSemN sem 1
418 waitQSemN sem n
419 mbE <- tryTakeMVar eVar
420 case mbE of
421 Nothing -> return ()
422 Just e -> throwIO (e :: SomeException)