packages feed

hpdft 0.4.6.2 → 0.4.6.3

raw patch · 12 files changed

+531/−121 lines, 12 filesbinary-addedPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

API changes (from Hackage documentation)

+ PDF.Character: cp932Map :: Map Int ByteString
+ PDF.Character: jisx0208Map :: Map Int ByteString
+ PDF.Definition: JISmap :: Encoding
+ PDF.Definition: SJISmap :: Encoding
+ PDF.Definition: UnicodeMap :: Encoding
+ PDF.Definition: [psFormDepth] :: PSR -> Int
+ PDF.Definition: [psResDict] :: PSR -> Maybe Dict
+ PDF.Interpret: bytesToCodes :: FontInfo -> [Int] -> [Int]
+ PDF.Interpret: encodingUnicode :: Encoding -> Int -> Text
+ PDF.Interpret: unicodeBytesToCodes :: [Int] -> [Int]
- PDF.ContentStream: parseStream :: PSR -> PDFStream -> PdfResult (PDFStream, [PdfWarning])
+ PDF.ContentStream: parseStream :: FormRunner -> PSR -> PDFStream -> PdfResult (PDFStream, [PdfWarning])
- PDF.Definition: PSR :: Double -> Double -> Double -> Double -> (Double, Double, Double, Double, Double, Double) -> (Double, Double, Double, Double, Double, Double) -> Bool -> Double -> Double -> Double -> Double -> Text -> Map Text CMap -> Map Text Encoding -> Text -> [Text] -> [PdfWarning] -> PSR
+ PDF.Definition: PSR :: Double -> Double -> Double -> Double -> (Double, Double, Double, Double, Double, Double) -> (Double, Double, Double, Double, Double, Double) -> Bool -> Double -> Double -> Double -> Double -> Text -> Map Text CMap -> Map Text Encoding -> Text -> [Text] -> [PdfWarning] -> Maybe Dict -> Int -> PSR

Files

CHANGELOG.md view
@@ -1,5 +1,24 @@ # Changelog +## 0.4.6.3 (2026-07-06)++### Added++- Predefined CMap support for Japanese Type0 fonts without `/ToUnicode`:+  - Shift-JIS: `/90ms-RKSJ-H/V`, `/90msp-RKSJ-H/V`, `/RKSJ-H/V` (new `SJISmap` encoding backed by an embedded CP932→Unicode table, `data/map/cp932.map`)+  - Unicode: `/UniJIS-UCS2-H/V`, `/UniJIS-UCS2-HW-H/V`, `/UniJIS-UTF16-H/V`, `/UniJIS2004-UTF16-H/V` (new `UnicodeMap` encoding; UTF-16BE splitting with surrogate-pair support, no table needed)+  - JIS X 0208: `/H`, `/V` (new `JISmap` encoding backed by an embedded JIS→Unicode table, `data/map/jisx0208.map`)+  - Vertical variants set the writing mode; `/V` is matched exactly in addition to the `-V` suffix.+- Legacy extraction (and the TUI viewer) now descends into Form XObjects invoked with `Do`, so text drawn inside forms is no longer silently dropped.+- Golden fixtures `data/fixtures/cmap-unijis.pdf` and `data/fixtures/cmap-jis-h.pdf` plus unit tests for the new code splitting and table lookups.++### Fixed++- Object stream (ObjStm) parsing now splits header and body strictly at the `/First` offset instead of relying on the parser stop position; a 1-byte misalignment previously made every object in an affected stream unparseable (mojibake or missing text in PDFs from pdfTeX and similar producers).+- PDFs using CR-only line endings no longer fail trailer/xref discovery (which previously caused a long "broken cross-reference" scan before erroring out).+- RC4 decryption rewritten with a mutable unboxed array (`Data.Array.ST`), turning the previous quadratic key-stream generation linear; large encrypted objects (e.g. full-page images) now decrypt in milliseconds instead of tens of seconds.+- Hex strings in content streams may contain embedded whitespace (e.g. `<65E5 672C 8A9E>`), as allowed by the PDF spec.+ ## 0.4.6.2 (2026-07-06)  ### Fixed
+ data/map/cp932.map view

binary file changed (absent → 50449 bytes)

+ data/map/jisx0208.map view

binary file changed (absent → 37468 bytes)

