streaming-png-0.1.0.0: src/Codec/Picture/Png/Streaming/Core.hs
{-|
Module : Codec.Picture.Png.Streaming.Core
Copyright : (c) Bradley Hardy 2016
License: LGPL3
Maintainer: bradleyhardy@live.com
Stability: experimental
Portability: non-portable
-}
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE RecordWildCards #-}
module Codec.Picture.Png.Streaming.Core
( ChunkType
, ChunkLength
, PNGDecodeError(..)
, PNGEncodeError(..)
, PNGChunk(..)
, decodePNGChunks
, encodePNGChunks
, decodeChunk
)
where
import Streaming.CRC
import Codec.Picture.Png.Streaming.Info
import Codec.Picture.Png.Streaming.Util
import Control.Monad (join, when)
import Control.Monad.Catch (Exception, MonadThrow (..))
import Control.Monad.Trans (lift)
import qualified Data.ByteString as B
import Data.Int (Int64)
import qualified Data.Serialize as C
import Data.Word (Word32)
import Data.ByteString.Streaming (ByteString)
import qualified Data.ByteString.Streaming as Q
import Streaming (Stream)
import qualified Streaming as S
import Streaming.Prelude (Of (..))
--------------------------------------------------------------------------------
-- Types
-- | A raw chunk of PNG data, containing the length, type and a streaming
-- 'ByteString' of data.
data PNGChunk m r =
PNGChunk
{ chunkLength :: ChunkLength
, chunkType :: ChunkType
, chunkData :: ByteString m r
}
deriving (Functor)
-- | The type of errors that might arise when decoding a PNG.
data PNGDecodeError
= IncorrectSignature
| CRCMismatch
| UnexpectedEOF
| BadChunkSize ChunkType
| UnexpectedChunk ChunkType
| UnsupportedImageType
| UnsupportedColourType
| UnsupportedFilterType FilterType
| ExpectedEOF
| UnknownError String
deriving (Show)
instance Exception PNGDecodeError where
-- | The type of errors that might arise when encoding a PNG.
data PNGEncodeError
= IncorrectLength
deriving (Show)
instance Exception PNGEncodeError where
--------------------------------------------------------------------------------
-- Raw decode and encode functions
-- | Fully decode a PNG to its raw streaming representation, returning the PNG
-- data chunks until "IEND" is reached. The return value is the (potentially
-- empty) rest of the 'ByteString' after the PNG.
{-|
Decode a stream of individual PNG chunks from a raw streaming 'ByteString'
input. The return value of the resulting stream is any remaining data after
encountering the final "IEND" chunk.
-}
decodePNGChunks :: (MonadThrow m) => ByteString m r -> Stream (PNGChunk m) m (ByteString m r)
decodePNGChunks = S.unfold decodeChunk . checkSig
-- | Encode a stream of individual PNG chunks into a raw streaming 'ByteString'.
encodePNGChunks :: (MonadThrow m) => Stream (PNGChunk m) m r -> ByteString m r
encodePNGChunks png =
do Q.fromStrict pngSignature
res <- S.iterTM (join . encodeChunk) png
encodeChunk chunkIEND
return res
--------------------------------------------------------------------------------
-- Decoding and encoding individual chunks
-- | Decode a chunk of a PNG file, returning the raw binary data in the chunk
-- followed by the rest of the input; or the remaining data in the 'ByteString'
-- if we have reached the "IEND" chunk.
decodeChunk
:: (MonadThrow m)
=> ByteString m r
-> m (Either (ByteString m r) (PNGChunk m (ByteString m r)))
decodeChunk input =
do -- The first 4 bytes tell us the length of the chunk
lenBS :> input' <- Q.toStrict $ Q.splitAt 4 input
-- Read the length (which is in big endian) using 'cereal',
-- throwing an error if it can't be read.
chunkLength <- readWord32 lenBS
-- Split at the end of the chunk, and calculate the CRC of the data, while
-- copying the chunk data so that we can hand it to 'reader'.
let input'' = calcCRC32 $ Q.copy $ Q.splitAt (fromIntegral chunkLength + 4) input'
-- The next 4 bytes tell us the chunk type.
chunkType :> input''' <- Q.toStrict $ Q.splitAt 4 input''
-- The length of the chunk type can't be more than 4 because we split at 4,
-- but if it's less then we have encountered an error.
when (B.length chunkType < 4) (throwM UnexpectedEOF)
-- Use the provided 'reader' function to read the input, while
-- simultaneously calculating the CRC of the data.
let readData = Q.splitAt (fromIntegral chunkLength) input'''
-- Discard the rest of the first ByteString in the result, as it's always
-- empty, then make sure we fail with an error if the CRC is wrong.
chunkData = checkCRC <$> Q.drained readData
if chunkType /= ctIEND
then return $ Right PNGChunk{..}
else Left <$> Q.effects chunkData
-- | Encode a chunk of a PNG file with a specified length and chunk type. The
-- input 'PNGChunk' must not lie about its length. If it does, an
-- 'IncorrectLength' error will be thrown. It is important to know the length
-- at the head of the chunk to maintain streaming.
encodeChunk
:: (MonadThrow m)
=> PNGChunk m r -- ^ The data segment to write.
-> ByteString m r -- ^ The resulting data encoded as a PNG chunk.
encodeChunk PNGChunk{..} =
do let lenBS = C.runPut . C.putWord32be $ chunkLength
-- write the
Q.fromStrict lenBS
let -- restrict the length of the input to the given data length as above
restrictedLen = checkLength (fromIntegral chunkLength) chunkData
-- tack the chunk type bytes to the front
-- it is important to do this BEFORE calculating the CRC, because the
-- CRC applies to the chunk type too (but not to the length)
withChunkType = Q.fromStrict chunkType >> restrictedLen
-- calculate the CRC and write that to the end
appendCRC32 withChunkType
--------------------------------------------------------------------------------
-- Extra helper functions
-- | Check the length of a ByteString against an expected length. Running the
-- resulting 'ByteString' to the end will throw an error if it is not exactly the
-- right length.
checkLength
:: MonadThrow m
=> Int64
-> ByteString m r
-> ByteString m r
checkLength expectedLength input =
do (len :> rest) <- Q.length $ Q.copy $ Q.splitAt expectedLength input
when (fromIntegral len /= expectedLength) $
lift $ throwM IncorrectLength
lift $ expectNull IncorrectLength rest
-- | Read 32 bits from a 'B.ByteString' into a 'Word32'. Throws an
-- 'UnexpectedEOF' exception if the bits can't be read.
readWord32 :: MonadThrow m => B.ByteString -> m Word32
readWord32 bs =
case C.runGet C.getWord32be bs of
Left _ -> throwM UnexpectedEOF
Right x -> return (fromIntegral x)
-- | Given an expected CRC and a 'ByteString' whose first 4 bytes should match
-- that CRC, check if there is a match. If not, throw a 'CRCMismatch' error. If
-- it matches, return the remainder of the 'ByteString'.
checkCRC :: MonadThrow m => Of Word32 (ByteString m r) -> ByteString m r
checkCRC (dataCRC :> bytes) = Q.mwrap $
do (crcBS :> bytes') <- Q.toStrict $ Q.splitAt 4 bytes
crc <- readWord32 crcBS
if crc == dataCRC
then return bytes'
else throwM CRCMismatch
-- | Check the fixed PNG signature, return the rest of the input after the
-- signature.
checkSig :: MonadThrow m => ByteString m r -> ByteString m r
checkSig input = Q.mwrap $ do
(header :> remainder) <- Q.toStrict $ Q.splitAt 8 input
if header == pngSignature
then return remainder
else throwM IncorrectSignature
chunkIEND :: (Monad m) => PNGChunk m ()
chunkIEND =
let chunkLength = 0
chunkType = ctIEND
chunkData = mempty
in PNGChunk{..}