hip-1.2.0.0: src/Graphics/Image/IO.hs
{-# OPTIONS_GHC -fno-warn-unused-imports #-}
{-# LANGUAGE CPP #-}
{-# LANGUAGE FlexibleContexts #-}
-- |
-- Module : Graphics.Image.IO
-- Copyright : (c) Alexey Kuleshevich 2016
-- License : BSD3
-- Maintainer : Alexey Kuleshevich <lehins@yandex.ru>
-- Stability : experimental
-- Portability : non-portable
--
module Graphics.Image.IO (
-- * Reading
readImage, readImageExact,
-- * Writing
writeImage, writeImageExact,
-- * Displaying
ExternalViewer(..),
displayImage,
displayImageUsing,
displayImageFile,
-- ** Common viewers
defaultViewer,
eogViewer,
gpicviewViewer,
fehViewer,
gimpViewer,
-- * Supported Image Formats
module Graphics.Image.IO.Formats
-- $supported
) where
import Prelude as P hiding (readFile, writeFile)
import qualified Control.Monad as M (foldM)
import Control.Concurrent (forkIO)
import Control.Monad (void)
import Data.Char (toLower)
import Data.Maybe (fromMaybe)
import qualified Data.ByteString as B (readFile)
import Graphics.Image.ColorSpace
import Graphics.Image.Interface
import Graphics.Image.IO.Base
import Graphics.Image.IO.Formats
import qualified Data.ByteString.Lazy as BL (writeFile, hPut)
import System.Directory (createDirectoryIfMissing, getTemporaryDirectory)
import System.FilePath (takeExtension, (</>))
import System.IO (hClose, openBinaryTempFile)
import System.Process (readProcess)
import Control.Exception (bracket)
-- | External viewing application to use for displaying images.
data ExternalViewer =
ExternalViewer FilePath [String] Int
-- ^ Any custom viewer, which can be specified:
--
-- * @FilePath@ - to the actual viewer executable.
-- * @[String]@ - command line arguments that will be passed to the executable.
-- * @Int@ - position index in the above list where `FilePath` to an image should be
-- injected
deriving Show
guessFormat :: (ImageFormat f, Enum f) => FilePath -> Maybe f
guessFormat path =
headMaybe . dropWhile (not . isFormat e) . enumFrom . toEnum $ 0
where e = P.map toLower . takeExtension $ path
headMaybe ls = if null ls then Nothing else Just $ head ls
-- | This function will try to guess an image format from file's extension,
-- then it will attempt to read it as such. It will fall back onto the rest of
-- the supported formats and will try to read them regarless of file's
-- extension. Whenever image cannot be decoded, 'Left' containing all errors for
-- each attempted format will be returned, and 'Right' containing an image
-- otherwise. Image will be read into a type signature specified 'ColorSpace'
-- ('Graphics.Image.ColorSpace.Y', 'Graphics.Image.ColorSpace.YA',
-- 'Graphics.Image.ColorSpace.RGB' and 'Graphics.Image.ColorSpace.RGBA' only)
-- with 'Double' precision, while doing all necessary conversions.
readImage :: Readable (Image arr cs Double) InputFormat =>
FilePath
-> IO (Either String (Image arr cs Double))
readImage path = do
imgstr <- B.readFile path
let maybeFormat = guessFormat path :: Maybe InputFormat
formats = enumFrom . toEnum $ 0
orderedFormats = maybe formats (\f -> f:filter (/=f) formats) maybeFormat
reader (Left err) format =
return $ either (Left . ((err++"\n")++)) Right (decode format imgstr)
reader img _ = return img
M.foldM reader (Left "") orderedFormats
-- | This function allows for reading any supported image in the exact colorspace
-- and precision it is currently encoded in. For instance, frog image can be
-- read into it's 'Graphics.Image.ColorSpace.YCbCr' colorspace with
-- 'Graphics.Image.ColorSpace.Word8' precision and into any supported array
-- representation.
--
-- >>> readImageExact JPG "images/frog.jpg" :: IO (Either String (Image RP YCbCr Word8))
-- Right <Image RepaParallel YCbCr (Word8): 200x320>
--
-- The drawback here is that colorspace and precision has to match exactly,
-- otherwise it will return an error:
--
-- >>> readImageExact JPG "images/frog.jpg" :: IO (Either String (Image RD RGB Word8))
-- Left "JuicyPixel decoding error: Input image is in YCbCr8 (Pixel YCbCr Word8), cannot convert it to RGB8 (Pixel RGB Word8) colorspace."
--
-- Attempt to read an image in a particular color space that is not supported by
-- the format, will result in a compile error. Refer to 'Readable' class for all
-- images that can be decoded.
readImageExact :: Readable img format =>
format
-- ^ A file format that an image should be read as. See
-- <#g:4 Supported Image Formats>
-> FilePath -- ^ Location of an image.
-> IO (Either String img)
readImageExact format path = fmap (decode format) (B.readFile path)
-- | Just like 'readImage', this function will guess an output file format from the
-- extension and write to file any image that is in one of 'Y', 'YA', 'RGB' or
-- 'RGBA' color spaces with 'Double' precision. While doing necessary
-- conversions the choice will be given to the most suited color space supported
-- by the format. For instance, in case of a 'PNG' format, an ('Image' @arr@
-- 'RGBA' 'Double') would be written as @RGBA16@, hence preserving transparency
-- and using highest supported precision 'Word16'. At the same time, writing
-- that image in 'GIF' format would save it in @RGB8@, since 'Word8' is the
-- highest precision 'GIF' supports and it currently cannot be saved with
-- transparency.
writeImage :: Writable (Image arr cs e) OutputFormat =>
FilePath -- ^ Location where an image should be written.
-> Image arr cs e -- ^ An image to write.
-> IO ()
writeImage path = BL.writeFile path . encode format [] where
format = fromMaybe (error ("Could not guess output format. Use 'writeImageExact' "++
"or supply a filename with supported format."))
(guessFormat path :: Maybe OutputFormat)
-- | Write an image in a specific format, while supplying any format specific
-- options. Precision and color space that an image will be written is decided
-- from image's type. Attempt to write image file in a format that does not
-- support color space and precision combination will result in a compile error.
writeImageExact :: Writable img format =>
format
-- ^ A file format that an image should be saved in. See
-- <#g:4 Supported Image Formats>
-> [SaveOption format] -- ^ A list of format specific options.
-> FilePath -- ^ Location where an image should be written.
-> img -- ^ An image to write. Can be a list of images in case
-- of formats supporting animation.
-> IO ()
writeImageExact format opts path = BL.writeFile path . encode format opts
{- | An image is written as a @.tiff@ file into an operating system's temporary
directory and passed as an argument to the external viewer program. -}
displayImageUsing :: Writable (Image arr cs e) TIF =>
ExternalViewer -- ^ External viewer to use
-> Bool -- ^ Should the call be blocking
-> Image arr cs e -- ^ Image to display
-> IO ()
displayImageUsing viewer block img = do
let display = do
tmpDir <- fmap (</> "hip") getTemporaryDirectory
createDirectoryIfMissing True tmpDir
bracket (openBinaryTempFile tmpDir "tmp-img.tiff")
(hClose . snd)
(\ (imgPath, imgHandle) -> do
BL.hPut imgHandle $ encode TIF [] img
hClose imgHandle
displayImageFile viewer imgPath)
if block
then display
else void $ forkIO display
-- | Displays an image file by calling an external image viewer.
displayImageFile :: ExternalViewer -> FilePath -> IO ()
displayImageFile (ExternalViewer exe args ix) imgPath =
void $ readProcess exe (argsBefore ++ [imgPath] ++ argsAfter) ""
where (argsBefore, argsAfter) = splitAt ix args
{- | Makes a call to an external viewer that is set as a default image viewer by
the OS. This is a non-blocking function call, so it will take some time before
an image will appear.
>>> frog <- readImageRGB "images/frog.jpg"
>>> displayImage frog
-}
displayImage :: Writable (Image arr cs e) TIF =>
Image arr cs e -- ^ Image to be displayed
-> IO ()
displayImage = displayImageUsing defaultViewer False
defaultViewer :: ExternalViewer
defaultViewer =
#if defined(OS_Win32)
(ExternalViewer "explorer.exe" [] 0)
#elif defined(OS_Linux)
(ExternalViewer "xdg-open" [] 0)
#elif defined(OS_Mac)
(ExternalViewer "open" [] 0)
#else
error "Graphics.Image.IO.defaultViewer: Could not determine default viewer."
#endif
-- | @eog /tmp/hip/img.tiff@
-- <https://help.gnome.org/users/eog/stable/ Eye of GNOME>
eogViewer :: ExternalViewer
eogViewer = ExternalViewer "eog" [] 0
-- | @feh --fullscreen --auto-zoom /tmp/hip/img.tiff@
-- <https://feh.finalrewind.org/ FEH>
fehViewer :: ExternalViewer
fehViewer = ExternalViewer "feh" ["--fullscreen", "--auto-zoom"] 2
-- | @gpicview /tmp/hip/img.tiff@
-- <http://lxde.sourceforge.net/gpicview/ GPicView>
gpicviewViewer :: ExternalViewer
gpicviewViewer = ExternalViewer "gpicview" [] 0
-- | @gimp /tmp/hip/img.tiff@
-- <https://www.gimp.org/ GIMP>
gimpViewer :: ExternalViewer
gimpViewer = ExternalViewer "gimp" [] 0
{- $supported
Encoding and decoding of images is done using
<http://hackage.haskell.org/package/JuicyPixels JuicyPixels> and
<http://hackage.haskell.org/package/netpbm netpbm> packages.
List of image formats that are currently supported, and their exact
'ColorSpace's and precision for reading and writing:
* 'BMP':
* __read__: ('Y' 'Word8'), ('RGB' 'Word8'), ('RGBA' 'Word8')
* __write__: ('Y' 'Word8'), ('RGB' 'Word8'), ('RGBA' 'Word8')
* 'GIF':
* __read__: ('RGB' 'Word8'), ('RGBA' 'Word8')
* __write__: ('RGB' 'Word8')
* Also supports reading and writing animated images, when used as @['GIF']@
* 'HDR':
* __read__: ('RGB' 'Float')
* __write__: ('RGB' 'Float')
* 'JPG':
* __read__: ('Y' 'Word8'), ('YA' 'Word8'), ('RGB' 'Word8'), ('CMYK' 'Word8'),
('YCbCr', 'Word8')
* __write__: ('Y' 'Word8'), ('YA', 'Word8'), ('RGB' 'Word8'), ('CMYK' 'Word8'),
('YCbCr', 'Word8')
* 'PNG':
* __read__: ('Y' 'Word8'), ('Y' 'Word16'), ('YA' 'Word8'), ('YA' 'Word16'),
('RGB' 'Word8'), ('RGB' 'Word16'), ('RGBA' 'Word8'), ('RGBA' 'Word16')
* __write__: ('Y' 'Word8'), ('Y' 'Word16'), ('YA' 'Word8'), ('YA' 'Word16'),
('RGB' 'Word8'), ('RGB' 'Word16'), ('RGBA' 'Word8'), ('RGBA' 'Word16')
* 'TGA':
* __read__: ('Y' 'Word8'), ('RGB' 'Word8'), ('RGBA' 'Word8')
* __write__: ('Y' 'Word8'), ('RGB' 'Word8'), ('RGBA' 'Word8')
* 'TIF':
* __read__: ('Y' 'Word8'), ('Y' 'Word16'), ('YA' 'Word8'), ('YA' 'Word16'),
('RGB' 'Word8'), ('RGB' 'Word16'), ('RGBA' 'Word8'), ('RGBA' 'Word16'),
('CMYK' 'Word8'), ('CMYK' 'Word16')
* __write__: ('Y' 'Word8'), ('Y' 'Word16'), ('YA' 'Word8'), ('YA' 'Word16'),
('RGB' 'Word8'), ('RGB' 'Word16'), ('RGBA' 'Word8'), ('RGBA' 'Word16')
('CMYK' 'Word8'), ('CMYK' 'Word16'), ('YCbCr' 'Word8')
* 'PBM':
* __read__: ('Binary' 'Bit')
* Also supports sequence of images in one file, when read as @['PBM']@
* 'PGM':
* __read__: ('Y' 'Word8'), ('Y' 'Word16')
* Also supports sequence of images in one file, when read as @['PGM']@
* 'PPM':
* __read__: ('RGB' 'Word8'), ('RGB' 'Word16')
* Also supports sequence of images in one file, when read as @['PPM']@
-}