hpdft.cabal view
@@ -1,6 +1,6 @@ cabal-version:       3.8 name:                hpdft-version:             0.4.6.2+version:             0.4.6.3 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.@@ -33,6 +33,8 @@ data-dir:            data/  data-files:          map/Adobe-Japan1-6.map+                     , map/cp932.map+                     , map/jisx0208.map  source-repository head   type:     git@@ -66,7 +68,8 @@                      , PDF.Structure   other-modules:       Paths_hpdft   other-extensions:    OverloadedStrings-  build-depends:       attoparsec >= 0.14.4 && < 0.15+  build-depends:       array >= 0.5 && < 0.6+                     , attoparsec >= 0.14.4 && < 0.15                      , base >= 4.18.0 && < 4.23                      , binary >= 0.8.9 && < 0.9                      , bytestring >= 0.11.4 && < 0.13
src/PDF/Character.hs view
@@ -4,7 +4,9 @@ module PDF.Character        ( pdfcharmap        , extendedAscii-       , adobeJapanOneSixMap) where+       , adobeJapanOneSixMap+       , cp932Map+       , jisx0208Map) where  import qualified Data.Text as T import qualified Data.Map as Map@@ -20,6 +22,12 @@  adobeJapanOneSixMap :: Map.Map Int BSLU.ByteString adobeJapanOneSixMap = decode . decompress . BSL.fromChunks . (:[]) $ $(embedFile "data/map/Adobe-Japan1-6.map")++cp932Map :: Map.Map Int BSLU.ByteString+cp932Map = decode . decompress . BSL.fromChunks . (:[]) $ $(embedFile "data/map/cp932.map")++jisx0208Map :: Map.Map Int BSLU.ByteString+jisx0208Map = decode . decompress . BSL.fromChunks . (:[]) $ $(embedFile "data/map/jisx0208.map")  extendedasciidict :: [(Int, Char)] extendedasciidict =
src/PDF/ContentStream.hs view
@@ -6,6 +6,7 @@        ) where  import Data.Char (chr, ord)+import Data.Bits (shiftL) import Data.String (fromString) import Data.List (isPrefixOf, dropWhileEnd) import Numeric (readOct, readHex)@@ -27,21 +28,26 @@  import PDF.Definition import PDF.Object-import PDF.Character (pdfcharmap, extendedAscii, adobeJapanOneSixMap)+import PDF.Character (pdfcharmap, extendedAscii, adobeJapanOneSixMap, cp932Map, jisx0208Map) import PDF.Error (PdfError(..), PdfResult, PdfWarning(..))  type PSParser a = GenParser Char PSR a  parseContentStream p st = runParser p st "" -parseStream :: PSR -> PDFStream -> PdfResult (PDFStream, [PdfWarning])-parseStream psr pdfstream =-  case parseContentStream contentParser psr pdfstream of+type FormRunner = T.Text -> PSR -> T.Text++noFormRunner :: FormRunner+noFormRunner _ _ = T.empty++parseStream :: FormRunner -> PSR -> PDFStream -> PdfResult (PDFStream, [PdfWarning])+parseStream formRunner psr pdfstream =+  case parseContentStream (contentParser formRunner) psr pdfstream of     Left err -> Left (ParseError ("content stream: " ++ show err) BS.empty)     Right (str, ws) -> Right (BSLC.pack $ BSSC.unpack $ encodeUtf8 str, reverse ws)   where-    contentParser = do-      str <- T.concat <$> (spaces >> many (try elems <|> skipOther))+    contentParser runner = do+      str <- T.concat <$> (spaces >> many (try (elems runner) <|> skipOther))       st <- getState       return (str, warnings st) @@ -49,7 +55,7 @@ parseColorSpace psr pdfstream =    case parseContentStream (many (choice [ try colorSpace                                         , try $ T.concat <$> xObject-                                        , (T.empty <$ elems)+                                        , (T.empty <$ elems noFormRunner)                                         ])) psr pdfstream of     Left  err -> Left (ParseError ("color space: " ++ show err) BS.empty)     Right str -> Right str@@ -57,8 +63,8 @@  -- | Parsers for Content Stream -elems :: PSParser T.Text-elems = choice [ try pdfopBT +elems :: FormRunner -> PSParser T.Text+elems formRunner = choice [ try (pdfopBT formRunner)                , try pdfopTf                , try pdfopTD                , try pdfopTd@@ -79,17 +85,26 @@                , try array <* spaces                , try pdfopGraphics                , try dashPattern-               , try $ T.empty <$ xObject+               , try (formDoOp formRunner)                , try graphicState                , try pdfopcm                , try $ T.empty <$ colorSpace                , try $ T.empty <$ renderingIntent-               , try pdfopBDC-               , try pdfopBMC+               , try (pdfopBDC formRunner)+               , try (pdfopBMC formRunner)                , try pdfopEMC                , unknowns                ] +formDoOp :: FormRunner -> PSParser T.Text+formDoOp runner = do+  n <- (++) <$> string "/" <*> manyTill anyChar (try space)+  spaces+  string "Do"+  spaces+  st <- getState+  return $ runner (T.pack n) st+ pdfopGraphics :: PSParser T.Text pdfopGraphics = do   spaces@@ -152,30 +167,30 @@ --  updateState (\s -> s {colorspace = xobjcs})   return xobjcs -pdfopBT :: PSParser T.Text-pdfopBT = do+pdfopBT :: FormRunner -> PSParser T.Text+pdfopBT formRunner = do   st <- getState   updateState (\s -> s{text_m = (1,0,0,1,0,0), text_break = False})   string "BT"   spaces-  t <- manyTill elems (try $ string "ET")+  t <- manyTill (elems formRunner) (try $ string "ET")   spaces   return $ T.concat t  -- should have refined according to the section 10.5 of PDF reference -pdfopBMC :: PSParser T.Text-pdfopBMC = do+pdfopBMC :: FormRunner -> PSParser T.Text+pdfopBMC formRunner = do   tag <- (++) <$> string "/" <*> manyTill anyChar (try space)   spaces   string "BMC"   spaces-  manyTill elems (try $ string "EMC")+  manyTill (elems formRunner) (try $ string "EMC")   spaces   return T.empty -pdfopBDC :: PSParser T.Text-pdfopBDC = do+pdfopBDC :: FormRunner -> PSParser T.Text+pdfopBDC formRunner = do   tag <- name   prop <- propertyList   spaces@@ -184,7 +199,7 @@   case tag of     "/Span"        | "/ActualText" == (fst prop)-        -> do {spaces >> manyTill elems (try $ string "EMC") >> return (snd prop)}+        -> do {spaces >> manyTill (elems formRunner) (try $ string "EMC") >> return (snd prop)}       | otherwise  -> return $ T.empty     _ -> return $ T.empty @@ -263,9 +278,9 @@ unknowns = do   ps <- manyTill anyChar (try $ oneOf "\r\n")   st <- getState-  case runParser elems st "" $ BSLC.pack ((Data.List.dropWhileEnd (=='\\') ps)++")Tj") of+  case runParser (elems noFormRunner) st "" $ BSLC.pack ((Data.List.dropWhileEnd (=='\\') ps)++")Tj") of     Right xs -> return xs-    Left _ -> case runParser elems st "" $ BSLC.pack ("("++ps) of+    Left _ -> case runParser (elems noFormRunner) st "" $ BSLC.pack ("("++ps) of       Right xs -> return xs       Left _ -> case ps of         "" -> return T.empty@@ -298,6 +313,9 @@       letterParser = case Map.lookup (curfont st) (fontmaps st) of         Just (Encoding m) -> psletter m         Just (CIDmap s) -> cidletter s+        Just SJISmap -> sjisletters+        Just UnicodeMap -> unicodeletters+        Just JISmap -> jisletters         Just (WithCharSet s) -> try $ bytesletter cmap <|> cidletters         Just NullMap -> psletter Map.empty         Nothing -> (T.pack) <$> (many1 $ choice [ try $ ')' <$ (string "\\)")@@ -326,7 +344,7 @@   byteStringToText cmap txt   where     byteStringToText cmap' str = do-      parts <- mapM (lookupUcs cmap') $ asInt16 $ map ord str+      parts <- mapM (lookupUcs Nothing cmap') $ asInt16 $ map ord str       return $ T.concat parts      asInt16 :: [Int] -> [Int]@@ -338,31 +356,141 @@ hexletters = do   st <- getState   char '<'-  hexChars <- many (oneOf "0123456789ABCDEFabcdef")+  hexChars <- many (oneOf "0123456789ABCDEFabcdef \t\r\n")   char '>'   spaces-  let cmap = Map.findWithDefault Map.empty (curfont st) (cmaps st)-      codes = hexStringToCodes (fontBytesPerCode st) hexChars-  parts <- mapM (lookupUcs cmap) codes+  let enc = Map.lookup (curfont st) (fontmaps st)+      cmap = Map.findWithDefault Map.empty (curfont st) (cmaps st)+      codes = hexStringToCodes enc (filter (\c -> c `notElem` (" \t\r\n" :: String)) hexChars)+  parts <- mapM (lookupUcs enc 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 =+-- | Split a hex string into character codes (matches 'PDF.Interpret.bytesToCodes').+hexStringToCodes :: Maybe Encoding -> String -> [Int]+hexStringToCodes enc hex =   let bytes = hexPairs hex-  in if bpc == 2 then pairBytes bytes else bytes+  in case enc of+    Just SJISmap -> sjisBytesToCodes bytes+    Just UnicodeMap -> unicodeBytesToCodes bytes+    Just JISmap -> pairBytes bytes+    Just (CIDmap _) -> pairBytes bytes+    _ -> bytes   where     pairBytes [] = []     pairBytes [_] = []     pairBytes (a:b:rest) = (a * 256 + b) : pairBytes rest +isUtf16HighSurrogate :: Int -> Bool+isUtf16HighSurrogate u = u >= 0xD800 && u <= 0xDBFF++isUtf16LowSurrogate :: Int -> Bool+isUtf16LowSurrogate u = u >= 0xDC00 && u <= 0xDFFF++surrogatePairToCode :: Int -> Int -> Int+surrogatePairToCode hi lo = 0x10000 + ((hi - 0xD800) `shiftL` 10) + (lo - 0xDC00)++unicodeBytesToCodes :: [Int] -> [Int]+unicodeBytesToCodes [] = []+unicodeBytesToCodes [_] = []+unicodeBytesToCodes (a:b:rest) =+  let unit = a * 256 + b+  in if isUtf16HighSurrogate unit+     then case rest of+       (c:d:rs) ->+         let unit2 = c * 256 + d+         in if isUtf16LowSurrogate unit2+            then surrogatePairToCode unit unit2 : unicodeBytesToCodes rs+            else unit : unicodeBytesToCodes rest+       _ -> [unit]+     else unit : unicodeBytesToCodes rest++isSjisLead :: Int -> Bool+isSjisLead b = (b >= 0x81 && b <= 0x9F) || (b >= 0xE0 && b <= 0xFC)++sjisBytesToCodes :: [Int] -> [Int]+sjisBytesToCodes [] = []+sjisBytesToCodes (b:rest)+  | isSjisLead b = case rest of+      (t:rs) -> (b * 256 + t) : sjisBytesToCodes rs+      _ -> [b]+  | otherwise = b : sjisBytesToCodes rest++sjisCodeToText :: Int -> T.Text+sjisCodeToText code =+  case Map.lookup code cp932Map of+    Just bs -> T.pack $ BSLU.toString bs+    Nothing ->+      if code >= 0 && code <= 0x7F+      then T.singleton (chr code)+      else "\xFFFD"++unicodeCodeToText :: Int -> T.Text+unicodeCodeToText code =+  if code >= 0 && code <= 0x10FFFF+  then T.singleton (chr code)+  else "\xFFFD"++jisCodeToText :: Int -> T.Text+jisCodeToText code =+  case Map.lookup code jisx0208Map of+    Just bs -> T.pack $ BSLU.toString bs+    Nothing ->+      if code >= 0 && code <= 0x7F+      then T.singleton (chr code)+      else "\xFFFD"++sjisletters :: PSParser T.Text+sjisletters = do+  txt <- (many1 $ choice [ try $ ')' <$ (string "\\)")+                         , try $ '(' <$ (string "\\(")+                         , try $ (chr 10) <$ (string "\\n")+                         , try $ (chr 13) <$ (string "\\r")+                         , try $ (chr 8) <$ (string "\\b")+                         , try $ (chr 9) <$ (string "\\t")+                         , try $ (chr 12) <$ (string "\\f")+                         , try $ (chr 92) <$ (string "\\\\")+                         , try $ chr <$> ((string "\\") *> octnum)+                         , try $ noneOf ")"+                         ])+  let codes = sjisBytesToCodes $ map ord txt+  return $ T.concat $ map sjisCodeToText codes++unicodeletters :: PSParser T.Text+unicodeletters = do+  txt <- (many1 $ choice [ try $ ')' <$ (string "\\)")+                         , try $ '(' <$ (string "\\(")+                         , try $ (chr 10) <$ (string "\\n")+                         , try $ (chr 13) <$ (string "\\r")+                         , try $ (chr 8) <$ (string "\\b")+                         , try $ (chr 9) <$ (string "\\t")+                         , try $ (chr 12) <$ (string "\\f")+                         , try $ (chr 92) <$ (string "\\\\")+                         , try $ chr <$> ((string "\\") *> octnum)+                         , try $ noneOf ")"+                         ])+  let codes = unicodeBytesToCodes $ map ord txt+  return $ T.concat $ map unicodeCodeToText codes++jisletters :: PSParser T.Text+jisletters = do+  txt <- (many1 $ choice [ try $ ')' <$ (string "\\)")+                         , try $ '(' <$ (string "\\(")+                         , try $ (chr 10) <$ (string "\\n")+                         , try $ (chr 13) <$ (string "\\r")+                         , try $ (chr 8) <$ (string "\\b")+                         , try $ (chr 9) <$ (string "\\t")+                         , try $ (chr 12) <$ (string "\\f")+                         , try $ (chr 92) <$ (string "\\\\")+                         , try $ chr <$> ((string "\\") *> octnum)+                         , try $ noneOf ")"+                         ])+  let codes = pairBytes $ map ord txt+  return $ T.concat $ map jisCodeToText codes+  where+    pairBytes [] = []+    pairBytes [_] = []+    pairBytes (a:b:rest) = (a * 256 + b) : pairBytes rest+ hexPairs :: String -> [Int] hexPairs [] = [] hexPairs [x] =@@ -391,16 +519,21 @@   Just cs -> T.pack $ BSLU.toString cs   Nothing -> T.pack $ "[" ++ show a ++ "]" -lookupUcs :: CMap -> Int -> PSParser T.Text-lookupUcs m h = case Map.lookup h m of+lookupUcs :: Maybe Encoding -> CMap -> Int -> PSParser T.Text+lookupUcs enc m h = case Map.lookup h m of   Just ucs -> return ucs-  Nothing-    | Map.null m -> case Map.lookup h adobeJapanOneSixMap of-        Just cs -> return $ T.pack $ BSLU.toString cs-        Nothing -> do-          updateState (\s -> s {warnings = UnmappedCid h : warnings s})-          return $ adobeOneSix h-    | otherwise -> return $ T.singleton (chr h)+  Nothing ->+    case enc of+      Just SJISmap -> return $ sjisCodeToText h+      Just UnicodeMap -> return $ unicodeCodeToText h+      Just JISmap -> return $ jisCodeToText h+      _ | Map.null m ->+          case Map.lookup h adobeJapanOneSixMap of+            Just cs -> return $ T.pack $ BSLU.toString cs+            Nothing -> do+              updateState (\s -> s {warnings = UnmappedCid h : warnings s})+              return $ adobeOneSix h+      _ -> return $ T.singleton (chr h)  cidletters = choice [try hexletter, try octletter] @@ -413,7 +546,7 @@                       , try $ (:"0") <$> (oneOf "0123456789ABCDEFabcdef")                       ]   case readHex hexDigits of-    [(h, "")] -> lookupUcs cmap h+    [(h, "")] -> lookupUcs (Map.lookup (curfont st) (fontmaps st)) cmap h     _ -> return "????"  octletter :: PSParser T.Text@@ -421,7 +554,7 @@   st <- getState   let cmap = Map.findWithDefault Map.empty (curfont st) (cmaps st)   o <- octnum-  lookupUcs cmap o+  lookupUcs (Map.lookup (curfont st) (fontmaps st)) cmap o  psletter :: Map.Map Char T.Text -> PSParser T.Text psletter fontmap = do@@ -453,7 +586,7 @@   o1 <- octnum   o2 <- octnum   let d = 256 * o1 + o2-  lookupUcs Map.empty d+  lookupUcs Nothing Map.empty d  octnum :: PSParser Int octnum = do
src/PDF/Definition.hs view
@@ -67,12 +67,15 @@ ppObjAt _ PdfNull = ""  -data Encoding = CIDmap T.Text | Encoding (Map Char T.Text) | WithCharSet T.Text | NullMap+data Encoding = CIDmap T.Text | Encoding (Map Char T.Text) | WithCharSet T.Text | SJISmap | UnicodeMap | JISmap | NullMap  instance Show Encoding where   show (CIDmap s) = "CIDmap" ++ T.unpack s   show (Encoding a) = "Encoding" ++ show a   show (WithCharSet s) = "WithCharSet" ++ T.unpack s+  show SJISmap = "SJISmap"+  show UnicodeMap = "UnicodeMap"+  show JISmap = "JISmap"   show NullMap = []  type CMap = Map Int T.Text@@ -104,5 +107,7 @@                , colorspace :: T.Text                , xcolorspaces :: [T.Text]                , warnings     :: [PdfWarning]+               , psResDict    :: Maybe Dict+               , psFormDepth  :: Int                }          deriving (Show)
src/PDF/DocumentStructure.hs view
@@ -45,6 +45,7 @@ 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)@@ -129,9 +130,10 @@           in maybe [PdfNull] (parseObjStmObject body) off         Nothing -> [PdfNull]     objStmBody cnum =-      case rawStream msec cnum (M.findWithDefault [PdfNull] cnum objs) of+      let containerObjs = M.findWithDefault [PdfNull] cnum objs+      in case rawStream msec cnum containerObjs of         Right streamBytes ->-          case parseObjStmHeader (BSL.toStrict streamBytes) of+          case parseObjStmHeader (objStmFirst containerObjs) (BSL.toStrict streamBytes) of             Right cache -> cache             Left _ -> ([], BS.empty)         Left _ -> ([], BS.empty)@@ -203,10 +205,51 @@  parseContentStream :: Dict -> PSR -> Maybe Security -> PDFObjIndex -> BSL.ByteString -> PdfResult (PDFStream, [PdfWarning]) parseContentStream dict st sec objs s =-  parseStream (st {fontmaps=fontdict, cmaps=cmap}) 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@@ -384,13 +427,23 @@       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 = case BS.breakEnd (== '\n') trimmed of-        (_, n) | not (BS.null n) -> BS.dropWhile isSpaceChar n-        _ -> trimmed-  in readDec' $ BS.takeWhile (\c -> c >= '0' && c <= '9') numLine+      (_, 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)@@ -404,17 +457,17 @@   _ -> Right xref  findTrailer :: BS.ByteString -> PdfResult Dict-findTrailer bs = case BS.breakEnd (== '\n') bs of+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 (BS.init bs)+    | otherwise -> findTrailer source  findTrailer' :: BS.ByteString -> PdfResult (Dict, XREF)-findTrailer' bs = case BS.breakEnd (== '\n') bs of+findTrailer' bs = case splitLastLine bs of   (source, eofLine)     | isPdfEofLine eofLine -> do         offset <- getStartxrefOffset source@@ -424,7 +477,7 @@           Just (PdfNumber x) -> xrefs (truncate x) bs (dict, xref')           _ -> Right (dict, xref')     | source == "" -> Left (BrokenXref "no %%EOF or startxref found")-    | otherwise -> findTrailer' (BS.init bs)+    | otherwise -> findTrailer' source   where     xrefs n all (dict, sofar) = do       (dict', xref) <- findTrailerDictXREF $ BS.drop n all@@ -653,33 +706,52 @@ objStm :: Maybe Security -> PDFObj -> PdfResult [PDFObj] objStm sec (n, obj) = case findDictOfType "/ObjStm" obj of   Nothing -> Right [(n,obj)]-  Just _  -> do+  Just d  -> do     streamBytes <- rawStream sec n obj-    pdfObjStm n (BSL.toStrict streamBytes)-  -refOffset :: Parser ([(Int, Int)], String)-refOffset = spaces *> ((,) -                       <$> many1 refPair-                       <*> many1 anyChar)-  where-    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"+    pdfObjStm n (objStmFirstFromDict d) (BSL.toStrict streamBytes) -pdfObjStm n s =-  case parseObjStmHeader s of+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 :: BS.ByteString -> Either String ([(Int, Int)], BS.ByteString)-parseObjStmHeader 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, objstr) -> Right (location, BS.pack objstr)+    Right (location, body) -> Right (location, body)     Left err -> Left (show err)  parseObjStmObject :: BS.ByteString -> Int -> [Obj]@@ -735,6 +807,10 @@     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@@ -768,6 +844,21 @@     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 @@ -898,13 +989,16 @@       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 = 2+    , fiBytesPerCode = bpc     , fiDefaultWidth = defaultW     } @@ -988,6 +1082,7 @@   _ -> (880, defaultVerticalW1)  wmodeFromEncoding :: Maybe Obj -> Int+wmodeFromEncoding (Just (PdfName "/V")) = 1 wmodeFromEncoding (Just (PdfName n)) | "-V" `T.isSuffixOf` n = 1 wmodeFromEncoding _ = 0 
src/PDF/Encrypt.hs view
@@ -17,6 +17,10 @@ import Data.List (foldl') import qualified Data.Map as M import Control.Applicative ((<|>))+import Control.Monad (forM_, forM)+import Control.Monad.ST (ST, runST)+import Data.Array.ST (STUArray, newArray, readArray, writeArray)+import Data.STRef (newSTRef, readSTRef, writeSTRef) import Data.Word (Word8, Word32) import Numeric (readHex) @@ -225,7 +229,7 @@ rc4Decrypt :: BS.ByteString -> BS.ByteString -> BS.ByteString rc4Decrypt key ciphertext =   let ks = rc4KeyStream key (BS.length ciphertext)-  in BS.pack $ zipWith xor (BS.unpack ciphertext) ks+  in BS.pack $ zipWith xor (BS.unpack ciphertext) (BS.unpack ks)  rc4DecryptRev3 :: Int -> BS.ByteString -> BS.ByteString -> BS.ByteString rc4DecryptRev3 keyLen key ciphertext =@@ -233,31 +237,45 @@          ciphertext          [19,18..0] -rc4KeyStream :: BS.ByteString -> Int -> [Word8]-rc4KeyStream key nbytes =-  let keyInts = map fromIntegral (BS.unpack key) :: [Int]-      state = ksaInit keyInts-  in map fromIntegral $ take nbytes $ prga state-  where-    ksaInit k' =-      let len = length k'-          step (st, j) i =-            let j' = (j + st !! i + k' !! (i `mod` len)) `mod` 256-                st' = swap st i j'-            in (st', j')-      in fst $ foldl' step ([0..255], 0) [0..255]-    prga st = reverse $ go st 0 0 []-      where go sbox i j out-              | length out >= nbytes = out-              | otherwise =-                  let i' = (i + 1) `mod` 256-                      j' = (j + sbox !! i') `mod` 256-                      s' = swap sbox i' j'-                      byte = s' !! ((s' !! i' + s' !! j') `mod` 256)-                  in go s' i' j' (byte:out)-    swap s i j =-      [ if x == i then s !! j else if x == j then s !! i else v-      | (x, v) <- zip [0..] s ]+rc4KeyStream :: BS.ByteString -> Int -> BS.ByteString+rc4KeyStream _ nbytes | nbytes <= 0 = BS.empty+rc4KeyStream key nbytes = runST $ do+  let keyLen = BS.length key+  sbox <- newArray (0, 255) 0 :: ST s (STUArray s Int Word8)+  forM_ [0..255] $ \i -> writeArray sbox i (fromIntegral i :: Word8)+  jRef <- newSTRef (0 :: Int)+  forM_ [0..255] $ \i -> do+    j <- readSTRef jRef+    si <- fmap fromIntegral (readArray sbox i)+    kj <- return $ fromIntegral (BS.index key (i `mod` keyLen))+    let j' = (j + si + kj) `mod` 256+    rc4Swap sbox i j'+    writeSTRef jRef j'+  out <- (newArray (0, nbytes - 1) (0 :: Word8) :: ST s (STUArray s Int Word8))+  iRef <- newSTRef 0+  jRef2 <- newSTRef 0+  forM_ [0..nbytes - 1] $ \n -> do+    i <- readSTRef iRef+    j <- readSTRef jRef2+    let i' = (i + 1) `mod` 256+    si <- fmap fromIntegral (readArray sbox i')+    let j' = (j + si) `mod` 256+    rc4Swap sbox i' j'+    a <- fmap fromIntegral (readArray sbox i')+    b <- fmap fromIntegral (readArray sbox j')+    byte <- readArray sbox ((a + b) `mod` 256)+    writeArray out n byte+    writeSTRef iRef i'+    writeSTRef jRef2 j'+  arr <- forM [0..nbytes - 1] (readArray out)+  return (BS.pack arr)++rc4Swap :: STUArray s Int Word8 -> Int -> Int -> ST s ()+rc4Swap arr i j = do+  a <- readArray arr i+  b <- readArray arr j+  writeArray arr i b+  writeArray arr j a  decryptString :: Maybe Security -> Int -> Int -> BS.ByteString -> BS.ByteString decryptString Nothing _ _ bs = bs
src/PDF/Interpret.hs view
@@ -14,6 +14,9 @@   , interpretContentItems   , interpretContentWithFonts   , interpretContentWithFontsItems+  , bytesToCodes+  , encodingUnicode+  , unicodeBytesToCodes   ) where  import PDF.Definition@@ -30,13 +33,14 @@   , rawStreamByRef   ) import PDF.Matrix-import PDF.Character (pdfcharmap, adobeJapanOneSixMap)+import PDF.Character (pdfcharmap, adobeJapanOneSixMap, cp932Map, jisx0208Map) import PDF.Encrypt (Security) import PDF.Error (PdfError(..), PdfResult) import PDF.Object (parseRefsArray)  import Data.Char (chr, isDigit, ord) import Data.List (find, foldl', isPrefixOf)+import Data.Bits (shiftL) import Data.Maybe (fromMaybe, isJust) import Data.Word (Word8) import qualified Numeric as Num@@ -577,6 +581,9 @@   | not (isCidFontName name) = fi   | fiBytesPerCode fi == 2, CIDmap{} <- fiEncoding fi = fi   | fiBytesPerCode fi == 2, Encoding{} <- fiEncoding fi = fi+  | SJISmap <- fiEncoding fi = fi+  | UnicodeMap <- fiEncoding fi = fi+  | JISmap <- fiEncoding fi = fi   | otherwise = defaultAdobeJapanFontInfo fi  isCidFontName :: T.Text -> Bool@@ -674,14 +681,57 @@  bytesToCodes :: FontInfo -> [Int] -> [Int] bytesToCodes fi bytes =-  if fiBytesPerCode fi == 2-  then pairs bytes-  else bytes+  case fiEncoding fi of+    SJISmap -> sjisBytesToCodes bytes+    UnicodeMap -> unicodeBytesToCodes bytes+    JISmap -> jisBytesToCodes bytes+    _ | fiBytesPerCode fi == 2 -> pairs bytes+    _ -> bytes   where     pairs [] = []     pairs [_] = []     pairs (a:b:rest) = (a * 256 + b) : pairs rest +isUtf16HighSurrogate :: Int -> Bool+isUtf16HighSurrogate u = u >= 0xD800 && u <= 0xDBFF++isUtf16LowSurrogate :: Int -> Bool+isUtf16LowSurrogate u = u >= 0xDC00 && u <= 0xDFFF++surrogatePairToCode :: Int -> Int -> Int+surrogatePairToCode hi lo = 0x10000 + ((hi - 0xD800) `shiftL` 10) + (lo - 0xDC00)++unicodeBytesToCodes :: [Int] -> [Int]+unicodeBytesToCodes [] = []+unicodeBytesToCodes [_] = []+unicodeBytesToCodes (a:b:rest) =+  let unit = a * 256 + b+  in if isUtf16HighSurrogate unit+     then case rest of+       (c:d:rs) ->+         let unit2 = c * 256 + d+         in if isUtf16LowSurrogate unit2+            then surrogatePairToCode unit unit2 : unicodeBytesToCodes rs+            else unit : unicodeBytesToCodes rest+       _ -> [unit]+     else unit : unicodeBytesToCodes rest++jisBytesToCodes :: [Int] -> [Int]+jisBytesToCodes [] = []+jisBytesToCodes [_] = []+jisBytesToCodes (a:b:rest) = (a * 256 + b) : jisBytesToCodes rest++isSjisLead :: Int -> Bool+isSjisLead b = (b >= 0x81 && b <= 0x9F) || (b >= 0xE0 && b <= 0xFC)++sjisBytesToCodes :: [Int] -> [Int]+sjisBytesToCodes [] = []+sjisBytesToCodes (b:rest)+  | isSjisLead b = case rest of+      (t:rs) -> (b * 256 + t) : sjisBytesToCodes rs+      _ -> [b]+  | otherwise = b : sjisBytesToCodes rest+ codeToUnicode :: FontInfo -> Int -> T.Text codeToUnicode fi code =   case M.lookup code (fiToUnicode fi) of@@ -710,6 +760,21 @@     Just bs -> T.pack (BSLU.toString bs)     Nothing -> T.singleton (safeChr code) encodingUnicode (CIDmap _) code = T.singleton (safeChr code)+encodingUnicode SJISmap code =+  case M.lookup code cp932Map of+    Just bs -> T.pack (BSLU.toString bs)+    Nothing ->+      if code >= 0 && code <= 0x7F+      then T.singleton (safeChr code)+      else "\xFFFD"+encodingUnicode UnicodeMap code = T.singleton (safeChr code)+encodingUnicode JISmap code =+  case M.lookup code jisx0208Map of+    Just bs -> T.pack (BSLU.toString bs)+    Nothing ->+      if code >= 0 && code <= 0x7F+      then T.singleton (safeChr code)+      else "\xFFFD" encodingUnicode (WithCharSet "ZapfDingbats") code =   fromMaybe (T.singleton (safeChr code)) (dingbatCodeUnicode code) encodingUnicode (WithCharSet _) code = T.singleton (safeChr code)@@ -1012,11 +1077,15 @@  readHexString :: BSL.ByteString -> Maybe (Token, BSL.ByteString) readHexString bs =-  let hex = BSL.takeWhile hexDigit8 (BSL.tail bs)-      rest = BSL.drop (1 + BSL.length hex) bs-  in case BSL.uncons rest of-       Just (62, r) -> Just (TokOperand (PdfHex (T.pack (map w2c (BSL.unpack hex)))), r)-       _ -> Nothing+  case BSL.break ((== 62) . fromIntegral) (BSL.tail bs) of+    (hexBody, rest) ->+      case BSL.uncons rest of+        Just (_, r) ->+          let hex = BSL.filter hexDigit8 hexBody+          in if BSL.null hex+             then Nothing+             else Just (TokOperand (PdfHex (T.pack (map w2c (BSL.unpack hex)))), r)+        _ -> Nothing  readArray :: BSL.ByteString -> Maybe (Token, BSL.ByteString) readArray bs = parseArrayElems (BSL.tail bs) []
src/PDF/Text.hs view
@@ -87,6 +87,8 @@                 , colorspace=T.empty                 , xcolorspaces=[]                 , warnings=[]+                , psResDict=Nothing+                , psFormDepth=0                 }  pdfToTextBS :: FilePath -> Maybe String -> IO (PdfResult BSL.ByteString)
test/Unit.hs view
@@ -8,6 +8,9 @@   , interpretContentWithFonts   , interpretContentWithFontsItems   , interpretPageImageHits+  , bytesToCodes+  , encodingUnicode+  , unicodeBytesToCodes   ) import PDF.Layout (LayoutOptions(..), defaultLayoutOptions, needsAozoraBar, aozoraRuby, layoutParagraphs, layoutParagraphsWith, layoutPageText, layoutDocument, sortLinesByReadingOrder, linesFromGlyphs, Line(..)) import PDF.Structure (StructElem(..), StructKid(..), structTree, logicalOrder)@@ -15,6 +18,7 @@ import PDF.Page (pageCount, pageRefAt, pageParagraphs) import PDF.Diff (TextChange(..), compareDocuments, diffParagraphs) import PDF.DocumentStructure (parseCIDWidths, simpleWidthAt, decodeStreamBytes)+import PDF.Character (jisx0208Map) import PDF.Image   ( ImageFormat(..)   , PageImage(..)@@ -182,6 +186,7 @@           ++ formExtractResults           ++ taggedEndToEndResults           ++ textStreamResults+          ++ cmapEncodingResults           ++ tuiScrollResults   let failures = [msg | Fail msg <- results]       passed = length results - length failures@@ -1317,5 +1322,58 @@           (padToDisplayWidth 6 "あ" == "あ    ")       , assertBool "tuiScroll pad straddling wide char"           (stringDisplayWidth (padToDisplayWidth 4 "あい") == 4)+      ]++cmapTestFontInfo :: Encoding -> FontInfo+cmapTestFontInfo enc =+  FontInfo+    { fiEncoding = enc+    , fiToUnicode = M.empty+    , fiWidth = const 1000+    , fiWidthV = const 1000+    , fiWMode = 0+    , fiBytesPerCode = 2+    , fiDefaultWidth = 1000+    }++cmapEncodingResults :: [Result]+cmapEncodingResults =+  let ufi = cmapTestFontInfo UnicodeMap+      jfi = cmapTestFontInfo JISmap+      bmp = [0x65, 0xE5, 0x67, 0x2C]+      mixed = [0x65, 0xE5, 0xD8, 0x3D, 0xDE, 0x00, 0x67, 0x2C]+      odd = [0x65, 0xE5, 0x67]+   in [ assertBool "unicodeBytesToCodes BMP"+          (unicodeBytesToCodes bmp == [0x65E5, 0x672C])+      , assertBool "unicodeBytesToCodes surrogate pair"+          (unicodeBytesToCodes [0xD8, 0x3D, 0xDE, 0x00] == [0x1F600])+      , assertBool "unicodeBytesToCodes BMP and surrogate mixed"+          (unicodeBytesToCodes mixed == [0x65E5, 0x1F600, 0x672C])+      , assertBool "unicodeBytesToCodes odd trailing byte"+          (unicodeBytesToCodes odd == [0x65E5])+      , assertBool "bytesToCodes UnicodeMap via FontInfo"+          (bytesToCodes ufi bmp == [0x65E5, 0x672C])+      , assertTextEq "encodingUnicode UnicodeMap BMP"+          "\x65E5"+          (encodingUnicode UnicodeMap 0x65E5)+      , assertTextEq "encodingUnicode UnicodeMap invalid"+          "\xFFFD"+          (encodingUnicode UnicodeMap 0x110000)+      , assertTextEq "encodingUnicode JISmap 0x3021"+          "\x4E9C"+          (encodingUnicode JISmap 0x3021)+      , assertTextEq "encodingUnicode JISmap 0x2422"+          "\x3042"+          (encodingUnicode JISmap 0x2422)+      , assertTextEq "encodingUnicode JISmap ASCII"+          "A"+          (encodingUnicode JISmap 0x41)+      , assertTextEq "encodingUnicode JISmap missing"+          "\xFFFD"+          (encodingUnicode JISmap 0x2021)+      , assertBool "jisx0208Map has 日本語 codes"+          (M.member 0x467C jisx0208Map && M.member 0x4B5C jisx0208Map)+      , assertBool "bytesToCodes JISmap 2-byte fixed"+          (bytesToCodes jfi [0x46, 0x7C, 0x4B, 0x5C] == [0x467C, 0x4B5C])       ]