hpdft-0.4.6.3: src/PDF/DocumentStructure.hs
{-# LANGUAGE OverloadedStrings #-}
{-|
Module : PDF.DocumentStructure
Description : Function to walk around Document Structure of a PDF file
Copyright : (c) Keiichiro Shikano, 2020
License : MIT
Maintainer : k16.shikano@gmail.com
-}
module PDF.DocumentStructure
( buildIndex
, buildIndexEager
, expandObjStm
, rootRef
, contentsStream
, rawStreamByRef
, findKids
, findPages
, findDict
, findDictByRef
, findDictOfType
, findObjFromDict
, findObjFromDictWithRef
, findRefs
, findObjsByRef
, findObjs
, findObjs'
, findTrailer
, findTrailer'
, indexPDFObjs
, extractObjBody
, rawStream
, fontInfo
, fontInfoFromDict
, parseCIDWidths
, simpleWidthAt
, findResourcesDict
, decodeStreamBytes
, streamFilterNames
) where
import Data.Char (chr, isDigit, ord)
import Data.List (find, foldl', isSuffixOf, nub)
import Data.Bits ((.&.), shiftR)
import qualified Data.ByteString.Char8 as BS
import qualified Data.ByteString.Lazy.Char8 as BSL
import qualified Data.ByteString.Lazy.UTF8 as BSLU
import qualified Data.ByteString.Builder as B
import qualified Data.Text as T
import Data.Maybe (fromMaybe, listToMaybe)
import Numeric (readDec)
import Data.Attoparsec.ByteString.Char8 hiding (take, isDigit)
import Data.Attoparsec.ByteString.Char8 as P (take, takeWhile, takeWhile1)
import Data.Attoparsec.Combinator
import Control.Applicative
import Control.Monad (replicateM, foldM)
import Codec.Compression.Zlib (decompress)
import qualified Data.Map as M
import Data.Map (Map)
import PDF.Definition
import PDF.Object
import PDF.Error (PdfError(..), PdfResult, PdfWarning(..))
import PDF.Encrypt (Security, decryptStream)
import PDF.ContentStream (parseStream, parseColorSpace)
import PDF.Cmap (parseCMap)
import qualified PDF.OpenType as OpenType
import qualified PDF.CFF as CFF
import qualified PDF.Type1 as Type1
spaces = skipSpace
oneOf = satisfy . inClass
noneOf = satisfy . notInClass
-- find objects
findObjs :: BS.ByteString -> [PDFBS]
findObjs = collectPDFObjs
findObjs' :: BS.ByteString -> PdfResult [PDFBS]
findObjs' contents = case findTrailer' contents of
Right (_, xref) ->
Right
[ (n, extractObjBody contents off)
| (n, InFile off) <- M.toAscList xref
]
Left xrefErr -> case findObjs contents of
[] -> Left xrefErr
objs -> Right objs
extractObjBody :: BS.ByteString -> Int -> BS.ByteString
extractObjBody contents offset =
case sliceObjectAt (BS.drop offset contents) of
Just body -> body
Nothing ->
case BS.breakSubstring "endobj" (BS.drop offset contents) of
(before, _) ->
let (_, body) = BS.breakSubstring " obj" before
in BS.dropWhile isPdfSpace body
isPdfSpace :: Char -> Bool
isPdfSpace w = w `elem` ['\0', '\t', '\n', '\f', '\r', ' ']
findObjsByRef :: Int -> PDFObjIndex -> Maybe [Obj]
findObjsByRef = M.lookup
indexPDFObjs :: [PDFObj] -> PDFObjIndex
indexPDFObjs = M.fromList
buildIndex :: BS.ByteString -> Maybe Security -> XREF -> PDFObjIndex
buildIndex bytes msec xref = objs
where
objs = M.mapWithKey resolve xref
containerCache =
M.fromList
[ (cnum, objStmBody cnum)
| cnum <- nub [c | InObjStm c _ <- M.elems xref]
]
resolve objNum (InFile off) =
let body = extractObjBody bytes off
in snd (parsePDFObj msec (objNum, body))
resolve objNum (InObjStm cnum idx) =
case M.lookup cnum containerCache of
Just (locations, body) ->
let off = case listToMaybe (drop idx locations) of
Just (_, o) -> Just o
Nothing -> lookup objNum locations
in maybe [PdfNull] (parseObjStmObject body) off
Nothing -> [PdfNull]
objStmBody cnum =
let containerObjs = M.findWithDefault [PdfNull] cnum objs
in case rawStream msec cnum containerObjs of
Right streamBytes ->
case parseObjStmHeader (objStmFirst containerObjs) (BSL.toStrict streamBytes) of
Right cache -> cache
Left _ -> ([], BS.empty)
Left _ -> ([], BS.empty)
buildIndexEager :: BS.ByteString -> Maybe Security -> PdfResult PDFObjIndex
buildIndexEager bytes msec = do
rawObjs <- case findObjs bytes of
[] -> Left (BrokenXref "no objects found without xref")
objs -> Right objs
let parsed = map (parsePDFObj msec) rawObjs
expanded <- expandObjStm msec parsed
return (indexPDFObjs expanded)
findObjFromDictWithRef :: Int -> T.Text -> PDFObjIndex -> Maybe Obj
findObjFromDictWithRef ref name objs = case findDictByRef ref objs of
Just d -> findObjFromDict d name
Nothing -> Nothing
findObjFromDict :: Dict -> T.Text -> Maybe Obj
findObjFromDict d name = M.lookup name d
findDictByRef :: Int -> PDFObjIndex -> Maybe Dict
findDictByRef ref objs = case findObjsByRef ref objs of
Just os -> findDict os
Nothing -> Nothing
findDictOfType :: T.Text -> [Obj] -> Maybe Dict
findDictOfType typename objs = case findDict objs of
Just d -> if isType d then Just d else Nothing
Nothing -> Nothing
where
isType dict = M.lookup "/Type" dict == Just (PdfName typename)
findDict :: [Obj] -> Maybe Dict
findDict objs = case find isDict objs of
Just (PdfDict d) -> Just d
otherwise -> Nothing
where
isDict :: Obj -> Bool
isDict (PdfDict d) = True
isDict _ = False
findPages :: Dict -> Maybe Int
findPages dict = case M.lookup "/Pages" dict of
Just (ObjRef x) -> Just x
_ -> Nothing
findKids :: Dict -> Maybe [Int]
findKids dict = case M.lookup "/Kids" dict of
Just (PdfArray arr) -> Just (parseRefsArray arr)
_ -> Nothing
contentsStream :: Dict -> PSR -> Maybe Security -> PDFObjIndex -> PdfResult (PDFStream, [PdfWarning])
contentsStream dict st sec objs = case M.lookup "/Contents" dict of
Just (PdfArray arr) -> getContentArray arr
Just (ObjRef r) ->
case findObjsByRef r objs of
Just [PdfArray arr] -> getContentArray arr
Just _ -> getContent r
Nothing -> Left (MissingKey "/Contents" (show r))
Nothing -> Left (MissingKey "/Contents" "page")
where
getContentArray arr = parseContentStream dict st sec objs $
BSL.concat [s | ref <- parseRefsArray arr
, Right s <- [rawStreamByRef sec objs ref]]
getContent r = do
s <- rawStreamByRef sec objs r
parseContentStream dict st sec objs s
parseContentStream :: Dict -> PSR -> Maybe Security -> PDFObjIndex -> BSL.ByteString -> PdfResult (PDFStream, [PdfWarning])
parseContentStream dict st sec objs s =
parseStream (formTextRunner sec objs) (st {fontmaps=fontdict, cmaps=cmap, psResDict=Just dict}) s
where fontdict = findFontEncoding dict sec objs
cmap = findCMap dict sec objs
maxLegacyFormDepth :: Int
maxLegacyFormDepth = 12
formTextRunner :: Maybe Security -> PDFObjIndex -> T.Text -> PSR -> T.Text
formTextRunner sec objs name st
| psFormDepth st >= maxLegacyFormDepth = T.empty
| otherwise =
case formStream sec objs (psResDict st) name of
Nothing -> T.empty
Just (formDict, stream) ->
let runner = formTextRunner sec objs
st' = st { fontmaps = M.union (findFontEncoding formDict sec objs) (fontmaps st)
, cmaps = M.union (findCMap formDict sec objs) (cmaps st)
, psResDict = Just formDict
, psFormDepth = psFormDepth st + 1
}
in case parseStream runner st' stream of
Right (txt, _) -> T.pack (BSLU.toString txt)
Left _ -> T.empty
formStream :: Maybe Security -> PDFObjIndex -> Maybe Dict -> T.Text -> Maybe (Dict, PDFStream)
formStream sec objs resDict name =
case resDict >>= xobjectDict objs >>= M.lookup name of
Just (ObjRef r) ->
case findDictByRef r objs of
Just d | M.lookup "/Subtype" d == Just (PdfName "/Form") ->
case rawStreamByRef sec objs r of
Right stream -> Just (d, stream)
Left _ -> Nothing
_ -> Nothing
_ -> Nothing
xobjectDict :: PDFObjIndex -> Dict -> Maybe (M.Map T.Text Obj)
xobjectDict objs d =
case findResourcesDict d objs of
Just rd -> case findObjFromDict rd "/XObject" of
Just (PdfDict xd) -> Just xd
Just (ObjRef xr) -> findDictByRef xr objs
_ -> Nothing
Nothing -> Nothing
rawStreamByRef :: Maybe Security -> PDFObjIndex -> Int -> PdfResult BSL.ByteString
rawStreamByRef sec pdfobjs x = case findObjsByRef x pdfobjs of
Just objs -> rawStream sec x objs
Nothing -> Left (ParseError "No object with stream to be shown" BS.empty)
rawStream :: Maybe Security -> Int -> [Obj] -> PdfResult BSL.ByteString
rawStream sec objNum objs = case find isStream objs of
Just (PdfStream strm) -> rawStream' sec objNum (fromMaybe M.empty $ findDict objs) strm
Nothing -> Left (ParseError "No object with stream to be shown"
(BS.take 80 $ BS.pack $ show objs))
where
isStream (PdfStream s) = True
isStream _ = False
rawStream' :: Maybe Security -> Int -> Dict -> BSL.ByteString -> PdfResult BSL.ByteString
rawStream' sec' objNum' d s = do
decoded <- decodeStreamBytes d $ BSL.fromStrict $
decryptStream sec' objNum' 0 $ BSL.toStrict s
return $ BSL.fromStrict decoded
supportedStreamFilters :: [String]
supportedStreamFilters =
[ "/FlateDecode", "/DCTDecode", "/ASCII85Decode" ]
decodeStreamBytes :: Dict -> BSL.ByteString -> PdfResult BS.ByteString
decodeStreamBytes d s = do
filters <- streamFilterNames d
filtered <- applyStreamFilters filters s
applyPredictor d filtered
streamFilterNames :: Dict -> PdfResult [T.Text]
streamFilterNames d = case M.lookup "/Filter" d of
Nothing -> Right []
Just (PdfName n) -> Right [n]
Just (PdfArray arr) ->
Right [ n | PdfName n <- arr ]
Just _ ->
Left (UnsupportedFeature "invalid /Filter entry (expected name or array of names)")
applyStreamFilters :: [T.Text] -> BSL.ByteString -> PdfResult BS.ByteString
applyStreamFilters [] s = Right (BSL.toStrict s)
applyStreamFilters (f : fs) s = do
step <- decodeOneStreamFilter f s
applyStreamFilters fs (BSL.fromStrict step)
decodeOneStreamFilter :: T.Text -> BSL.ByteString -> PdfResult BS.ByteString
decodeOneStreamFilter "/FlateDecode" s =
Right $ BSL.toStrict $ decompress s
decodeOneStreamFilter "/DCTDecode" s =
Right $ BSL.toStrict s
decodeOneStreamFilter "/ASCII85Decode" s =
decodeASCII85 (BSL.toStrict s)
decodeOneStreamFilter f _ =
Left $
UnsupportedFeature
( "unsupported stream filter "
++ T.unpack f
++ " (supported: "
++ unwords supportedStreamFilters
++ ")"
)
-- | PDF ASCII85Decode (base-85, 'z' = four zero bytes, '~>' EOD).
decodeASCII85 :: BS.ByteString -> PdfResult BS.ByteString
decodeASCII85 bs =
Right $ BS.pack $ go (Prelude.filter isAscii85Data (BS.unpack bs)) []
where
isAscii85Data c =
let o = ord c
in (o >= 33 && o <= 117) || c == 'z' || c == 'Z'
go [] acc = reverse acc
go ('z' : rest) acc = go rest ('\0' : '\0' : '\0' : '\0' : acc)
go ('Z' : rest) acc = go rest ('\0' : '\0' : '\0' : '\0' : acc)
go cs acc =
let grp = Prelude.take 5 cs
rest = drop 5 cs
pad = replicate (5 - Prelude.length grp) 'u'
vals = map (\c -> ord c - 33) (grp ++ pad)
n = foldl' (\a x -> a * 85 + x) 0 vals
bytes =
[ (n `shiftR` 24) .&. 0xff
, (n `shiftR` 16) .&. 0xff
, (n `shiftR` 8) .&. 0xff
, n .&. 0xff
]
out = map chr (Prelude.take (max 0 (Prelude.length grp - 1)) (reverse bytes))
in if null grp then reverse acc else go rest (out ++ acc)
applyPredictor :: Dict -> BS.ByteString -> PdfResult BS.ByteString
applyPredictor d bs = case findObjFromDict d "/DecodeParms" of
Just (PdfDict parms) ->
case findObjFromDict parms "/Predictor" of
Just (PdfNumber p) | truncate p >= 12 ->
case findObjFromDict parms "/Columns" of
Just (PdfNumber c) -> decodePNGPredictors bs (truncate c)
_ -> Right bs
_ -> Right bs
_ -> Right bs
decodePNGPredictors :: BS.ByteString -> Int -> PdfResult BS.ByteString
decodePNGPredictors bs columns = go BS.empty bs
where
go _ rest | BS.null rest = Right BS.empty
go prev rest = do
let filt = BS.head rest
enc = BS.take columns (BS.drop 1 rest)
rest' = BS.drop (1 + columns) rest
prevRow = if BS.null prev then BS.replicate columns (chr 0) else prev
row <- pngFilter filt enc prevRow
more <- go row rest'
return $ BS.append row more
pngFilter :: Char -> BS.ByteString -> BS.ByteString -> PdfResult BS.ByteString
pngFilter filt row prev
| ord filt == 0 = Right row
| ord filt == 1 = Right $ pngSub row
| ord filt == 2 = Right $ pngUp row prev
| otherwise =
Left (UnsupportedFeature ("unsupported PNG predictor " ++ show (ord filt)))
pngUp :: BS.ByteString -> BS.ByteString -> BS.ByteString
pngUp row prev = BS.pack $ zipWith addByte (BS.unpack row) (BS.unpack prev)
where addByte a b = chr ((ord a + ord b) `mod` 256)
pngSub :: BS.ByteString -> BS.ByteString
pngSub row =
BS.pack $ snd $ foldl' (\(prev, out) x ->
let n = (ord x + prev) `mod` 256 in (n, out ++ [chr n])) (0, []) (BS.unpack row)
contentsColorSpace :: Dict -> PSR -> Maybe Security -> PDFObjIndex -> PdfResult [T.Text]
contentsColorSpace dict st sec objs = case M.lookup "/Contents" dict of
Just (PdfArray arr) ->
Right $ concat [cs | ref <- parseRefsArray arr
, Right cs <- [parseColorSpaceEntry ref]]
Just (ObjRef x) ->
parseColorSpaceEntry x
Nothing -> Left (MissingKey "/Contents" "page")
where
xobjcs = findXObjectColorSpace dict objs
parseColorSpaceEntry ref = do
s <- rawStreamByRef sec objs ref
parseColorSpace (st {xcolorspaces=xobjcs}) s
-- find XObject
findXObjectColorSpace d os = xobjColorSpaceMap (findXObject d os) os
xobjColorSpaceMap dict objs =
[ xobjColorSpace r objs | (_, ObjRef r) <- M.toList dict ]
findXObject dict objs = case findResourcesDict dict objs of
Just d -> case findObjFromDict d "/XObject" of
Just (PdfDict d) -> d
otherwise -> M.empty
Nothing -> M.empty
xobjColorSpace :: Int -> PDFObjIndex -> T.Text
xobjColorSpace x objs = case findObjFromDictWithRef x "/ColorSpace" objs of
Just (PdfName cs) -> cs
otherwise -> T.empty
-- find root ref from Trailer Dictionary
readDec' :: BS.ByteString -> PdfResult Int
readDec' bs = case readDec $ BS.unpack bs of
[(n,_)] -> Right n
_ -> Left (ParseError "invalid decimal" (BS.take 80 bs))
isPdfEofLine :: BS.ByteString -> Bool
isPdfEofLine line =
case BS.dropWhile isSpaceChar line of
rest | "%%EOF" `BS.isPrefixOf` rest ->
BS.all isSpaceChar (BS.drop 5 rest)
_ -> False
isEol :: Char -> Bool
isEol c = c == '\n' || c == '\r'
splitLastLine :: BS.ByteString -> (BS.ByteString, BS.ByteString)
splitLastLine bs =
let trimmed = BS.dropWhileEnd isEol bs
rev = BS.reverse trimmed
(lineRev, restRev) = BS.break isEol rev
in ( BS.reverse restRev
, BS.reverse lineRev
)
getStartxrefOffset :: BS.ByteString -> PdfResult Int
getStartxrefOffset source =
let trimmed = BS.dropWhileEnd isSpaceChar source
(_, numLine) = splitLastLine trimmed
in readDec' $ BS.takeWhile (\c -> c >= '0' && c <= '9') (BS.dropWhile isSpaceChar numLine)
isSpaceChar :: Char -> Bool
isSpaceChar c = c `elem` (" \t\r\n" :: String)
mergeXRefStm :: BS.ByteString -> Dict -> XREF -> PdfResult XREF
mergeXRefStm all dict xref = case findObjFromDict dict "/XRefStm" of
Just (PdfNumber n) ->
case findTrailerDictXREFStream $ BS.drop (truncate n) all of
Right (_, stm) -> Right $ M.union xref stm
Left err -> Left err
_ -> Right xref
findTrailer :: BS.ByteString -> PdfResult Dict
findTrailer bs = case splitLastLine bs of
(source, eofLine)
| isPdfEofLine eofLine -> do
offset <- getStartxrefOffset source
(dict, _) <- findTrailerDictXREF $ BS.drop offset bs
return dict
| source == "" -> Left (BrokenXref "no %%EOF or startxref found")
| otherwise -> findTrailer source
findTrailer' :: BS.ByteString -> PdfResult (Dict, XREF)
findTrailer' bs = case splitLastLine bs of
(source, eofLine)
| isPdfEofLine eofLine -> do
offset <- getStartxrefOffset source
(dict, xref) <- findTrailerDictXREF $ BS.drop offset bs
xref' <- mergeXRefStm bs dict xref
case M.lookup "/Prev" dict of
Just (PdfNumber x) -> xrefs (truncate x) bs (dict, xref')
_ -> Right (dict, xref')
| source == "" -> Left (BrokenXref "no %%EOF or startxref found")
| otherwise -> findTrailer' source
where
xrefs n all (dict, sofar) = do
(dict', xref) <- findTrailerDictXREF $ BS.drop n all
xref' <- mergeXRefStm all dict' xref
case M.lookup "/Prev" dict' of
Just (PdfNumber x) ->
xrefs (truncate x) all (dict, M.union sofar xref')
_ -> Right (dict, M.union sofar xref')
findTrailerDictXREF :: BS.ByteString -> PdfResult (Dict, XREF)
findTrailerDictXREF xrefTrailer =
let trimmed = BS.dropWhile isPdfSpace xrefTrailer
in case BS.take 4 trimmed of
"xref" -> findTrailerDictXREFTable xrefTrailer
_ -> findTrailerDictXREFStream xrefTrailer
findTrailerDictXREFTable :: BS.ByteString -> PdfResult (Dict, XREF)
findTrailerDictXREFTable xrefTrailer = case BS.breakSubstring "trailer" xrefTrailer of
(xref, trailer) | BS.null trailer ->
Left (BrokenXref ("no trailer keyword: " ++ show (BS.take 80 xrefTrailer)))
(xref, trailer) -> do
dict <- case parseOnly (pdfdictionary <* spaces) (BS.drop 7 trailer) of
Left err -> Left (ParseError ("trailer dictionary: " ++ show err) (BS.take 80 trailer))
Right (PdfDict d) -> Right d
Right _ -> Left (ParseError "expected trailer dictionary" (BS.take 80 trailer))
xrefMap <- parseXref xref
return (dict, xrefMap)
findTrailerDictXREFStream :: BS.ByteString -> PdfResult (Dict, XREF)
findTrailerDictXREFStream blob = case parseOnly xrefStreamObject blob of
Left err ->
Left (BrokenXref ("xref stream: " ++ show err ++ ": " ++ show (BS.take 80 blob)))
Right (dict, s) -> do
xrefMap <- xrefStreamToMap dict s
return (dict, xrefMap)
parseXrefBlob :: BS.ByteString -> PdfResult XREF
parseXrefBlob bs =
let trimmed = BS.dropWhile isPdfSpace bs
in case BS.take 4 trimmed of
"xref" -> parseXref bs
_ -> parseXrefStream bs
parseXrefStream :: BS.ByteString -> PdfResult XREF
parseXrefStream bs = case parseOnly xrefStreamObject bs of
Left err ->
Left (BrokenXref ("xref stream: " ++ show err ++ ": " ++ show (BS.take 80 bs)))
Right (dict, s) -> xrefStreamToMap dict s
xrefStreamObject :: Parser (Dict, BSL.ByteString)
xrefStreamObject = do
spaces
_ <- many1 digit
spaces
_ <- many1 digit
string " obj"
spaces
dictObj <- pdfdictionary
d <- case dictObj of
PdfDict d' -> return d'
_ -> fail "expected dictionary in xref stream object"
spaces
s <- readStreamByLength d
spaces
optional (string "endobj")
return (d, s)
readStreamByLength :: Dict -> Parser BSL.ByteString
readStreamByLength dict = do
string "stream"
_ <- string "\r\n" <|> string "\n" <|> string "\r"
len <- case findObjFromDict dict "/Length" of
Just (PdfNumber n) -> return (truncate n)
_ -> fail "stream without /Length"
bs <- P.take len
optional (try $ string "\r\n" <|> string "\n" <|> string "\r")
return $ BSL.fromStrict bs
xrefStreamToMap :: Dict -> BSL.ByteString -> PdfResult XREF
xrefStreamToMap dict s = do
ws <- wFields dict
sections <- indexSections dict
raw <- decodeStreamBytes dict s
entries <- xrefStreamEntries ws sections raw
return $ M.fromList
[ (n, entry)
| (n, typ, f2, f3) <- entries
, Just entry <- [xrefEntry typ f2 f3]
]
where
xrefEntry 1 off _ = Just (InFile off)
xrefEntry 2 cnum idx = Just (InObjStm cnum idx)
xrefEntry 0 _ _ = Nothing
xrefEntry _ _ _ = Nothing
wFields :: Dict -> PdfResult (Int, Int, Int)
wFields d = case findObjFromDict d "/W" of
Just (PdfArray [PdfNumber a, PdfNumber b, PdfNumber c]) ->
Right (truncate a, truncate b, truncate c)
_ -> Left (MissingKey "/W" "xref stream")
indexSections :: Dict -> PdfResult [(Int, Int)]
indexSections d = case findObjFromDict d "/Index" of
Just (PdfArray arr) -> indexPairs arr
Nothing -> case findObjFromDict d "/Size" of
Just (PdfNumber s) -> Right [(0, truncate s)]
_ -> Left (MissingKey "/Size" "xref stream")
where
indexPairs (PdfNumber a : PdfNumber b : xs) =
((truncate a, truncate b) :) <$> indexPairs xs
indexPairs [] = Right []
indexPairs _ = Left (BrokenXref "malformed /Index in xref stream")
xrefStreamEntries :: (Int, Int, Int) -> [(Int, Int)] -> BS.ByteString -> PdfResult [(Int, Int, Int, Int)]
xrefStreamEntries widths sections raw =
fst <$> foldM (parseSection widths) ([], raw) sections
where
parseSection (w0, w1, w2) (acc, bs) (start, count) = do
(ents, rest) <- parseN (w0, w1, w2) start count bs
return (acc ++ ents, rest)
parseN (w0, w1, w2) start count bs =
foldM (\(es, b) objNum -> do
(typ, b1) <- readField w0 b
(f2, b2) <- readField w1 b1
(f3, b3) <- readField w2 b2
return ((objNum, typ, f2, f3) : es, b3))
([], bs) [start .. start + count - 1]
readField :: Int -> BS.ByteString -> PdfResult (Int, BS.ByteString)
readField 0 bs = Right (0, bs)
readField w bs
| BS.length bs < w =
Left (BrokenXref ("xref stream field truncated: need "
++ show w ++ " bytes, have " ++ show (BS.length bs)))
| otherwise = Right (bytesToInt (BS.take w bs), BS.drop w bs)
bytesToInt :: BS.ByteString -> Int
bytesToInt = BS.foldl' (\acc w -> acc * 256 + ord w) 0
parseXref :: BS.ByteString -> PdfResult XREF
parseXref xref = case parseOnly xrefParser xref of
Left err ->
Left (BrokenXref ("xref table: " ++ show err ++ ": " ++ show (BS.take 80 xref)))
Right xs -> do
entries <- mapM concatSubsections xs
return $ M.fromList $ map dropFN $ filter (\(_,_,inUse) -> inUse) $ concat entries
where
xrefParser = do
string "xref" >> spaces
es <- (:) <$> subsections <*> many (try subsections)
return es
subsections = do
begin <- takeWhile1 isDigit <* spaces
num <- takeWhile1 isDigit <* spaces
count <- parseDec num
es <- replicateM count entries
beginNum <- parseDec begin
return (beginNum, count, es)
entries = do
offset <- P.take 10 <* spaces
_gennum <- P.take 5 <* spaces
status <- P.take 1 <* spaces
_ <- optional (string "\r\n" <|> string "\n" <|> string "\r")
off <- parseDec offset
inUse <- forn status
return (off, inUse)
parseDec bs = case readDec $ BS.unpack bs of
[(n,_)] -> return n
_ -> fail ("invalid decimal: " ++ show (BS.take 80 bs))
forn "f" = return False
forn "n" = return True
forn s = fail $ "xref entry status neither f nor n: " ++ show s
concatSubsections (objn, 0, []) = Right []
concatSubsections (objn, 1, e:[]) = Right [(objn, fst e, snd e)]
concatSubsections (objn, 1, e:_) =
Left (BrokenXref ("xref subsection count mismatch: " ++ show e))
concatSubsections (objn, i, e:es) = do
rest <- concatSubsections (objn+1, i-1, es)
return ((objn, fst e, snd e) : rest)
concatSubsections (_, _, e) =
Left (BrokenXref ("xref subsection malformed: " ++ show e))
dropFN (n, offset, fn) = (n, InFile offset)
rootRef :: BS.ByteString -> PdfResult (Maybe Int)
rootRef bs = case findTrailer bs of
Right dict -> Right $ findRefs "/Root" dict
Left _ -> rootRefFromCRStream bs
rootRefFromCRStream :: BS.ByteString -> PdfResult (Maybe Int)
rootRefFromCRStream bs = do
let ls = BS.lines bs
case reverse ls of
(_: numLine:_) -> do
offset <- readDec' (BS.takeWhile (\c -> c >= '0' && c <= '9') numLine)
objs <- findObjs' (BS.drop offset bs)
case objs of
((_, crstrm):_) -> do
(crdict, _) <- findTrailerDictXREF crstrm
return $ findRefs "/Root" crdict
[] -> Right Nothing
_ -> Left (BrokenXref "cannot locate cross-reference stream offset")
findRefs :: T.Text -> Dict -> Maybe Int
findRefs key dict = case M.lookup key dict of
Just (ObjRef x) -> Just x
_ -> Nothing
-- find Info
infoRef :: BS.ByteString -> PdfResult Int
infoRef bs = case findTrailer bs of
Right dict -> case findRefs "/Info" dict of
Just r -> Right r
Nothing -> Left (ParseError "No ref for info" BS.empty)
Left err -> Left err
-- expand PDF 1.5 Object Stream
expandObjStm :: Maybe Security -> [PDFObj] -> PdfResult [PDFObj]
expandObjStm sec os = concat <$> traverse (objStm sec) os
objStm :: Maybe Security -> PDFObj -> PdfResult [PDFObj]
objStm sec (n, obj) = case findDictOfType "/ObjStm" obj of
Nothing -> Right [(n,obj)]
Just d -> do
streamBytes <- rawStream sec n obj
pdfObjStm n (objStmFirstFromDict d) (BSL.toStrict streamBytes)
objStmFirstFromDict :: Dict -> Maybe Int
objStmFirstFromDict d = case findObjFromDict d "/First" of
Just (PdfNumber n) -> Just (truncate n)
_ -> Nothing
objStmFirst :: [Obj] -> Maybe Int
objStmFirst objs = findDict objs >>= objStmFirstFromDict
refPair :: Parser (Int, Int)
refPair = do
rStr <- many1 digit <* spaces
oStr <- many1 digit <* spaces
case (readDec rStr, readDec oStr) of
([(r, "")], [(o, "")]) -> return (r, o)
_ -> fail "invalid object stream reference"
refPairs :: Parser [(Int, Int)]
refPairs = spaces *> many1 refPair
refOffset :: Parser ([(Int, Int)], BS.ByteString)
refOffset = do
location <- refPairs
rest <- BS.pack <$> many1 anyChar
return (location, rest)
pdfObjStm :: Int -> Maybe Int -> BS.ByteString -> PdfResult [PDFObj]
pdfObjStm n mFirst s =
case parseObjStmHeader mFirst s of
Right (location, body) ->
Right [ (r, parseObjStmObject body o) | (r, o) <- location ]
Left err ->
Left (ParseError ("Failed to parse Object Stream: " ++ show err) (BS.take 80 s))
parseObjStmHeader :: Maybe Int -> BS.ByteString -> Either String ([(Int, Int)], BS.ByteString)
parseObjStmHeader (Just first) s
| first >= 0 && first <= BS.length s =
case parseOnly refPairs (BS.take first s) of
Right location -> Right (location, BS.drop first s)
Left err -> Left (show err)
parseObjStmHeader _ s =
case parseOnly refOffset s of
Right (location, body) -> Right (location, body)
Left err -> Left (show err)
parseObjStmObject :: BS.ByteString -> Int -> [Obj]
parseObjStmObject body off =
case parseObjStmValue (BS.drop off body) of
Right obj -> obj
Left _ -> [PdfNull]
parseObjStmValue :: BS.ByteString -> Either String [Obj]
parseObjStmValue s' = case parseOnly pdfdictionary s' of
Right obj -> Right [obj]
Left _ -> case parseOnly pdfarray s' of
Right obj -> Right [obj]
Left _ -> case parseOnly pdfletters s' of
Right obj -> Right [obj]
Left err -> Left (show err)
-- make fontmap from page's /Resources (see 3.7.2 of PDF Ref.)
findFontEncoding d sec os = findEncoding (fontObjs d os) sec os
findEncoding :: Dict -> Maybe Security -> PDFObjIndex -> Map T.Text Encoding
findEncoding dict sec objs = M.fromListWith (flip const) $
[ (n, encoding sec r objs) | (n, ObjRef r) <- M.toList dict ]
fontObjs :: Dict -> PDFObjIndex -> Dict
fontObjs dict objs = case findResourcesDict dict objs of
Just d -> case findObjFromDict d "/Font" of
Just (PdfDict d') -> d'
Just (ObjRef x) -> case findDictByRef x objs of
Just d' -> d'
_ -> M.empty
_ -> M.empty
Nothing -> M.empty
findResourcesDict :: Dict -> PDFObjIndex -> Maybe Dict
findResourcesDict dict objs = case M.lookup "/Resources" dict of
Just (ObjRef x) -> findDictByRef x objs
Just (PdfDict d) -> Just d
_ -> Nothing
encoding :: Maybe Security -> Int -> PDFObjIndex -> Encoding
encoding sec x objs =
case findDictByRef x objs of
Just d -> encodingFromDict sec objs d
Nothing -> NullMap
encodingFromDict :: Maybe Security -> PDFObjIndex -> Dict -> Encoding
encodingFromDict sec objs d = case subtype of
Just (PdfName "/Type0") -> case encField of
Just (PdfName "/Identity-H") -> case cidSysInfo descendantFonts of
(e:_) -> e
[] -> NullMap
Just (PdfName n) | n `elem` sjisEncodings -> SJISmap
Just (PdfName n) | n `elem` unijisEncodings -> UnicodeMap
Just (PdfName "/H") -> JISmap
Just (PdfName "/V") -> JISmap
Just (PdfName _) -> NullMap
_ -> NullMap
Just (PdfName "/Type1") -> case encField of
Just (ObjRef r) -> case findObjFromDictWithRef r "/Differences" objs of
Just (PdfArray arr) -> charDiff arr
_ -> NullMap
Just (PdfDict ed) -> case findObjFromDict ed "/Differences" of
Just (PdfArray arr) -> charDiff arr
_ -> NullMap
Just (PdfName "/MacRomanEncoding") -> NullMap
Just (PdfName "/MacExpertEncoding") -> NullMap
Just (PdfName "/WinAnsiEncoding") -> NullMap
Just (PdfName "/ZapfDingbats") -> WithCharSet "ZapfDingbats"
Just (PdfName "/Symbol") -> WithCharSet "Symbol"
_ -> case findObjFromDict (fontDescriptorFromDict d objs) "/FontFile3" of
Just (ObjRef fontfile) ->
case rawStreamByRef sec objs fontfile of
Right bs -> CFF.encoding $ BSL.toStrict bs
Left _ -> NullMap
_ -> case findObjFromDict (fontDescriptorFromDict d objs) "/FontFile" of
Just (ObjRef fontfile) ->
case rawStreamByRef sec objs fontfile of
Right bs -> Type1.encoding $ BSL.toStrict bs
Left _ -> NullMap
_ -> NullMap
Just (PdfName "/Type2") -> NullMap
Just (PdfName "/Type3") -> NullMap
_ -> NullMap
where
subtype = M.lookup "/Subtype" d
encField = M.lookup "/Encoding" d
sjisEncodings :: [T.Text]
sjisEncodings =
[ "/90ms-RKSJ-H", "/90ms-RKSJ-V"
, "/90msp-RKSJ-H", "/90msp-RKSJ-V"
, "/RKSJ-H", "/RKSJ-V"
]
unijisEncodings :: [T.Text]
unijisEncodings =
[ "/UniJIS-UCS2-H", "/UniJIS-UCS2-V"
, "/UniJIS-UCS2-HW-H", "/UniJIS-UCS2-HW-V"
, "/UniJIS-UTF16-H", "/UniJIS-UTF16-V"
, "/UniJIS2004-UTF16-H", "/UniJIS2004-UTF16-V"
]
descendantFonts :: [Obj]
descendantFonts = descendantFontObjsFromDict d objs
cidSysInfo :: [Obj] -> [Encoding]
cidSysInfo [] = []
cidSysInfo ((ObjRef r):rs) = cidSysInfo' r : cidSysInfo rs
cidSysInfo' dfr = case findObjFromDictWithRef dfr "/CIDSystemInfo" objs of
Just (PdfDict dict) -> getCIDSystemInfo dict
Just (ObjRef r) -> case findDictByRef r objs of
Just dict -> getCIDSystemInfo dict
_ -> WithCharSet T.empty
_ -> WithCharSet T.empty
getCIDSystemInfo cidDict =
let registry = case findObjFromDict cidDict "/Registry" of
Just (PdfText r) -> r
_ -> T.empty
ordering = case findObjFromDict cidDict "/Ordering" of
Just (PdfText o) -> o
_ -> T.empty
cmap = registry `T.append` "-" `T.append` ordering
in CIDmap cmap
charDiff :: [Obj] -> Encoding
charDiff objs = Encoding $ M.fromListWith (flip const) $ charmap objs 0
where charmap (PdfNumber x : PdfName n : xs) i =
if i < truncate x then
(chr $ truncate x, n) : (charmap xs $ incr x)
else
(chr $ i, n) : (charmap xs $ i+1)
charmap (PdfName n : xs) i = (chr i, n) : (charmap xs $ i+1)
charmap (_:xs) i = charmap xs i
charmap [] _ = []
incr x = (truncate x) + 1
findCMap :: Dict -> Maybe Security -> PDFObjIndex -> Map T.Text CMap
findCMap d sec objs = M.fromListWith (flip const) $
[ (n, toUnicode sec r objs) | (n, ObjRef r) <- M.toList (fontObjs d objs) ]
toUnicode :: Maybe Security -> Int -> PDFObjIndex -> CMap
toUnicode sec x objs =
case findDictByRef x objs of
Just d -> toUnicodeFromDict sec objs d
Nothing -> M.empty
toUnicodeFromDict :: Maybe Security -> PDFObjIndex -> Dict -> CMap
toUnicodeFromDict sec objs d =
case findObjFromDict d "/ToUnicode" of
Just (ObjRef ref) ->
case rawStreamByRef sec objs ref of
Right s | BSL.null s -> noToUnicodeFromDict sec objs d
| otherwise -> parseCMap s
_ -> noToUnicodeFromDict sec objs d
_ -> noToUnicodeFromDict sec objs d
noToUnicode :: Maybe Security -> Int -> PDFObjIndex -> CMap
noToUnicode sec x objs =
case findDictByRef x objs of
Just d -> noToUnicodeFromDict sec objs d
Nothing -> M.empty
noToUnicodeFromDict :: Maybe Security -> PDFObjIndex -> Dict -> CMap
noToUnicodeFromDict sec objs d =
case firstDescendantFontDict d objs of
Nothing -> M.empty
Just cidDict ->
let fd = fontDescriptorFromDict cidDict objs
in case findObjFromDict fd "/FontFile2" of
Just (ObjRef fontfile) ->
case rawStreamByRef sec objs fontfile of
Right bs -> OpenType.cmap $ BSL.toStrict bs
Left _ -> M.empty
_ -> M.empty
fontInfo :: Maybe Security -> Int -> PDFObjIndex -> FontInfo
fontInfo sec x objs =
fontInfoFromDict sec objs (fromMaybe M.empty $ findDictByRef x objs)
fontInfoFromDict :: Maybe Security -> PDFObjIndex -> Dict -> FontInfo
fontInfoFromDict sec objs d =
case M.lookup "/Subtype" d of
Just (PdfName "/Type0") -> type0FontInfoDict sec objs d
_ -> simpleFontInfoDict sec objs d
simpleFontInfoDict :: Maybe Security -> PDFObjIndex -> Dict -> FontInfo
simpleFontInfoDict sec objs d =
let enc = encodingFromDict sec objs d
tuc = toUnicodeFromDict sec objs d
fd = fontDescriptorFromDict d objs
defaultW = case findObjFromDict fd "/MissingWidth" of
Just (PdfNumber w) -> w
_ -> 0
firstChar = case findObjFromDict d "/FirstChar" of
Just (PdfNumber n) -> truncate n
_ -> 0
widths = case findObjFromDict d "/Widths" of
Just wobj -> resolveObjArray wobj objs
_ -> []
widthFn code = simpleWidthAt firstChar widths defaultW code
in FontInfo
{ fiEncoding = enc
, fiToUnicode = tuc
, fiWidth = widthFn
, fiWidthV = const defaultVerticalW1
, fiWMode = 0
, fiBytesPerCode = 1
, fiDefaultWidth = defaultW
}
type0FontInfoDict :: Maybe Security -> PDFObjIndex -> Dict -> FontInfo
type0FontInfoDict sec objs d =
let enc = encodingFromDict sec objs d
tuc = toUnicodeFromDict sec objs d
cidDict = firstDescendantFontDict d objs
defaultW = case cidDict >>= \cd -> findObjFromDict cd "/DW" of
Just (PdfNumber w) -> w
_ -> 1000
widthMap = case cidDict >>= \cd -> findObjFromDict cd "/W" of
Just wobj -> parseCIDWidths (resolveObjArray wobj objs)
_ -> M.empty
(_, w1Default) = dw2Defaults cidDict
widthVMap = case cidDict >>= \cd -> findObjFromDict cd "/W2" of
Just wobj -> parseCIDVerticalWidths (resolveObjArray wobj objs)
_ -> M.empty
wmode = wmodeFromEncoding (findObjFromDict d "/Encoding")
widthFn cid = M.findWithDefault defaultW cid widthMap
widthVFn cid = M.findWithDefault w1Default cid widthVMap
bpc = case enc of
SJISmap -> 1
_ -> 2
in FontInfo
{ fiEncoding = enc
, fiToUnicode = tuc
, fiWidth = widthFn
, fiWidthV = widthVFn
, fiWMode = wmode
, fiBytesPerCode = bpc
, fiDefaultWidth = defaultW
}
defaultVerticalW1 :: Double
defaultVerticalW1 = -1000
fontDescriptorFor :: Int -> PDFObjIndex -> Dict
fontDescriptorFor fdr objs = case findDictByRef fdr objs of
Just d -> fontDescriptorFromDict d objs
Nothing -> M.empty
fontDescriptorFromDict :: Dict -> PDFObjIndex -> Dict
fontDescriptorFromDict d _objs = case findObjFromDict d "/FontDescriptor" of
Just (ObjRef r) -> fromMaybe M.empty $ findDictByRef r _objs
Just (PdfDict fd) -> fd
_ -> M.empty
resolveObjArray :: Obj -> PDFObjIndex -> [Obj]
resolveObjArray obj objs = case obj of
ObjRef r -> case findObjsByRef r objs of
Just [PdfArray arr] -> arr
Just os -> maybe [] id (findFirstArray os)
Nothing -> []
PdfArray arr -> arr
_ -> []
findFirstArray :: [Obj] -> Maybe [Obj]
findFirstArray os = case find isArrayObj os of
Just (PdfArray arr) -> Just arr
_ -> Nothing
where
isArrayObj (PdfArray _) = True
isArrayObj _ = False
firstDescendantFont :: Int -> PDFObjIndex -> Maybe Dict
firstDescendantFont fontRef objs =
case findDictByRef fontRef objs of
Just d -> firstDescendantFontDict d objs
Nothing -> Nothing
firstDescendantFontDict :: Dict -> PDFObjIndex -> Maybe Dict
firstDescendantFontDict d objs =
case descendantFontObjsFromDict d objs of
(df:_) -> cidFontDict df objs
_ -> Nothing
descendantFontObjs :: Int -> PDFObjIndex -> [Obj]
descendantFontObjs fontRef objs =
case findDictByRef fontRef objs of
Just d -> descendantFontObjsFromDict d objs
Nothing -> []
descendantFontObjsFromDict :: Dict -> PDFObjIndex -> [Obj]
descendantFontObjsFromDict d objs =
case findObjFromDict d "/DescendantFonts" of
Just (PdfArray dfrs) -> dfrs
Just (ObjRef r) -> case findObjsByRef r objs of
Just (PdfArray dfrs : _) -> dfrs
Just os | isDescendantFontObj os -> [ObjRef r]
Just os -> fromMaybe [] (findFirstArray os)
Nothing -> []
_ -> []
where
isDescendantFontObj os = case findDict os of
Just cd -> case M.lookup "/Subtype" cd of
Just (PdfName "/CIDFontType0") -> True
Just (PdfName "/CIDFontType2") -> True
_ -> False
Nothing -> False
cidFontDict :: Obj -> PDFObjIndex -> Maybe Dict
cidFontDict df objs = case df of
ObjRef r -> findDictByRef r objs
PdfDict d -> Just d
_ -> Nothing
dw2Defaults :: Maybe Dict -> (Double, Double)
dw2Defaults Nothing = (880, defaultVerticalW1)
dw2Defaults (Just d) = case findObjFromDict d "/DW2" of
Just (PdfArray [PdfNumber vy, PdfNumber w1]) -> (vy, w1)
_ -> (880, defaultVerticalW1)
wmodeFromEncoding :: Maybe Obj -> Int
wmodeFromEncoding (Just (PdfName "/V")) = 1
wmodeFromEncoding (Just (PdfName n)) | "-V" `T.isSuffixOf` n = 1
wmodeFromEncoding _ = 0
simpleWidthAt :: Int -> [Obj] -> Double -> Int -> Double
simpleWidthAt firstChar widths defaultWidth code =
let idx = code - firstChar
in if idx >= 0 && idx < length widths
then case widths !! idx of
PdfNumber w -> w
_ -> defaultWidth
else defaultWidth
parseCIDWidths :: [Obj] -> Map Int Double
parseCIDWidths = foldCIDMetrics widthFromObj M.empty
parseCIDVerticalWidths :: [Obj] -> Map Int Double
parseCIDVerticalWidths = foldCIDMetrics verticalW1FromObj M.empty
foldCIDMetrics :: (Obj -> Maybe Double) -> Map Int Double -> [Obj] -> Map Int Double
foldCIDMetrics metricFn = go
where
go m [] = m
go m (PdfNumber c : PdfArray ws : rest) =
let m' = foldl' insertAt m (zip [0..] ws)
insertAt acc (i, w) = case metricFn w of
Just n -> M.insert (truncate c + i) n acc
Nothing -> acc
in go m' rest
go m (PdfNumber cFirst : PdfNumber cLast : w : rest) =
case metricFn w of
Just n ->
let m' = foldl' (\acc cid -> M.insert cid n acc) m [truncate cFirst .. truncate cLast]
in go m' rest
Nothing -> go m rest
go m (_:rest) = go m rest
widthFromObj :: Obj -> Maybe Double
widthFromObj (PdfNumber w) = Just w
widthFromObj _ = Nothing
verticalW1FromObj :: Obj -> Maybe Double
verticalW1FromObj (PdfArray [PdfNumber _vx, PdfNumber vy]) = Just vy
verticalW1FromObj (PdfNumber w) = Just w
verticalW1FromObj _ = Nothing