packages feed

pdf-toolbox-viewer-0.0.5.0: viewer.hs

{-# LANGUAGE OverloadedStrings #-}

module Main
(
  main
)
where

import qualified Data.Traversable as Traversable
import qualified Data.Map as Map
import qualified Data.ByteString.Char8 as BS8
import qualified Data.Text as Text
import Data.IORef
import Control.Monad
import Control.Monad.IO.Class
import Control.Concurrent
import System.IO
import qualified System.IO.Streams as Streams
import qualified System.IO.Streams.Attoparsec as Streams
import System.Directory
import System.FilePath
import System.Random (randomIO)
import System.Process
import System.Exit
import Graphics.UI.Gtk hiding (Rectangle, FontMap, rectangle)
import Graphics.Rendering.Cairo as Cairo hiding (transform, Glyph)

import Pdf.Toolbox.Document
import Pdf.Toolbox.Document.Encryption
import Pdf.Toolbox.Content

data ViewerState = ViewerState {
  viewerPage :: Page,
  viewerPageNum :: Int,
  viewerRenderIM :: Bool,
  viewerRenderText :: Bool,
  viewerRenderGlyphs :: Bool
  }

main :: IO ()
main = do
  [file] <- initGUI

  mvar <- newEmptyMVar
  withBinaryFile file ReadMode $ \h -> do
  _ <- forkIO $ pdfThread h mvar

  (rootNode, totalPages, title) <- pdfSync mvar $ do
    encrypted <- isEncrypted
    when encrypted $ do
      liftIO $ putStrLn "WARNING: Document is encrypted, it is not fully supported yet"
      ok <- setUserPassword defaultUserPassword
      unless ok $ error "Need user password"
    pdf <- document
    title <- do
      info <- documentInfo pdf
      case info of
        Nothing -> return Nothing
        Just i -> infoTitle i
    root <- documentCatalog pdf >>= catalogPageNode
    total <- pageNodeNKids root
    return (root, total, title)

  firstPage <- pdfSync mvar $ do
    pageNodePageByNum rootNode 0

  viewerState <- newIORef ViewerState {
    viewerPage = firstPage,
    viewerPageNum = 0,
    viewerRenderIM = False,
    viewerRenderText = True,
    viewerRenderGlyphs = False
    }

  let winTitle = maybe "Untitled" (\(Str s) -> BS8.unpack s) title

  window <- windowNew
  set window [
    windowDefaultWidth := 300,
    windowDefaultHeight := 300,
    windowTitle := winTitle
    ]
  _ <- on window deleteEvent $ liftIO mainQuit >> return True

  vbox <- vBoxNew False 10
  containerAdd window vbox

  hbuttonBox <- hButtonBoxNew
  boxPackStart vbox hbuttonBox PackNatural 0

  prevButton <- buttonNewWithLabel ("Prev" :: String)
  boxPackStart hbuttonBox prevButton PackNatural 0

  nextButton <- buttonNewWithLabel ("Next" :: String)
  boxPackStart hbuttonBox nextButton PackNatural 0

  renderPdfToggle <- checkButtonNewWithLabel ("Render via ImageMagick" :: String)
  set renderPdfToggle [
    toggleButtonActive := False
    ]
  boxPackStart hbuttonBox renderPdfToggle PackNatural 0

  renderTextToggle <- checkButtonNewWithLabel ("Render extracted text" :: String)
  set renderTextToggle [
    toggleButtonActive := True
    ]
  boxPackStart hbuttonBox renderTextToggle PackNatural 0

  renderGlyphsToggle <- checkButtonNewWithLabel ("Render glyphs" :: String)
  set renderGlyphsToggle [
    toggleButtonActive := False
    ]
  boxPackStart hbuttonBox renderGlyphsToggle PackNatural 0

  frame <- frameNew
  boxPackStart vbox frame PackGrow 0

  canvas <- drawingAreaNew
  containerAdd frame canvas

  _ <- on renderPdfToggle toggled $ do
    st <- get renderPdfToggle toggleButtonActive
    modifyIORef viewerState $ \s -> s {
      viewerRenderIM = st
      }
    widgetQueueDraw canvas

  _ <- on renderTextToggle toggled $ do
    st <- get renderTextToggle toggleButtonActive
    modifyIORef viewerState $ \s -> s {
      viewerRenderText = st
      }
    widgetQueueDraw canvas

  _ <- on renderGlyphsToggle toggled $ do
    st <- get renderGlyphsToggle toggleButtonActive
    modifyIORef viewerState $ \s -> s {
      viewerRenderGlyphs = st
      }
    widgetQueueDraw canvas

  _ <- on prevButton buttonActivated $ do
    num <- viewerPageNum <$> readIORef viewerState
    when (num > 0) $ do
      p <- pdfSync mvar $ pageNodePageByNum rootNode (num - 1)
      modifyIORef viewerState $ \s -> s {
        viewerPage = p,
        viewerPageNum = num - 1
        }
      widgetQueueDraw canvas

  _ <- on nextButton buttonActivated $ do
    num <- viewerPageNum <$> readIORef viewerState
    when (num < totalPages - 1) $ do
      p <- pdfSync mvar $ pageNodePageByNum rootNode (num + 1)
      modifyIORef viewerState $ \s -> s {
        viewerPage = p,
        viewerPageNum = num + 1
        }
      widgetQueueDraw canvas

  widgetShowAll window
  draw <- widgetGetDrawWindow canvas
  _ <- on canvas exposeEvent $ do
    liftIO $ renderWithDrawable draw $ onDraw file mvar viewerState
    return True

  mainGUI

onDraw :: FilePath -> MVar (Pdf IO Bool) -> IORef ViewerState -> Render ()
onDraw file mvar viewerState = do
  Cairo.scale 2 2
  st <- liftIO $ readIORef viewerState
  let pg = viewerPage st
      num = viewerPageNum st

  when (viewerRenderIM st) $ do
    randomNum <- liftIO $ randomIO :: Render Int
    tmpDir <- liftIO $ getTemporaryDirectory
    let tmpFile = tmpDir </> ("pdf-toolbox-viewer-" ++ show randomNum ++ ".png")
    (_, _, _, procHandle) <- liftIO $ createProcess $ proc "convert" [file ++ "[" ++ show num ++ "]", tmpFile]
    hasPng <- (== ExitSuccess) <$> liftIO (waitForProcess procHandle)
    if hasPng
      then do
        surface <- liftIO $ imageSurfaceCreateFromPNG tmpFile
        setSourceSurface surface 0 0
        paint
        surfaceFinish surface
        liftIO $ removeFile tmpFile
      else liftIO $ putStrLn "Can't render pdf via ImageMagick. Please check that you have \"convert\" in PATH"

  setSourceRGB 1 1 1
  setLineWidth 1

  Rectangle llx lly urx ury <- liftIO $ pdfSync mvar $ pageMediaBox pg

  chan <- liftIO $ startRender mvar pg

  moveTo llx lly
  lineTo llx ury
  lineTo urx ury
  lineTo urx lly
  lineTo llx lly
  closePath
  stroke

  let loop = do
        cmd <- liftIO $ readChan chan
        case cmd of
          Nothing -> return ()
          Just glyph -> do
            let Vector x1 y1 = glyphTopLeft glyph
                Vector x2 y2 = glyphBottomRight glyph
            when (viewerRenderText st) $ do
              setSourceRGB 0 0 0
              case glyphText glyph of
                Nothing -> return ()
                Just txt -> do
                  moveTo x1 (ury - y1)
                  showText $ Text.unpack txt
                  stroke
            when (viewerRenderGlyphs st) $ do
              setSourceRGBA 0 0 0 0.2
              rectangle x1 (ury - y1) (x2 - x1) (y2 - y1)
              fill
            loop
  loop

pageGlyphDecoder :: (MonadPdf m, MonadIO m) => Page -> PdfE m GlyphDecoder
pageGlyphDecoder page = do
  fontDicts <- Map.fromList <$> pageFontDicts page
  decoders <- Traversable.forM fontDicts $ \fontDict -> do
    fontInfo <- fontDictLoadInfo fontDict
    return $ fontInfoDecodeGlyphs fontInfo
  return $ \fontName str ->
    case Map.lookup fontName decoders of
      Nothing -> error "Not font!?" -- []
      Just decode -> decode str

startRender :: MVar (Pdf IO Bool) -> Page -> IO (Chan (Maybe Glyph))
startRender mvar page = do
  chan <- newChan
  putMVar mvar $ do
    glyphDecoder <- pageGlyphDecoder page
    --pageExtractText page >>= liftIO . print

    contents <- pageContents page
    streams <- forM contents $ \ref -> do
      s@(Stream dict _) <- lookupObject ref >>= toStream
      len <- lookupDict "Length" dict >>= deref >>= fromObject >>= intValue
      return (s, ref, len)
    ris <- getRIS
    decryptor <- do
      dec <- getDecryptor
      case dec of
        Nothing -> return $ \_ is -> return is
        Just d -> return $ \ref is -> d ref DecryptStream is
    is <- parseContentStream ris knownFilters decryptor streams

    xobjects <- Map.fromList <$> pageXObjects page

    let loop s p = do
          next <- readNextOperator s
          case next of
            Nothing -> do
              return p
            Just (Op_Do, [OName n]) -> processDo n p >>= loop s
            Just (Op_quote, args) -> error $ "Op_quote (please report): " ++ show args
            Just op -> processOp op p >>= loop s

        processDo name p = do
          case Map.lookup name xobjects of
            Nothing -> return p
            Just (XObject cont gdec) -> do
              xois <- liftIO $ do
                cont_is <- Streams.fromLazyByteString cont
                Streams.parserToInputStream parseContent cont_is
              let gdec' = prGlyphDecoder p
              p' <- loop xois (p {prGlyphDecoder = gdec})
              return (p' {prGlyphDecoder = gdec'})

    p <- loop is $ mkProcessor {
      prGlyphDecoder = glyphDecoder
      }
    forM_ (prGlyphs p) $ \glyphs ->
      forM_ glyphs $ \glyph ->
        liftIO $ writeChan chan (Just glyph)
    --liftIO $ print $ prGlyphs p

    liftIO $ writeChan chan Nothing
    return False
  return chan

pdfThread :: Handle -> MVar (Pdf IO Bool) -> IO ()
pdfThread handle mvar = do
  res <- runPdfWithHandle handle knownFilters loop
  print res
  where
  loop = do
    action <- liftIO $ takeMVar mvar
    exit <- action
    unless exit loop

pdfSync :: MVar (Pdf IO Bool) -> Pdf IO a -> IO a
pdfSync mvar action = do
  mvar' <- newEmptyMVar
  putMVar mvar $ do
    res <- (Right <$> action) `catchE` (return . Left)
    liftIO $ putMVar mvar' res
    return False
  res <- takeMVar mvar'
  case res of
    Left e -> print e >> fail (show e)
    Right r -> return r