packages feed

hpdft-0.4.6.0: src/PDF/Outlines.hs

{-# LANGUAGE OverloadedStrings #-}

{-|
Module      : PDF.Outlines
Description : Function to get /Outlines object
Copyright   : (c) Keiichiro Shikano, 2016
License     : MIT
Maintainer  : k16.shikano@gmail.com

Function to grub /Outlines in PDF trailer. It mainly provides texts for Table of Contents.
-}

module PDF.Outlines
       ( getOutlines
       ) where

import qualified Data.Map as M
import Control.Monad (msum)
import Data.Attoparsec.ByteString hiding (inClass, notInClass, satisfy)
import Data.Attoparsec.ByteString.Char8
import Data.Attoparsec.Combinator
import qualified Data.ByteString.Char8 as BS

import PDF.Definition hiding (toString)
import PDF.Document (Document(..), openDocument, docRootRef)
import PDF.DocumentStructure
import PDF.Error (PdfError(..), PdfResult)
import PDF.Object (parseRefsArray, parsePdfLetters)

import qualified Data.Text as T

data PDFOutlines = PDFOutlinesTree [PDFOutlines]
                 | PDFOutlinesEntry { dest :: Int
                                    , text :: String
                                    , subs :: PDFOutlines
                                    }
                 | PDFOutlinesNE

instance Show PDFOutlines where
  show = toString 0

toString :: Int -> PDFOutlines -> String
toString depth PDFOutlinesEntry {dest=d, text=t, subs=s} = (replicate depth ' ' ++ t) ++ toString (depth+1) s
toString depth (PDFOutlinesTree os) = concatMap (toString depth) os
toString depth PDFOutlinesNE = ""

getOutlines :: FilePath -> Maybe String -> IO (PdfResult PDFOutlines)
getOutlines filename password = do
  docResult <- openDocument filename password
  case docResult of
    Left err -> return (Left err)
    Right doc -> do
      edict <- outlineFromDoc doc
      case edict of
        Left err -> return (Left err)
        Right (dict, destsRoot) -> do
          let objs = docObjs doc
          case findFirst dict of
            Nothing -> return $ Left (MissingKey "/First" "outlines")
            Just firstref -> case findObjsByRef firstref objs of
              Just [PdfDict d] -> return $ gatherOutlines d objs destsRoot
              Just s -> return $ Left (ParseError ("Unknown outline object: " ++ show s) BS.empty)
              Nothing -> return $ Left (MissingObject firstref)

gatherChildren :: Dict -> PDFObjIndex -> Maybe Int -> PdfResult PDFOutlines
gatherChildren dict objs destsRoot = case findFirst dict of
  Just r -> case findObjsByRef r objs of
    Just [PdfDict d] -> gatherOutlines d objs destsRoot
    Just s -> Left (ParseError ("Unknown outline child: " ++ show s) BS.empty)
    Nothing -> Left (MissingObject r)
  Nothing -> Right PDFOutlinesNE

gatherOutlines :: Dict -> PDFObjIndex -> Maybe Int -> PdfResult PDFOutlines
gatherOutlines dict objs destsRoot = do
  c <- gatherChildren dict objs destsRoot
  let dest = destPage dict objs destsRoot
  title <- findTitle dict objs
  case findNext dict of
    Just r -> case findObjsByRef r objs of
      Just [PdfDict d] -> do
        next <- gatherOutlines d objs destsRoot
        return $ PDFOutlinesTree (PDFOutlinesEntry { dest = dest
                                                   , text = title ++ "\n"
                                                   , subs = c}
                                  : [next])
      Just s -> Left (ParseError ("Unknown outline sibling: " ++ show s) BS.empty)
      Nothing -> Left (MissingObject r)
    Nothing -> return $ PDFOutlinesEntry { dest = dest
                                         , text = title ++ "\n"
                                         , subs = c}

destPage :: Dict -> PDFObjIndex -> Maybe Int -> Int
destPage dict objs destsRoot =
  maybe 0 id $ listToMaybe $ findDest dict objs destsRoot

findDest :: Dict -> PDFObjIndex -> Maybe Int -> [Int]
findDest dict objs destsRoot =
  case findObjFromDict dict "/Dest" of
    Just o -> destFromObj o objs
    Nothing -> case findObjFromDict dict "/A" of
      Just (ObjRef r) -> actionDest r objs destsRoot
      Just (PdfDict d) -> destFromAction d objs destsRoot
      _ -> []

destFromObj :: Obj -> PDFObjIndex -> [Int]
destFromObj (PdfArray a) _ = parseRefsArray a
destFromObj (ObjRef r) objs =
  case findObjsByRef r objs of
    Just (o:_) -> destFromObj o objs
    _ -> []
destFromObj (PdfNumber n) _ | truncate n >= 0 = [truncate n]
destFromObj _ _ = []

actionDest :: Int -> PDFObjIndex -> Maybe Int -> [Int]
actionDest r objs destsRoot = case findObjsByRef r objs of
  Just (PdfDict d : _) -> destFromAction d objs destsRoot
  _ -> []

destFromAction :: Dict -> PDFObjIndex -> Maybe Int -> [Int]
destFromAction d objs destsRoot =
  case findObjFromDict d "/D" of
    Just o -> destFromGoTo o objs destsRoot
    Nothing -> []

destFromGoTo :: Obj -> PDFObjIndex -> Maybe Int -> [Int]
destFromGoTo o objs destsRoot =
  case objAsName o of
    Just name -> lookupNamedDest destsRoot name objs
    Nothing -> destFromObj o objs

lookupNamedDest :: Maybe Int -> T.Text -> PDFObjIndex -> [Int]
lookupNamedDest Nothing _ _ = []
lookupNamedDest (Just root) name objs =
  case lookupNameNode root name objs of
    Just o -> destFromNamedDest o objs
    Nothing -> []

lookupNameNode :: Int -> T.Text -> PDFObjIndex -> Maybe Obj
lookupNameNode ref name objs =
  resolveDict ref objs >>= \d ->
    case findObjFromDict d "/Names" of
      Just (PdfArray arr) -> lookupNamePair arr name
      _ -> case findObjFromDict d "/Kids" of
        Just (PdfArray kids) ->
          msum [ lookupNameNode r name objs
               | ObjRef r <- kids
               , nameInLimits name r objs
               ]
        _ -> Nothing

lookupNamePair :: [Obj] -> T.Text -> Maybe Obj
lookupNamePair (n : v : rest) name =
  case objAsName n of
    Just t | t == name -> Just v
    _ -> lookupNamePair rest name
lookupNamePair _ _ = Nothing

nameInLimits :: T.Text -> Int -> PDFObjIndex -> Bool
nameInLimits name ref objs =
  case resolveDict ref objs of
    Nothing -> True
    Just d -> case findObjFromDict d "/Limits" of
      Just (PdfArray [lo, hi]) ->
        case (objAsName lo, objAsName hi) of
          (Just a, Just b) -> a <= name && name <= b
          _ -> True
      _ -> True

destFromNamedDest :: Obj -> PDFObjIndex -> [Int]
destFromNamedDest o objs =
  case o of
    ObjRef r -> case findObjsByRef r objs of
      Just (destObj:_) ->
        case destObj of
          PdfDict d -> maybe (destFromObj destObj objs) (\dd -> destFromObj dd objs)
                         $ findObjFromDict d "/D"
          _ -> destFromObj destObj objs
      _ -> []
    PdfDict d -> maybe [] (`destFromObj` objs) $ findObjFromDict d "/D"
    _ -> destFromObj o objs

resolveDict :: Int -> PDFObjIndex -> Maybe Dict
resolveDict r objs = case findObjsByRef r objs of
  Just (PdfDict d : _) -> Just d
  _ -> Nothing

objAsName :: Obj -> Maybe T.Text
objAsName (PdfName n) = Just n
objAsName (PdfText t) = Just t
objAsName _ = Nothing

outlinesRef :: Dict -> PdfResult Int
outlinesRef dict = case M.lookup "/Outlines" dict of
  Just (ObjRef x) -> Right x
  Just s -> Left (ParseError ("Unknown /Outlines: " ++ show s) BS.empty)
  Nothing -> Left (MissingKey "/Outlines" "root")

outlineFromDoc :: Document -> IO (PdfResult (Dict, Maybe Int))
outlineFromDoc doc =
  case docRootRef doc of
    Left err -> return (Left err)
    Right rootref -> outlineFromRoot rootref (docObjs doc)

outlineFromRoot :: Int -> PDFObjIndex -> IO (PdfResult (Dict, Maybe Int))
outlineFromRoot rootref objs =
  case findObjsByRef rootref objs of
    Nothing -> return $ Left (MissingObject rootref)
    Just rootobj -> case findDict rootobj of
      Nothing -> return $ Left (ParseError "root is not a dictionary" BS.empty)
      Just dict -> case outlinesRef dict of
        Left err -> return (Left err)
        Right outlineref -> case findObjsByRef outlineref objs of
          Just [PdfDict d] -> return (Right (d, destsRootRef dict objs))
          Just s -> return $ Left (ParseError ("Unknown outlines object: " ++ show s) BS.empty)
          Nothing -> return $ Left (MissingObject outlineref)

destsRootRef :: Dict -> PDFObjIndex -> Maybe Int
destsRootRef dict objs =
  resolveNamesDict dict objs >>= \names ->
    case findObjFromDict names "/Dests" of
      Just (ObjRef r) -> Just r
      _ -> Nothing

resolveNamesDict :: Dict -> PDFObjIndex -> Maybe Dict
resolveNamesDict dict objs =
  case findObjFromDict dict "/Names" of
    Just (PdfDict names) -> Just names
    Just (ObjRef r) -> resolveDict r objs
    _ -> Nothing

findTitle :: Dict -> PDFObjIndex -> PdfResult String
findTitle dict objs =
  case findObjFromDict dict "/Title" of
    Just (PdfText s) -> case parseOnly parsePdfLetters (BS.pack (T.unpack s)) of
      Right t -> Right (T.unpack t)
      Left _  -> Right (T.unpack s)
    Just (ObjRef r) -> case findObjsByRef r objs of
      Just [PdfText s] -> Right (T.unpack s)
      Just s -> Left (ParseError ("Unknown title object: " ++ show s) BS.empty)
      Nothing -> Left (MissingObject r)
    Just x -> Right (show x)
    Nothing -> Left (MissingKey "/Title" "outline")

listToMaybe :: [a] -> Maybe a
listToMaybe (x:_) = Just x
listToMaybe []    = Nothing

findNext :: Dict -> Maybe Int
findNext dict =
  case findObjFromDict dict "/Next" of
    Just (ObjRef x) -> Just x
    _ -> Nothing

findFirst :: Dict -> Maybe Int
findFirst dict =
  case findObjFromDict dict "/First" of
    Just (ObjRef x) -> Just x
    _ -> Nothing