packages feed

hpdft-0.4.6.0: src/PDF/Structure.hs

{-# LANGUAGE OverloadedStrings #-}

module PDF.Structure
  ( StructElem(..)
  , StructKid(..)
  , RubySpan(..)
  , structTree
  , logicalOrder
  , collectRubySpans
  ) where

import PDF.Definition
import PDF.Document (Document(..), docRootRef)
import PDF.DocumentStructure
  ( findDict
  , findDictByRef
  , findDictOfType
  , findObjFromDict
  , findObjsByRef
  )
import PDF.Error (PdfError(..), PdfResult)

import qualified Data.Text as T
import Data.List (foldl')
import Data.Maybe (listToMaybe, maybeToList)
import qualified Data.Set as S

data StructElem = StructElem
  { seType :: T.Text
  , seKids :: [StructKid]
  } deriving (Eq, Show)

data StructKid = KidElem StructElem | KidMCID Int Int deriving (Eq, Show)

-- Parallel base/ruby MCID lists from a /Ruby element's /RB and /RT kids.
data RubySpan = RubySpan
  { rsPage   :: Int
  , rsBases  :: [Int]
  , rsRubies :: [Int]
  } deriving (Eq, Show)

maxStructDepth :: Int
maxStructDepth = 512

structTree :: Document -> PdfResult (Maybe StructElem)
structTree doc = do
  rootRef <- docRootRef doc
  let objs = docObjs doc
  case findObjsByRef rootRef objs of
    Just os -> case findDictOfType "/Catalog" os of
      Just catalog -> case findObjFromDict catalog "/StructTreeRoot" of
        Just (ObjRef r) -> parseStructRef r Nothing objs S.empty 0
        Just (PdfDict d) -> parseStructDict d Nothing objs S.empty 0
        _ -> Right Nothing
      Nothing -> Left (MissingKey "/Type" "catalog")
    Nothing -> Left (MissingObject rootRef)

parseStructRef :: Int -> Maybe Int -> PDFObjIndex -> S.Set Int -> Int
             -> PdfResult (Maybe StructElem)
parseStructRef ref pg objs visited depth
  | depth >= maxStructDepth = Right Nothing
  | S.member ref visited = Right Nothing
  | otherwise =
      case findObjsByRef ref objs of
        Just os -> case findDict os of
          Just d -> parseStructDict d pg objs (S.insert ref visited) depth
          Nothing -> Right Nothing
        Nothing -> Left (MissingObject ref)

parseStructDict :: Dict -> Maybe Int -> PDFObjIndex -> S.Set Int -> Int
                -> PdfResult (Maybe StructElem)
parseStructDict d pg objs visited depth = do
  let pg' = pageRefFromDict d pg objs
      stype = structTypeName d
  kids <- parseKids (findObjFromDict d "/K") pg' objs visited (depth + 1)
  if T.null stype && null kids
     then return Nothing
     else return (Just (StructElem stype kids))

structTypeName :: Dict -> T.Text
structTypeName d =
  case findObjFromDict d "/S" of
    Just (PdfName n) -> n
    _ -> case findObjFromDict d "/Type" of
      Just (PdfName n) -> n
      _ -> T.empty

logicalOrder :: StructElem -> [([T.Text], Int, Int)]
logicalOrder root = walk [] root
  where
    walk ancestors (StructElem stype kids) =
      let path = ancestors ++ [stype]
      in concatMap (kidWalk path) kids

    kidWalk path (KidMCID page mcid) = [(path, page, mcid)]
    kidWalk path (KidElem elem) = walk path elem

collectRubySpans :: StructElem -> [RubySpan]
collectRubySpans root = walk root
  where
    walk (StructElem stype kids) =
      let childSpans = concatMap kidWalk kids
          here = if stype == "/Ruby" then maybeToList (rubySpan kids) else []
      in here ++ childSpans

    kidWalk (KidElem e) = walk e
    kidWalk _ = []

rubySpan :: [StructKid] -> Maybe RubySpan
rubySpan kids =
  case (findKidElem "/RB" kids, findKidElem "/RT" kids) of
    (Just rb, Just rt) ->
      let bases = mcidsFromElem rb
          rubies = mcidsFromElem rt
      in case bases of
        (page, _) : _ ->
          if null bases || null rubies
             then Nothing
             else Just (RubySpan page (map snd bases) (map snd rubies))
        [] -> Nothing
    _ -> Nothing

findKidElem :: T.Text -> [StructKid] -> Maybe StructElem
findKidElem want = foldr go Nothing
  where
    go (KidElem e@(StructElem t _)) Nothing
      | t == want = Just e
    go _ acc = acc

mcidsFromElem :: StructElem -> [(Int, Int)]
mcidsFromElem (StructElem _ kids) = concatMap kidMcids kids
  where
    kidMcids (KidMCID page mcid) = [(page, mcid)]
    kidMcids (KidElem e) = mcidsFromElem e
    kidMcids _ = []

pageRefFromDict :: Dict -> Maybe Int -> PDFObjIndex -> Maybe Int
pageRefFromDict d pg _ =
  case findObjFromDict d "/Pg" of
    Just (ObjRef r) -> Just r
    _               -> pg

parseKids :: Maybe Obj -> Maybe Int -> PDFObjIndex -> S.Set Int -> Int
          -> PdfResult [StructKid]
parseKids Nothing _ _ _ _ = Right []
parseKids (Just (PdfNumber n)) pg _ _ _ =
  case pg of
    Just p -> Right [KidMCID p (truncate n)]
    Nothing -> Right []
parseKids (Just (PdfArray arr)) pg objs visited depth =
  foldl' (\acc o -> do
    ks <- acc
    more <- parseKid o pg objs visited depth
    return (ks ++ more)) (Right []) arr
parseKids (Just o) pg objs visited depth = parseKid o pg objs visited depth

parseKid :: Obj -> Maybe Int -> PDFObjIndex -> S.Set Int -> Int
         -> PdfResult [StructKid]
parseKid (PdfNumber n) pg _ _ _ =
  case pg of
    Just p -> Right [KidMCID p (truncate n)]
    Nothing -> Right []
parseKid (ObjRef r) pg objs visited depth = do
  case findObjsByRef r objs of
    Just os -> case findDict os of
      Just d -> parseKidDict d pg objs visited depth
      Nothing -> Right []
    Nothing -> Left (MissingObject r)
parseKid (PdfDict d) pg objs visited depth = parseKidDict d pg objs visited depth
parseKid _ _ _ _ _ = Right []

parseKidDict :: Dict -> Maybe Int -> PDFObjIndex -> S.Set Int -> Int
             -> PdfResult [StructKid]
parseKidDict d pg objs visited depth =
  case findObjFromDict d "/Type" of
    Just (PdfName "/MCR") ->
      let pg' = pageRefFromDict d pg objs
          mcid = case findObjFromDict d "/MCID" of
            Just (PdfNumber n) -> Just (truncate n)
            _                  -> Nothing
      in case (pg', mcid) of
        (Just p, Just m) -> Right [KidMCID p m]
        _                -> Right []
    Just (PdfName "/OBJR") -> Right []
    _ -> do
      elem' <- parseStructDict d pg objs visited depth
      case elem' of
        Just e  -> Right [KidElem e]
        Nothing -> Right []