pdf-toolbox-core-0.0.1.0: lib/Pdf/Toolbox/Core/Writer.hs
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE OverloadedStrings #-}
-- | Write PDF files
--
-- It could be used to generate new PDF file
-- or to incrementally update the existent one
--
-- To generate new file, first call 'writePdfHeader',
-- then a number of 'writeObject' and finally 'writeXRefTable'
--
-- To incrementally update PDF file just ommit the
-- `writePdfHeader` and append the result to the existent file
module Pdf.Toolbox.Core.Writer
(
PdfWriter,
runPdfWriter,
writePdfHeader,
writeObject,
deleteObject,
writeXRefTable
)
where
import Data.Int
import Data.Set (Set)
import qualified Data.Set as Set
import Data.ByteString (ByteString)
import qualified Data.ByteString.Lazy as BSL
import Data.ByteString.Lazy.Builder
import Data.ByteString.Lazy.Builder.ASCII
import Data.Function
import Data.Monoid
import Control.Monad
import Control.Monad.Trans.Class
import Control.Monad.Trans.State
import Control.Monad.IO.Class
import System.IO.Streams (OutputStream)
import qualified System.IO.Streams as Streams
import Pdf.Toolbox.Core.Object.Types
import Pdf.Toolbox.Core.Object.Builder
-- | The monad
newtype PdfWriter m a = PdfWriter (StateT PdfState m a)
deriving (Monad, MonadIO, MonadTrans)
-- | Execute writer action
runPdfWriter :: MonadIO m
=> OutputStream ByteString -- ^ streams to write to
-> PdfWriter m a -- ^ action to run
-> m a
runPdfWriter output (PdfWriter action) = do
(out, count) <- liftIO $ Streams.countOutput output
let emptyState = PdfState {
stOutput = out,
stObjects = Set.empty,
stCount = count,
stOffset = 0
}
evalStateT action emptyState
data Elem = Elem {
elemIndex :: {-# UNPACK #-} !Int,
elemGen :: {-# UNPACK #-} !Int,
elemOffset :: {-# UNPACK #-} !Int64,
elemFree :: !Bool
}
instance Eq Elem where
(==) = (==) `on` elemIndex
instance Ord Elem where
compare = compare `on` elemIndex
data PdfState = PdfState {
stOutput :: OutputStream ByteString,
stObjects :: !(Set Elem),
stCount :: IO Int64,
stOffset :: {-# UNPACK #-} !Int64
}
-- | Write PDF header. Used for generating new PDF files.
-- Should be the first call. Not used fo incremental updates
writePdfHeader :: MonadIO m => PdfWriter m ()
writePdfHeader = do
output <- PdfWriter $ gets stOutput
liftIO $ Streams.write (Just "%PDF-1.7\n") output
-- | Write object
writeObject :: MonadIO m => Ref -> Object BSL.ByteString -> PdfWriter m ()
writeObject ref@(Ref index gen) obj = do
st <- PdfWriter get
pos <- countWritten
addElem $ Elem index gen pos False
dumpObject (stOutput st) ref obj
return ()
-- | Delete object
deleteObject :: MonadIO m => Ref -> Int64 -> PdfWriter m ()
deleteObject (Ref index gen) nextFree =
addElem $ Elem index gen nextFree True
-- | Write xref table. Should be the last call.
-- Used for generating and incremental updates.
writeXRefTable :: MonadIO m
=> Int64 -- ^ size of the original PDF file. Should be 0 for new file
-> Dict -- ^ trailer
-> PdfWriter m ()
writeXRefTable offset tr = do
st <- PdfWriter get
off <- (+ offset) `liftM` countWritten
let elems = Set.mapMonotonic (\e -> e {elemOffset = elemOffset e + offset}) $ stObjects st
content = byteString "xref\n" `mappend`
buildXRefTable (Set.toAscList elems) `mappend`
byteString "trailer\n" `mappend`
buildDict tr `mappend`
byteString "\nstartxref\n" `mappend`
int64Dec off `mappend`
byteString "\n%%EOF\n"
liftIO $ Streams.writeLazyByteString (toLazyByteString content) (stOutput st)
countWritten :: MonadIO m => PdfWriter m Int64
countWritten = do
st <- PdfWriter get
c <- (stOffset st +) `liftM` liftIO (stCount st)
PdfWriter $ put $ st {stOffset = c}
return $! c
addElem :: Monad m => Elem -> PdfWriter m ()
addElem e = do
st <- PdfWriter get
when (Set.member e $ stObjects st) $ error $ "PdfWriter: attempt to write object with the same index: " ++ show (elemIndex e)
PdfWriter $ put st {stObjects = Set.insert e $ stObjects st}
dumpObject :: MonadIO m => OutputStream ByteString -> Ref -> Object BSL.ByteString -> m ()
dumpObject out ref o = liftIO $ Streams.writeLazyByteString (toLazyByteString $ buildIndirectObject ref o) out
buildXRefTable :: [Elem] -> Builder
buildXRefTable entries =
mconcat (map buildXRefSection $ sections entries)
where
sections :: [Elem] -> [[Elem]]
sections [] = []
sections xs = let (s, rest) = section xs in s : sections rest
section [] = error "impossible"
section (x:xs) = go (elemIndex x + 1) [x] xs
where
go _ res [] = (reverse res, [])
go i res (y:ys) =
if i == elemIndex y
then go (i + 1) (y : res) ys
else (reverse res, y:ys)
buildXRefSection :: [Elem] -> Builder
buildXRefSection [] = error "impossible"
buildXRefSection s@(e:_) =
intDec (elemIndex e) `mappend`
char7 ' ' `mappend`
intDec (length s) `mappend`
char7 '\n' `mappend`
loop s
where
loop (x:xs) =
buildFixed 10 '0' (elemOffset x) `mappend`
char7 ' ' `mappend`
buildFixed 5 '0' (elemGen x) `mappend`
char7 ' ' `mappend`
char7 (if elemFree x then 'f' else 'n') `mappend`
string7 "\r\n" `mappend`
loop xs
loop [] = mempty
buildFixed :: Show a => Int -> Char -> a -> Builder
buildFixed len c i =
let v = take len $ show i
l = length v
in string7 $ replicate (len - l) c ++ v
{-
-- At attempt to do it directly with Set.
-- Actually uses 2x memory...
buildXRefTable :: Set Elem -> Builder
buildXRefTable elems
| Set.null elems = mempty
| otherwise = buildXRefSection elems
buildXRefSection :: Set Elem -> Builder
buildXRefSection elems =
intDec (elemIndex start) `mappend`
char7 ' ' `mappend`
intDec len `mappend`
char7 '\n' `mappend`
section `mappend`
buildXRefTable rest
where
(start, len, rest) = sectionLength elems
section = buildSection len elems
buildSection :: Int -> Set Elem -> Builder
buildSection 0 _ = mempty
buildSection l els =
let (x, xs) = Set.deleteFindMin els
in buildFixed 10 '0' (elemOffset x) `mappend`
char7 ' ' `mappend`
buildFixed 5 '0' (elemGen x) `mappend`
char7 ' ' `mappend`
char7 (if elemFree x then 'f' else 'n') `mappend`
string7 "\r\n" `mappend`
buildSection (l - 1) xs
sectionLength :: Set Elem -> (Elem, Int, Set Elem)
sectionLength els =
let (x, xs) = Set.deleteFindMin els
(count, rest) = go 1 (elemIndex x) xs
in (x, count, rest)
where
go n val xs
| Set.null xs = (n, xs)
| otherwise = let (next, rest) = Set.deleteFindMin xs
in if elemIndex next == val + 1
then go (n + 1) (val + 1) rest
else (n, xs)
-}