hpdft-0.4.6.0: hpdft.hs
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE PackageImports #-}
{-# LANGUAGE OverloadedStrings #-}
module Main where
import PDF.Definition
import PDF.Object
import PDF.Document (Document(..), openDocument, docRootRef, docInfoDict)
import PDF.DocumentStructure
import PDF.PDFIO
import PDF.Outlines
import PDF.Encrypt (Security)
import PDF.Text (pdfToTextGeomBSWith, pdfToTextTaggedBSWith, pageTextGeomWith, pdfToTextStreamDoc)
import PDF.Layout (LayoutOptions(..), defaultLayoutOptions)
import PDF.Diff (TextChange(..), compareDocuments)
import PDF.Image (extractPageImagesToDir)
import PDF.FormExtract (pageFormNames, extractFormToFile)
import PDF.Error (PdfError(..), PdfResult, PdfWarning(..), renderPdfWarning)
import System.Environment (getArgs)
import System.Exit (exitWith, ExitCode(..))
import System.IO (hPutStr, hPutStrLn, stderr, stdout, hFlush, hIsTerminalDevice)
import System.IO.Error (isDoesNotExistError)
import Control.Exception (catch, IOException, ioError)
import Control.Monad (when)
import Data.ByteString.UTF8 (ByteString)
import qualified Data.ByteString.Char8 as BS
import qualified Data.ByteString.Lazy.Char8 as BSL
import Data.Text.Lazy as TL (unpack)
import Data.Text.Lazy.Encoding as TL
import qualified Data.Text as T
import Data.List (find, intercalate)
import Data.Maybe (listToMaybe)
import Options.Applicative
import Data.Semigroup ((<>))
import Text.Regex.Base.RegexLike ( makeRegex )
import Text.Regex.TDFA.String ( regexec )
import TuiPreview (runTuiPreview)
import PDF.Definition (Obj(PdfStream))
import Data.IORef (newIORef, readIORef, writeIORef)
import qualified Paths_hpdft as Autogen (version)
import Data.Version (showVersion)
deprecationMsg :: String
deprecationMsg =
"hpdft: flat flags are deprecated; use subcommands (e.g. hpdft text, hpdft info)"
subcommandNames :: [String]
subcommandNames =
[ "text", "image", "form"
, "diff", "info", "title", "toc", "trailer", "object", "refs", "grep"
]
main :: IO ()
main = do
args <- getArgs
when (listToMaybe args == Just "extract") $ do
hPutStrLn stderr "hpdft: 'extract' subcommand removed; use text, image, or form"
exitWith (ExitFailure 1)
let useSubcommands = case listToMaybe args of
Just a | a `elem` subcommandNames -> True
_ -> False
isMeta = any (`elem` args) ["--help", "-h", "--version", "-V"]
parserExtras = helper <**> simpleVersioner versionInfo
cmd <- if useSubcommands
then execParser (info (commandParser <**> parserExtras) (fullDesc <> header versionInfo))
else if isMeta
then execParser (info (legacyCmd <**> parserExtras) (fullDesc <> header versionInfo))
else do
when (legacyNeedsDeprecation args) $
hPutStrLn stderr deprecationMsg
execParser (info (legacyCmd <**> parserExtras) (fullDesc <> header versionInfo))
runCmd cmd
versionInfo :: String
versionInfo = "hpdft - a PDF to text converter, version " <> showVersion Autogen.version
-- | Top-level command dispatch.
data Cmd
= CmdText ExtractOpt
| CmdView FilePath (Maybe String)
| CmdImage ImagesOpt
| CmdForm FormOpt
| CmdDiff DiffOpt
| CmdInfo FilePath (Maybe String)
| CmdTitle FilePath (Maybe String)
| CmdToc FilePath (Maybe String)
| CmdTrailer FilePath
| CmdObject Int FilePath (Maybe String)
| CmdRefs FilePath (Maybe String)
| CmdGrep String FilePath (Maybe String)
data ExtractOpt = ExtractOpt
{ eoPage :: Int
, eoGeom :: Bool
, eoTagged :: Bool
, eoLegacy :: Bool
, eoQuiet :: Bool
, eoFootnotes :: Bool
, eoRuby :: Bool
, eoPassword :: String
, eoFile :: FilePath
}
data ImagesOpt = ImagesOpt
{ ioPage :: Int
, ioOut :: FilePath
, ioPassword :: String
, ioFile :: FilePath
}
data FormOpt = FormOpt
{ foPage :: Int
, foName :: Maybe String
, foOut :: FilePath
, foPassword :: String
, foFile :: FilePath
}
data DiffOpt = DiffOpt
{ doGeom :: Bool
, doLegacy :: Bool
, doRuby :: Bool
, doJson :: Bool
, doPassword :: String
, doFileA :: FilePath
, doFileB :: FilePath
}
commandParser :: Parser Cmd
commandParser = subparser
( command "text" (info (CmdText <$> extractOpts) (progDesc "Extract text to stdout (tagged -> geometry)"))
<> command "image" (info (CmdImage <$> imagesOpts) (progDesc "Extract image XObjects from a page"))
<> command "form" (info (CmdForm <$> formOpts) (progDesc "List or extract top-level Form XObjects on a page"))
<> command "diff" (info diffCommand (progDesc "Compare two PDFs (paragraph-level diff)"))
<> command "info" (info infoCommand (progDesc "Show PDF metadata"))
<> command "title" (info titleCommand (progDesc "Show document title"))
<> command "toc" (info tocCommand (progDesc "Show table of contents"))
<> command "trailer" (info trailerCommand (progDesc "Show PDF trailer"))
<> command "object" (info objectCommand (progDesc "Show object by reference"))
<> command "refs" (info refsCommand (progDesc "Show page object references"))
<> command "grep" (info grepCommand (progDesc "Search text in PDF"))
)
imagesOpts :: Parser ImagesOpt
imagesOpts = ImagesOpt
<$> option auto
( long "page"
<> short 'p'
<> metavar "PAGE"
<> help "Page number (1-based, required)" )
<*> strOption
( long "output"
<> short 'o'
<> value "."
<> metavar "DIR"
<> help "Output directory (default: current directory)" )
<*> strOption
( long "password"
<> short 'P'
<> value ""
<> metavar "PASSWORD"
<> help "Password for encrypted PDF" )
<*> strArgument
( help "input pdf file"
<> metavar "FILE"
<> action "file" )
formOpts :: Parser FormOpt
formOpts = FormOpt
<$> option auto
( long "page"
<> short 'p'
<> metavar "PAGE"
<> help "Page number (1-based, required)" )
<*> optional
( strOption
( long "name"
<> short 'n'
<> metavar "NAME"
<> help "Top-level Form XObject name (e.g. Fm42); omit to list extractable names on stdout" ) )
<*> strOption
( long "output"
<> short 'o'
<> value "."
<> metavar "DIR"
<> help "Output directory (default: current directory)" )
<*> strOption
( long "password"
<> short 'P'
<> value ""
<> metavar "PASSWORD"
<> help "Password for encrypted PDF" )
<*> strArgument
( help "input pdf file"
<> metavar "FILE"
<> action "file" )
extractOpts :: Parser ExtractOpt
extractOpts = ExtractOpt
<$> option auto
( long "page"
<> short 'p'
<> value 0
<> metavar "PAGE"
<> help "Page number (1-based; 0 = all pages)" )
<*> switch
( long "geom"
<> help "Extract text using geometry-based layout" )
<*> switch
( long "tagged"
<> help "Extract text using tagged PDF structure" )
<*> switch
( long "legacy"
<> help "Extract text using the pre-0.3 stream-order extractor" )
<*> switch
( long "quiet"
<> help "Suppress page progress on stderr during --legacy streaming" )
<*> switch
( long "footnotes"
<> help "Inline footnote bodies at their anchors as <footnote> tags (geometry pipeline)" )
<*> switch
( long "ruby"
<> help "Embed ruby in Aozora bunko notation (geometry/tagged pipeline)" )
<*> strOption
( long "password"
<> short 'P'
<> value ""
<> metavar "PASSWORD"
<> help "Password for encrypted PDF" )
<*> strArgument
( help "input pdf file"
<> metavar "FILE"
<> action "file" )
diffCommand :: Parser Cmd
diffCommand = CmdDiff <$> diffOpts
diffOpts :: Parser DiffOpt
diffOpts = DiffOpt
<$> switch
( long "geom"
<> help "Use geometry-based layout (default for diff)" )
<*> switch
( long "legacy"
<> help "Ignored for diff (geometry pipeline always used)" )
<*> switch
( long "ruby"
<> help "Embed ruby in Aozora bunko notation during layout" )
<*> switch
( long "json"
<> help "Emit JSON instead of human-readable diff" )
<*> strOption
( long "password"
<> short 'P'
<> value ""
<> metavar "PASSWORD"
<> help "Password for encrypted PDF (applied to both files)" )
<*> strArgument
( help "first PDF file"
<> metavar "FILE_A"
<> action "file" )
<*> strArgument
( help "second PDF file"
<> metavar "FILE_B"
<> action "file" )
passwordOpt :: Parser String
passwordOpt = strOption
( long "password"
<> short 'P'
<> value ""
<> metavar "PASSWORD"
<> help "Password for encrypted PDF" )
fileArg :: Parser FilePath
fileArg = strArgument
( help "input pdf file"
<> metavar "FILE"
<> action "file" )
maybePassword :: String -> Maybe String
maybePassword pw = if null pw then Nothing else Just pw
infoCommand :: Parser Cmd
infoCommand = CmdInfo <$> fileArg <*> (maybePassword <$> passwordOpt)
titleCommand :: Parser Cmd
titleCommand = CmdTitle <$> fileArg <*> (maybePassword <$> passwordOpt)
tocCommand :: Parser Cmd
tocCommand = CmdToc <$> fileArg <*> (maybePassword <$> passwordOpt)
trailerCommand :: Parser Cmd
trailerCommand = CmdTrailer <$> fileArg
objectCommand :: Parser Cmd
objectCommand = CmdObject
<$> option auto
( long "ref"
<> short 'r'
<> metavar "REF"
<> help "Object reference number" )
<*> fileArg
<*> (maybePassword <$> passwordOpt)
refsCommand :: Parser Cmd
refsCommand = CmdRefs <$> fileArg <*> (maybePassword <$> passwordOpt)
grepCommand :: Parser Cmd
grepCommand = CmdGrep
<$> strOption
( long "grep"
<> short 'g'
<> metavar "REGEXP"
<> help "Regular expression to search for" )
<*> fileArg
<*> (maybePassword <$> passwordOpt)
-- Legacy flat-flag parser (deprecated).
legacyCmd :: Parser Cmd
legacyCmd = legacyToCmd <$> legacyOptions
legacyOptions :: Parser LegacyOpt
legacyOptions = LegacyOpt
<$> option auto
( long "page"
<> short 'p'
<> value 0
<> metavar "PAGE"
<> help "Page number (nomble)" )
<*> option auto
( long "ref"
<> short 'r'
<> value 0
<> metavar "REF"
<> help "Object reference" )
<*> strOption
( long "grep"
<> short 'g'
<> value ""
<> metavar "RegExp"
<> help "grep PDF" )
<*> switch
( long "refs"
<> short 'R'
<> help "Show object references in page order" )
<*> switch
( long "geom"
<> help "Extract text using geometry-based layout" )
<*> switch
( long "tagged"
<> help "Extract text using tagged PDF structure" )
<*> switch
( long "legacy"
<> help "Extract text using the pre-0.3 stream-order extractor" )
<*> switch
( long "footnotes"
<> help "Inline footnote bodies at their anchors as <footnote> tags (geometry pipeline)" )
<*> switch
( long "ruby"
<> help "Embed ruby in Aozora bunko notation (geometry/tagged pipeline)" )
<*> switch
( long "title"
<> short 'T'
<> help "Show title (from metadata)" )
<*> switch
( long "info"
<> short 'I'
<> help "Show PDF metainfo" )
<*> switch
( long "toc"
<> short 'O'
<> help "Show table of contents (from metadata) " )
<*> switch
( long "trailer"
<> help "Show the trailer of PDF" )
<*> strOption
( long "password"
<> short 'P'
<> value ""
<> metavar "PASSWORD"
<> help "Password for encrypted PDF" )
<*> strArgument
( help "input pdf file"
<> metavar "FILE"
<> action "file" )
data LegacyOpt = LegacyOpt
{ loPage :: Int
, loRef :: Int
, loGrep :: String
, loRefs :: Bool
, loGeom :: Bool
, loTagged :: Bool
, loLegacy :: Bool
, loFootnotes :: Bool
, loRuby :: Bool
, loTitle :: Bool
, loInfo :: Bool
, loToc :: Bool
, loTrailer :: Bool
, loPassword :: String
, loFile :: FilePath
}
legacyToCmd :: LegacyOpt -> Cmd
legacyToCmd LegacyOpt{loPage=pg, loRef=rf, loGrep=gr, loRefs=rs, loGeom=gm,
loTagged=tg, loLegacy=lg, loFootnotes=fnn, loRuby=rb,
loTitle=tt, loInfo=ii, loToc=oo, loTrailer=tr,
loPassword=pw, loFile=fn} =
let mpw = maybePassword pw
noMode = not gm && not tg && not lg
plainView = noMode && not fnn && not rb
textCmd = CmdText (ExtractOpt pg gm tg lg False fnn rb pw fn)
in case () of
_ | pg==0 && rf==0 && null gr && not rs && lg && not gm && not tg && not tt && not ii && not oo && not tr ->
textCmd
| pg==0 && rf==0 && null gr && not rs && gm && not tg && not lg && not tt && not ii && not oo && not tr ->
textCmd
| pg==0 && rf==0 && null gr && not rs && plainView && not tt && not ii && not oo && not tr ->
CmdView fn mpw
| pg==0 && rf==0 && null gr && not rs && (tg || noMode) && not gm && not lg && not tt && not ii && not oo && not tr ->
textCmd
| pg==0 && rf==0 && null gr && not rs && noMode && tt ->
CmdTitle fn mpw
| pg==0 && rf==0 && null gr && not rs && noMode && ii ->
CmdInfo fn mpw
| pg==0 && rf==0 && null gr && not rs && noMode && oo ->
CmdToc fn mpw
| pg==0 && rf==0 && null gr && not rs && noMode && tr ->
CmdTrailer fn
| pg==0 && rf==0 && null gr && rs && noMode ->
CmdRefs fn mpw
| rf==0 && null gr && pg/=0 ->
textCmd
| pg==0 && null gr && rf/=0 ->
CmdObject rf fn mpw
| pg==0 && rf==0 && not (null gr) ->
CmdGrep gr fn mpw
| otherwise ->
textCmd
legacyNeedsDeprecation :: [String] -> Bool
legacyNeedsDeprecation args =
any (`elem` args)
[ "-I","--info","-T","--title","-O","--toc","--trailer","-R","--refs","-g","--grep" ]
|| containsFlag args "-r"
containsFlag :: [String] -> String -> Bool
containsFlag [] _ = False
containsFlag (a:as) f = a == f || containsFlag as f
runCmd :: Cmd -> IO ()
runCmd cmd = case cmd of
CmdText opt -> runExtractText opt
CmdView fn mpw -> withFile fn $ runViewer fn mpw
CmdImage opt -> runExtractImages opt
CmdForm opt -> runExtractForm opt
CmdDiff opt -> runDiff opt
CmdInfo fn mpw -> withFile fn $ showInfo fn mpw
CmdTitle fn mpw -> withFile fn $ showTitle fn mpw
CmdToc fn mpw -> withFile fn $ showOutlines fn mpw
CmdTrailer fn -> withFile fn $ showTrailer fn
CmdObject rf fn mpw -> withFile fn $ showContent fn mpw rf
CmdRefs fn mpw -> withFile fn $ showRefs fn mpw
CmdGrep re fn mpw -> withFile fn $ grepPDF defaultLayoutOptions fn mpw re
runExtractText :: ExtractOpt -> IO ()
runExtractText ExtractOpt{eoPage=pg, eoGeom=gm, eoTagged=tg, eoLegacy=lg,
eoQuiet=quiet, eoFootnotes=fnn, eoRuby=rb,
eoPassword=pw, eoFile=fn} =
withFile fn $
let mpw = maybePassword pw
lopts = defaultLayoutOptions {optFootnotes = fnn, optRuby = rb}
in if pg /= 0
then showPage lopts fn mpw pg
else case () of
_ | lg && not gm && not tg -> do
doc <- runOrDie (openDocument fn mpw)
streamLegacyToStdout doc quiet
| gm && not tg && not lg -> pdfToTextGeom lopts fn mpw
| otherwise -> pdfToTextTagged lopts fn mpw
runExtractImages :: ImagesOpt -> IO ()
runExtractImages ImagesOpt{ioPage=pg, ioOut=out, ioPassword=pw, ioFile=fn} =
withFile fn $
if pg < 1
then do
hPutStrLn stderr "hpdft: image requires -p PAGE (1-based)"
exitWith (ExitFailure 1)
else do
doc <- runOrDie (openDocument fn (maybePassword pw))
paths <- runOrDie (extractPageImagesToDir doc pg out)
if null paths
then hPutStrLn stderr ("hpdft: no image XObjects on page " ++ show pg)
else mapM_ putStrLn paths
runExtractForm :: FormOpt -> IO ()
runExtractForm FormOpt{foPage=pg, foName=mn, foOut=out, foPassword=pw, foFile=fn} =
withFile fn $
if pg < 1
then do
hPutStrLn stderr "hpdft: form requires -p PAGE (1-based)"
exitWith (ExitFailure 1)
else do
doc <- runOrDie (openDocument fn (maybePassword pw))
case mn of
Nothing -> do
names <- runOrDie (return (pageFormNames doc pg))
if null names
then hPutStrLn stderr ("hpdft: no Form XObjects on page " ++ show pg)
else mapM_ (putStrLn . T.unpack) names
Just nm -> do
path <- runOrDie (extractFormToFile doc pg (T.pack nm) out)
putStrLn path
runDiff :: DiffOpt -> IO ()
runDiff DiffOpt{doRuby=rb, doJson=json, doPassword=pw, doFileA=fa, doFileB=fb} =
withFile fa $
withFile fb $
let mpw = maybePassword pw
lopts = defaultLayoutOptions {optRuby = rb}
in do
docA <- runOrDie (openDocument fa mpw)
docB <- runOrDie (openDocument fb mpw)
changes <- runOrDie (return (compareDocuments lopts docA docB))
if json
then putStrLn (renderDiffJson changes)
else mapM_ putStrLn (renderDiffHuman changes)
renderDiffHuman :: [TextChange] -> [String]
renderDiffHuman = map renderOne
where
renderOne (PageCountMismatch pa pb) =
"page count mismatch: " ++ show pa ++ " vs " ++ show pb
renderOne TextChange{changePageA = pa, changePageB = pb,
changeParaA = pxa, changeParaB = pxb,
changeOld = old, changeNew = new} =
unlines
( pageLine
: paraLine
: ("- old: " ++ T.unpack old) : ("+ new: " ++ T.unpack new) : []
)
where
pageLine =
case (pa, pb) of
(Just a, Just b) | a == b -> "page " ++ show a ++ ":"
(Just a, Just b) -> "page " ++ show a ++ " vs " ++ show b ++ ":"
(Just a, Nothing) -> "page " ++ show a ++ " (only in first file):"
(Nothing, Just b) -> "page " ++ show b ++ " (only in second file):"
_ -> "page ?:"
paraLine =
case (pxa, pxb) of
(Just a, Just b) | a == b -> "para " ++ show (a + 1) ++ ":"
(Just a, Just b) -> "para " ++ show (a + 1) ++ " vs " ++ show (b + 1) ++ ":"
(Just a, Nothing) -> "para " ++ show (a + 1) ++ ":"
(Nothing, Just b) -> "para " ++ show (b + 1) ++ ":"
_ -> "para ?:"
renderDiffJson :: [TextChange] -> String
renderDiffJson changes = "[" ++ intercalate "," (map encodeChange changes) ++ "]"
where
encodeChange (PageCountMismatch pa pb) =
"{\"type\":\"pageCountMismatch\",\"pagesA\":" ++ show pa
++ ",\"pagesB\":" ++ show pb ++ "}"
encodeChange TextChange{changePageA = pa, changePageB = pb,
changeParaA = pxa, changeParaB = pxb,
changeOld = old, changeNew = new} =
"{\"type\":\"textChange\""
++ maybeField "pageA" pa
++ maybeField "pageB" pb
++ maybeField "paraA" pxa
++ maybeField "paraB" pxb
++ ",\"old\":" ++ jsonString old
++ ",\"new\":" ++ jsonString new
++ "}"
maybeField _ Nothing = ""
maybeField k (Just v) = ",\"" ++ k ++ "\":" ++ show v
jsonString t =
"\"" ++ concatMap esc (T.unpack t) ++ "\""
esc '\\' = "\\\\"
esc '"' = "\\\""
esc '\n' = "\\n"
esc '\r' = "\\r"
esc '\t' = "\\t"
esc c = [c]
describeError :: PdfError -> String
describeError (ParseError msg _) = "parse error: " ++ msg
describeError (BrokenXref msg) = "broken cross-reference: " ++ msg
describeError (MissingObject n) = "missing object: " ++ show n ++ " 0 R"
describeError (MissingKey key ctx) = "missing key " ++ key ++ " in " ++ ctx
describeError (UnsupportedFeature msg) = "unsupported feature: " ++ msg
describeError (DecryptionError msg) =
"cannot decrypt: " ++ msg ++ ". Use -P to supply a password."
describeError (FontError n msg) = "font error in object " ++ show n ++ ": " ++ msg
printWarnings :: [PdfWarning] -> IO ()
printWarnings = mapM_ (hPutStrLn stderr . ("hpdft: warning: " ++) . renderPdfWarning)
runOrDie :: IO (PdfResult a) -> IO a
runOrDie action = do
result <- action
case result of
Right a -> return a
Left err -> do
hPutStrLn stderr ("hpdft: " ++ describeError err)
exitWith (ExitFailure 1)
withFile :: FilePath -> IO () -> IO ()
withFile fp action =
action `catch` \e -> do
if isDoesNotExistError (e :: IOException)
then do
hPutStrLn stderr ("hpdft: " ++ fp ++ ": does not exist")
exitWith (ExitFailure 1)
else ioError e
-- | Bare @hpdft FILE@: TUI preview on a TTY, legacy streaming on a pipe.
runViewer :: FilePath -> Maybe String -> IO ()
runViewer filename mpw = do
doc <- runOrDie (openDocument filename mpw)
stdoutTTY <- hIsTerminalDevice stdout
if stdoutTTY
then runTuiPreview filename doc
else streamLegacyToStdout doc False
streamLegacyToStdout :: Document -> Bool -> IO ()
streamLegacyToStdout doc quiet = do
stderrTTY <- hIsTerminalDevice stderr
let showProgress pg total =
when (not quiet && stderrTTY && total > 0) $
hPutStr stderr ("\rhpdft: page " ++ show pg ++ "/" ++ show total ++ "...")
clearProgress total =
when (not quiet && stderrTTY && total > 0) $
hPutStr stderr ("\r\ESC[K")
totalRef <- newIORef (0 :: Int)
ws <- pdfToTextStreamDoc doc $ \pg total txt -> do
writeIORef totalRef total
showProgress pg total
BSL.putStr txt
hFlush stdout
total <- readIORef totalRef
clearProgress total
-- Trailing newline for parity with the pre-0.4.6 batch output (putStrLn).
putStrLn ""
printWarnings ws
pdfToTextGeom :: LayoutOptions -> FilePath -> Maybe String -> IO ()
pdfToTextGeom lopts filename mpw = do
txt <- runOrDie (pdfToTextGeomBSWith lopts filename mpw)
BSL.putStrLn txt
pdfToTextTagged :: LayoutOptions -> FilePath -> Maybe String -> IO ()
pdfToTextTagged lopts filename mpw = do
txt <- runOrDie (pdfToTextTaggedBSWith lopts filename mpw)
BSL.putStrLn txt
data PageTree = Nop | Page Int | Pages [PageTree]
deriving Show
showRefs :: FilePath -> Maybe String -> IO ()
showRefs filename mpw = do
doc <- runOrDie (openDocument filename mpw)
root <- runOrDie (return (docRootRef doc))
print $ pageTreeToList $ pageorder root (docObjs doc)
refByPage :: FilePath -> Maybe String -> IO [Int]
refByPage filename mpw = do
doc <- runOrDie (openDocument filename mpw)
root <- runOrDie (return (docRootRef doc))
return $ pageTreeToList $ pageorder root (docObjs doc)
pageorder :: Int -> PDFObjIndex -> PageTree
pageorder parent objs =
case findObjsByRef parent objs of
Just os -> case findDictOfType "/Catalog" os of
Just dict -> case findPages dict of
Just pr -> pageorder pr objs
Nothing -> Nop
Nothing -> case findDictOfType "/Pages" os of
Just dict -> case findKids dict of
Just kidsrefs -> Pages $ map (\f -> f objs) (map pageorder kidsrefs)
Nothing -> Nop
Nothing -> case findDictOfType "/Page" os of
Just dict -> Page parent
Nothing -> Nop
Nothing -> Nop
pageTreeToList :: PageTree -> [Int]
pageTreeToList (Pages ps) = concatMap pageTreeToList ps
pageTreeToList (Page n) = [n]
pageTreeToList Nop = []
showPage :: LayoutOptions -> FilePath -> Maybe String -> Int -> IO ()
showPage lopts filename mpw page = do
doc <- runOrDie (openDocument filename mpw)
root <- runOrDie (return (docRootRef doc))
let pagetree = pageTreeToList $ pageorder root (docObjs doc)
if page >= 1 && length pagetree >= page
then do
txt <- runOrDie (return (pageTextGeomWith lopts doc (pagetree !! (page - 1))))
BSL.putStr txt
else putStrLn $ "hpdft: No Page "++(show page)
showContent :: FilePath -> Maybe String -> Int -> IO ()
showContent filename mpw ref = do
doc <- runOrDie (openDocument filename mpw)
let objs = docObjs doc
sec = docSecurity doc
obj <- runOrDie (getObjectByRef ref objs)
if hasStream obj
then case findDict obj of
Just d | hasSubtype d -> printStreamWithDict sec ref d obj
_ -> do
strm <- runOrDie (getStream sec ref False obj)
BSL.putStrLn strm
else do
objs' <- runOrDie (getObjectByRef ref objs)
putStrLn $ "[" ++ intercalate ", " (map ppObj objs') ++ "]"
where
hasStream obj = case find isStream obj of
Just _ -> True
Nothing -> False
isStream (PdfStream _) = True
isStream _ = False
hasSubtype d = case findObjFromDict d "/Subtype" of
Just _ -> True
Nothing -> False
printStreamWithDict :: Maybe Security -> Int -> Dict -> [Obj] -> IO ()
printStreamWithDict sec' ref' d obj = do
putStrLn $ ppObj (PdfDict d)
strm <- runOrDie (getStream sec' ref' True obj)
BSL.putStrLn strm
showTitle :: FilePath -> Maybe String -> IO ()
showTitle filename mpw = do
doc <- runOrDie (openDocument filename mpw)
d <- runOrDie (return (docInfoDict doc))
let title =
case findObjFromDict d "/Title" of
Just (PdfText s) -> T.unpack s
Just x -> ppObj x
Nothing -> "No title anyway"
putStrLn title
showInfo :: FilePath -> Maybe String -> IO ()
showInfo filename mpw = do
doc <- runOrDie (openDocument filename mpw)
d <- runOrDie (return (docInfoDict doc))
putStrLn $ ppObj (PdfDict d)
showOutlines :: FilePath -> Maybe String -> IO ()
showOutlines filename mpw = do
d <- runOrDie (getOutlines filename mpw)
putStrLn $ show d
showTrailer :: FilePath -> IO ()
showTrailer filename = do
doc <- runOrDie (openDocument filename Nothing)
putStrLn $ ppDictEntries (docTrailer doc)
grepPDF :: LayoutOptions -> FilePath -> Maybe String -> String -> IO ()
grepPDF lopts filename mpw re = do
doc <- runOrDie (openDocument filename mpw)
root <- runOrDie (return (docRootRef doc))
let objs = docObjs doc
mapM_
(\(ref, pagenm) -> grepByPage pagenm re (pageText doc ref))
$ zip (pageTreeToList $ pageorder root objs) [1..]
where
pageText doc ref =
case pageTextGeomWith lopts doc ref of
Right txt -> txt
Left _ -> ""
grepByPage :: Int -> String -> BSL.ByteString -> IO ()
grepByPage pagenm re txt = do
let matched = filter (not . null) $ map (grepByLine re) $ BSL.lines txt
when (not $ null matched) (showResult pagenm matched)
return ()
where
showResult p m = do
putStrLn $ "At page " <> show p <> "..."
mapM (putStrLn . (" | " <>)) m
return ()
grepByLine :: String -> PDFStream -> String
grepByLine re txt =
case regexec (makeRegex re) $ TL.unpack $ TL.decodeUtf8 txt of
Left _ -> ""
Right m -> case m of
Just (b, m, a, _) -> (b <> (highlight m) <> a)
Nothing -> ""
highlight m = "\ESC[31m" <> m <> "\ESC[0m"