packages feed

streaming-png-0.1.0.0: src/Codec/Picture/Png/Streaming/MainData.hs

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

-}
{-# LANGUAGE RecordWildCards #-}
module Codec.Picture.Png.Streaming.MainData
       ( decodeImageData )
       where

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

import           Control.Monad.Catch                (MonadThrow (..))
import           Control.Monad.IO.Class             (MonadIO (..))

import qualified Data.ByteString                    as B
import qualified Data.ByteString.Unsafe             as B
import           Data.Functor.Identity              (Identity (..))
import           Data.Functor.Sum                   (Sum (..))
import           Data.Int                           (Int64)
import qualified Data.Vector.Storable               as Vec
import qualified Data.Vector.Storable.Mutable       as Vec
import           Data.Word                          (Word64)
import           Data.Foldable                          (minimumBy)
import           Data.Ord                          (comparing)

import           Data.ByteString.Streaming          (ByteString)
import qualified Data.ByteString.Streaming          as Q
import           Streaming                          (Of (..), Stream)
import qualified Streaming                          as S

--------------------------------------------------------------------------------
-- Main function

{-|
Given a stream of PNG chunks, decode the image data from it. The resulting bytes
can be directly interpreted as pixels, whose format depends on the image's
colour type.
-}
decodeImageData
  :: (MonadThrow m, MonadIO m)
     => HeaderData
     -> Stream (PNGChunk m) m r
     -> ByteString m r
decodeImageData hd@HeaderData{..} =
  let prevByteDistance =
        case getBitsPerPixel hdBitDepth hdColourType of
          Just bpp -> (fromIntegral bpp + 4) `div` 8
          Nothing -> 1
  in Q.fromChunks
    . reconstructScanlines prevByteDistance
    . splitImageDataByScanlines hd
    . getDecompressedImageData

--------------------------------------------------------------------------------
-- Internals

{- |

Given a chunk type and a mapping function from a streaming ByteString to some
other functor, returns a filtering function suitable for passing to
'filterMapped' which accepts only the specified chunk type and applies the given
function to the bytes of the chunks of that type.

-}
mapChunkOfType
  :: (Monad m, Functor f)
     => ChunkType
     -> (ByteString m r -> m (f r))
     -> PNGChunk m r
     -> m (Sum Identity f r)
mapChunkOfType desiredType f PNGChunk{..}
  | chunkType == desiredType = InR <$> f chunkData
  | otherwise = InL . Identity <$> Q.effects chunkData

-- | From a raw stream of PNG chunks, filter out the IDAT chunks and decompress
-- them.
getDecompressedImageData
  :: (MonadIO m, MonadThrow m)
     => Stream (PNGChunk m) m r
     -> ByteString m r
getDecompressedImageData
  = decompressStream
  . Q.concat
  . filterMapped (mapChunkOfType ctIDAT return)

-- | Returns the number of bytes per scanline in a PNG with the given header, so
-- long as the colour type and bit depth fields are compatible with each other.
getScanlineLengthBytes :: HeaderData -> Maybe Int64
getScanlineLengthBytes HeaderData{..}
  = (1 +) -- Each scanline is preceded by a filter type byte
  . ceiling
  . (/ (8.0 :: Float))
  . fromIntegral
  . (* (fromIntegral hdWidth :: Word64))
  . fromIntegral
  <$> getBitsPerPixel hdBitDepth hdColourType

-- | Split a stream of raw, decompressed, PNG image data into scanlines.
splitImageDataByScanlines
  :: (MonadThrow m)
     => HeaderData
     -> ByteString m r
     -> Stream (ByteString m) m r
splitImageDataByScanlines hd@HeaderData{..} input = S.effect $
  do scanlineLength <-
       maybe (throwM UnsupportedColourType) return (getScanlineLengthBytes hd)

     return (chunksOfBS scanlineLength input)

type UnfilteredScanline = B.ByteString
type FilteredScanline = B.ByteString

-- | Given a stream of filtered scanlines, reconstruct each of them.
reconstructScanlines
  :: (MonadThrow m)
     => Int
     -> Stream (ByteString m) m r
     -> Stream (Of UnfilteredScanline) m r
reconstructScanlines prevByteDistance
  = mapWithMemory (reconstructScanline prevByteDistance)
  . S.mapped Q.toStrict

{- |

Given a filtered scanline (whose first byte encodes the filter method), along
with the previous unfiltered scanline (which is 'Nothing' if we're at the first
scanline), reconstruct the pixel data. Fails at runtime, possible even with a
segfault, if either input scanline is the wrong size.

This is a giant ugly mess, but it's fast. Can it be refactored into something
nicer while retaining its speed?

Also, it still takes up roughly 50% of the running time of decoding a PNG. Maybe
it can be made even faster?
-}
reconstructScanline
  :: (MonadThrow m)
     => Int
     -> Maybe UnfilteredScanline
     -> FilteredScanline
     -> m UnfilteredScanline
reconstructScanline prevByteDistance mprev filteredLine
  | Just (filterType, this) <- B.uncons filteredLine =
      let lenThis = B.length this

          -- This function is equivalent to @forM_ [0 .. lenThis - 1]@, but
          -- slightly faster in my benchmarks
          loop :: Monad m => (Int -> m ()) -> m ()
          loop action = go 0
            where
              go n | n < lenThis = do action n; go (n + 1)
                   | otherwise = return ()
          {-# INLINE loop #-}

          noFilter = this

          subFilter = vectorToBytestring $ Vec.create $
            do v <- Vec.new lenThis
               loop $ \i ->
                 do a <- if i >= prevByteDistance
                         then Vec.read v (i - prevByteDistance)
                         else return 0
                    Vec.write v i (B.unsafeIndex this i + a)
               return v

          upFilter =
            case mprev of
              Just prev ->
                vectorToBytestring $ Vec.generate lenThis $ \i ->
                  B.unsafeIndex prev i + B.unsafeIndex this i
              Nothing -> this

          averageFilter = vectorToBytestring $ Vec.create $
            case mprev of
              Just prev ->
                do v <- Vec.new lenThis
                   loop $ \i ->
                     do a <- if i >= prevByteDistance
                             then Vec.read v (i - prevByteDistance)
                             else return 0
                        Vec.write v i (reconAverage a (B.unsafeIndex prev i) (B.unsafeIndex this i))
                   return v
              Nothing ->
                do v <- Vec.new lenThis
                   loop $ \i ->
                     do a <- if i >= prevByteDistance
                             then Vec.read v (i - prevByteDistance)
                             else return 0
                        Vec.write v i (reconAverage a 0 (B.unsafeIndex this i))
                   return v

          paethFilter = vectorToBytestring $ Vec.create $
            case mprev of
              Just prev ->
                do v <- Vec.new lenThis
                   loop $ \i ->
                     do (a, c) <- if i >= prevByteDistance
                                  then do a <- Vec.read v (i - prevByteDistance)
                                          return (a, B.unsafeIndex prev (i - prevByteDistance))
                                  else return (0, 0)
                        Vec.write v i (reconPaeth a (B.unsafeIndex prev i) c (B.unsafeIndex this i))
                   return v
              Nothing ->
                do v <- Vec.new lenThis
                   loop $ \i ->
                     do a <- if i >= prevByteDistance
                             then Vec.read v (i - prevByteDistance)
                             else return 0
                        Vec.write v i (reconPaeth a 0 0 (B.unsafeIndex this i))
                   return v

          res | filterType == 0 = noFilter
              | filterType == 1 = subFilter
              | filterType == 2 = upFilter
              | filterType == 3 = averageFilter
              | filterType == 4 = paethFilter
              | otherwise = mempty

         in if B.length res == lenThis
            then return res
            else throwM (UnsupportedFilterType filterType)

  | otherwise = error "reconstructScanline: empty input"

filterScanlineFixed
  :: Int
  -> Maybe FilteredScanline
  -> UnfilteredScanline
  -> FilterType
  -> FilteredScanline
filterScanlineFixed prevByteDistance mprev this filterType =
  let lenThis = B.length this

      -- This function is equivalent to @forM_ [0 .. lenThis - 1]@, but
      -- slightly faster in my benchmarks
      loop :: Monad m => (Int -> m ()) -> m ()
      loop action = go 0
        where
          go n | n < lenThis = do action n; go (n + 1)
               | otherwise = return ()
      {-# INLINE loop #-}

      noFilter = this

      subFilter = vectorToBytestring $ Vec.create $
        do v <- Vec.new lenThis
           loop $ \i ->
             do a <- if i >= prevByteDistance
                     then Vec.read v (i - prevByteDistance)
                     else return 0
                Vec.write v i (B.unsafeIndex this i - a)
           return v

      upFilter =
        case mprev of
          Just prev ->
            vectorToBytestring $ Vec.generate lenThis $ \i ->
              B.unsafeIndex this i - B.unsafeIndex prev i
          Nothing -> this

      averageFilter = vectorToBytestring $ Vec.create $
        case mprev of
          Just prev ->
            do v <- Vec.new lenThis
               loop $ \i ->
                 do a <- if i >= prevByteDistance
                         then Vec.read v (i - prevByteDistance)
                         else return 0
                    Vec.write v i (filterAverage a (B.unsafeIndex prev i) (B.unsafeIndex this i))
               return v
          Nothing ->
            do v <- Vec.new lenThis
               loop $ \i ->
                 do a <- if i >= prevByteDistance
                         then Vec.read v (i - prevByteDistance)
                         else return 0
                    Vec.write v i (filterAverage a 0 (B.unsafeIndex this i))
               return v

      paethFilter = vectorToBytestring $ Vec.create $
        case mprev of
          Just prev ->
            do v <- Vec.new lenThis
               loop $ \i ->
                 do (a, c) <- if i >= prevByteDistance
                              then do a <- Vec.read v (i - prevByteDistance)
                                      return (a, B.unsafeIndex prev (i - prevByteDistance))
                              else return (0, 0)
                    Vec.write v i (filterPaeth a (B.unsafeIndex prev i) c (B.unsafeIndex this i))
               return v
          Nothing ->
            do v <- Vec.new lenThis
               loop $ \i ->
                 do a <- if i >= prevByteDistance
                         then Vec.read v (i - prevByteDistance)
                         else return 0
                    Vec.write v i (filterPaeth a 0 0 (B.unsafeIndex this i))
               return v

      res | filterType == 0 = noFilter
          | filterType == 1 = subFilter
          | filterType == 2 = upFilter
          | filterType == 3 = averageFilter
          | filterType == 4 = paethFilter
          | otherwise = error "filterScanlineFixed: bad filter type"

  in res

-- | Calculate the absolute value of the signed sum of the input bytes.
absSumBytes :: B.ByteString -> Int64
absSumBytes = abs . B.foldl' addSigned 0
  where addSigned a b = a + fromIntegral b - 128

data FilterHeuristic
  = FixedFilter FilterType
  | MinAbsSum

filterScanline
  :: FilterHeuristic
  -> Int
  -> Maybe FilteredScanline
  -> UnfilteredScanline
  -> FilteredScanline
filterScanline (FixedFilter filterType) pbd mprev this = filterScanlineFixed pbd mprev this filterType
filterScanline MinAbsSum pbd mprev this =
  let differentMethods = map (filterScanlineFixed pbd mprev this) [0..4]
  in minimumBy (comparing absSumBytes) differentMethods