packages feed

hpdft-0.4.6.0: src/PDF/Object.hs

{-# LANGUAGE OverloadedStrings #-}

{-|
Module      : PDF.Object
Description : Function to parse objects in a PDF file
Copyright   : (c) Keiichiro Shikano, 2016
License     : MIT
Maintainer  : k16.shikano@gmail.com

Functions to parsea and show objects in a PDF file. 
It provides a basic way to find information from a PDF file.
-}

module PDF.Object
  ( parsePdfLetters
  , parsePDFObj
  , parseRefsArray
  , pdfObj
  , objectBody
  , sliceObjectAt
  , collectPDFObjs
  , pdfletters
  , pdfarray
  , pdfdictionary
  , pdfstream
  , xref
  ) where

import Data.Char (chr)
import qualified Data.Map as M
import qualified Data.ByteString.Char8 as BS
import qualified Data.ByteString.Lazy.Char8 as BSL
import qualified Data.Text as T
import Data.Text.Encoding (decodeUtf16BEWith)
import Data.Text.Encoding.Error (strictDecode)
import Numeric (readOct, readHex, readDec)
import Data.ByteString.Builder (toLazyByteString, word16BE)

import Data.Attoparsec.ByteString.Char8 hiding (take, skipWhile, takeTill)
import qualified Data.Attoparsec.ByteString.Char8 as AP
import Data.Attoparsec.ByteString as W (skipWhile, word8, notWord8, takeTill, anyWord8)
import Data.Attoparsec.Combinator
import Control.Applicative
import Control.Monad (void, guard)

import Data.Word (Word8)

import PDF.Definition
import PDF.Encrypt (Security, decryptString, decryptStream)

octalCharSafe :: String -> Char
octalCharSafe ds = case readOct ds of
  [(n, "")] -> chr n
  _         -> '?'

hexCharSafe :: String -> Char
hexCharSafe s = case readHex s of
  [(n, "")] -> chr n
  _         -> '?'

spaces = many (comment <|> pdfspaces)
oneOf = satisfy . inClass
noneOf = satisfy . notInClass

-- parse pdf objects

pdfObj :: Parser PDFBS
pdfObj = do
  objn <- objectHeader
  (body, _) <- match (objectBody Nothing objn)
  spaces
  string "endobj"
  spaces
  many xref
  many startxref
  return (objn, body)

objectHeader :: Parser Int
objectHeader = do
  spaces
  objnStr <- many1 digit
  spaces
  _ <- many1 digit
  string " obj"
  spaces
  case readDec objnStr of
    [(n, "")] -> return n
    _         -> fail ("invalid object number: " ++ objnStr)

sliceOneObject :: BS.ByteString -> Maybe ((Int, BS.ByteString), BS.ByteString)
sliceOneObject bs = case AP.parse objectSlice bs of
  AP.Done rest (objn, body) -> Just ((objn, body), rest)
  _ -> Nothing
  where
    objectSlice = do
      objn <- objectHeader
      (body, _) <- match (objectBody Nothing objn)
      spaces
      string "endobj"
      return (objn, body)

sliceObjectAt :: BS.ByteString -> Maybe BS.ByteString
sliceObjectAt bs = fmap snd $ fmap fst $ sliceOneObject bs

collectPDFObjs :: BS.ByteString -> [PDFBS]
collectPDFObjs bs = loop (BS.dropWhile isPdfSpaceChar bs)
  where
    loop rest
      | BS.null rest = []
      | otherwise = case sliceOneObject rest of
          Just ((n, body), after) -> (n, body) : loop (BS.dropWhile isPdfSpaceChar after)
          Nothing -> []

isPdfSpaceChar :: Char -> Bool
isPdfSpaceChar c = c `elem` ['\0', '\t', '\n', '\f', '\r', ' ']

pdfspaces = do
  W.word8 0 <|> W.word8 9 <|> W.word8 10 <|> W.word8 12 <|> W.word8 13 <|> W.word8 32
  return ""

objectBody :: Maybe Security -> Int -> Parser [Obj]
objectBody sec objNum = pdfobjElem sec objNum

pdfobjElem :: Maybe Security -> Int -> Parser [Obj]
pdfobjElem sec objNum =
  try (dictAndStream sec objNum) <|>
  fmap (:[]) (pdfobjAtom sec objNum)

parsePDFObj :: Maybe Security -> PDFBS -> PDFObj
parsePDFObj sec (n,pdfobject) =
  case parseOnly (spaces >> objectBody sec n) pdfobject of
    Left  err -> (n,[PdfNull])
    Right obj -> (n, obj)

pdfobjAtom :: Maybe Security -> Int -> Parser Obj
pdfobjAtom Nothing _ = pdfobjAtomPlain
pdfobjAtom (Just sec) objNum = choice
  [ try rrefs
  , try pdfname
  , try pdfnumber
  , try (pdfhexSec (Just sec) objNum) <* spaces
  , try pdfbool <* spaces
  , try pdfnull <* spaces
  , try (pdfarraySec (Just sec) objNum) <* spaces
  , try (pdfdictionarySec (Just sec) objNum) <* spaces
  , pdflettersSec (Just sec) objNum <* spaces
  ]

pdfobjAtomPlain :: Parser Obj
pdfobjAtomPlain = choice
  [ try rrefs
  , try pdfname
  , try pdfnumber
  , try pdfhex <* spaces
  , try pdfbool <* spaces
  , try pdfnull <* spaces
  , try pdfarray <* spaces
  , try pdfdictionary <* spaces
  , pdfletters <* spaces
  ]

pdfobjSec :: Maybe Security -> Int -> Parser Obj
pdfobjSec sec objNum = do
  elems <- pdfobjElem sec objNum
  case elems of
    [o] -> return o
    _   -> fail "multi-element object in nested context"

pdfdictionarySec :: Maybe Security -> Int -> Parser Obj
pdfdictionarySec sec objNum = do
  pairs <- spaces >> string "<<" >> spaces *> manyTill (dictEntrySec sec objNum) (try $ spaces >> string ">>")
  return $ PdfDict (pairsToDict pairs)

dictEntrySec :: Maybe Security -> Int -> Parser (Obj, Obj)
dictEntrySec sec objNum = (,) <$> pdfname <*> pdfobjSec sec objNum

pdfarraySec :: Maybe Security -> Int -> Parser Obj
pdfarraySec sec objNum =
  PdfArray <$> (string "[" >> spaces *> manyTill (pdfobjSec sec objNum) (try $ spaces >> string "]"))

pdflettersSec :: Maybe Security -> Int -> Parser Obj
pdflettersSec sec objNum = PdfText <$> parsePdfLettersSec sec objNum

parsePdfLettersSec :: Maybe Security -> Int -> Parser T.Text
parsePdfLettersSec sec objNum = do
  encrypted <- pdfLiteralBytes
  let body = decryptString sec objNum 0 encrypted
  case parseOnly parsePdfLetters (BS.cons '(' (BS.snoc body ')')) of
    Right s -> return s
    Left _  -> return $ T.pack $ BS.unpack body

pdfLiteralBytes :: Parser BS.ByteString
pdfLiteralBytes = BS.pack <$> (char '(' *> manyTill pdfEscapedChar (char ')'))
  where
    pdfEscapedChar = choice
      [ '(' <$ try (string "\\(")
      , ')' <$ try (string "\\)")
      , '\\' <$ try (string "\\\\")
      , '\n' <$ try (string "\\n")
      , '\r' <$ try (string "\\r")
      , '\t' <$ try (string "\\t")
      , '\b' <$ try (string "\\b")
      , '\f' <$ try (string "\\f")
      , octChar <$> try (char '\\' *> count 3 (oneOf "01234567"))
      , octChar <$> try (char '\\' *> count 2 (oneOf "01234567"))
      , octChar . (:[]) <$> try (char '\\' *> oneOf "01234567")
      , noneOf "\\"
      , '\\' <$ string "\\"
      ]
    octChar = octalCharSafe

pdfhexSec :: Maybe Security -> Int -> Parser Obj
pdfhexSec sec objNum = hexSec sec objNum
  where hexSec sec objNum = do
          char '<'
          lets <- BS.pack <$> manyTill (oneOf "0123456789abcdefABCDEF") (try $ char '>')
          let decrypted = decryptString sec objNum 0 (decodeHexBytes lets)
          case parseOnly ((try $ string "feff" <|> string "FEFF") *> (many1 (oneOf "0123456789abcdefABCDEF"))) (BS.pack $ BS.unpack decrypted) of
            Right s -> return $ PdfHex (T.pack $ pdfhexletter (BS.pack s))
            Left _ ->
              case parseOnly parsePdfLetters (BS.cons '(' (BS.snoc decrypted ')')) of
                Right t -> return (PdfText t)
                Left _  -> return (PdfHex (T.pack $ BS.unpack decrypted))

decodeHexBytes :: BS.ByteString -> BS.ByteString
decodeHexBytes bs =
  let hex = filter (`elem` ("0123456789abcdefABCDEF" :: String)) (BS.unpack bs)
      pairs = [ take 2 (drop i hex) | i <- [0,2..length hex - 1], i + 1 < length hex ]
      hexByte s = hexCharSafe s
  in BS.pack $ map hexByte pairs

lookupDictInt :: Dict -> T.Text -> Maybe Int
lookupDictInt d key = case M.lookup key d of
  Just (PdfNumber x) | x >= 0 -> Just (truncate x)
  _ -> Nothing

pairsToDict :: [(Obj, Obj)] -> Dict
pairsToDict pairs =
  M.fromListWith (\_new old -> old) [(n, v) | (PdfName n, v) <- pairs]

skipStreamEOL :: Parser ()
skipStreamEOL =
  void $ optional $ try (string "\r\n") <|> try (string "\n") <|> try (string "\r")

streamEndMarker :: Parser ()
streamEndMarker = void $ do
  optional (try (string "\r\n") <|> try (string "\n") <|> try (string "\r"))
  string "endstream"

scanTillEndstream :: Parser BS.ByteString
scanTillEndstream = go BS.empty
  where
    go acc =
      (try $ do
         guard (BS.null acc || isPdfEol (BS.last acc))
         string "endstream"
         return acc) <|>
      (do w <- W.anyWord8; go (BS.snoc acc (chr (fromIntegral w))))
    isPdfEol c = c == '\r' || c == '\n'

readStreamBody :: Dict -> Parser BSL.ByteString
readStreamBody dict = do
  skipStreamEOL
  case lookupDictInt dict "/Length" of
    Just len ->
      (try $ do
        data_ <- AP.take len
        streamEndMarker
        return $ BSL.fromStrict data_) <|>
      (BSL.fromStrict <$> scanTillEndstream)
    Nothing ->
      BSL.fromStrict <$> scanTillEndstream

dictAndStream :: Maybe Security -> Int -> Parser [Obj]
dictAndStream sec objNum = do
  d@(PdfDict dict) <- case sec of
    Nothing -> pdfdictionary
    Just s  -> pdfdictionarySec (Just s) objNum
  spaces
  more <- optional (lookAhead (string "stream"))
  case more of
    Nothing -> return [d]
    Just _  -> do
      string "stream"
      stm <- readStreamBody dict
      spaces
      return [d, PdfStream stm]

comment = do
  W.word8 37 >> W.notWord8 37 >> W.takeTill (\w-> w == 13 || w == 10)
  return " "

xref :: Parser String
xref = do
  spaces
  string "xref"
  spaces
  ref <- manyTill anyChar (try $ string "%%EOF")
  spaces
  return ""

startxref :: Parser String
startxref = do
  spaces
  string "startxref"
  spaces
  ref <- manyTill anyChar (try $ string "%%EOF")
  spaces
  return ""
  
stream :: Parser PDFStream
stream = do
  string "stream"
  readStreamBody M.empty

pdfdictionary :: Parser Obj
pdfdictionary = do
  pairs <- spaces >> string "<<" >> spaces *> manyTill dictEntry (try $ spaces >> string ">>")
  return $ PdfDict (pairsToDict pairs)

dictEntry :: Parser (Obj, Obj)
dictEntry = (,) <$> pdfname <*> pdfobj

pdfarray :: Parser Obj
pdfarray = PdfArray <$> (string "[" >> spaces *> manyTill pdfobj (try $ spaces >> string "]"))

pdfname :: Parser Obj
pdfname = PdfName . T.pack . BS.unpack <$> (BS.append <$> string "/" <*> (BS.pack <$> manyTill anyChar (try $ lookAhead $ oneOf "><][)( \n\r/"))) <* spaces

pdfletters :: Parser Obj
pdfletters = PdfText <$> parsePdfLetters

parsePdfLetters :: Parser T.Text
parsePdfLetters = T.pack <$> (concat <$> (char '(' *>
                               manyTill (choice [ try pdfutf
                                                , try pdfoctutf
                                                , pdfletter])
                               (try $ char ')')))
  where pdfletter =
          choice [ "(" <$ try (string "\\(")
                 , ")" <$ try (string "\\)")
                 , "\\" <$ try (string "\\\\")
                 , "\n" <$ try (string "\\n")
                 , "\r" <$ try (string "\\r")
                 , "\t" <$ try (string "\\t")
                 , "\b" <$ try (string "\\b")
                 , "\f" <$ try (string "\\f")
                 , octal <$> try (char '\\' *> count 3 (oneOf "01234567"))
                 , octal <$> try (char '\\' *> count 2 (oneOf "01234567"))
                 , octal . (:[]) <$> try (char '\\' *> oneOf "01234567")
                 , return <$> try (noneOf "\\")
                 , "" <$ string "\\"
                 ]

        octal = return . octalCharSafe
        
        pdfutf :: Parser String
        pdfutf = do 
          str <- string "\254\255" *> manyTill pdfletter (lookAhead $ string ")")
          return $ utf16be $ concat str
        
        pdfoctutf :: Parser String
        pdfoctutf = do
          str <- string "\\376\\377" *> manyTill pdfletter (lookAhead $ string ")")
          return $ utf16be $ concat str
        
utf16be = T.unpack . decodeUtf16BEWith strictDecode . BS.pack  -- used only for String concat in pdfutf

pdfstream :: Parser Obj
pdfstream = PdfStream <$> stream

pdfnumber :: Parser Obj
pdfnumber = PdfNumber <$> pdfdigit
  where pdfdigit = do 
          sign <- many $ char '-'
          num <- ((++) <$> (("0"++) . BS.unpack <$> string ".") <*> (many1 digit))
                 <|>
                 ((++) <$> (many1 digit) <*> ((++) <$> (many $ char '.') <*> many digit))
          spaces
          return $ read $ sign ++ num

pdfhex :: Parser Obj
pdfhex = PdfHex <$> hex
  where hex = do
          char '<'
          lets <- BS.pack <$> manyTill (oneOf "0123456789abcdefABCDEF") (try $ char '>')
          case parseOnly ((try $ string "feff" <|> string "FEFF") *> (many1 (oneOf "0123456789abcdefABCDEF"))) lets of
            Right s -> return $ T.pack $ pdfhexletter $ BS.pack s
            Left _ -> return $ T.pack $ BS.unpack lets

pdfhexletter s = case parseOnly (concat <$> many1 pdfhexutf16be) s of
  Right t -> utf16be t
  Left e -> BS.unpack s

pdfhexutf16be :: Parser String
pdfhexutf16be = do
  c <- count 4 $ oneOf "0123456789ABCDEFabcdef"
  let b = case readHex c of
        [(n, "")] -> BSL.unpack . toLazyByteString . word16BE $ n
        _         -> "?"
  return $ b

pdfbool :: Parser Obj
pdfbool = PdfBool <$> (True <$ string "true"
                       <|> 
                       False <$ string "false")

pdfnull :: Parser Obj
pdfnull = PdfNull <$ string "null"

pdfobj :: Parser Obj
pdfobj = pdfobjAtomPlain

rrefs :: Parser Obj
rrefs = do  
  objnum <- many1 digit
  spaces
  oneOf "0123456789"
  spaces
  string "R"
  spaces
  case readDec objnum of
    [(n, "")] -> return $ ObjRef n
    _         -> fail ("invalid object reference: " ++ objnum)

objother :: Parser Obj
objother = ObjOther . T.pack . show <$> (W.takeTill (\w-> w == 10 || w == 13) <* (W.word8 10 <|> W.word8 13))

parseRefsArray :: [Obj] -> [Int]
parseRefsArray (ObjRef x:y) = (x:parseRefsArray y)
parseRefsArray (x:y)  = (parseRefsArray y)
parseRefsArray [] = []