packages feed

hpdft-0.4.6.0: src/PDF/Page.hs

{-# LANGUAGE OverloadedStrings #-}

{-|
Module      : PDF.Page
Description : Public page-level API for hpdft (0.4)
License     : MIT

Stable entry points for page enumeration and structured extraction.
Prefer this module over 'PDF.DocumentStructure' internals in scripts and
downstream tools.

Typical workflow: 'openDocument', then 'pageCount' / 'pageRefAt', then
'pageParagraphs' or 'pageRegions' with 'PDF.Layout.LayoutOptions'.

@example
import PDF.Document (openDocument)
import PDF.Layout (defaultLayoutOptions)
import PDF.Page (pageCount, pageRefAt, pageParagraphs)

extractPage :: Document -> Int -> PdfResult [Text]
extractPage doc n = do
  ref <- pageRefAt doc n
  pageParagraphs doc ref defaultLayoutOptions
-}
module PDF.Page
  ( PageRef
  , pageCount
  , pageRefAt
  , pageRefs
  , pageItems
  , pageGlyphs
  , pageLines
  , pageParagraphs
  , PageRegion(..)
  , pageRegions
  ) where

import PDF.Definition (PDFObjIndex)
import PDF.Document (Document(..), docRootRef)
import PDF.DocumentStructure
  ( findDictOfType
  , findKids
  , findObjsByRef
  , findPages
  )
import PDF.Error (PdfError(..), PdfResult)
import PDF.Interpret (Glyph(..), PageItem(..), Rect(..), interpretPageItems)
import PDF.Layout
  ( LayoutOptions
  , Line(..)
  , layoutParagraphsWith
  , pageItemLines
  , pageItemParagraphGroups
  )

import Data.List (elemIndex)

import qualified Data.Text as T

-- | PDF page object reference number (same as catalog\/pages leaf ref).
type PageRef = Int

-- | Number of pages in document order (1-based page numbers run @1 .. pageCount@).
pageCount :: Document -> PdfResult Int
pageCount doc = fmap length (pageRefs doc)

-- | Resolve a 1-based page number to its object reference.
pageRefAt :: Document -> Int -> PdfResult PageRef
pageRefAt doc n
  | n < 1 = Left (UnsupportedFeature ("page number " ++ show n ++ " out of range"))
  | otherwise = do
      refs <- pageRefs doc
      if n > length refs
        then Left (UnsupportedFeature ("page number " ++ show n ++ " out of range (1-" ++ show (length refs) ++ ")"))
        else Right (refs !! (n - 1))

pageItems :: Document -> PageRef -> PdfResult [PageItem]
pageItems = interpretPageItems

pageGlyphs :: Document -> PageRef -> PdfResult [Glyph]
pageGlyphs doc ref = fmap extractGlyphs (pageItems doc ref)
  where
    extractGlyphs items = [g | ItemGlyph g <- items]

pageLines :: Document -> PageRef -> LayoutOptions -> PdfResult [Line]
pageLines doc ref opts = fmap (pageItemLines opts) (pageItems doc ref)

pageParagraphs :: Document -> PageRef -> LayoutOptions -> PdfResult [T.Text]
pageParagraphs doc ref opts = fmap (layoutParagraphsWith opts) (pageItems doc ref)

data PageRegion = PageRegion
  { regionPage      :: !Int
  , regionParagraph :: !Int
  , regionBBox      :: !Rect
  , regionText      :: !T.Text
  } deriving (Eq, Show)

-- | Per-page paragraph regions without document-level cross-page merge.
-- Each 'PageRegion' carries the 1-based page number, paragraph index, bounding box,
-- and paragraph text.
--
-- @example
-- ref <- pageRefAt doc 2
-- regions <- pageRegions doc ref defaultLayoutOptions
-- mapM_ print regions
pageRegions :: Document -> PageRef -> LayoutOptions -> PdfResult [PageRegion]
pageRegions doc ref opts = do
  items <- pageItems doc ref
  pageNo <- pageNumberOf doc ref
  let texts = layoutParagraphsWith opts items
      groups = pageItemParagraphGroups opts items
  return
    [ PageRegion pageNo idx (paraBBox grp) txt
    | (idx, (txt, grp)) <- zip [1 ..] (zip texts groups)
    ]

pageRefs :: Document -> PdfResult [PageRef]
pageRefs doc = do
  root <- docRootRef doc
  return (pageRefsFromRoot root (docObjs doc))

pageNumberOf :: Document -> PageRef -> PdfResult Int
pageNumberOf doc ref = do
  refs <- pageRefs doc
  case elemIndex ref refs of
    Nothing -> Left (MissingObject ref)
    Just i  -> Right (i + 1)

pageRefsFromRoot :: Int -> PDFObjIndex -> [PageRef]
pageRefsFromRoot parent objs =
  case findObjsByRef parent objs of
    Just os -> case findDictOfType "/Catalog" os of
      Just dict -> case findPages dict of
        Just pr -> pageRefsFromRoot pr objs
        Nothing -> []
      Nothing -> case findDictOfType "/Pages" os of
        Just dict -> case findKids dict of
          Just kidsrefs -> concatMap (\k -> pageRefsFromRoot k objs) kidsrefs
          Nothing -> []
        Nothing -> case findDictOfType "/Page" os of
          Just _ -> [parent]
          Nothing -> []
    Nothing -> []

paraBBox :: [Line] -> Rect
paraBBox [] = Rect 0 0 0 0
paraBBox ls =
  Rect
    { rectX0 = minimum (map lineInlineStart ls)
    , rectY0 = minimum (map (\l -> lineBaseline l - lineSize l) ls)
    , rectX1 = maximum (map lineInlineEnd ls)
    , rectY1 = maximum (map lineBaseline ls)
    }