hpdft 0.4.6.0 → 0.4.6.1
raw patch · 4 files changed
+113/−64 lines, 4 filesdep ~basedep ~bytestringdep ~containers
Dependency ranges changed: base, bytestring, containers, directory, filepath, text
Files
- CHANGELOG.md +7/−0
- hpdft.cabal +31/−31
- src/PDF/ContentStream.hs +39/−7
- src/PDF/DocumentStructure.hs +36/−26
CHANGELOG.md view
@@ -1,5 +1,12 @@ # Changelog +## 0.4.6.1 (2026-07-06)++### Fixed++- Object streams whose entries mix dictionaries and arrays (e.g. page-tree nodes stored in a single ObjStm) no longer leave intermediate `/Pages` nodes unresolved; page 1 and document order match the PDF catalog again.+- `hpdft.cabal`: `cabal-version` lowered to 3.8 and dependency bounds widened for GHC 9.8 (Hackage docbuilder compatibility).+ ## 0.4.6.0 (2026-07-05) ### Changed
hpdft.cabal view
@@ -1,6 +1,6 @@-cabal-version: 3.12+cabal-version: 3.8 name: hpdft-version: 0.4.6.0+version: 0.4.6.1 synopsis: PDF parsing library and CLI for text, layout, diff, images, and forms description: hpdft is a Haskell library and command-line tool for parsing PDF files.@@ -67,22 +67,22 @@ other-modules: Paths_hpdft other-extensions: OverloadedStrings build-depends: attoparsec >= 0.14.4 && < 0.15- , base >= 4.20.0 && < 4.23+ , base >= 4.18.0 && < 4.23 , binary >= 0.8.9 && < 0.9- , bytestring >= 0.11.5 && < 0.13- , containers >= 0.7 && < 0.9+ , bytestring >= 0.11.4 && < 0.13+ , containers >= 0.6.7 && < 0.9 , crypton >= 0.34 && < 0.36- , directory >= 1.3.9 && < 1.4+ , directory >= 1.3.8 && < 1.4 , dlist >= 1.0 && < 1.1 , file-embed >= 0.0.16 && < 0.1- , filepath >= 1.5.2 && < 1.6+ , filepath >= 1.4.2 && < 1.6 , memory >= 0.18.0 && < 0.19 , optparse-applicative >= 0.18.1 && < 0.19 , parallel >= 3.2.2 && < 3.4 , deepseq >= 1.4.8 && < 1.6 , parsec >= 3.1.18 && < 3.2 , semigroups >= 0.20 && < 0.21- , text >= 2.1.2 && < 2.2.2+ , text >= 2.0.2 && < 2.2.2 , utf8-string >= 1.0.2 && < 1.1 , zlib >= 0.7.1 && < 0.8 autogen-modules: Paths_hpdft@@ -98,8 +98,8 @@ , TuiScroll other-extensions: OverloadedStrings build-depends: array >= 0.5 && < 0.6- , base >= 4.20.0 && < 4.23- , bytestring >= 0.11.5 && < 0.13+ , base >= 4.18.0 && < 4.23+ , bytestring >= 0.11.4 && < 0.13 , hpdft , memory >= 0.18.0 && < 0.19 , optparse-applicative >= 0.18.1 && < 0.19@@ -107,7 +107,7 @@ , regex-tdfa >= 1.3.2 && < 1.4 , semigroups >= 0.20 && < 0.21 , terminal-size >= 0.3 && < 0.4- , text >= 2.1.2 && < 2.2.2+ , text >= 2.0.2 && < 2.2.2 , utf8-string >= 1.0.2 && < 1.1 autogen-modules: Paths_hpdft default-language: Haskell2010@@ -118,10 +118,10 @@ main-is: Golden.hs hs-source-dirs: test ghc-options: -threaded -rtsopts "-with-rtsopts=-N -A64m"- build-depends: base >= 4.20.0 && < 4.23- , bytestring >= 0.11.5 && < 0.13- , directory >= 1.3.9 && < 1.4- , filepath >= 1.5.2 && < 1.6+ build-depends: base >= 4.18.0 && < 4.23+ , bytestring >= 0.11.4 && < 0.13+ , directory >= 1.3.8 && < 1.4+ , filepath >= 1.4.2 && < 1.6 , hpdft default-language: Haskell2010 @@ -131,32 +131,32 @@ hs-source-dirs: test , . ghc-options: -threaded -rtsopts "-with-rtsopts=-N -A64m"- build-depends: base >= 4.20.0 && < 4.23- , bytestring >= 0.11.5 && < 0.13- , containers >= 0.7 && < 0.9- , directory >= 1.3.9 && < 1.4- , filepath >= 1.5.2 && < 1.6+ build-depends: base >= 4.18.0 && < 4.23+ , bytestring >= 0.11.4 && < 0.13+ , containers >= 0.6.7 && < 0.9+ , directory >= 1.3.8 && < 1.4+ , filepath >= 1.4.2 && < 1.6 , hpdft- , text >= 2.1.2 && < 2.2.2+ , text >= 2.0.2 && < 2.2.2 other-modules: TuiScroll default-language: Haskell2010 executable interpret-page main-is: scripts/interpret_page.hs hs-source-dirs: .- build-depends: base >= 4.20.0 && < 4.23+ build-depends: base >= 4.18.0 && < 4.23 , hpdft- , text >= 2.1.2 && < 2.2.2+ , text >= 2.0.2 && < 2.2.2 default-language: Haskell2010 buildable: True executable extract-text main-is: Main.hs hs-source-dirs: examples/extract-text- build-depends: base >= 4.20.0 && < 4.23- , bytestring >= 0.11.5 && < 0.13- , directory >= 1.3.9 && < 1.4- , filepath >= 1.5.2 && < 1.6+ build-depends: base >= 4.18.0 && < 4.23+ , bytestring >= 0.11.4 && < 0.13+ , directory >= 1.3.8 && < 1.4+ , filepath >= 1.4.2 && < 1.6 , hpdft default-language: Haskell2010 buildable: True@@ -164,10 +164,10 @@ executable page-api main-is: Main.hs hs-source-dirs: examples/page-api- build-depends: base >= 4.20.0 && < 4.23- , directory >= 1.3.9 && < 1.4- , filepath >= 1.5.2 && < 1.6+ build-depends: base >= 4.18.0 && < 4.23+ , directory >= 1.3.8 && < 1.4+ , filepath >= 1.4.2 && < 1.6 , hpdft- , text >= 2.1.2 && < 2.2.2+ , text >= 2.0.2 && < 2.2.2 default-language: Haskell2010 buildable: True
src/PDF/ContentStream.hs view
@@ -336,11 +336,44 @@ hexletters :: PSParser T.Text hexletters = do+ st <- getState char '<'- lets <- manyTill hexletter (try $ char '>')+ hexChars <- many (oneOf "0123456789ABCDEFabcdef")+ char '>' spaces- return $ T.concat lets+ let cmap = Map.findWithDefault Map.empty (curfont st) (cmaps st)+ codes = hexStringToCodes (fontBytesPerCode st) hexChars+ parts <- mapM (lookupUcs cmap) codes+ return $ T.concat parts +-- | Split a hex string into character codes (matches 'PDF.Interpret.hexPairs'+-- and 'bytesToCodes' for 1- vs 2-byte fonts).+fontBytesPerCode :: PSR -> Int+fontBytesPerCode st =+ case Map.lookup (curfont st) (fontmaps st) of+ Just (CIDmap _) -> 2+ _ -> 1++hexStringToCodes :: Int -> String -> [Int]+hexStringToCodes bpc hex =+ let bytes = hexPairs hex+ in if bpc == 2 then pairBytes bytes else bytes+ where+ pairBytes [] = []+ pairBytes [_] = []+ pairBytes (a:b:rest) = (a * 256 + b) : pairBytes rest++hexPairs :: String -> [Int]+hexPairs [] = []+hexPairs [x] =+ case readHex [x, '0'] of+ [(n, "")] -> [n]+ _ -> []+hexPairs (a:b:rest) =+ case readHex [a, b] of+ [(n, "")] -> n : hexPairs rest+ _ -> hexPairs rest+ octletters :: PSParser T.Text octletters = do char '('@@ -374,14 +407,13 @@ hexletter :: PSParser T.Text hexletter = do st <- getState- let font = curfont st- cmap = Map.findWithDefault Map.empty font (cmaps st)- hexDigits <- choice [ try $ count 4 $ oneOf "0123456789ABCDEFabcdef"- , try $ count 2 $ oneOf "0123456789ABCDEFabcdef"+ let cmap = Map.findWithDefault Map.empty (curfont st) (cmaps st)+ -- Single code inside a hex string fragment (legacy path; prefer hexletters).+ hexDigits <- choice [ try $ count 2 $ oneOf "0123456789ABCDEFabcdef" , try $ (:"0") <$> (oneOf "0123456789ABCDEFabcdef") ] case readHex hexDigits of- [(h,"")] -> lookupUcs cmap h+ [(h, "")] -> lookupUcs cmap h _ -> return "????" octletter :: PSParser T.Text
src/PDF/DocumentStructure.hs view
@@ -47,7 +47,7 @@ import qualified Data.ByteString.Lazy.Char8 as BSL import qualified Data.ByteString.Builder as B import qualified Data.Text as T-import Data.Maybe (fromMaybe)+import Data.Maybe (fromMaybe, listToMaybe) import Numeric (readDec) import Data.Attoparsec.ByteString.Char8 hiding (take, isDigit)@@ -114,7 +114,7 @@ objs = M.mapWithKey resolve xref containerCache = M.fromList- [ (cnum, objStmEntries cnum)+ [ (cnum, objStmBody cnum) | cnum <- nub [c | InObjStm c _ <- M.elems xref] ] resolve objNum (InFile off) =@@ -122,21 +122,19 @@ in snd (parsePDFObj msec (objNum, body)) resolve objNum (InObjStm cnum idx) = case M.lookup cnum containerCache of- Just entries ->- case lookup objNum entries of- Just o -> o- Nothing ->- case drop idx entries of- ((_, o) : _) -> o- _ -> [PdfNull]+ 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]- objStmEntries cnum =+ objStmBody cnum = case rawStream msec cnum (M.findWithDefault [PdfNull] cnum objs) of Right streamBytes ->- case pdfObjStm cnum (BSL.toStrict streamBytes) of- Right entries -> entries- Left _ -> []- Left _ -> []+ case parseObjStmHeader (BSL.toStrict streamBytes) of+ Right cache -> cache+ Left _ -> ([], BS.empty)+ Left _ -> ([], BS.empty) buildIndexEager :: BS.ByteString -> Maybe Security -> PdfResult PDFObjIndex buildIndexEager bytes msec = do@@ -672,20 +670,32 @@ _ -> fail "invalid object stream reference" pdfObjStm n s =- case parseOnly refOffset s of- Right (location, objstr) ->- mapM (\(r,o) -> (,) r <$> parseDict (BS.pack $ drop o objstr)) location+ case parseObjStmHeader 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))- where parseDict 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 (ParseError ((show err) ++ ": Failed to parse obj around")- (BS.take 100 s'))++parseObjStmHeader :: BS.ByteString -> Either String ([(Int, Int)], BS.ByteString)+parseObjStmHeader s =+ case parseOnly refOffset s of+ Right (location, objstr) -> Right (location, BS.pack objstr)+ 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.)