module Gifcurry (
gif
, GifParams(..)
, defaultGifParams
, gifParamsValid
) where
import System.Environment
import System.Process
import System.IO.Temp
import System.Directory
import System.Exit
import Data.List
import Text.Printf
import Control.Exception
import Control.Monad
data GifParams = GifParams {
inputFile :: String
, outputFile :: String
, startTime :: Float
, durationTime :: Float
, widthSize :: Int
, qualityPercent :: Float
, topText :: String
, bottomText :: String
} deriving (Show, Read)
defaultGifParams = GifParams {
inputFile = ""
, outputFile = ""
, startTime = 0.0
, durationTime = 1.0
, widthSize = 500
, qualityPercent = 100.0
, topText = ""
, bottomText = ""
}
gif :: GifParams -> IO (Either IOError String)
gif gifParams =
withTempDirectory "." "frames" $ \tmpdir -> do
printGifParams gifParams tmpdir
validParams <- gifParamsValid gifParams
if validParams
then do
result <- tryFfmpeg gifParams tmpdir
result' <- case result of
Left err -> return False
Right val -> return True
if result'
then do
putStrLn $ "Writing your GIF to... " ++ outputFile gifParams
result <- tryConvert gifParams tmpdir
result' <- case result of
Left err -> return False
Right val -> return True
if result'
then putStrLn "Done."
else putStrLn "[Error] Something when wrong with ImageMagick."
return result
else do
putStrLn "[Error] Something went wrong with FFmpeg."
return result
else return $ Left (userError "[Error] Invalid params.")
gifParamsValid :: GifParams -> IO Bool
gifParamsValid GifParams {
inputFile = ipf
, outputFile = opf
, startTime = st
, durationTime = dt
, widthSize = ws
, qualityPercent = qp
, topText = tt
, bottomText = bt
} = do
inputFileExists <- case length ipf of
0 -> return False
_ -> doesFileExist ipf
unless inputFileExists $ putStrLn "\n[Error] Input video file does not exist."
let outputFileValid = length opf > 5
unless outputFileValid $ putStrLn "\n[Error] Output video file blank."
let valid = inputFileExists && outputFileValid && (st >= 0.0) && (dt >= 0.0) && (ws > 0) && (qp > 0.0)
unless valid $ putStrLn "\n[Error] Invalid params."
return valid
printGifParams :: GifParams -> String -> IO ()
printGifParams
GifParams {
inputFile = ipf
, outputFile = opf
, startTime = st
, durationTime = dt
, widthSize = ws
, qualityPercent = qp
, topText = tt
, bottomText = bt
}
tmpdir = mapM_ putStrLn [
"\nInput file: " ++ ipf
, "Output file: " ++ opf
, "Start second: " ++ printf "%.3f" st
, "Duration: " ++ printf "%.3f" dt ++ " seconds"
, "GIF width: " ++ show ws ++ "px"
, "Quality: " ++ show (qualityPercentClamp qp) ++ "%"
, "Top text: " ++ tt
, "Bottom text: " ++ bt
, "\nWriting temporary frames to... " ++ tmpdir
]
tryFfmpeg :: GifParams -> String -> IO (Either IOError String)
tryFfmpeg
GifParams {
inputFile = ipf
, startTime = st
, durationTime = dt
, widthSize = ws
}
tmpdir = try(
readProcess "ffmpeg" [
"-nostats"
, "-loglevel"
, "panic"
, "-an"
, "-ss"
, sts
, "-i"
, ipf
, "-t"
, dts
, "-r"
, "15"
, "-q:v"
, "2"
, "-vf"
, "scale=" ++ wss ++ ":-1"
, "-f"
, "image2"
, tmpdir ++ "/%010d.png"
] ""
) :: IO (Either IOError String)
where sts = printf "%.3f" st
dts = printf "%.3f" dt
wss = show ws
tryConvert :: GifParams -> String -> IO (Either IOError String)
tryConvert
GifParams {
outputFile = opf
, widthSize = ws
, qualityPercent = qp
, topText = tt
, bottomText = bt
}
tmpdir = try(
readProcess "convert" (
[
"-quiet"
, "-delay"
, "6"
, "-colors"
, show $ ncolors qp
, "-coalesce"
, "-layers"
, "OptimizeTransparency"
, "-layers"
, "RemoveDups"
, tmpdir ++ "/*.png"
, "-dither"
, "FloydSteinberg"
, "-loop"
, "0"
] ++ annotate ws tt "north" ++ annotate ws bt "south" ++ [opf]
) ""
)
qualityPercentClamp :: Float -> Float
qualityPercentClamp qp
| qp > 100.0 = 100.0
| qp < 0.0 = 2.0
| otherwise = qp
ncolors :: Float -> Int
ncolors qp
| qpc < 0.0 = 1
| qpc >= 100.0 = 256
| otherwise = truncate (qpc / 100.0 * 256.0)
where qpc = qualityPercentClamp qp
annotate :: Int -> String -> String -> [String]
annotate widthSize text topBottom = [
"-gravity"
, topBottom
, "-stroke"
, "#000C"
, "-strokewidth"
, "10"
, "-pointsize"
, ps
, "-annotate"
, "+0+10"
, text
, "-stroke"
, "none"
, "-fill"
, "white"
, "-pointsize"
, ps
, "-annotate"
, "+0+10"
, text
]
where ps = show $ pointSize widthSize text
pointSize :: Int -> String -> Int
pointsize _ "" = 0
pointSize widthSize text
| widthSize <= 0 = 0
| otherwise = truncate ((wsf * 0.4) / l * (72.0 / 34.0))
where wsf = fromIntegral widthSize
l = fromIntegral (length text)