pdfinfo 0.1.3.3 → 1.5.4
raw patch · 2 files changed
Files
- pdfinfo.cabal +18/−9
- src/Text/PDF/Info.hs +113/−65
pdfinfo.cabal view
@@ -1,21 +1,30 @@ name: pdfinfo-version: 0.1.3.3+version: 1.5.4 synopsis: Wrapper around the pdfinfo command.-description: Just a wrapper around the pdfinfo command. See man pdfinfo.+description: Just a wrapper around the pdfinfo command (for collecting PDF file info). See man pdfinfo. license: BSD3 license-file: LICENSE author: Chris Done maintainer: chrisdone@gmail.com-copyright: 2010 Chris Done+copyright: 2013 Chris Done category: Text build-type: Simple-cabal-version: >=1.2+cabal-version: >=1.8+homepage: https://github.com/chrisdone/pdfinfo++source-repository head+ type: git+ location: git@github.com:chrisdone/pdfinfo.git+ library- ghc-options: -O2 -Wall+ ghc-options: -O2 -Wall exposed-modules: Text.PDF.Info hs-source-dirs: src/ build-depends: base >= 4 && < 5,- old-locale >= 1.0.0.0 && < 1.1,- time >= 1.1 && < 1.5,- process >= 1.0 && < 1.2,- mtl >= 1.1 && < 2.2+ old-locale,+ time >= 1.1,+ time-locale-compat,+ process-extras,+ text,+ -- mtl-2.1 contains a severe bug+ mtl >= 1.1 && < 2.1 || >= 2.1.1
src/Text/PDF/Info.hs view
@@ -1,11 +1,15 @@-{-# LANGUAGE GeneralizedNewtypeDeriving, MultiParamTypeClasses, ViewPatterns #-} {-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE ViewPatterns #-}+{-# LANGUAGE OverloadedStrings #-} {-# OPTIONS -Wall #-}+ module Text.PDF.Info (-- * Reading PDF info pdfInfo ,PDFInfo(..) ,PDFSize(..)+ ,PDFEncryptionInfo(..) ,PDFInfoError(..) -- * Internals ,ParsePDFInfo@@ -13,49 +17,69 @@ ,parse ,parseSize ,parseDate+ ,parseEncrypted ,readRight) where -import Prelude-import Control.Monad.Error-import System.Process-import Control.Applicative-import Control.Arrow-import Data.Char-import Data.Time-import System.Locale-import Control.Exception as E+import Control.Applicative+import Control.Arrow+import Control.Exception as E+import Control.Monad.Error +import Data.Char (isSpace)+import Data.Text (Text)+import qualified Data.Text as T+import Data.Time (UTCTime,parseTime)+import Data.Time.Locale.Compat (defaultTimeLocale)+import Prelude+import System.Exit+import System.Process.Text+ -- | A type representing the output from the pdfinfo command. data PDFInfo = PDFInfo {- pdfInfoTitle :: Maybe String -- ^ Title- , pdfInfoSubject :: Maybe String -- ^ Subject- , pdfInfoAuthor :: Maybe String -- ^ Author: E.g. Chris Done- , pdfInfoCreator :: Maybe String -- ^ Creator: E.g. Microsoft Office Word 2007- , pdfInfoProducer :: Maybe String -- ^ Producer: E.g. Microsoft Office Word 2007- , pdfInfoCreationDate :: Maybe UTCTime -- ^ Creation Date- , pdfInfoModDate :: Maybe UTCTime -- ^ Modification Date- , pdfInfoTagged :: Maybe Bool -- ^ Tagged?- , pdfInfoPages :: Maybe Integer -- ^ Pages: E.g. 238- , pdfInfoEncrypted :: Maybe Bool -- ^ Encrypted?- , pdfInfoPageSize :: Maybe PDFSize -- ^ Page: E.g. 595.32 x 841.92 pts (A4)- , pdfInfoFileSize :: Maybe Integer -- ^ File: E.g. 4061737 bytes- , pdfInfoOptimized :: Maybe Bool -- ^ Optimized?- , pdfInfoPDFVersion :: Maybe Double -- ^ PDF: E.g. 1.5+ pdfInfoTitle :: !(Maybe Text) -- ^ Title+ , pdfInfoSubject :: !(Maybe Text) -- ^ Subject+ , pdfInfoAuthor :: !(Maybe Text) -- ^ Author: E.g. Chris Done+ , pdfInfoCreator :: !(Maybe Text) -- ^ Creator: E.g. Microsoft Office Word 2007+ , pdfInfoProducer :: !(Maybe Text) -- ^ Producer: E.g. Microsoft Office Word 2007+ , pdfInfoCreationDate :: !(Maybe UTCTime) -- ^ Creation Date+ , pdfInfoModDate :: !(Maybe UTCTime) -- ^ Modification Date+ , pdfInfoTagged :: !(Maybe Bool) -- ^ Tagged?+ , pdfInfoPages :: !(Maybe Integer) -- ^ Pages: E.g. 238+ , pdfInfoEncrypted :: !(Maybe PDFEncryptionInfo) -- ^ Encryption information+ , pdfInfoPageSize :: !(Maybe PDFSize) -- ^ Page: E.g. 595.32 x 841.92 pts (A4)+ , pdfInfoFileSize :: !(Maybe Integer) -- ^ File: E.g. 4061737 bytes+ , pdfInfoOptimized :: !(Maybe Bool) -- ^ Optimized?+ , pdfInfoPDFVersion :: !(Maybe Double) -- ^ PDF: E.g. 1.5 } deriving Show -- | Possible things that can go wrong while reading the info.-data PDFInfoError =- ParseError String -- ^ Couldn't parse a property value.- | ProcessError IOException -- ^ Error to do with the pdfinfo process.- | NoMessage -- ^ No message given.- | SomeError String -- ^ Some nonspecific error.+data PDFInfoError+ = ParseError !String -- ^ Couldn't parse a property value.+ | ProcessFailure !Text -- ^ Process exited with this stderr.+ | ProcessError !IOException -- ^ Error to do with the pdfinfo process.+ | NoMessage -- ^ No message given.+ | SomeError String -- ^ Some nonspecific error. deriving Show -- | Size of the PDF in pts.-data PDFSize = PDFSize { pdfSizeW :: Float, pdfSizeH :: Float }+data PDFSize = PDFSize { pdfSizeW :: !Float, pdfSizeH :: !Float } deriving (Eq,Show) +-- | Encryption and restricted permissions+data PDFEncryptionInfo+ -- | Not encrypted+ = PDFNoEncryption+ -- | Encrypted with possible permission restrictions+ | PDFEncryption {+ pdfCanPrint :: !(Maybe Bool) -- ^ Can the file be printed?+ , pdfCanCopy :: !(Maybe Bool) -- ^ Can the file be copied?+ , pdfCanChange :: !(Maybe Bool) -- ^ Can the file be changed?+ , pdfCanAddNotes :: !(Maybe Bool) -- ^ Can notes be added?+ , pdfEncryptionAlgorithm :: !(Maybe Text) -- ^ Encryption algorithm: e.g. unknown, RC4, AES, AES-256+ }+ deriving (Eq,Show)+ instance Error PDFInfoError where noMsg = NoMessage; strMsg = SomeError newtype ParsePDFInfo a = ParsePDFInfo { runParse :: Either PDFInfoError a } deriving (Monad,Functor,MonadError PDFInfoError)@@ -65,68 +89,92 @@ -- running the process. pdfInfo :: MonadIO m => FilePath -> m (Either PDFInfoError PDFInfo) pdfInfo path = liftIO $ loadInfo `E.catch` ioErrorHandler where- loadInfo = parse <$> readProcess "pdfinfo" [path] ""+ loadInfo = do (code,out,err) <- readProcessWithExitCode "pdfinfo" ["-enc","UTF-8",path] ""+ case code of+ ExitSuccess -> return (parse out)+ ExitFailure{} -> return (Left (ProcessFailure err)) ioErrorHandler = return . Left . ProcessError -- | Parse PDFInfo's output.-parse :: String -> Either PDFInfoError PDFInfo+parse :: Text -> Either PDFInfoError PDFInfo parse out = runParse $- PDFInfo <$> string "Title"- <*> string "Subject"- <*> string "Author"- <*> string "Creator"- <*> string "Producer"+ PDFInfo <$> string props "Title"+ <*> string props "Subject"+ <*> string props "Author"+ <*> string props "Creator"+ <*> string props "Producer" <*> date "CreationDate" <*> date "ModDate"- <*> bool "Tagged"+ <*> bool props "Tagged" <*> integer "Pages"- <*> bool "Encrypted"+ <*> encrypted "Encrypted" <*> size "Page size" <*> integer "File size"- <*> bool "Optimized"+ <*> bool props "Optimized" <*> floating "PDF version"-- where string = get id- date = get (>>= parseDate)- size = get (>>= parseSize)- bool = get $ fmap $ \yes -> yes == "yes"+ where date = get parseDate+ size = get parseSize+ encrypted = get parseEncrypted floating = readIt integer = readIt- readIt :: Read a => String -> ParsePDFInfo (Maybe a)- readIt = get (>>= readRight)+ readIt :: Read a => Text -> ParsePDFInfo (Maybe a)+ readIt = get readRight+ props = map split . T.lines $ out+ get = withProps props - properties = map split . lines $ out- get f name =- case lookup name properties of- Just ok -> catchError (Just <$> (f $ return $ trim ok))- (\_ -> return Nothing)- Nothing -> return Nothing+type Props = [(Text,Text)] - split = second (drop 2) . span (/=':')- trim = bi reverse (dropWhile isSpace) . dropWhile isSpace+-- | Look up a name in a finite map of "properties" and apply a (parsing)+-- function.+withProps :: Props -> (Text -> ParsePDFInfo a) -> Text -> ParsePDFInfo (Maybe a)+withProps properties f name =+ case lookup name properties of+ Just ok -> catchError (Just <$> (f $ T.strip ok))+ (\_ -> return Nothing)+ Nothing -> return Nothing +split :: Text -> (Text, Text)+split = second (T.drop 1) . T.span (/=':')++string :: Props -> Text -> ParsePDFInfo (Maybe Text)+string props = withProps props return++bool :: Props -> Text -> ParsePDFInfo (Maybe Bool)+bool props = withProps props $ \yes -> return $ yes == "yes"+ -- | Parse a page size. This is loosely defined.-parseSize :: String -> ParsePDFInfo PDFSize+parseSize :: Text -> ParsePDFInfo PDFSize parseSize s =- case words s of+ case T.words s of ((readRight -> Right x):"x":(readRight -> Right y):_) -> return $ PDFSize x y _ -> throwError $ ParseError $ "Unable to read size: " ++ show s -- | Parse a date according to pdfinfo's format.-parseDate :: String -> ParsePDFInfo UTCTime+parseDate :: Text -> ParsePDFInfo UTCTime parseDate s =- case parseTime defaultTimeLocale "%a %b %e %H:%M:%S %Y" s of+ case parseTime defaultTimeLocale "%a %b %e %H:%M:%S %Y" (T.unpack s) of Just ok -> return ok Nothing -> throwError $ ParseError $ "Unable to parse date: " ++ show s +-- | Parse encryption information according to pdfinfo's format.+parseEncrypted :: Text -> ParsePDFInfo PDFEncryptionInfo+parseEncrypted s =+ case T.break isSpace s of+ ("yes",rest) ->+ PDFEncryption <$> bool props "print"+ <*> bool props "copy"+ <*> bool props "change"+ <*> bool props "addNotes"+ <*> string props "algorithm"+ where+ props = map split $ T.words $ T.filter (flip notElem ['(',')']) rest+ ("no",_) -> return PDFNoEncryption+ _ -> throwError $ ParseError $ "Unable to parse encryption: " ++ show s+ -- | Read a value, maybe, allow misc trailing data.-readRight :: (MonadError PDFInfoError m,Read a) => String -> m a+readRight :: (MonadError PDFInfoError m,Read a) => Text -> m a readRight s =- case reads s of+ case reads (T.unpack s) of [(v,_)] -> return v _ -> throwError $ ParseError $ "Couldn't read value: " ++ show s---- | Untwist a value, apply a function, twist it back.-bi :: (c1 -> c) -> (c -> c1) -> c1 -> c-bi g f = g . f . g