tsuntsun-0.2.0.0: src/Main.hs
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
module Main (main) where
import Control.Applicative ((<*>))
import Control.Concurrent
import qualified Control.Concurrent.STM as STM
import Control.Monad
import Control.Monad.IO.Class (liftIO)
import qualified Control.Monad.Trans.Resource as R
import qualified Data.Conduit as C
import qualified Data.Conduit.Binary as C
import qualified Data.Conduit.Combinators as CO
import Data.Function ((&))
import Data.Monoid ((<>), mempty)
import Data.Text (Text)
import Graphics.UI.Gtk
import NaturalLanguageProcessing.TsunTsun.TH (litFile)
import qualified Options.Applicative as Opt
import System.Directory (createDirectoryIfMissing, doesFileExist)
import System.Exit (ExitCode(..))
import System.FilePath (takeDirectory)
import qualified System.Process.Typed as P
runWindow :: TsunConfig -> IO ()
runWindow TsunConfig { _imagePath = iFile, _tesseractPath = tPath } = do
_ <- initGUI
-- Load the UI from glade file.
let gladeContent = [litFile|res/mainwindow.glade|] :: String
builder <- builderNew
builderAddFromString builder gladeContent
let getObj :: GObjectClass cls => (GObject -> cls) -> Text -> IO cls
getObj = builderGetObject builder
-- Register quit callbacks.
window <- getObj castToWindow "mainWindow"
_ <- window `on` keyPressEvent $ do
tryEvent $ eventKeyName >>= \"q" -> liftIO mainQuit
_ <- window `on` destroyEvent $ tryEvent (liftIO mainQuit)
_ <- window `on` deleteEvent $ tryEvent (liftIO mainQuit)
-- Maps radio buttons to integers accepted by tesseract's -psm flag.
let imageModes :: [(Text, Int)]
imageModes = [ ("verticalRadio", 5)
, ("columnRadio", 4)
, ("blockRadio", 6)
, ("circledCharacterRadio", 9)
, ("characterRadio", 10)
]
-- Technically we always expect one default choice but just let the
-- code set it depending on what's active in the UI.
ocrMode <- STM.newTVarIO Nothing
forM_ imageModes $ \(r, m) -> getObj castToRadioButton r >>= \obj -> do
let updateMode = toggleButtonGetActive obj >>= \case
True -> STM.atomically $ STM.writeTVar ocrMode (Just m)
False -> pure ()
updateMode
obj `on` toggled $ updateMode
_ <- getObj castToButton "quitButton" >>= \b -> b `on` buttonActivated $ do
mainQuit
-- TVar that's fired when scrot has finished its work which should
-- indicate new contents at iFile. As a bonus, if the file already
-- exists at program start, we'll pass the STM guard straight away
-- and load it.
imageUpdated <- doesFileExist iFile >>= STM.newTVarIO
_ <- getObj castToButton "scrotButton" >>= \b -> do
buttonSetLabel b $ "scrot -s " <> iFile
b `on` buttonActivated $ void . forkIO $ do
createDirectoryIfMissing True $ takeDirectory iFile
P.runProcess (P.proc "scrot" ["-s", iFile]) >>= \case
ExitSuccess -> STM.atomically $ STM.writeTVar imageUpdated True
_ -> return ()
-- When a signal comes on imageUpdated after scrot terminates,
-- display the new image and feed it into tesseract, updating text
-- area when it's done.
textBuffer <- getObj castToTextView "textArea" >>= textViewGetBuffer
runImageUpdate <- postGUIAsync . flip imageSetFromFile iFile
<$> getObj castToImage "imageArea"
void . forkIO . forever $ do
-- Block until we're notified the image file has been updated.
STM.atomically $ do
STM.readTVar imageUpdated >>= STM.check
STM.writeTVar imageUpdated False
runImageUpdate
psmFlags <- STM.readTVarIO ocrMode >>= \case
Nothing -> pure mempty
Just m' -> pure ["-psm", show m']
let procConf = P.proc tPath (psmFlags <> ["-l", "jpn", "stdin", "stdout"])
& P.setStdout P.createSource
& P.setStdin P.createSink
P.withProcess procConf $ \p -> do
R.runResourceT . C.runConduit $ do
C.sourceFile iFile C.$$ P.getStdin p
P.getStdout p
C..| CO.decodeUtf8
C..| CO.mapM_ (liftIO . postGUIAsync . textBufferSetText textBuffer)
widgetShowAll window
mainGUI
data TsunConfig = TsunConfig
{ _imagePath :: !FilePath
, _tesseractPath :: !FilePath
} deriving (Show, Eq, Ord)
main :: IO ()
main = Opt.execParser opts >>= runWindow
where
tc = TsunConfig
<$> Opt.argument Opt.str
( Opt.metavar "SCROT_FILE"
<> Opt.help "File to save to with scrot before it's processed with tesseract." )
<*> Opt.argument Opt.str
( Opt.metavar "TESSERACT_BIN"
<> Opt.help "Path to the tesseract binary."
<> Opt.showDefaultWith id
<> Opt.value "tesseract" )
opts = Opt.info (tc Opt.<**> Opt.helper)
( Opt.fullDesc
<> Opt.progDesc "Run a frontend to tesseract's Japanese OCR."
<> Opt.header "Frontend to tesseract's Japanese OCR." )