{-# LANGUAGE CPP #-} module Sound.Jammit.Internal.Image ( partsToPages , jpegsToPDF ) where import qualified Codec.Picture as P import Codec.Picture.Jpg (encodeJpegAtQuality) import Codec.Picture.Types (convertImage) import Control.Exception (bracket) import Control.Monad (forM_, replicateM) import Control.Monad.IO.Class (MonadIO (..)) import qualified Data.ByteString as B import qualified Data.ByteString.Lazy as BL import Data.Conduit ((.|)) import qualified Data.Conduit as C import Data.Conduit.List (consume) import Data.Maybe (catMaybes) import qualified Data.Vector.Storable as V import Foreign import Foreign.C #ifdef WINDOWS import System.Win32.Info (getShortPathName) #endif -- data PDFInfo data PDFDoc data PDFObject -- foreign import ccall unsafe "pdf_create" -- pdf_create :: CInt -> CInt -> Ptr PDFInfo -> IO (Ptr PDFDoc) foreign import ccall unsafe "pdf_create_nostruct" pdf_create_nostruct :: CInt -> CInt -> CString -> CString -> CString -> CString -> CString -> CString -> IO (Ptr PDFDoc) foreign import ccall unsafe "pdf_append_page" pdf_append_page :: Ptr PDFDoc -> IO (Ptr PDFObject) foreign import ccall unsafe "pdf_page_set_size" pdf_page_set_size :: Ptr PDFDoc -> Ptr PDFObject -> CInt -> CInt -> IO CInt -- foreign import ccall unsafe "pdf_add_jpeg" -- pdf_add_jpeg :: Ptr PDFDoc -> Ptr PDFObject -> CInt -> CInt -> CInt -> CInt -> CString -> IO CInt foreign import ccall unsafe "pdf_add_jpeg_direct" pdf_add_jpeg_direct :: Ptr PDFDoc -> Ptr PDFObject -> CInt -> CInt -> CInt -> CInt -> CInt -> CInt -> Ptr Word8 -> CSize -> IO CInt foreign import ccall unsafe "pdf_save" pdf_save :: Ptr PDFDoc -> CString -> IO CInt foreign import ccall unsafe "pdf_destroy" pdf_destroy :: Ptr PDFDoc -> IO () foreign import ccall unsafe "pdf_get_err" pdf_get_err :: Ptr PDFDoc -> Ptr CInt -> IO CString loadPNG :: FilePath -> IO (P.Image P.PixelRGB8) loadPNG fp = do Right dyn <- P.readImage fp return $ P.convertRGB8 dyn pngChunks :: (MonadIO m) => Int -> [FilePath] -> C.ConduitT () (P.Image P.PixelRGB8) m () pngChunks h fps = let raw :: (MonadIO m) => C.ConduitT () (P.Image P.PixelRGB8) m () raw = mapM_ (\fp -> liftIO (loadPNG fp) >>= C.yield) fps chunk :: (Monad m) => C.ConduitT (P.Image P.PixelRGB8) (P.Image P.PixelRGB8) m () chunk = C.await >>= \x -> case x of Nothing -> return () Just page -> case span (\c -> P.imageHeight c == h) $ vertSplit h page of (full, [] ) -> mapM_ C.yield full >> chunk (full, part) -> mapM_ C.yield full >> C.await >>= \y -> case y of Nothing -> mapM_ C.yield part Just page' -> C.leftover (vertConcat $ part ++ [page']) >> chunk in raw .| chunk chunksToPages :: (Monad m) => Int -> C.ConduitT [P.Image P.PixelRGB8] (P.Image P.PixelRGB8) m () chunksToPages n = fmap catMaybes (replicateM n C.await) >>= \systems -> case systems of [] -> return () _ -> C.yield (vertConcat $ concat systems) >> chunksToPages n partsToPages :: [([FilePath], Integer)] -- ^ [(images, system height)] -> Int -- ^ systems per page -> IO [P.Image P.PixelRGB8] partsToPages [] _ = return [] partsToPages parts n = let sources = map (\(imgs, h) -> pngChunks (fromIntegral h) imgs) parts in C.runConduit $ C.sequenceSources sources .| chunksToPages n .| consume vertConcat :: [P.Image P.PixelRGB8] -> P.Image P.PixelRGB8 vertConcat [] = P.Image 0 0 V.empty -- efficient version: all images have same width, just concat vectors vertConcat allimgs@(img : imgs) | all (\i -> P.imageWidth i == P.imageWidth img) imgs = P.Image { P.imageWidth = P.imageWidth img , P.imageHeight = sum $ map P.imageHeight allimgs , P.imageData = V.concat $ map P.imageData allimgs } -- this algorithm is probably not needed vertConcat imgs = P.generateImage f w h where w = foldr max 0 $ map P.imageWidth imgs h = sum $ map P.imageHeight imgs f = go imgs empty = P.PixelRGB8 0 0 0 go [] _ _ = empty go (i : is) x y = if y < P.imageHeight i then if x < P.imageWidth i then P.pixelAt i x y else empty else go is x $ y - P.imageHeight i vertSplit :: Int -> P.Image P.PixelRGB8 -> [P.Image P.PixelRGB8] vertSplit h img = if P.imageHeight img <= h then [img] else let chunkSize = P.pixelBaseIndex img 0 h first = P.Image { P.imageWidth = P.imageWidth img , P.imageHeight = h , P.imageData = V.take chunkSize $ P.imageData img } rest = P.Image { P.imageWidth = P.imageWidth img , P.imageHeight = P.imageHeight img - h , P.imageData = V.drop chunkSize $ P.imageData img } in first : vertSplit h rest jpegsToPDF :: [P.Image P.PixelRGB8] -> FilePath -> IO () jpegsToPDF [] _ = return () -- Buddy Rich "Love for Sale" Kick channel jpegsToPDF jpegs pdf = let inch i = round $ (i :: Rational) * 72 pageWidth = inch 8.5 pageHeight = inch 11 in withCString "" $ \mt -> do bracket (pdf_create_nostruct pageWidth pageHeight mt mt mt mt mt mt) pdf_destroy $ \doc -> do let check fn = fn >>= \ret -> case ret of 0 -> return () e -> do str <- pdf_get_err doc nullPtr >>= peekCString error $ "PDF generation error (" <> show e <> "): " <> str forM_ jpegs $ \jpeg@(P.Image w h _) -> do let thisHeight = round $ (toRational h / toRational w) * toRational pageWidth page <- pdf_append_page doc check $ pdf_page_set_size doc page pageWidth thisHeight let bs = encodeJpegAtQuality 100 $ convertImage jpeg check $ B.useAsCStringLen (BL.toStrict bs) $ \(p, len) -> pdf_add_jpeg_direct doc page (fromIntegral w) (fromIntegral h) 0 0 pageWidth thisHeight (castPtr p) (fromIntegral len) #ifdef WINDOWS B.writeFile pdf B.empty -- file has to exist before you get short name pdf' <- getShortPathName pdf #else let pdf' = pdf #endif check $ withCString pdf' $ pdf_save doc