packages feed

hpdft-0.4.6.0: src/PDF/Document.hs

{-# LANGUAGE OverloadedStrings #-}

{-|
Module      : PDF.Document
Description : Single-read PDF document handle
License     : MIT

Open a PDF once and reuse the parsed object index for text extraction,
page APIs, diff, images, and form export.

@example
import PDF.Document (Document, openDocument)
import PDF.Error (PdfResult)

load :: FilePath -> IO (PdfResult Document)
load path = openDocument path Nothing
-}
module PDF.Document
  ( Document(..)
  , openDocument
  , docRootRef
  , docInfoDict
  ) where

import PDF.Definition
import PDF.DocumentStructure
  ( buildIndex
  , buildIndexEager
  , findTrailer
  , findTrailer'
  , findObjFromDict
  , findDict
  , findDictByRef
  , findRefs
  , extractObjBody
  , findObjs
  , rawStream
  , fontInfoFromDict
  )
import PDF.Encrypt (Security, securityFromEncryptDict)
import PDF.Error (PdfError(..), PdfResult)
import PDF.Object (parsePDFObj)

import Data.List (find)
import Data.Maybe (fromMaybe)

import qualified Data.ByteString.Char8 as BS
import qualified Data.ByteString.Lazy as BSL
import qualified Data.Map as Map

-- | Parsed PDF: trailer dictionary, object index, and optional encryption state.
-- Use 'openDocument' to construct; pass the same value to extraction and page APIs.
data Document = Document
  { docTrailer  :: Dict
  , docObjs     :: PDFObjIndex
  , docSecurity :: Maybe Security
  , docStreamCache :: Map.Map Int (PdfResult BSL.ByteString)
  , docFontCache   :: Map.Map Int FontInfo
  }

-- | Read a PDF file from disk. @password@ is @Nothing@ for unencrypted files,
-- or @Just@ the user password for encrypted documents.
--
-- Returns 'PdfResult' so xref, object, and decryption failures are reported
-- without crashing the process.
--
-- @example
-- result <- openDocument "paper.pdf" Nothing
-- case result of
--   Left err  -> print err
--   Right doc -> ...
openDocument :: FilePath -> Maybe String -> IO (PdfResult Document)
openDocument f password = do
  bytes <- BS.readFile f
  return (openDocumentBytes bytes password)

openDocumentBytes :: BS.ByteString -> Maybe String -> PdfResult Document
openDocumentBytes bytes password =
  case findTrailer' bytes of
    Right (trailer, xref) -> do
      msec <- loadSecurity bytes trailer xref password
      let objs = buildIndex bytes msec xref
          streamCache = Map.mapWithKey (\r os -> rawStream msec r os) objs
          fontCache =
            Map.mapWithKey
              (\r _ -> fontInfoFromDict msec objs (fromMaybe Map.empty (findDictByRef r objs)))
              objs
      return (Document trailer objs msec streamCache fontCache)
    Left _ -> do
      trailer <- findTrailer bytes
      msec <- loadSecurityScan bytes trailer password
      objs <- buildIndexEager bytes msec
      let streamCache = Map.mapWithKey (\r os -> rawStream msec r os) objs
          fontCache =
            Map.mapWithKey
              (\r _ -> fontInfoFromDict msec objs (fromMaybe Map.empty (findDictByRef r objs)))
              objs
      return (Document trailer objs msec streamCache fontCache)

loadSecurity :: BS.ByteString -> Dict -> XREF -> Maybe String -> PdfResult (Maybe Security)
loadSecurity bytes trailer xref password =
  case findObjFromDict trailer "/Encrypt" of
    Nothing -> Right Nothing
    Just (ObjRef ref) ->
      case findEncryptDict bytes xref ref of
        Just d ->
          let pw = fromMaybe "" password
          in case securityFromEncryptDict d trailer (Just pw) of
               Just sec -> Right (Just sec)
               Nothing  -> Left (DecryptionError "invalid or missing password")
        Nothing -> Left (DecryptionError "invalid or missing password")
    _ -> Left (DecryptionError "invalid or missing password")

loadSecurityScan :: BS.ByteString -> Dict -> Maybe String -> PdfResult (Maybe Security)
loadSecurityScan bytes trailer password =
  case findObjFromDict trailer "/Encrypt" of
    Nothing -> Right Nothing
    Just (ObjRef ref) ->
      case findEncryptDictScan bytes ref of
        Just d ->
          let pw = fromMaybe "" password
          in case securityFromEncryptDict d trailer (Just pw) of
               Just sec -> Right (Just sec)
               Nothing  -> Left (DecryptionError "invalid or missing password")
        Nothing -> Left (DecryptionError "invalid or missing password")
    _ -> Left (DecryptionError "invalid or missing password")

findEncryptDict :: BS.ByteString -> XREF -> Int -> Maybe Dict
findEncryptDict bytes xref ref =
  case Map.lookup ref xref of
    Just (InFile off) ->
      let body = extractObjBody bytes off
      in findDict (snd (parsePDFObj Nothing (ref, body)))
    _ -> findEncryptDictScan bytes ref

findEncryptDictScan :: BS.ByteString -> Int -> Maybe Dict
findEncryptDictScan bytes ref =
  case find ((== ref) . fst) (findObjs bytes) of
    Just objbs -> findDict (snd (parsePDFObj Nothing objbs))
    Nothing -> Nothing

docRootRef :: Document -> PdfResult Int
docRootRef doc =
  case findRefs "/Root" (docTrailer doc) of
    Just r -> Right r
    Nothing -> Left (MissingKey "/Root" "trailer")

docInfoDict :: Document -> PdfResult Dict
docInfoDict doc =
  case findRefs "/Info" (docTrailer doc) of
    Nothing -> Left (MissingKey "/Info" "trailer")
    Just inforef ->
      case findDictByRef inforef (docObjs doc) of
        Just d -> Right d
        Nothing -> Left (MissingObject inforef)