packages feed

pdf-toolbox-core-0.1.1: lib/Pdf/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 'writeHeader',
-- then a number of 'writeObject' and finally 'writeXRefTable'
-- or `writeXRefStream`.
--
-- To incrementally update PDF file just omit the
-- `writeHeader` and append the result to the existent file.
-- Make sure to use `writeXRefTable` if the original file uses xref table,
-- or use `writeXRefStream` if it uses xref stream.

module Pdf.Core.Writer
( Writer
, makeWriter
, writeHeader
, writeObject
, writeStream
, deleteObject
, writeXRefTable
, writeXRefStream
)
where

import Pdf.Core.Object
import Pdf.Core.Object.Builder

import Data.IORef
import Data.Int
import qualified Data.Vector as Vector
import Data.Set (Set)
import qualified Data.Set as Set
import qualified Data.HashMap.Strict as HashMap
import Data.ByteString (ByteString)
import qualified Data.ByteString.Lazy as BSL
import Data.ByteString.Builder
import Data.Function
import Control.Monad
import System.IO.Streams (OutputStream)
import qualified System.IO.Streams as Streams

newtype Writer = Writer {toStateRef :: IORef State}

makeWriter :: OutputStream ByteString -> IO Writer
makeWriter output = do
  (out, count) <- Streams.countOutput output
  let emptyState = State {
        stOutput = out,
        stObjects = Set.empty,
        stCount = count,
        stOffset = 0
        }
  Writer <$> newIORef 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 State = State {
  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
writeHeader :: Writer -> IO ()
writeHeader writer = do
  st <- readIORef (toStateRef writer)
  Streams.write (Just "%PDF-1.7\n") (stOutput st)

-- | Write object
writeObject :: Writer -> Ref -> Object -> IO ()
writeObject writer ref@(R index gen) obj = do
  pos <- countWritten writer
  st <- readIORef (toStateRef writer)
  addElem writer $ Elem index gen pos False
  dumpObject (stOutput st) ref obj

-- | Write stream
writeStream :: Writer -> Ref -> Dict -> BSL.ByteString -> IO ()
writeStream writer ref@(R index gen) dict dat = do
  pos <- countWritten writer
  st <- readIORef (toStateRef writer)
  addElem writer $ Elem index gen pos False
  dumpStream (stOutput st) ref dict dat

-- | Delete object
deleteObject :: Writer -> Ref -> Int64 -> IO ()
deleteObject writer (R index gen) nextFree =
  addElem writer $ Elem index gen nextFree True

-- | Write xref table. Should be the last call.
-- Used for generating and incremental updates.
--
-- Note that when doing incremental update you should use this function
-- only if the original PDF file has xref table. If it has xref stream,
-- then use `writeXRefStream`.
writeXRefTable
  :: Writer
  -> Int64    -- ^ size of the original PDF file. Should be 0 for new file
  -> Dict     -- ^ trailer
  -> IO ()
writeXRefTable writer offset tr = do
  off <- (+ offset) <$> countWritten writer
  st <- readIORef (toStateRef writer)
  let elems = Set.mapMonotonic (\e -> e {elemOffset = elemOffset e + offset})
            $ stObjects st
      content = mconcat
        [ byteString "xref\n"
        , buildXRefTable (Set.toAscList elems)
        , byteString "trailer\n"
        , buildDict tr
        , byteString "\nstartxref\n"
        , int64Dec off
        , byteString "\n%%EOF\n"
        ]
  Streams.writeLazyByteString (toLazyByteString content) (stOutput st)

-- | Write xref stream. Should be the last call.
-- Used for generating and incremental updates.
--
-- Note that when doing incremental update you should use this function
-- only if the original PDF file has xref stream. If it has xref table,
-- then use `writeXRefTable`.
--
-- This function will update/delete the following keys in the trailer:
-- Type, W, Index, Filter, Length.
writeXRefStream
  :: Writer
  -> Int64    -- ^ size of the original PDF file. Should be 0 for new file
  -> Ref
  -> Dict     -- ^ trailer
  -> IO ()
writeXRefStream writer offset ref@(R index gen) tr = do
  pos <- countWritten writer
  addElem writer $ Elem index gen pos False
  st <- readIORef (toStateRef writer)
  let elems = Set.mapMonotonic (\e -> e {elemOffset = elemOffset e + offset})
            $ stObjects st
      off = pos + offset
      content = toLazyByteString $ buildXRefStream (Set.toAscList elems)
      dict
        = HashMap.insert "Type" (Name "XRef")
        . HashMap.insert "W" (Array $ Vector.fromList $ map Number [1, 8, 8])
        . HashMap.insert "Index" (Array $ Vector.fromList $ map Number trIndex)
        . HashMap.insert "Length" (Number $ fromIntegral $ BSL.length content)
        . HashMap.delete "Filter"
        $ tr
      trIndex = concatMap sectionIndex (xrefSections (Set.toAscList elems))
      sectionIndex [] = error "impossible"
      sectionIndex s@(e:_) = map fromIntegral [elemIndex e, length s]
      end = mconcat
        [ "\nstartxref\n"
        , int64Dec off
        , "\n%%EOF\n"
        ]
  dumpStream (stOutput st) ref dict content
  Streams.writeLazyByteString (toLazyByteString end) (stOutput st)

countWritten :: Writer -> IO Int64
countWritten writer = do
  st <- readIORef (toStateRef writer)
  c <- (stOffset st +) <$> stCount st
  writeIORef (toStateRef writer) st{stOffset = c}
  return $! c

addElem :: Writer -> Elem -> IO ()
addElem writer e = do
  st <- readIORef (toStateRef writer)
  when (Set.member e $ stObjects st) $
    error $ "Writer: attempt to write object with the same index: " ++ show (elemIndex e)
  writeIORef (toStateRef writer) $ st
    { stObjects = Set.insert e $ stObjects st
    }

dumpObject :: OutputStream ByteString -> Ref -> Object -> IO ()
dumpObject out ref o =
  Streams.writeLazyByteString
    (toLazyByteString $ buildIndirectObject ref o)
    out

dumpStream :: OutputStream ByteString -> Ref -> Dict -> BSL.ByteString -> IO ()
dumpStream out ref dict dat =
  Streams.writeLazyByteString
    (toLazyByteString $ buildIndirectStream ref dict dat) out

buildXRefTable :: [Elem] -> Builder
buildXRefTable entries =
  mconcat (map buildXRefTableSection $ xrefSections entries)

xrefSections :: [Elem] -> [[Elem]]
xrefSections [] = []
xrefSections xs = let (s, rest) = xrefSection xs in s : xrefSections rest

xrefSection :: [Elem] -> ([Elem], [Elem])
xrefSection [] = error "impossible"
xrefSection (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)

buildXRefTableSection :: [Elem] -> Builder
buildXRefTableSection [] = error "impossible"
buildXRefTableSection s@(e:_) = mconcat
  [ intDec (elemIndex e)
  , char7 ' '
  , intDec (length s)
  , char7 '\n'
  , loop s
  ]
  where
  loop (x:xs) = mconcat
    [ buildFixed 10 '0' (elemOffset x)
    , char7 ' '
    , buildFixed 5 '0' (elemGen x)
    , char7 ' '
    , char7 (if elemFree x then 'f' else 'n')
    , string7 "\r\n"
    ] `mappend` loop xs
  loop [] = mempty

buildXRefStream :: [Elem] -> Builder
buildXRefStream entries =
  mconcat (map buildXRefStreamSection $ xrefSections entries)

buildXRefStreamSection :: [Elem] -> Builder
buildXRefStreamSection = mconcat . map buildOne
  where
  buildOne e =
    let (tp, field1, field2) = if elemFree e
          then (0, 0, succ (elemGen e))
          else (1, elemOffset e, elemGen e)
    in mconcat
      [ int8 tp
      , int64BE field1
      , int64BE (fromIntegral field2)
      ]

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