{-|
Module : Codec.Picture.Png.Streaming
Copyright : (c) Bradley Hardy 2016
License: LGPL3
Maintainer: bradleyhardy@live.com
Stability: experimental
Portability: non-portable

A perfectly streaming PNG decoding library.
-}

{-# LANGUAGE RecordWildCards #-}
module Codec.Picture.Png.Streaming
       (
         -- * Decoding
         decodePNG
       , decodePNGComplete
       , decodePNGFile
       , decodeHeader

         -- * Types
       , PNGDecodeError(..)
       , DecodedPNG
       , HeaderData(..)

         -- * Misc
       , ChunkType
       , BitDepth
       , ColourType
       , CompressionMethod
       , FilterMethod
       , FilterType
       , InterlaceMethod

       , isColourTypeSupported
       , isCompressionMethodSupported
       , isFilterMethodSupported
       , isInterlaceMethodSupported
       )
       where

import           Codec.Picture.Png.Streaming.Core
import           Codec.Picture.Png.Streaming.Header
import           Codec.Picture.Png.Streaming.Info
import           Codec.Picture.Png.Streaming.MainData
import           Codec.Picture.Png.Streaming.Util

import           Control.Monad                        (unless)
import           Control.Monad.Catch                  (MonadThrow (..))
import           Control.Monad.IO.Class               (MonadIO (..))
import           Control.Monad.Trans                  (MonadTrans (..))
import           Control.Monad.Trans.Resource         (MonadResource)

import           Data.ByteString.Streaming            (ByteString)
import qualified Data.ByteString.Streaming            as Q
import           Streaming.Prelude                    (Of (..))

-- | A decoded PNG: header information followed by decoded pixel data whose
-- format depends on the image's colour type and bit depth.
type DecodedPNG m r = Of HeaderData (ByteString m r)

{-|
Decode a PNG from the given raw streaming 'ByteString'. The result is header
information followed by a stream of bytes that can be interpreted directly as
pixels whose format depends on the image's colour type and bit depth.

Any remaining data after the end of the PNG image is returned untouched.
-}
decodePNG :: (MonadThrow m, MonadIO m) => ByteString m r -> m (DecodedPNG m (ByteString m r))
decodePNG input =
  do (hd :> rest) <- takeHeaderData (decodePNGChunks input)
     unless (isImageTypeSupported hd) (throwM UnsupportedImageType)
     return (hd :> decodeImageData hd rest)

{-|
Decode just the PNG header data from the given raw streaming 'ByteString',
inspecting the minimum number of bytes from the input necessary to do so. The
remaining bytes are returned also.
-}
decodeHeader :: (MonadThrow m) => ByteString m r -> m (Of HeaderData (ByteString m r))
decodeHeader input =
  do PNGChunk{..} <-
       either (const $ throwM UnexpectedEOF) return
       =<< decodeChunk input

     unless (chunkType == ctIHDR) (throwM (UnexpectedChunk chunkType))
     tryDecodeHeader chunkData

{-|
Decode a PNG from the given raw streaming 'ByteString'. The result is header
information followed by a stream of bytes that can be interpreted directly as
pixels whose format depends on the image's colour type and bit depth.

If there is any more data after the end of the PNG image, an 'ExpectedEOF'
exception is thrown.
-}
decodePNGComplete :: (MonadThrow m, MonadIO m) => ByteString m r -> m (DecodedPNG m r)
decodePNGComplete input =
  do (hd :> rest) <- decodePNG input
     let rest' = lift . expectNull ExpectedEOF =<< rest
     return (hd :> rest')

{-|
Decode a PNG from the given file. The result is header information followed by a
stream of bytes that can be interpreted directly as pixels whose format depends
on the image's colour type and bit depth.

If there is any more data after the end of the PNG image, an 'ExpectedEOF'
exception is thrown.
-}
decodePNGFile :: (MonadResource m) => FilePath -> m (DecodedPNG m ())
decodePNGFile = decodePNGComplete . Q.readFile

-- | Is the PNG image type described by the given header supported?
isImageTypeSupported :: HeaderData -> Bool
isImageTypeSupported HeaderData{..} =
  isColourTypeSupported hdColourType &&
  isCompressionMethodSupported hdCompressionMethod &&
  isFilterMethodSupported hdFilterMethod &&
  isInterlaceMethodSupported hdInterlaceMethod

-- | Is the given PNG image colour type supported? Currently we only support
-- non-indexed colour types.
isColourTypeSupported :: ColourType -> Bool
isColourTypeSupported = (`elem` [0, 2, 4, 6])

-- | Is the given PNG compression method supported? Currently we only support
-- method 0.
isCompressionMethodSupported :: CompressionMethod -> Bool
isCompressionMethodSupported = (== 0)

-- | Is the given PNG filter method supported? Currently we only support method
-- 0.
isFilterMethodSupported :: FilterMethod -> Bool
isFilterMethodSupported = (== 0)

-- | Is the given PNG interlace method supported? Currently only the null
-- interlace method (no interlacing) is supported.
isInterlaceMethodSupported :: InterlaceMethod -> Bool
isInterlaceMethodSupported = (== 0)