{- |
This module provides pixel-oriented graphics operations.
It defines an 'ImageBuffer' type, which is a thin layer
over a GTK+ 'Pixbuf'. It then provides functions for
doing pixel-oriented graphics on this without having to
do a lot of bit-twiddling math by hand. It also
provides for quickly and easily loading and saving
images, and displaying them on the screen.
In the simplest case, you can import this module alone
and never need to touch the actual GTK modules;
however, this module provides access to the underlying
GTK resources as well, in case you want to use it as
part of a larger GTK program.
Note that /displaying/ an image on screen requires
you to run the GTK event loop. If you only want to
load, save and process image files, you can completely
ignore the GTK event loop. (You do still need to call
'init_system', however.)
-}
module Graphics.EasyRaster.GTK
(
-- * System initialisation
init_system,
-- * Types
Coord, Channel,
ImageFileType (..), type_string,
ImageBuffer (),
-- * Image buffer creation
ib_new, ib_load,
-- * Image buffer output
ib_save,
-- * Image buffer queries
ib_size, ib_coords, ib_coords2, ib_valid_coords,
-- * Pixel I/O
ib_write_pixel, ib_read_pixel,
-- * Image display
ib_display, ib_canvas,
-- * GTK event loop
process_event, wait_event, main_loop,
-- * Low-level GTK access
ib_pixbuf
-- * Example code
{- |
The following short program should give you an idea
of how to use this library. It draws a simple graphic
and saves it to disk.
> module Main where
>
> import Graphics.EasyRaster.GTK
>
> main = do
> init_system
> ibuf <- ib_new (640, 480)
> mapM_ (\p -> ib_write_pixel ibuf p (colour p)) (ib_coords ibuf)
> ib_save ibuf IFT_PNG "Example1.png"
>
> colour :: (Coord, Coord) -> (Channel, Channel, Channel)
> colour (x, y) = (floor $ fromIntegral x / 640 * 255, floor $ fromIntegral y / 480 * 255, 0)
-}
)
where
import Data.Word
import Data.Array.IO
import Data.Array.Base (unsafeWrite, unsafeRead)
import Graphics.UI.Gtk
{- |
Initialise the GTK runtime system. This function /must/
be called before any other functions in this module.
(If not, your program will likely summarily crash with
an obscure error to 'stderr'.)
Note that Gtk2hs by default will crash with a warning
message if the program is compiled with the threaded
RTS; this module disables that warning. However, be
warned: You must /only/ call the GTK-based functions
from thread 0.
-}
init_system :: IO ()
init_system = unsafeInitGUIForThreadedRTS >> return ()
{- |
Process one GTK event if there are any waiting, otherwise
do nothing.
-}
process_event :: IO ()
process_event = mainIterationDo False >> return ()
{- |
Process one GTK event. Wait for an event to arrive if
necessary.
-}
wait_event :: IO ()
wait_event = mainIterationDo True >> return ()
{- |
Run the GTK \"main loop\" until GTK's 'mainQuit'
function is called. (Note that this means that
thread 0 can do no other work while the main
loop is running.)
-}
main_loop :: IO ()
main_loop = mainGUI
-- | The type of pixel coordinates.
type Coord = Int
-- | The type of pixel channels.
type Channel = Word8
{- |
An @ImageBuffer@ is a space-efficient grid of 24-bit
RGB pixels (i.e., 8 bits per channel) that the GTK
library can also access. (The latter gives us the
ability to load/save/display the graphics, rather
than just manipulate it in memory.)
-}
data ImageBuffer = ImageBuffer
{
pixbuf :: Pixbuf,
sizeX, sizeY :: Coord,
rowstride :: Coord,
raw :: PixbufData Coord Channel
}
{- |
Used to indicate image file type.
-}
data ImageFileType =
IFT_PNG | -- ^ PNG image.
IFT_JPEG -- ^ JPEG image.
deriving (Eq, Enum, Bounded, Read, Show)
{- |
Convert an 'ImageFileType' to a plain string (all
lower-case).
-}
type_string :: ImageFileType -> String
type_string IFT_PNG = "png"
type_string IFT_JPEG = "jpeg"
{- |
Make a new 'ImageBuffer' of the specified size. Valid
coordinates will be in the range (0,0) to (x-1, y-1),
with (0,0) being the top-left corner of the image.
-}
ib_new :: (Int,Int) -> IO ImageBuffer
ib_new (x,y) = do
core <- pixbufNew ColorspaceRgb False 8 x y
build core
{- |
Load a graphics file into a newly created 'ImageBuffer'.
The file type is determined automatically (any type that
GTK+ supports), as is the size for the 'ImageBuffer'.
(This can be queried later.)
-}
ib_load :: FilePath -> IO ImageBuffer
ib_load f = do
core <- pixbufNewFromFile f
build core
{- |
Save an 'ImageBuffer' into a file with the specified
file type. Note that the filename should include an
appropriate suffix (e.g., @.png@). The type of file is
determined by the 'ImageFileType' argument, /not/ by
the filename.
-}
ib_save :: ImageBuffer -> ImageFileType -> FilePath -> IO ()
ib_save buf t f = do
pixbufSave (pixbuf buf) f (type_string t) []
return ()
{- |
Returns a GTK+ canvas object which displays the graphcs
in the 'ImageBuffer' and will automatically repaint
itself in response of damage.
-}
ib_canvas :: ImageBuffer -> IO DrawingArea
ib_canvas ib = do
canvas <- drawingAreaNew
canvas `onExposeRect` (redraw canvas)
return canvas
where
redraw canvas _ = do
dc <- widgetGetDrawWindow canvas
gc <- gcNew dc
drawPixbuf dc gc (pixbuf ib) 0 0 0 0 (-1) (-1) RgbDitherNone 0 0
return ()
{- |
This is a quick shortcut for displaying an image
on screen. Given an 'ImageBuffer' and a window
title, this will display the image in a new window.
Clicking the window's close button will call GTK's
'mainQuit' function. For example,
@
ib_display buf "My Window Title"
main_loop
@
will display @buf@ on screen and halt the program
(or at least thread 0) until the user clicks the
close button. Crude, but useful for quick testing.
-}
ib_display :: ImageBuffer -> String -> IO ()
ib_display ib title = do
window <- windowNew
windowSetTitle window title
windowSetDefaultSize window (sizeX ib) (sizeY ib)
canvas <- ib_canvas ib
containerAdd window canvas
window `onDestroy` mainQuit
widgetShowAll window
build :: Pixbuf -> IO ImageBuffer
build core = do
sx <- pixbufGetWidth core
sy <- pixbufGetHeight core
s <- pixbufGetRowstride core
r <- pixbufGetPixels core
return $ ImageBuffer
{
pixbuf = core,
sizeX = sx,
sizeY = sy,
rowstride = s,
raw = r
}
{- |
Return the underlying GTK 'Pixbuf' used by an 'ImageBuffer'.
(In case you want to do something to it with GTK.)
-}
ib_pixbuf :: ImageBuffer -> Pixbuf
ib_pixbuf = pixbuf
{- |
Query the size of an 'ImageBuffer'. This returns the width
and height in pixels. Valid coordinates for this @ImageBuffer@
are from (0,0) to (x-1, y-1), where (x,y) is the value returned
from @ib_size@.
-}
ib_size :: ImageBuffer -> (Coord,Coord)
ib_size buf = (sizeX buf, sizeY buf)
{- |
Determine whether the specified pixel coordinates are valid
for this 'ImageBuffer' (i.e., check they're not off the
edge of the image).
-}
ib_valid_coords :: ImageBuffer -> (Coord,Coord) -> Bool
ib_valid_coords buf (x,y) = x < sizeX buf && y < sizeY buf
{- |
Return a list containing all valid coordinates for this 'ImageBuffer'.
Useful when you want to process all pixels in the image; now you
can just use 'mapM' or whatever.
-}
ib_coords :: ImageBuffer -> [(Coord,Coord)]
ib_coords buf = [ (x,y) | y <- [0 .. (sizeY buf) - 1], x <- [0 .. (sizeX buf) - 1] ]
{- |
Return all valid coordinates as a list of lists instead of a single
flat list. (In case you need to do something at the end of each row.)
-}
ib_coords2 :: ImageBuffer -> [[(Coord,Coord)]]
ib_coords2 buf = [ [ (x,y) | x <- [0 .. (sizeX buf) - 1] ] | y <- [0 .. (sizeY buf) - 1] ]
{- |
Overwrite the specified pixel with the specified colour (R, G, B).
Note that pixel coordinates are /not/ checked, for efficiency reasons.
However, supplying invalid pixel coordinates may result in behaviour ranging
from corrupted graphical output to random program crashes. See
"Graphics.EasyRaster.GTK.Paranoid" for a version of this function with
bounds checking turned on.
-}
ib_write_pixel :: ImageBuffer -> (Coord,Coord) -> (Channel,Channel,Channel) -> IO ()
ib_write_pixel buf (x,y) (r,g,b) = do
-- Caution: No range checks!!
let p = y * rowstride buf + x * 3
unsafeWrite (raw buf) (p + 0) r
unsafeWrite (raw buf) (p + 1) g
unsafeWrite (raw buf) (p + 2) b
{- |
Read the current (R, G, B) colour valid of the specified pixel.
Note that pixel coordinates are /not/ checked, for efficiency reasons.
However, supplying invalid pixel coordinates will at best result in
gibberish being returned, and at works a segmentation fault. See
"Graphics.EasyRaster.Paranoid" for a version of this function with
bounds checking turned on.
-}
ib_read_pixel :: ImageBuffer -> (Coord,Coord) -> IO (Channel,Channel,Channel)
ib_read_pixel buf (x,y) = do
-- Cation: No range checks!!
let p = y * rowstride buf + x * 3
r <- unsafeRead (raw buf) (p + 0)
g <- unsafeRead (raw buf) (p + 1)
b <- unsafeRead (raw buf) (p + 2)
return (r,g,b)