{-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE DerivingStrategies #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE OverloadedStrings #-} -- | -- -- Module : SDL.Image -- Copyright : (c) 2015 Siniša Biđin, 2021 Daniel Firth -- License : MIT -- Maintainer : sinisa@bidin.eu, dan.firth@homotopic.tech -- Stability : experimental -- -- Bindings to the @SDL2_image@ library. These should allow you to load various -- types of images as @SDL@ 'Surface's, as well as detect image formats. -- -- You can safely assume that any monadic function listed here is capable of -- throwing an 'SDLException' in case it encounters an error. module SDL.Image ( -- * Loading images -- -- | Use the following functions to read any @PNG@, @JPG@, @TIF@, @GIF@, -- @WEBP@, @CUR@, @ICO@, @BMP@, @PNM@, @XPM@, @XCF@, @PCX@ and @XV@ formatted -- data. -- -- If you have @TGA@-formatted data, you might wish to use the functions from -- the <#tga following section> instead. load, decode, loadTexture, decodeTexture, -- * Loading TGA images -- -- | #tga# Since @TGA@ images don't contain a specific unique signature, the -- following functions might succeed even when given files not formatted as -- @TGA@ images. -- -- Only use these functions if you're certain the inputs are @TGA@-formatted, -- otherwise they'll throw an exception. loadTGA, decodeTGA, loadTextureTGA, decodeTextureTGA, -- * Format detection formattedAs, format, Format (..), -- * Other initialize, InitFlag (..), version, quit, ) where import Control.Exception (bracket, throwIO) import Control.Monad.IO.Class (MonadIO, liftIO) import Data.Bits ((.|.)) import Data.ByteString (ByteString) import Data.ByteString.Unsafe (unsafeUseAsCStringLen) import Data.List (find) import Data.Text (pack) import Foreign.C.String (withCString) import Foreign.C.Types (CInt) import Foreign.Ptr (Ptr, castPtr) import Foreign.Storable (peek) import GHC.Generics (Generic) import SDL (Renderer, SDLException (..), Surface (..), Texture) import qualified SDL import SDL.Internal.Exception (throwIfNull, throwIf_) import qualified SDL.Raw import SDL.Raw.Filesystem (rwFromConstMem, rwFromFile) import qualified SDL.Raw.Image import SDL.Raw.Types (RWops) import System.IO.Unsafe (unsafePerformIO) -- | Initializes @SDL2_image@ by loading support for the chosen image formats. -- Explicit initialization is optional. -- -- You should call this function if you prefer to load image support yourself, -- at a time when your process isn't as busy. Otherwise, image support will be -- loaded dynamically when you attempt to load a @JPG@, @PNG@, @TIF@ or -- @WEBP@-formatted file. -- -- You may call this function multiple times. initialize :: (Foldable f, MonadIO m) => f InitFlag -> m () initialize flags = do let cint = foldl (\a b -> a .|. flagToCInt b) 0 flags throwIf_ (\result -> cint /= 0 && cint /= result) "SDL.Image.initialize" "IMG_Init" (SDL.Raw.Image.init cint) -- | Flags intended to be fed to 'initialize'. -- -- Each designates early loading of support for a particular image format. data InitFlag = -- | Load support for reading @JPG@ files. InitJPG | -- | Same, but for @PNG@ files. InitPNG | -- | @TIF@ files. InitTIF | -- | @WEBP@ files. InitWEBP deriving stock (Eq, Enum, Ord, Bounded, Generic, Read, Show) flagToCInt :: InitFlag -> CInt flagToCInt = \case InitJPG -> SDL.Raw.Image.IMG_INIT_JPG InitPNG -> SDL.Raw.Image.IMG_INIT_PNG InitTIF -> SDL.Raw.Image.IMG_INIT_TIF InitWEBP -> SDL.Raw.Image.IMG_INIT_WEBP -- | A helper for unmanaged 'Surface's, since it is not exposed by SDL itself. unmanaged :: Ptr SDL.Raw.Surface -> Surface unmanaged p = Surface p Nothing -- | Loads any given file of a supported image type as a 'Surface', including -- @TGA@ if the filename ends with @\".tga\"@. -- -- If you have @TGA@ files that don't have names ending with @\".tga\"@, use -- 'loadTGA' instead. load :: MonadIO m => FilePath -> m Surface load path = fmap unmanaged . throwIfNull "SDL.Image.load" "IMG_Load" . liftIO $ withCString path SDL.Raw.Image.load -- | Same as 'load', but returning a 'Texture' instead. -- -- For @TGA@ files not ending in ".tga", use 'loadTextureTGA' instead. loadTexture :: MonadIO m => Renderer -> FilePath -> m Texture loadTexture r path = liftIO . bracket (load path) SDL.freeSurface $ SDL.createTextureFromSurface r -- | Reads an image from a 'ByteString'. -- -- This will work for all supported image types, __except TGA__. If you need to -- decode a @TGA@ 'ByteString', use 'decodeTGA' instead. decode :: MonadIO m => ByteString -> m Surface decode bytes = liftIO . unsafeUseAsCStringLen bytes $ \(cstr, len) -> do rw <- rwFromConstMem (castPtr cstr) (fromIntegral len) fmap unmanaged . throwIfNull "SDL.Image.decode" "IMG_Load_RW" $ SDL.Raw.Image.load_RW rw 0 -- | Same as 'decode', but returning a 'Texture' instead. -- -- If you need to decode a @TGA@ 'ByteString', use 'decodeTextureTGA' instead. decodeTexture :: MonadIO m => Renderer -> ByteString -> m Texture decodeTexture r bytes = liftIO . bracket (decode bytes) SDL.freeSurface $ SDL.createTextureFromSurface r -- | If your @TGA@ files aren't in a filename ending with @\".tga\"@, you can -- load them using this function. loadTGA :: MonadIO m => FilePath -> m Surface loadTGA path = fmap unmanaged . throwIfNull "SDL.Image.loadTGA" "IMG_LoadTGA_RW" . liftIO $ do rw <- withCString "rb" $ withCString path . flip rwFromFile SDL.Raw.Image.loadTGA_RW rw -- | Same as 'loadTGA', only returning a 'Texture' instead. loadTextureTGA :: MonadIO m => Renderer -> FilePath -> m Texture loadTextureTGA r path = liftIO . bracket (loadTGA path) SDL.freeSurface $ SDL.createTextureFromSurface r -- | Reads a @TGA@ image from a 'ByteString'. -- -- Assumes the input is a @TGA@-formatted image. decodeTGA :: MonadIO m => ByteString -> m Surface decodeTGA bytes = liftIO . unsafeUseAsCStringLen bytes $ \(cstr, len) -> do rw <- rwFromConstMem (castPtr cstr) (fromIntegral len) fmap unmanaged . throwIfNull "SDL.Image.decodeTGA" "IMG_LoadTGA_RW" $ SDL.Raw.Image.loadTGA_RW rw -- | Same as 'decodeTGA', but returns a 'Texture' instead. decodeTextureTGA :: MonadIO m => Renderer -> ByteString -> m Texture decodeTextureTGA r bytes = liftIO . bracket (decodeTGA bytes) SDL.freeSurface $ SDL.createTextureFromSurface r -- | Tests whether a 'ByteString' contains an image of a given format. formattedAs :: Format -> ByteString -> Bool formattedAs f bytes = unsafePerformIO . unsafeUseAsCStringLen bytes $ \(cstr, len) -> do rw <- rwFromConstMem (castPtr cstr) (fromIntegral len) formatPredicate f rw >>= \case 1 -> return True 0 -> return False e -> do let err = "Expected 1 or 0, got " `mappend` pack (show e) `mappend` "." let fun = "IMG_is" `mappend` pack (show f) throwIO $ SDLCallFailed "SDL.Image.formattedAs" fun err -- | Tries to detect the image format by attempting 'formattedAs' with each -- possible 'Format'. -- -- If you're trying to test for a specific format, use a specific 'formattedAs' -- directly instead. format :: ByteString -> Maybe Format format bytes = fst <$> find snd attempts where attempts = map (\f -> (f, formattedAs f bytes)) [minBound ..] -- | Each of the supported image formats. data Format = CUR | ICO | BMP | PNM | XPM | XCF | PCX | GIF | LBM | XV | JPG | PNG | TIF | WEBP deriving stock (Eq, Enum, Ord, Bounded, Generic, Read, Show) -- Given an image format, return its raw predicate function. formatPredicate :: MonadIO m => Format -> Ptr RWops -> m CInt formatPredicate = \case CUR -> SDL.Raw.Image.isCUR ICO -> SDL.Raw.Image.isICO BMP -> SDL.Raw.Image.isBMP PNM -> SDL.Raw.Image.isPNM XPM -> SDL.Raw.Image.isXPM XCF -> SDL.Raw.Image.isXCF PCX -> SDL.Raw.Image.isPCX GIF -> SDL.Raw.Image.isGIF LBM -> SDL.Raw.Image.isLBM XV -> SDL.Raw.Image.isXV JPG -> SDL.Raw.Image.isJPG PNG -> SDL.Raw.Image.isPNG TIF -> SDL.Raw.Image.isTIF WEBP -> SDL.Raw.Image.isWEBP -- | Gets the major, minor, patch versions of the linked @SDL2_image@ library. version :: (Integral a, MonadIO m) => m (a, a, a) version = liftIO $ do SDL.Raw.Version major minor patch <- peek =<< SDL.Raw.Image.getVersion return (fromIntegral major, fromIntegral minor, fromIntegral patch) -- | Cleans up any loaded image libraries, freeing memory. You only need to -- call this function once. quit :: MonadIO m => m () quit = SDL.Raw.Image.quit