pngload (empty) → 0.1
raw patch · 10 files changed
+594/−0 lines, 10 filesdep +arraydep +basedep +bytestringsetup-changedbinary-added
Dependencies added: array, base, bytestring, haskell98, mtl, parsec, zlib
Files
- Codec/Image/PNG.hs +244/−0
- Codec/Image/PNG/Internal/CRC.hs +29/−0
- Codec/Image/PNG/Internal/Filters.hs +98/−0
- Codec/Image/PNG/Internal/Parser.hs +74/−0
- LICENSE +23/−0
- README +20/−0
- Setup.lhs +5/−0
- Test.hs +78/−0
- pngload.cabal +23/−0
- test_images/mustang.png binary
+ Codec/Image/PNG.hs view
@@ -0,0 +1,244 @@+{-# OPTIONS -fglasgow-exts #-}+-----------------------------------------------------------------------------+-- |+-- Module : Graphics.Formats.PNG+-- Copyright : (c) Marko Lauronen 2008+-- License : BSD+-- +-- Maintainer : marko.lauronen@pp1.inet.fi+-- Stability : experimental+-- Portability : non-portable (GHC only)+--+-- A simple, pure Haskell PNG loader. Currently supports 24bit RGB(A) images+-- with no interlacing. Also lacks support for color indexed (paletted) images.+--+-- The image is stored in a StorableArray for compatibility with OpenGL (the+-- array supports getting Ptr Word8 to the image data using withStorableArray+-- function).+--+-----------------------------------------------------------------------------+module Codec.Image.PNG+ (+ -- * Types+ PNGImage, Width, Height+ -- * Functions+ , loadPNGFile+ , dimensions+ --, pixelWidth+ , imageData+ , hasAlphaChannel+ ) where++import Codec.Compression.Zlib++import Text.Parsec.Combinator+import Text.Parsec.Prim++import Data.Array.Unboxed+import Data.Array.Storable+import Data.Word+import Data.List+import qualified Data.ByteString.Lazy as LB+import qualified Data.ByteString.Lazy.Char8 as C+import Data.Int+import Data.Char+import System.IO++import Control.Monad.Error++import Codec.Image.PNG.Internal.Parser+import Codec.Image.PNG.Internal.CRC+import Codec.Image.PNG.Internal.Filters++-- | Type for raw PNG chunks+-- The parsing happens in two phases: first the file is read into+-- raw chunks, then the raw chunks are parsed into actual PNGChunks+-- This is due to the CRC checksum that requires is easiest to compute+-- with raw chunk data.+data RawPNGChunk = RawPNGChunk {+ rawPngChunk_type :: !String,+ rawPngChunk_data :: !LB.ByteString+ } deriving (Show)++type Width = Word32+type Height = Word32+type Rgb = (Word8, Word8, Word8)++-- | The actual fully parsed chunk type+data PNGChunk =+ IHDR {+ ihdr_width :: !Width+ , ihdr_height :: !Height+ , ihdr_bitDepth :: !BitDepth+ , ihdr_colorType :: !ColorType+ , ihdr_compressionMethod :: !CompressionMethod+ , ihdr_filterMethod :: !FilterMethod+ , ihdr_interlaceMethod :: !InterlaceMethod }+ | PLTE {+ plte_entries :: !(Array Word8 Rgb) }+ | IDAT {+ idat_data :: !LB.ByteString }+ | UnknownChunk RawPNGChunk -- chunk types not supported yet+ | IEND+ deriving (Show)++data ColorType = Ct0 | Ct2 | Ct3 | Ct4 | Ct6 deriving (Show,Eq)+data BitDepth = Bd1 | Bd2 | Bd4 | Bd8 | Bd16 deriving (Show,Eq)+data CompressionMethod = Deflate deriving (Show,Eq)+data FilterMethod = Adaptive deriving (Show,Eq)+data InterlaceMethod = NoInterlace | Adam7 deriving (Show,Eq)++isIDAT :: PNGChunk -> Bool+isIDAT (IDAT _) = True+isIDAT _ = False++data PNGImage = PNGImage {+ pngImg_header :: !PNGChunk+ , pngImg_otherChunks :: ![PNGChunk]+ , pngImg_imageData :: !(StorableArray (Int,Int) Word8)+ }++instance Show PNGImage where+ show _ = "PNGImage"++-- | Raw chunk parsing++pngHeaderBytes :: LB.ByteString+pngHeaderBytes = LB.pack [137, 80, 78, 71, 13, 10, 26, 10]++pngFile :: Parser [RawPNGChunk]+pngFile = do+ string pngHeaderBytes+ hdr <- rawPngChunk+ when (rawPngChunk_type hdr /= "IHDR") $+ fail "expecting IHDR as the first chunk"+ rest <- many1 rawPngChunk+ return (hdr:rest)++rawPngChunk :: Parser RawPNGChunk+rawPngChunk = do+ len <- anyWord32+ chunkType <- block 4+ chunkData <- block (fromIntegral len)+ let expectedCrc = crc (LB.concat [chunkType,chunkData])+ word32 expectedCrc <?> "valid crc"+ return $ RawPNGChunk (C.unpack chunkType) chunkData++-- | Final chunk parsing++parsePlte :: Parser PNGChunk+parsePlte = do+ paletteEntries <- many1 paletteEntry+ return . PLTE $ listArray (0, fromIntegral (length paletteEntries-1)) paletteEntries+ where+ paletteEntry = liftM3 (,,) anyWord8 anyWord8 anyWord8++parseIhdr :: Parser PNGChunk+parseIhdr = do+ width <- anyWord32+ height <- anyWord32+ -- [(1,Bd1), (2,Bd2), (4,Bd4), (8,Bd8), (16,Bd16)]+ bitDepth <- allowedValues word8 [(8,Bd8)]+ <?> "valid bit depth (supported: Bd8)"+ --[(0,Ct0), (2,Ct2), (3,Ct3), (4,Ct4), (6,Ct6)]+ colorType <- allowedValues word8 [(2,Ct2), (6,Ct6)]+ <?> "valid colorType: supported Ct2,Ct6"+ compressionMethod <- allowedValues word8 [(0, Deflate)] + <?> "valid compression method: supported Deflate"+ filterMethod <- allowedValues word8 [(0, Adaptive)] + <?> "valid filter method: supported Adaptive"+ -- [(0, NoInterlace), (1, Adam7)] + interlaceMethod <- allowedValues word8 [(0, NoInterlace)] + <?> "valid interlace method: supported NoInterlace"+ return $ IHDR {+ ihdr_width = width+ , ihdr_height = height+ , ihdr_bitDepth = bitDepth+ , ihdr_colorType = colorType+ , ihdr_compressionMethod = compressionMethod+ , ihdr_filterMethod = filterMethod+ , ihdr_interlaceMethod = interlaceMethod+ }++-- | conversion from raw chunks to final chunks+toPngChunk :: RawPNGChunk -> Either String PNGChunk+toPngChunk raw =+ case chunkName of+ "IHDR" -> parseChunkData parseIhdr+ "PLTE" -> parseChunkData parsePlte+ "IEND" -> return IEND+ "IDAT" -> return $ IDAT (rawPngChunk_data raw)+ _ -> return $ UnknownChunk raw+ where+ parseChunkData a =+ case runP a () "" (rawPngChunk_data raw) of+ Left e -> fail $ "failed to parse chunk " ++ show chunkName ++ ", " ++ show e+ Right x -> return x+ chunkName = rawPngChunk_type raw++toPngImage :: [RawPNGChunk] -> IO (Either String PNGImage)+toPngImage chunks = do+ case mapM toPngChunk chunks >>= return . partition isIDAT of+ Right (_, []) -> return $ Left "File has no chunks!"+ Right (dataChunks, hdr:otherChunks) -> do+ let dataDecompressed = decompress . LB.concat . map idat_data $ dataChunks+ bpp = bytesPerPixel (ihdr_colorType hdr) (ihdr_bitDepth hdr)+ w = fromIntegral (ihdr_width hdr)+ h = fromIntegral (ihdr_height hdr)+ sls <- defilter_scanlines_arr (w,h) (fromIntegral bpp) dataDecompressed+ return $ Right (PNGImage hdr otherChunks sls)+ Left x -> return $ Left x++-- |Load a PNG file, Left value contains a description of a problem as a String,+-- if any+loadPNGFile :: FilePath -> IO (Either String PNGImage)+loadPNGFile fn = do+ rawChunks <- parseFromFile pngFile fn+ case rawChunks of+ Right chunks -> toPngImage chunks `catchError` (\e -> return (Left (show e)))+ Left s -> return (Left s)++sampleWidth :: BitDepth -> Int+sampleWidth Bd1 = 1+sampleWidth Bd2 = 2+sampleWidth Bd4 = 4+sampleWidth Bd8 = 8+sampleWidth Bd16 = 16++bytesPerPixel :: ColorType -> BitDepth -> Int+bytesPerPixel Ct0 Bd16 = 2+bytesPerPixel Ct0 _ = 1+bytesPerPixel Ct2 Bd1 = 1+bytesPerPixel Ct2 Bd2 = 1+bytesPerPixel Ct2 Bd4 = 2+bytesPerPixel Ct2 Bd8 = 3+bytesPerPixel Ct2 Bd16 = 6+bytesPerPixel Ct3 _ = 3+bytesPerPixel Ct4 Bd8 = 2+bytesPerPixel Ct4 Bd16 = 4+bytesPerPixel Ct4 _ = 1+bytesPerPixel Ct6 Bd8 = 4+bytesPerPixel Ct6 Bd16 = 8+bytesPerPixel Ct6 Bd4 = 2+bytesPerPixel Ct6 _ = 1++-- |Check if the image has alpha channel+hasAlphaChannel :: PNGImage -> Bool+hasAlphaChannel img = case ihdr_colorType hdr of+ Ct6 -> True+ _ -> False+ where hdr = pngImg_header img++-- |Get dimensions of the image (in pixels)+dimensions :: PNGImage -> (Width,Height)+dimensions img = (ihdr_width hdr, ihdr_height hdr)+ where hdr = pngImg_header img++-- |Bytes per pixel+pixelWidth :: PNGImage -> Int+pixelWidth img = bytesPerPixel (ihdr_colorType hdr) (ihdr_bitDepth hdr)+ where hdr = pngImg_header img++-- |Get image data as C-compatible StorableArray+imageData :: PNGImage -> StorableArray (Int,Int) Word8+imageData img = pngImg_imageData img
+ Codec/Image/PNG/Internal/CRC.hs view
@@ -0,0 +1,29 @@+{-# OPTIONS -fglasgow-exts -fbang-patterns #-}++module Codec.Image.PNG.Internal.CRC (update_crc, crc) where++import Data.Word+import Data.Array.Unboxed+import Data.Bits+import qualified Data.ByteString.Lazy as LB++crc_table :: UArray Word32 Word32+crc_table = listArray (0,255) . map iterate_c $ [0..]+ where+ iterate_c = (!! 8) . iterate compute_c+ compute_c c+ | c .&. 1 == 1 = 0xedb88320 `xor` (c `shiftR` 1)+ | otherwise = c `shiftR` 1++update_crc :: Word32 -> LB.ByteString -> Word32+update_crc !c bs+ | LB.null bs = c+ | otherwise = let w = LB.head bs+ newcrc = (crc_table ! ((c `xor` fromIntegral w) .&. 0xff)) `xor` (c `shiftR` 8)+ in+ update_crc newcrc (LB.tail bs)++crc :: LB.ByteString -> Word32+crc = (`xor` 0xffffffff) . update_crc 0xffffffff++--test = crc $ LB.replicate 10000000 128
+ Codec/Image/PNG/Internal/Filters.hs view
@@ -0,0 +1,98 @@+{-# OPTIONS -fglasgow-exts -fbang-patterns #-}++module Codec.Image.PNG.Internal.Filters (defilter_scanlines_arr) where++import Data.Array.Storable+import Data.Array.IO++import Data.Word+import Data.Maybe++import qualified Data.ByteString.Lazy as LB++type Width = Int+type Height = Int++-- | Defilter filtered PNG data (data includes scanlines prepended with filter+-- types).+defilter_scanlines_arr :: (Width,Height) -> Int -> LB.ByteString+ -> IO (StorableArray (Width,Height) Word8)+defilter_scanlines_arr (width,height) bpp bs = do+ (slTypes, imgArr) <- imageArray (widthInBytes, height) bs+ doFilter widthInBytes bpp imgArr slTypes+ return imgArr+ where+ widthInBytes = bpp*width++-- | create mutable array for defiltering the image and break out scanline types+imageArray :: (Width,Height) -> LB.ByteString+ -> IO ([Word8],StorableArray (Int,Int) Word8)+imageArray (width,height) bs = do+ a <- newListArray ((0,0), (height-1, width-1)) $ LB.unpack imageData+ return (scanlineTypes, a)+ where+ imageData = LB.concat scanlineData+ (scanlineTypes, scanlineData) = unzip scanlines+ scanlines = map (fromJust . LB.uncons) (chop bs)+ chop b+ | LB.null b = []+ | otherwise = let (sl,rest) = LB.splitAt slWidth b+ in sl : chop rest+ slWidth = fromIntegral (width+1)++-- | Perform filtering on the image array (standard adaptive filters supported)+doFilter :: Width -> Int -> StorableArray (Int,Int) Word8 -> [Word8]+ -> IO ()+doFilter width bpp image scanlineTypes = doFilter' scanlineTypes 0+ where+ doFilter' [] _ = return ()+ doFilter' (0:rest) !y = doFilter' rest (y+1) -- no filter on this line+ doFilter' (1:rest) !y = sub_filter 0 >> doFilter' rest (y+1)+ where sub_filter !x+ | x<width = do subx <- readByte (y,x)+ raw <- readByte (y,x-bpp)+ writeByte (y,x) (subx+raw)+ sub_filter (x+1)+ | otherwise = return ()+ doFilter' (2:rest) !y = up_filter 0 >> doFilter' rest (y+1)+ where up_filter !x+ | x<width = do upx <- readByte (y,x)+ prior <- readByte (y-1,x)+ writeByte (y,x) (upx+prior)+ up_filter (x+1)+ | otherwise = return ()+ doFilter' (3:rest) !y = avg_filter 0 >> doFilter' rest (y+1)+ where avg_filter !x+ | x<width = do avgx <- readByte (y,x)+ raw <- readByte (y,x-bpp)+ prior <- readByte (y-1,x)+ let s = ((fromIntegral raw + fromIntegral prior) `div` (2::Word16))+ writeByte (y,x) (avgx + fromIntegral s)+ avg_filter (x+1)+ | otherwise = return ()+ doFilter' (4:rest) !y = paeth_filter 0 >> doFilter' rest (y+1)+ where paeth_filter !x+ | x<width = do paethx <- readByte (y,x)+ a <- readByte (y,x-bpp)+ b <- readByte (y-1,x)+ c <- readByte (y-1, x-bpp)+ writeByte (y,x) (paethx + fromIntegral (paeth_predictor (fromIntegral a) (fromIntegral b) (fromIntegral c)))+ paeth_filter (x+1)+ | otherwise = return ()+ doFilter' (_:rest) !y = doFilter' rest (y+1) -- unknown filter, leave as is (and mess up image ;)+ {-# INLINE readByte #-}+ readByte (!y,!x) = if x<0 then return 0 else readArray image (y,x)+ {-# INLINE writeByte #-}+ writeByte = writeArray image++{-# INLINE paeth_predictor #-}+paeth_predictor :: Int -> Int -> Int -> Int+paeth_predictor !a !b !c+ | pa <= pb && pa <= pc = a+ | pb <= pc = b+ | otherwise = c+ where+ p = a + b - c+ pa = abs(p-a)+ pb = abs(p-b)+ pc = abs(p-c)
+ Codec/Image/PNG/Internal/Parser.hs view
@@ -0,0 +1,74 @@+{-# OPTIONS -fglasgow-exts #-}++module Codec.Image.PNG.Internal.Parser where++import Text.Parsec.Prim+import Text.Parsec.Combinator++import Data.Word+import Data.Bits+import Numeric (showHex)++import qualified Data.ByteString.Lazy as LB++instance (Monad m) => Stream LB.ByteString m Word8 where+ uncons = return . LB.uncons++type Parser = Parsec LB.ByteString ()++word8 :: (Stream LB.ByteString m Word8) => Word8 -> ParsecT LB.ByteString u m Word8+word8 = satisfy . (==)++word16 :: (Stream LB.ByteString m Word8) => Word16 -> ParsecT LB.ByteString u m Word16+word16 w = (word8 hi >> word8 lo >> return w) <?> "0x" ++ showHex w ""+ where+ hi = fromIntegral (w `shiftR` 8)+ lo = fromIntegral w++word32 :: (Stream LB.ByteString m Word8) => Word32 -> ParsecT LB.ByteString u m Word32+word32 w = (word16 hi >> word16 lo >> return w) <?> "0x" ++ showHex w ""+ where+ hi = fromIntegral (w `shiftR` 16)+ lo = fromIntegral w++satisfy :: (Stream LB.ByteString m Word8) => (Word8 -> Bool) -> ParsecT LB.ByteString u m Word8+satisfy f = tokenPrim (\c -> "0x" ++ showHex c "")+ (\pos _ _ -> pos)+ (\c -> if f c then Just c else Nothing)++anyWord8 :: (Stream LB.ByteString m Word8) => ParsecT LB.ByteString u m Word8+anyWord8 = anyToken++anyWord16 :: (Stream LB.ByteString m Word8) => ParsecT LB.ByteString u m Word16+anyWord16 = do+ hi <- anyWord8+ lo <- anyWord8+ return $ (fromIntegral hi `shiftL` 8) .|. fromIntegral lo++anyWord32 :: (Stream LB.ByteString m Word8) => ParsecT LB.ByteString u m Word32+anyWord32 = do+ hi <- anyWord16+ lo <- anyWord16+ return $ (fromIntegral hi `shiftL` 16) .|. fromIntegral lo++string :: (Stream LB.ByteString m Word8) => LB.ByteString -> ParsecT LB.ByteString u m LB.ByteString+string s = mapM_ word8 (LB.unpack s) >> return s++block :: (Stream LB.ByteString m Word8) => Int -> ParsecT LB.ByteString u m LB.ByteString+block size = do -- count size anyWord8 >>= return . LB.pack+ i <- getInput+ let (s,r) = LB.splitAt (fromIntegral size) i+ setInput r+ return s++allowedValues :: (a -> Parser a) -> [(a,b)] -> Parser b+allowedValues fn = choice . map (\(val,res) -> fn val >> return res) ++parseFromFile :: Parser a -> FilePath -> IO (Either String a)+parseFromFile p fname+ = do input <- LB.readFile fname+ return $ case runP p () fname input of+ Left err -> Left (show err)+ Right x -> Right x++
+ LICENSE view
@@ -0,0 +1,23 @@+Copyright (c) 2008, Marko Lauronen+All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++1. Redistributions of source code must retain the above copyright notice,+ this list of conditions and the following disclaimer.+2. Redistributions in binary form must reproduce the above copyright+ notice, this list of conditions and the following disclaimer in the+ documentation and/or other materials provided with the distribution.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE+LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE+POSSIBILITY OF SUCH DAMAGE.
+ README view
@@ -0,0 +1,20 @@++This is the beginnings of a pure Haskell loader for PNG images. It+stores the uncompressed image in a StorableArray (Int,Int) Word8, so+it should be convenient to use as a texture loader in OpenGL+applications since you can get a Ptr Word8 to the image data easily.++The performance is reasonable (the following run loads an 1.5megabyte+1024x768 24bit PNG image and writes out the raw bytes on a 2GHz+MacBook):++Mars2:PNG marko$ time ./Test scenery_1++real 0m0.315s+user 0m0.267s+sys 0m0.027s++Source for the test is in Test.hs.++Note that the library has not been tested with every possible kind of+PNG images, so use at your own risk.
+ Setup.lhs view
@@ -0,0 +1,5 @@+#!/usr/bin/env runhaskell++> import Distribution.Simple++> main = defaultMain
+ Test.hs view
@@ -0,0 +1,78 @@++module Main where++import Graphics.UI.GLUT+import Graphics.Rendering.OpenGL++import Control.Monad++import Data.ByteString.Unsafe+import qualified Data.ByteString as BS+import Codec.Image.PNG+import System.IO+import Data.Array.Storable+import System+import System.CPUTime++main :: IO ()+main = setupOpenGL++(winWidth, winHeight) = (640,480)++setupOpenGL :: IO ()+setupOpenGL = do+ (_, [fn]) <- getArgsAndInitialize+ initialWindowSize $= Size winWidth winHeight+ initialDisplayMode $= [RGBAMode, WithDepthBuffer]+ win <- createWindow "PNGLoader test"+ displayCallback $= displayFn+ setupTexture fn+ mainLoop++displayFn :: IO ()+displayFn = do+ viewport $= (Position 0 0, Size winWidth winHeight)+ matrixMode $= Projection+ loadIdentity+ ortho2D 0 (fromIntegral winWidth) 0 (fromIntegral winHeight)+ scale 1.0 (-1.0) (1.0::GLfloat)+ translate (Vector3 0.0 (fromIntegral (-winHeight)) (0.0::GLfloat))+ matrixMode $= Modelview 0+ loadIdentity++ clearColor $= Color4 0 0 0 0+ clear [ColorBuffer, DepthBuffer]+ texture Texture2D $= Enabled+ textureBinding Texture2D $= Just (TextureObject 0)+ renderTexture 0 0 640 480+ swapBuffers++renderTexture :: GLfloat -> GLfloat -> GLfloat -> GLfloat -> IO ()+renderTexture x y w h = do+ renderPrimitive Quads $ do texCoord (TexCoord2 0 (0::GLfloat))+ vertex (Vertex2 x y)+ texCoord (TexCoord2 1 (0::GLfloat))+ vertex (Vertex2 (x+w) y)+ texCoord (TexCoord2 1 (1::GLfloat))+ vertex (Vertex2 (x+w) (y+h))+ texCoord (TexCoord2 0 (1::GLfloat))+ vertex (Vertex2 x (y+h))++setupTexture :: FilePath -> IO ()+setupTexture fn = do+ startTime <- getCPUTime+ x <- loadPNGFile fn+ print x+ stopTime <- getCPUTime+ putStrLn $ "Loading took " ++ show ((stopTime - startTime) `div` 1000000000) ++ " milliseconds"+ case x of+ Left s -> (putStrLn $ "ERROR: " ++ s) >> exitWith (ExitFailure (-1))+ Right img -> do + texture Texture2D $= Enabled+ textureBinding Texture2D $= Just (TextureObject 0)+ let (w,h) = dimensions img+ (a,b) = case hasAlphaChannel img of+ True -> (RGBA8, RGBA)+ False -> (RGB8, RGB)+ withStorableArray (imageData img) $ \ptr ->+ build2DMipmaps Texture2D a (fromIntegral w) (fromIntegral h) (PixelData b UnsignedByte ptr)
+ pngload.cabal view
@@ -0,0 +1,23 @@+name: pngload+version: 0.1+copyright: (c) 2008 Marko Lauronen+license: BSD3+license-file: LICENSE+author: Marko Lauronen <marko.lauronen@pp1.inet.fi>+maintainer: Marko Lauronen <marko.lauronen@pp1.inet.fi>+category: Codecs+synopsis: Pure Haskell loader for PNG images+description: This package provides a simple PNG loader for PNG images. It currently supports+ 24bit RGB(A) images with no interlacing.+stability: experimental+build-type: Simple+cabal-version: >= 1.2.1+extra-source-files: Test.hs+data-files: LICENSE, README, test_images/mustang.png++library+ exposed-modules: Codec.Image.PNG, Codec.Image.PNG.Internal.CRC,+ Codec.Image.PNG.Internal.Filters,+ Codec.Image.PNG.Internal.Parser+ build-depends: base >= 3, zlib, parsec >= 3.0.0, bytestring, array, mtl, haskell98+ ghc-options: -Wall
+ test_images/mustang.png view
binary file changed (absent → 537021 bytes)