Chapter 5. Displaying an Image

This chapter describes how to display and manage a set of images on the screen using IL's display facility. As part of this facility, numerous functions are provided to help you develop an interactive image-processing program. You can use these functions to move images, perform wipes, roam around an image, and create split views of multiple images.

This chapter describes IL's display facility in the following major sections:

Overview of the Display Facility

IL display classes described in this chapter are shown shaded in Figure 5-1.

Figure 5-1. IL Display Classes

Figure 5-1 IL Display Classes

With IL's display facility, you can display any combination of IL or X images in an X or OpenGL window. These images can be positioned anywhere within the window and can overlap each other.

Overlapped regions are displayed based on a stacking order such that the image on top is visible, as shown in Figure 5-2.

Figure 5-2. Stacked Images in an X Window

Figure 5-2 Stacked Images in an X Window

In order to assemble such a display, you must follow these steps:

  1. Create or open the images.

    You can display any combination of ilImage, ilXImage, or XImage using either OpenGL or X to render them. (An XImage is an X Window struct.) Often, displayed images are the product of an image processing chain. For example, you might want to display the original unprocessed image, an intermediate stage of the chain, and the final image. In some cases, you might want to display only a portion of an image.

  2. Configure and open a window.

    To open an X window, use the standard X calls. To open a OpenGL window, use the OpenGL calls explained in “A Sample Program in C++”.

  3. Create a display.

    Use calls to ilDisplay functions.

  4. Add the images to the display.

    Use calls to ilDisplay functions.

  5. Cause the images to be displayed.

    Use calls to ilView and ilDisplay functions.


Note: You should assume that any function discussed in this chapter is an IL function, unless it is explicitly identified as a OpenGL or X function.

The three principal classes within the IL display facility are

  • ilDisplay—Manages one or more ilViews in an X or OpenGL window. The entire window is used for display. An ilDisplay object maintains a stack of ilViews and provides functions to manipulate them. ilViewer derives from ilDisplay, which manages the display of images in an X window with X event handling.

  • ilView—Maps an ilImage or XImage to a region within the ilDisplay. It has various attributes such as view position, view size, image position, border color, and border width.

  • ilFrameBufferImg—Acts as a base class for images that reside in the frame buffer. There is one derived classes: ilXWindowImg.

When an ilDisplay is created, it creates an ilXWindowImg for X rendering. The display image is configured to occupy the entire window specified by the application. As Figure 5-3 illustrates, the creation of an ilDisplay object defines a display area in which views are drawn.

Figure 5-3. ilDisplay Object Creates a Display Area

Figure 5-3 ilDisplay Object Creates a Display Area

When you want to add an image to ilDisplay, create an ilView to control where you want the image to be displayed. This view is pushed onto an indexed view stack and a pointer to it is returned to your application. As you add more images, you must create an ilView for each image. These views are pushed onto the view stack. When a view is added to the stack, it is pushed onto the top by default. However, you can specify a particular index to control where the view is put in the view stack. An ilView has various attributes, such as

  • view position, which controls where in the window the image is displayed

  • view size, which controls how much of the image is displayed

  • image position, which controls what part of the image is displayed

The position of an ilView in the view stack controls its visibility on the screen, as shown in Figure 5-2. The view on the top of the stack is fully visible. A view at the bottom of the stack is obscured by the views above it.

In Figure 5-4, two ilView objects have been created and the positions of the corresponding views in the display defined. Two images to be displayed have been added to the view stack.

Figure 5-4. ilView Objects Map Images to Display Regions

Figure 5-4 ilView Objects Map Images to Display Regions

When ilDisplay draws its contents, the position and size of each ilView, as well as the stacking order, are used to determine what portion of each view is visible. ilFrameBufferImg is called to render the images into the frame buffer. Each image is converted to the proper data type, order, color model, and coordinate space as necessary for displaying. Figure 5-5 shows the display after the views have been drawn.

Figure 5-5. Display Area After Views Are Drawn

Figure 5-5 Display Area After Views Are Drawn

In addition to the views added by an application, ilDisplay creates a background view. This background view is the size of the window and is always at the bottom of the view stack. You cannot control it other than to change its color from the default, which is black.

ilDisplay provides several operators to manipulate ilViews as well as functions to facilitate interactive display. The display operators enable you to move a view, change its size, or move the image within the view. The display operators are discussed in detail in “Applying a Display Operator”. By default, view manipulation also causes the display to be redrawn; however, a sequence of display operations can be performed with drawing deferred. In addition, an application can explicitly control drawing. ilDisplay is optimized to draw only the areas that have changed or that have been exposed.

Scrolling Windows

You can change the size of the image by reducing its display size. By making the image size smaller than the display, you, in effect, create a scrollable window. To set and read the visible area in which an image can display, you use the setVisibleArea() and getVisibleArea() functions in ilDisplay, defined as follows:

void setVisibleArea(int x = 0, int y = 0, int nx = 0, 
int ny = 0);
void getVisibleArea(int& x, int& y, int& nx, int& ny);

The arguments define the lower left and upper right coordinates of the visible display area.

A Simple Interactive Display Program

Now look at a simple interactive program that shows the IL display facility in action. This program opens an image file and applies a threshold operator to it. Both the original image and the processed image are displayed in a window, stacked on top of each other. You can wipe the original image away gradually so that you can see the processed image beneath it. Wiping changes the view size. The best way to understand wiping, of course, is to compile and run the display program. It is available online in

/usr/share/src/il/guide/displayEx.c++

When you run the program, you see a window displaying the original image. The processed image is actually underneath the original one, but you cannot see it, since the images are opaque and of the same size. If you click in the window with the left mouse button, a red highlight border appears around the image, indicating that it is ready to be wiped. To wipe, click the left mouse button near any edge of the image and drag toward the center of the image. As you drag, the processed image becomes visible as the original image is wiped away; release the button to stop the wiping. You can wipe any edge or corner of the original image. To exit the program, use the normal window manager menu command.

Sample Program Code

The code for the sample program is shown in Example 5-1 and discussed in the paragraphs following that. All the ilDisplay functions used in the program are explained in more detail in the appropriate sections in this chapter.

Example 5-1. A Simple Interactive Display Program


#include <stdlib.h>
#include <stdio.h>
#include <il/ilFileImg.h>
#include <il/ilThreshImg.h>
#include <il/ilDisplay.h>

const int Border = 10;  // Threshold in pixels for edge finding operation


void
main (int argc, char* argv[])
{
    if (argc < 2) {
        printf (“Usage: %s in-image1\n”, argv[0]);
        exit(0);
    }

// Step 1: Open an image file and create a threshold image.

    ilFileImg in(argv[1]);
    if (in.getStatus() != ilOKAY) {
        char buf[400];
        fprintf(stderr, “Could not open image file %s: %s\n”, 
                argv[1], ilStatusToString(in.getStatus(), buf, sizeof(buf)));
        exit(0);
    }

// Step 2: Create threshold image, open an X window, and create
// ilDisplay object.

    float threshVal = 100.;
    iflPixel threshPix(iflFloat, 1, &threshVal);
    ilThreshImg thresh(&in, threshPix);

    iflSize size;
    in.getSize(size);
    Display* dpy = XOpenDisplay(NULL);
    ilDisplay disp(dpy, size.x, size.y);
    disp.addView(&thresh);
    ilView* inView = disp.addView(&in);
    disp.setBorders(TRUE);

// Step 3: Process the image, allowing to wipe between original and
// processed images using the left mouse button. 

    int active = TRUE;
    int wipemode = 0;
    Window win = disp.getWindow();
    
    while (active) {
        iflXYint winSize;

        XEvent event;
        XNextEvent(dpy, &event);
        switch (event.type) {
            
        case MotionNotify:
            // flush the event queue
            Window rw, cw;
            int rx, ry, x, y;
            unsigned int state;
            XQueryPointer(dpy, win, &rw, &cw, &rx, &ry, &x,                  &y, &state);
            
            if (event.xmotion.state&Button1Mask) 
                inView->wipe(x, y, wipemode|ilClip);
            else
                wipemode = inView->findEdge(x, y, Border);
            break;
            
        case ButtonPress:
            disp.setMouse(event.xbutton.x, event.xbutton.y);
            break;
            
        case Expose:
            disp.display(NULL, ilDefer|ilCenter);
            disp.redraw();
            disp.getSize(winSize.x, winSize.y);
            break;
            
        case DestroyNotify: 
            disp.destroyNotify();
            active = FALSE; 
            break;
        }
    }
    
    XCloseDisplay(dpy);
}

Sample Program Comments

The first half of this program should be familiar to you; it is very similar to the sample program in Chapter 1, “Writing an ImageVision Library Program.” The first several lines of code include the necessary header files. If the user specifies fewer than two arguments (the name of the program and the name of the image file), the program prints an error message and then exits.

Step 1: Open an Input Image File and Create a Threshold Image

The specified image file is opened as an ilFileImg object called in. Next, an iflPixel object is created for use by the threshold operator; the threshold value chosen for this example is 100.0. The ilThreshImg operator thresh sets each pixel to its maximum possible value if the pixel value is greater than or equal to the threshold value, or to its minimum possible value if it is less than the threshold value.

Step 2: Open an X Window and Create an ilDisplay Object

After the threshold image is created, establish a connection to the X server with the X function XOpenDisplay().

Next, you create an ilDisplay object by passing in the appropriate X window and display IDs. As shown, the processed image thresh is added first, and then the original image, in, is added using addView(). The addView() function creates an ilView for the specified image, adds it to the display's view stack, and returns a pointer to the ilView. The pointer to the ilView associated with in is used later in the program.

The order in which images are added determines their stacking order when they are displayed; the last view added is on top of the others. In this case, the original image is displayed on top of (and completely obscuring) the processed image. You can reorder the views as needed.

The setBorders() function is used with default arguments in this example to highlight all views in the view stack. You can also specify the border color and width.

Step 3: Process Events

Processing events is a critical task for interactive programs. All of the previously identified inputs (events) must be handled. The event loop in this example processes events continuously while the program is active.

The code in the event loop uses the following variables:

active 

Indicates that the window is still active. It becomes FALSE when the user selects “Quit” from the window menu.

event  

Holds the event read from the X event queue with XNextEvent().

winSize  

Indicates the current size of the window.

wipemode  

Indicates which edge of the image to wipe.

x and y  

Hold the x and y positions of the mouse, respectively, as the user drags to perform a wipe.

The code in the event loop takes the following actions in response to user actions:

  • MotionNotify. As the mouse is moved, the x and y values are accessed with the X function XQueryPointer(). The current xy location of the mouse is passed to wipe(), which changes the size of the view by moving one or more edges. While the mouse button is pressed, motion causes a wipe to be performed. If the mouse button is not pressed, then findEdge() is called. The findEdge() function returns the edge(s) near the xy location specified and is saved in wipemode. (In this program, the user must click within 10 pixels of the edge.) When wipe() is called, wipemode specifies which edges to wipe.

  • Button Press. When the user presses the left mouse button to start wiping, the setMouse() function is called to save the current x and y mouse positions.

  • Expose. The first time through the loop, an Expose event is processed, causing the entire display to be drawn on the screen with the redraw() function.

  • DestroyNotify. When the user quits, the active flag is set to FALSE.

Creating an ilDisplay

To incorporate IL's display facility in your program, you must follow these steps:

  1. Open and configure a window.

  2. Create an ilDisplay object to manage the window.

  3. Add and manipulate images you want to display.

  4. Apply the desired display operator(s) to one or more of the views.

This section discusses the first two of these items. The remaining two items are covered in detail in following sections.

Opening an X Window and Creating an ilDisplay Object

Before creating an ilDisplay object, you must open a window. To do this, use the standard X call XOpenDisplay(),. as follows:

iflSize size;
in.getSize(size);
Display* dpy = XOpenDisplay(NULL);

An ilDisplay object manages views of images within the window passed to it. If an X window is passed, render mode specifies whether X or OpenGL should be used to render the images. The constructor for ilDisplay is shown below:

ilDisplay(Display* display, int width, int height, 
              int attr=ilVisDoubleBuffer, int minComponentSize=8, 
              int mode = ilDefault, 
              long eventMask = ExposureMask | KeyPressMask |
                               PointerMotionMask | PointerMotionHintMask | 
                               ButtonPressMask | ButtonReleaseMask |
                               StructureNotifyMask);

The following statement creates an ilDisplay object. It takes as its first argument dpy, the X connection created with XOpenDisplay(). The other arguments define the size of the window created by ilDisplay:

ilDisplay disp(dpy, size.x, size.y);

The in.getSize() function returns the size of the window to create, and the values of size.x and size.y define the X and Y dimensions of the window accordingly.

When an ilDisplay object is created, the display origin is the lower left corner. The entire window is used for drawing, but this window may be a subwindow within an application.


Note: The ilDisplay class uses many enumerated types, which are listed in “Enumerated Types and Constants” and the header file il/ilDisplayDefs.h.


Adding a View to the ilDisplay Object

Once you have an ilDisplay object bound to an X window, use the ilDisplay member functions addView() and setBorders() to display the image, as shown in the following code:

ilView* inView = disp.addView(&in);
disp.setBorders(TRUE);

The &in value is the input image that is ready for manipulation and display. The setBorders() function enables the display of the default border, ilDefault, around the view.

Deallocating the Display

After the user has finished with the image, you deallocate the ilView object, ilDisplay object, and the X window by calling XCloseDisplay(displayName), where displayName is the name of the ilDisplay object.

Choosing OpenGL or X Rendering

The ImageVision library supports OpenGL as well as X rendering. IL version 3.1 allows you to choose between the two with the following ilConfigure methods:

ilStatus ilSetRenderingStyle(Display* dpy, ilRenderingStyle style)
ilRenderingStyle ilGetRenderingStyle(Display* dpy)

where ilRenderingStyle is one of the following enumerations: ilOpenGLRendering or ilXRendering. By default, ilOpenGLRendering is chosen if available. ilXRendering is used primarily for rendering to a remote machine that does not support OpenGL.

You use ilSetRenderingStyle() to request a rendering style explicitly. If ilOpenGLRendering is requested but not supported on the X connection, a status of ilUNSUPPORTED is returned, and ilXRendering is set.

ilGetRenderingStyle() returns the current style.

View and Display Basics

Once you have created a display object, the next step is to add views to this display and then apply display operators to these views. Before learning more about views, however, you need to be familiar with some basic concepts that apply to both displays and the views contained within them.

Background Color

If the images being displayed do not cover the entire display area, the ilDisplay's background view is seen in the uncovered areas. The background may also be revealed if images are dragged around or resized by the user. By default, an ilDisplay uses black as the background color. You can set the color to any pixel value with setBackground():

myDisp.setBackground(float 0, float 1, float 0);

The three arguments correspond to red, green, and blue, respectively. Each color value is between 0 and 1, inclusive.

In the previous line of code, the background color is set to green.

You can also retrieve an ilDisplay's current background color:

myDisp.getBackground(float& red, float& green, float& blue);

The returned values for the references are between 0 and 1, inclusive.

Borders

All ilViews have borders, but by default they are not drawn (that is, they are turned off). You can use ilView's setBorders() to turn borders on (TRUE) or off (FALSE):

void setBorders(int flag);

When borders are turned off, the highlight flag (see “Finding a View”) is also turned off. The borders are painted or erased immediately unless painting is deferred. Note that borders are painted inside the view.

In addition, both the borders and the NOP flag can be controlled using the select functions on ilView (see “Preventing View Operations” to learn more about the nop flag). When select() is called, borders are turned on and its nop flag is turned off. When unselect() is called, borders are turned off and its nop flag is turned on. The isSelected() function returns TRUE if the view is selected, or FALSE otherwise:

void select();
void unselect ();
int isSelected();

You can also specify the width and color of the borders:

void setBorderWidth(unsigned int bordWidth);
void setBorderColor(float red, float green, float blue);

The first function sets the width of the border in pixels to bordWidth; by default, a border has a width of two pixels. (bordWidth should be a number greater than or equal to zero.) The second function sets the color of the border to the specified colors, each with a value between 0 and 1, inclusive.

For convenience, you may set border parameters on all the views in an ilDisplay's view stack by calling the corresponding functions on ilDisplay. (You can exclude particular views in the stack from being acted upon by these functions by setting a nop flag in each view you wish to exclude. See “Deferring Drawing”.) For example:

myDisp.setBorders(TRUE);
myDisp.setBorderWidth(5);
myDisp.setBorderColor(0, 1, 0);

There are no convenience functions for getBorders(), getBorderWidth(), or getBorderColor() in the ilDisplay class since the information may vary from view to view.

Border Styles

You can set and read the style of the border using the setBorderStyle() and setBorderStyle() functions in ilView, defined as follows:

void setBorderStyle(int style = ilViewBdrSolidLines)
{ bStyle = style; setState(ilViewBorders); }
int  getBorderStyle() { return bStyle; }

The possible border styles are defined by the following enum:

enum ilViewBorderStyle {
    ilViewBdrSolidLines     = 0, // Solid lines (old style)
    ilViewBdrDashedLines    = 1, // Dashed lines
    ilViewBdrCornerHandles  = 2, // Handles on corners
    ilViewBdrMiddleHandles  = 3  // Handles on mid-side

Preventing View Operations

To keep any view in the stack from being operated upon, use the setNop() function to set the nop flag:

void ilDisplay::setNop(int nop, ilView* view);
void ilView::setNop(int nop);

If the nop argument is TRUE, then the view will not be operated on. To allow operations to take place on a view, nop should be FALSE. You can use the function isNop() to determine the state of the nop flag:

int ilDisplay::isNop(ilView* view);
int ilView::isNop();

If you need to perform an operation on each view in the stack regardless of the value of each view's nop flag, pass the ilDop flag in the mode for that operation.

If an operation is called on a view, the nop flag is overridden. For example, the statement below ignores the nop flag on the specified view:

view->wipe();

Deferring Drawing

Drawing can be deferred by calling setDefer() on ilDisplay or ilView. When used to defer the display, nothing is drawn; however, each view can be individually deferred as well. These calls are shown below:

void ilDisplay::setDefer(int def, ilView *view=NULL);
void ilView::setDefer(int def);

In the ilDisplay version, you specify the view in which you wish to defer drawing (the default is all views) by setting the ilView pointer argument to

  • NULL, which causes all views in the view stack to be affected

  • a pointer to an ilView in the view stack

You might want to defer drawing until you have made a series of changes to an ilDisplay's attributes (or to those of its views) so that they all take effect simultaneously. You might also want to defer drawing while you apply more than one display operator to avoid drawing intermediate results. In addition, most of the display operators allow you to pass the ilDefer flag (see “Mode Flags”) to defer drawing. (Display operators are described in more detail in “Applying a Display Operator”.)

To defer drawing, call setDefer() and pass TRUE as its def argument. After that, the display is not redrawn until you call setDefer() with FALSE as its def argument. You can check whether drawing is deferred with isDefer():

int ilDisplay::isDefer(ilView *view=NULL);
int ilView::isDefer();

This function returns TRUE or FALSE to indicate whether drawing has been deferred or not.

The Drawing Area

An ilDisplay assumes that it can draw anywhere in the window that is been passed to it. You can retrieve the current size of the drawing area with getSize(), which returns the x and y dimensions by reference:

void getSize(int& x, int& y);

Managing the Cache

With global cache management, using ilView to manage the cache on its input is unnecessary. The various cache management methods on ilView have no effect. For more information, refer to “The Cache” and “Optimizing Use of Cache” for a discussion of the global cache management scheme.

Mode Flags

All the display operators use a mode argument to control the display of views. This mode is a bitwise-OR'd combination of flags that control the operator. You can use the ilDisplay member functions, setMode() and clrMode() to set and clear the mode flags.

The flags are defined as enumerated values (see il/ilDisplayDefs.h). Some flag types are described below.

Display Flags

Display flags specify various display modes. Examples are:

  • ilClip to clip an image to the edge of the display or view

  • ilDefer to defer painting

  • ilDop to override the nop flag

Coordinate Flags

Coordinate flags specify how the resizing, relocating, and update operators are to interpret coordinate values. Examples are:

  • ilDelVal where x,y is interpreted as delta relative to the current values

  • ilRelVal to interpret the x,y coordinates relative to the starting x,y (starting x,y is updated)

  • ilAbsVal to interpret the x,y coordinates as absolute values

  • ilOldRel to interpret the x,y coordinates relative to the starting x,y (starting x,y is not updated)

Wipe Mode Flags

Wipe mode flags specify the edges in a wipe operation. Some examples are:

  • ilTopEdge to do the wipe from the top edge

  • ilLeftEdge to do the wipe from the left edge

Align Mode Flags

Align mode flags specify image alignment. Some examples are:

  • ilTopLeft to align the view from the top left corner

  • ilCenter to align the view to the center of the window or the image to the center of the view

The sample program shown at the beginning of this chapter contains an example of the use of the mode argument. In this example, the display operator initializes all views in the view stack, aligns the views to the center of the image, and defers the painting of the view until later.

disp.display(NULL, ilDefer|ilCenter);

Managing Views

Once an ilDisplay has been created, you can create views of the images you want displayed. As views are created, they are pushed onto the view stack. You can also retrieve views from the stack, replace the images within the views with other images, remove views, and reorder the views in the stack. This section explains how to perform these tasks.


Note: If an error occurs while rendering part of a view, the offending tile is painted with the error color (see getErrorColor() or setErrorColor()), and the status is set on ilDisplay. The error color defaults to magenta, but can be set per view with setErrorColor().

The error color functions are defined in ilView, as follows:

void getErrorColor(float& red, float& green, float& blue)
        { err.get(red, green, blue); }
void setErrorColor(float red, float green, float blue)
        { err.set(red, green, blue); }

The values of the colors are between 0 and 1, inclusive.

Adding Images

The addView() function creates an ilView and adds it to the view stack. The image is drawn when addView() is called, unless ilDefer is passed in mode. It returns a pointer to the ilView for the ilImage (or XImage) pointer passed in

ilView* addView(ilImage* img, int index, int mode); 
ilView* addView(ilImage* img, int mode=ilCenter);
ilView* addView(XImage* img, int index, int mode); 
ilView* addView(XImage* img, int mode=ilCenter); 

You can call addView() with just the image or the image and the display mode. In this case, the view index defaults to 0 (top of the stack). If you use the version of addView() that takes an index, you can specify the location in the view stack where the image is to go.

The mode parameter controls the creation and position of the ilView. By default, the view is centered, not clipped to the display window, and is painted after being added. However, this behavior can be modified using various display mode flags such as ilClip and ilDefer. See “Mode Flags” and the ilDisplay reference page for more details.

If an image has a z dimension that is greater than one, you can choose which xy plane of the image to display. By default, the first plane (z = 0) is displayed. To display a different plane, call setZ() on ilView:

void setZ(int startZ);

The startZ argument specifies the desired plane of the image in the view that the function is called on. ilView's getZ() function takes no arguments and returns the current z plane being displayed of the corresponding image.

Stereo Viewing

If your machine is capable of stereo and stereo is supported by IL on that machine, you can turn on stereo viewing mode. A stereo view can be created as shown below:

ilStereoView(ilDisplay* disply, ilImage* LImg, 
ilImage* RImg, int mode = 0);
ilStereoView(ilDisplay* disply, ilImage* img, int zLeft = 0,
int zRight = 1, int mode = 0);

Using the first version of the constructor, pointers to the left and right images are passed to this method on ilDisplay. The last argument specifies the display mode for the view. Currently, only OpenGL render mode is supported. This constructor requires that you set up an IL chain for the left and right images.

The second version of the constructor takes a single image with the left and right images stored in the z dimension. The parameters zLeft and zRight specify the index in the z dimension corresponding to the left and right images. The benefit of this approach is that you can use a single IL chain to process both images.

With either constructor, the relative screen positions of the left and right images can be adjusted (see the ilStereoView reference page). If the hardware does not support stereo, or if stereo mode is disabled, only the left image is displayed. Also note that IL can display a mixture of monoscopic and stereoscopic views in the same stereo window.

To review an example of a stereo view application, see /usr/share/src/il/ilstereoview.c++.

Retrieving Views

You can obtain a pointer to any view in the stack with getView(). There are two versions of this function, one that takes an index and another that takes a pointer to an ilImage:

ilView* getView(int index = 0); 
ilView* getView(ilImage* img);

Both functions return a pointer to the corresponding ilView. If the image appears in more than one view, the view that is nearest the top of the stack is returned.

You can also retrieve the index corresponding to a particular view:

int theIndx = myDisp.getViewIndex(someView);
int theIndx = myDisp.getViewIndex(someImg);

The getViewIndex() function takes a pointer to an ilView (someView) or a pointer to an ilImage (someImg) and returns its index as an int (theIndx).

To determine how many views are in the view stack, call getNumViews().

Retrieving Images

You can obtain a pointer to the image in a particular view with getImg() or getXImg():

ilImage* myImg = someView->getImg();
XImage* myXImg = someOtherView->getXImg();

A pointer to the ilImage or XImage in the view is returned. (Here, someView and someOtherView are ilView pointers.)

To obtain pointers to the images in a stereo view, use getLImg() and getRImg():

ilImage* myLeft = someStereoView->getLImg();
ilImage* myRight = someStereoView->getRImg();

A pointer to the left ilImage is returned from getLImg() and a pointer to the right from getRImg(). (Here, someStereoView is an ilStereoView pointer.)

Removing Views

You can remove a view from the stack by deleting the view or by calling deleteView() on ilDisplay. This function removes the specified view from the stack and deletes it:

void deleteView(ilView* view);

Replacing Images

An ilView object allows you to replace its image:

void setImg(ilImage* ilInImg);
void setXImg(XImage* xInImg);

The argument is a pointer to the image you want the view to hold. This image replaces the image mapped to the view.

Reordering the View Stack

Several functions are provided by ilDisplay to reorder the view stack. The push() function pushes the specified view down count places in the stack. By default, it pushes the view to the bottom. Similarly, the pop() function pops the specified view up count places in the stack. By default, it pops the view to the top. On both push and pop, when count is 1, the view is moved one position in the view stack. In addition, the swap() function swaps two views in the stack. These functions are shown below:

push(ilView *view, int count=0);
pop(ilView *view, int count=0);
swap(ilView *view1, ilView *view2);

Finding a View

Sometimes you need to find the view at a specified location. In an interactive program, the mouse is typically used to select a view. To find the view at a given x,y location, findView() can be called on ilDisplay, as shown below:

ilView* findView(int x, int y, int mode = ilDspCoord);

This function returns a pointer to the topmost ilView found at location xy within the display. If there is no view at xy, it returns NULL. If ilHighlight is passed in mode, the view is highlighted if found. When a view is highlighted, its borders are turned on. However, only one view at a time can be highlighted. If ilDspCoord is passed in mode (the default), the xy coordinates are interpreted relative to the origin of the display area (display coordinates). If ilScrCoord in passed in mode, then the xy coordinates are interpreted relative to the screen (screen coordinates). Recall that the origin of the display area coincides with that of the window.

Finding an Edge

You may need an edge of a view for certain operations. Sometimes, you want to determine which edge of a view the cursor is near. This is especially useful for wiping, as described in “Applying a Display Operator”. For this, use ilView's findEdge() function:

int findEdge(int x, int y, int margin = -1,
int mode = ilDspCoord);

This function determines which edge of the view is nearest to the specified xy coordinates. If the specified point is within margin pixels from an edge, that edge is returned. By default, the margin is either the default margin (15) or the current border width, whichever is greater. The mode argument can be either ilDspCoord (the default) or ilScrCoord to indicate whether x and y are specified in display or screen coordinates.

The value returned by findEdge() is a bitwise-OR'd combination of the following values:

ilNoView 

The coordinates lie outside margin pixels of all views.

ilRightEdge 

The coordinates are within margin from the right edge.

ilLeftEdge 

The coordinates are within margin from the left edge.

ilTopEdge 

The coordinates are within margin from the top edge.

ilBottomEdge 

The coordinates are within margin from the bottom edge.

ilAllEdge 

The coordinates are within margin from all edges; this is an unusual case since it implies that margin is very large relative to the image. The ilAllEdge value is used primarily as an argument for wipe(), which is described in “Applying a Display Operator”.

ilNoEdge 

The coordinates do not lie within margin from any edge.

If a combination of two intersecting edges is returned—for example, ilRightEdge|ilTopEdge—you can treat the value as corresponding to a corner, in this case the upper right corner. Note that you can also receive a value such as ilTopRight, which is equivalent to ilTopEdge|ilRightEdge.

ilDisplay also defines a findEdge() function, which finds the edge on all views. For each view, it saves the edge for later use with wipeSplit().

Operating on a Pixel

You can obtain the actual pixel data at a specified point in a view with getPixel() (defined by both ilDisplay and ilView):

ilStatus getPixel(int x, int y, ilPixel& pix, int mode = 0);

In ilView's version, this function copies the pixel data at the point x,y into pix. If the point lies outside the view, the fill value is returned by reference. In ilDisplay's version, the topmost view pointed to by the point x,y is found with findView(); the pixel data from the point in that view is copied into pix. If the point refers to no view, no pixel data is returned by reference. (The x,y point is specified in display coordinates.)

You can also set a pixel value with setPixel():

void setPixel(int x, int y, ilPixel pix, int mode = 0);

Locating a Point

You can find out where you are in an image by passing the display coordinates to getLoc() (defined by both ilDisplay and ilView):

void getLoc(int x, int y, int& ix, int& iy, 
           int mode = ilLocIn);
void getLoc(float x, float y, float& ix, float& iy, 
           int mode = ilLocIn);
void getLoc(float& ix, float& iy, 
           int mode = ilLocIn|ilCenter);

The getloc() function returns the location in the image corresponding to x and y. The location in the image is returned in ix and iy. If ilLocIn is passed in mode, the location is returned in the input space of the image. If ilLocOut is passed in mode, the location is returned in the output space of the image. For example, if an ilRotZoomIng is mapped to the view and ilLocIn is specified, ix and iy correspond to the location in the unzoomed image. However, if ilLocOut is specified, ix and iy correspond to the location in the zoomed image. If ilLocImg is specified (default), then the image is moved to the specified location. If ilLocView is specified, then the view is moved to the specified location.

The second version uses floating point values for more accuracy. The third version determines the desired location based on mode. For example, if ilCenter is specified, the location corresponding to the center of the view is returned.

When called on ilDisplay, the topmost view pointed to by x, y is found with findView(). Then the location is returned for that view. On both ilDisplay and ilView, a version is provided that does not require an xy location to be specified. Instead, the mode is used to specify the center or a corner.

Similarly, you can set the location of an image within the display by calling setLoc() on ilDisplay or ilView. This allows you to move a point within the image to a specific location within the display, as show below:

void setLoc(int ix, int iy, int x, int y, 
           int mode = ilLocIn);
void setLoc(float ix, float iy, int mode = ilLocIn|ilCenter);
void setLoc(float ix, float iy, float x, float y, 
           int mode = ilLocIn);

The relocation can be accomplished by moving the image or the view. If ilLocView is specified, then the view is moved, otherwise the image is moved (ilLocImg).

Applying a Display Operator

Display operators alter views, typically in response to input from the user. These operators may draw all or portions of a view. Also, they can change the size and/or location of all or some of the displayed views and then update the display accordingly. These are ImageVision Library's display operators; they can be called on both ilDisplay and ilView (except for display(), which may be called only on ilDisplay):

  • Drawing operators—Operators whose primary purpose is to draw all or part of the display. This group includes display(), paint(), qpaint(), redraw(), setStaticUpdate(), and save().

  • Relocating operators—Operators whose primary purpose is to change the location of views or images. This group includes alignView(), alignImg(), moveView(), moveImg(), and split().

  • Resizing operators—Operators whose primary purpose is to change the size of views or images. This group includes wipe(), wipeSize(), wipeSplit(), and resize().

  • update()—Generalized operator that combines the capabilities of moveView(), moveImg(), and wipe(). However, because it is a generalized operator, it is not as optimized as some of the other operators.

There is only one difference between calling a function on ilDisplay and calling it on ilView. When called on ilView, the function operates only on that view regardless of the state of the nop flag. In contrast, when called on ilDisplay, a view must be specified. If NULL is passed, then all views in the stack are operated on (except those with the nop flag set). If a pointer to a view is passed, the function operates only on that view.

In this section, all operators are given in their ilDisplay forms. The ilView versions are easily derived by leaving out the argument specifying the view.

Drawing Views

The functions used primarily for drawing are described in this section:

  • display() reinitializes the specified view and optionally aligns the view and image. The specified view is then painted. If NULL is specified, then all views are initialized (except those with nop flag set).

  • paint() does not resize or reposition the view. It simply paints the specified view if it needs to be painted. If ilPaintExpose is passed, then the view is forced to be painted.

  • qpaint() queues painted views for multi-processor operations.

  • redraw() resizes the display image and background view to occupy the entire window. It then paints all views regardless of the nop flag. It does not resize or reposition any views.

  • save() paints the specified region of the display to an ilImage. A starting location within the display and a pointer to an ilImage are passed. The save region is specified by the starting location and the size of the image.

  • setStaticUpdate() sets the staticUpdate mode to paint a rectangular region as one tile rather than many smaller ones.

display()

The display() function takes three arguments, all of which have default values, as shown below:

void ilDisplay::display(ilView* view = NULL, 
                  int vmode = ilCenter,
                  int imode = ilCenter);

view 

Reinitializes the specified view. If NULL is passed, then it reinitializes all views (except those with nop flag set). If the ilDop flag is passed in mode, the nop flag is ignored.

vmode  

Specifies how to align the view within the display.

imode  

Specifies how to align the image within the view. As explained above, only the visible portion of each view is drawn.

Both vmode and imode are a bitwise-OR'd combination of values that allow you to specify alignment. You can align to any corner or edge using any combination of ilTopEdge, ilBottomEdge, ilLeftEdge, or ilRightEdge. In addition, ilTopLeft, ilBottomLeft, ilTopRight, or ilBottomRight can be used to specify a corner. By default, ilCenter is used. If no alignment is desired, ilNoEdge or ilNoAlign can be passed instead. See “Relocating Views and Images” for more information about the alignView() and alignImg() functions.

By default, a view is the size of its image; however, if ilClip is passed in vmode, then the view is clipped to the size of the display or window.

paint()

The paint() function is typically used when a view needs to be redrawn after several deferred operations. This function takes a view pointer and a mode as arguments:

void paint(ilView* view = NULL, int mode = 0);

The view argument has the same meaning as that for display(). The mode argument can include any of the generic display flags.

You can get the change of position and size from one painting to another using the getDel() (get delta) function in ilView, defined as follows:

void getDel(iflXYint& dVPos, iflXYint& dVSize, iflXYfloat& 
dIPos);
void getDel(iflXYint& dVPos, iflXYint& dVSize, iflXYZfloat& 
dIPos);

The first and third references provide the delta of the image's position since the last paint(). The second reference provides the delta of the image's size since the last paint(). The two constructors provide two- and three-dimensional alternatives.

qpaint()

The qpaint() function is used to queue the painting of a specified view. It is used most often to optimize performance in multi-processor operations. The function is defined as follows:

void qPaint(ilMpNode* parent, int x, int y, int nx, int ny,
                iflOrientation orientation = iflUpperLeftOrigin,
                ilView* view = NULL, int mode = 0,
                ilMpManager** pMgr = NULL);
void qPaint(ilMpNode* parent, ilView* view = NULL, int mode = 0,
                ilMpManager** pMgr = NULL)
                {
                    qPaint(parent,
                           visArea.x, visArea.y, visArea.nx, visArea.ny,
                           workOrientation, view, mode, pMgr);
                }

The first constructor allows you to define or specify the view being queued. The second constructor is for use with scrolling lists where the view is clipped by the size of the scrolling list.

redraw()

The redraw() function is called when a REDRAW (OpenGL) or Expose (X) event occurs (for example, if the window is exposed or resized):

void redraw(int mode = ilDefault);

The redraw() function resizes the drawing area (display image) and the background view to match the new size of the window, and paints all views.

save()

The save() function saves a region of the display by painting to an ilImage. The region saved is specified by the origin x,y and is the size of the image passed in

ilStatus save(ilImage* img, int x = 0, int y = 0, 
                         int mode = ilDefault);

By default, borders are not painted. However, if ilPaintBorder is passed in mode, the borders are painted. Note that on 8-bit graphics systems, displayed images may be dithered. Therefore, the save function provides a higher quality result than copying from the screen.

setStaticUpdate()

The setStaticUpdate() function allows you to enable or disable the staticUpdate mode. Static update paints a rectangular region as one large tile rather than as many smaller tiles. When staticUpdate mode is enabled, it forces a static update to occur whenever the view is painted.

void setStaticUpdate(int enable)

The setAutoStaticUpdate() function forces a static update after a reset has occurred. A reset is caused by changing inputs or processing parameters in the chain. In this case, since the entire exposed region of the view must be painted, the performance can be improved by painting the region as one large tile. After the static update has been completed, normal tiled painting resumes. By default, automatic static update is enabled.

void setAutoStaticUpdate(int enable)

The isStaticUpdate() function allows you to retrieve the current staticUpdate mode:

isStaticUpdate()


Note: Static update mode only has effect for hardware acceleration.


Relocating Views and Images

The functions used to relocate views and images are described in this section:

  • alignImg() aligns an image within its view.

  • alignView() aligns a view with a reference view.

  • moveImg() moves an image within a view.

  • moveView() moves a view within the display area.

  • split() repositions all views into rows and/or columns and resizes the views to fit.

The following mode flags are also used in conjunction with the functions discussed in this section:

ilAbsVal 

The xy pair represents absolute values. In other words, the view is simply moved to the location specified.

ilDelVal 

The coordinates represent a change (delta) in the current view or image position. For example, if moveView is called with (2,5) and the specified view is located at (1,1), then the view is moved to (3,6).

ilRelVal 

The xy pair is interpreted relative to the starting xy set by calling setMouse(). The starting x,y values are updated. The setMouse() function must have been called previously to initialize ilDisplay's coordinate values. This is the default mode for most functions.

ilOldRel 

Same as ilRelVal except that the starting xy values are not updated.

alignImg()

The alignImg() function is defined on both ilDisplay and ilView. This function aligns the image in view. If view is NULL (the default), the function aligns the images in all the views in the view stack (except those with the nop flag set). It is called as shown below:

void alignImg(ilView* view=NULL, int mode=ilCenter);

Alignment means that an edge, corner, or center of an image is aligned within the view, as shown in Figure 5-6. The mode argument specifies how to align the image. For example, the default, ilCenter, indicates that the image is to be centered in the view. In Figure 5-6, ilBottomLeft is passed in mode, causing the lower left corner of the image to be aligned to the lower left corner of the view.

Figure 5-6. Aligning an Image to Bottom Left Corner

Figure 5-6 Aligning an Image to Bottom Left Corner

alignView()

The alignView() function is defined on both ilDisplay and ilView. This function aligns the specified view with a reference view. If NULL is passed, all views are aligned (except those with the nop flag set). The function is called as shown below:

void alignView(ilView* view = NULL, int mode = ilCenter,
ilView* rView = NULL);

The reference view is specified by rview. If it is NULL, then the back view is used. Alignment means that edges, corners, or centers of the views are aligned, as shown in Figure 5-7. The mode argument specifies how to align the views. By default, ilCenter causes views to be aligned by their centers. In Figure 5-7, the views are aligned by their lower left corners with ilBottomLeft.

Figure 5-7. Aligning Views

Figure 5-7 Aligning Views

moveImg()

The moveImg() function changes the location of images within their respective views. To use this function, you need to specify the desired location and the view to which the image corresponds:

void moveImg(float x, float y, ilView* view = NULL, 
   int mode = ilRelVal);

This function moves the image within the specified view. In other words, the view remains fixed relative to the display while the corresponding image moves within the view. This function allows a user to roam around an image. This is particularly useful for large images that are bigger than the screen. Thus, the coordinate values x,y specify the desired location of the image's origin. they are interpreted according to the relevant flags passed in mode (such as ilDelVal, ilRelVal, and so on). The mode argument can also include flags indicating that drawing should be deferred (ilDefer) and that the image should not be moved beyond its edge (ilClip). By default, the image can be moved beyond its view, in which case the image's fill value is used to paint the view.

When roam is enabled, the speed with which you can roam around a picture is related to the displacement of the mouse: the farther you move the mouse, the greater the displacement between consecutively-displayed frames.

You can change this behavior by calling setRoamLimit(). This function limits the displacement between consecutively-displayed frames for example, if the limit is set to four, regardless of how far you move your mouse, consecutively displayed frames will always be displaced by four pixels. This function has the effect of smoothing out roam motion.

float getRoamLimit();
void  setRoamLimit(float maxRoamDel = 0.0);
void  getRoamRate(float& x, float& y);

The getRoamRate() returns the displacement, in pixels, in the X and Y directions between consecutively-displayed frames.

moveView()

The moveView() function changes the location of views within the display. You might use this function to allow a user to drag a view around the display area using one of the mouse buttons. To use this function, specify the desired location and the view to be moved:

void moveView(int x, int y, ilView* view = NULL,
int mode = ilRelVal);

The view pointer argument view specifies which view to move (or all the views if NULL, the default). The x and y arguments indicate where to move the view, and mode specifies how these arguments should be interpreted (with ilDelVal, ilRelVal, and so on).

You can include ilDefer in the mode argument if you do not want the display updated. Also, by default, you can move the views out of the window. For example, a user can continue dragging a view past the edge of the window; the view will not be visible, but ilDisplay keeps track of its location so that if the user drags it in the opposite direction, eventually the view becomes visible in the window. You can prevent a view from being moved past the window's edge by specifying ilClip as part of the mode argument.

split()

The split() function allows you to display all views next to one another in rows and/or columns rather than randomly overlapping one another. All views are resized and repositioned based on the number of views in the view stack. Starting at the bottom of the stack, views are positioned starting at the lower left corner of the display. The split() function is called as shown below:

void split(int mode = ilAbsSplit|ilRowSplit|ilColSplit)

The mode argument controls the layout. It can be a combination of the following modes:

ilAbsSplit 

Aligns images to the origin regardless of the view position. (See Figure 5-8.)

ilRelSplit 

Positions images relative to view position. (See Figure 5-9.)

ilRowSplit 

Divides the drawing area into rows. (See Figure 5-8.)

ilColSplit 

Divides the drawing area into columns.

ilPackSplit 

Clips views to an image if needed and packs them together.

Figure 5-8. split() with ilAbsSplit | ilRowSplit | ilColSplit

Figure 5-8 split() with ilAbsSplit | ilRowSplit | ilColSplit

Figure 5-9. split() with ilRelSplit | ilRowSplit | ilColSplit

Figure 5-9 split() with ilRelSplit | ilRowSplit | ilColSplit

If both ilRowSplit and ilColSplit are specified, split() divides the drawing area into equal-sized rectangles such that the number of rows and columns is nearly equal. (See Figure 5-9.) Note that if both ilAbsSplit and ilRelSplit are specified, split defaults to ilAbsSplit. In addition, an alignment mode can be specified with ilAbsSplit, such as ilCenter.

Resizing Views

The functions used to resize one or more views are shown below and are described in this section:

  • resize() resizes a view (defined only on ilView).

  • wipe() moves one or more edges of a view.

  • wipeSplit() wipes the nearest edge of all views.

  • wipeSize() wipes an edge or corner and the opposite edge or corner.

As with the relocating functions, if ilAbsVal is passed in mode, the xy values specify the new size of the view. For ilDelVal, the xy values represent changes to the current size of the view. ilRelVal means that the xy values are interpreted relative to the start values previously set with setMouse(). The start values are then updated by ilDisplay unless ilOldRel is specified.

resize()

The resize() function reinitializes the size of a view to the size of the image it displays. This useful after setting the image in ilView. The resize() function is called as shown below:

void resize(int mode = 0);

If ilClip is passed in mode, then the view is clipped to the size of the display. After the view is resized, it is painted unless ilDefer is passed.

wipe()

The wipe() function moves one or more edges on the specified view. It is called as shown below:

void wipe(int x, int y, ilView* view = NULL, 
int mode = ilRelVal);

The values x and y specify how to move the specified edge of the view. they are interpreted according to the flags passed in mode (such as ilRelVal, ilDelVal, and so on). The default is ilRelVal. If NULL is passed for view, then all views are wiped (except those with nop flag set).

The edge to wipe is specified in mode. Any combination of the following edge modes can be used: ilRightEdge, ilLeftEdge, ilTopEdge, or ilBottomEdge. For example, ilTopEdge|ilRightEdge (or ilTopRight) allows the user to wipe the upper right corner, thus resizing the view. In addition, the value returned by findEdge() can be used directly. (See “Finding an Edge”.)

If ilAllEdge (or a bitwise OR of all four edges) is used, the effect is slightly different from a normal wipe. In this mode, called an inset, the view moves while the image remains fixed (opposite of moveImg()). This mode is useful to move a processed view of an image around on top of the original image for comparison.

By default, the view is painted after it is wiped unless ilDefer is passed in mode. Also by default, the edge of a view can be moved beyond the edge of the image, unless ilClip is passed. When the view is allowed to be wiped beyond the edge of the image, the image's fill value is used to paint the exposed region. Note that the wipe function is optimized to paint only the wiped region.

wipeSplit()

The wipeSplit() function is used in conjunction with findEdge() on ilDisplay to wipe the nearest edge of all views. It is called as shown below:

void wipeSplit(int x, int y, int mode = ilRelVal);

The x and y parameters control how the edges are moved. No view is specified because it operates on all views in the view stack. The mode parameter specifies only how to interpret x and y. Note that the edge on each view is not specified by mode. Instead, findEdge() must be called on ilDisplay first to find the edge on all views. If no edge is found for a particular view, then that view is not wiped.

This function is useful after a split operation. For example, if the display is split to show two views side by side, it allows you to wipe the right edge of the left view and the left edge of the right view simultaneously. This is useful when comparing two or more images. In general, adjacent views can be wiped using this function.

wipeSize()

The wipeSize() function wipes the specified edge and the opposite edge to resize the view. It is called as shown below:

void wipeSize(int x, int y, ilView* view = NULL, 
int mode = ilDelVal | ilTopRight);

The x and y parameters control which way to move the edge specified in mode. In addition, the opposite edge is moved in the opposite direction, causing the view to grow or shrink in size. For example, if the right edge is moved to the right, then the left edge is moved to the left as well. In this case, the view would grow in width, as show in Figure 5-10.

Figure 5-10. Using wipeSize()

Figure 5-10 Using wipeSize()

Updating Views

The update() function can change the view position, view size, and image position as shown below:

void update(int x=0, int y=0, int nx=0, int ny=0,
int imgX=0, int imgY=0, 
ilView* view=NULL, int mode=ilRelVal);

The view is moved to the position specified by x and y and is resized to nx and ny. The image within the view is moved to the position specified by imgX and imgY. If view is NULL, then all views in the view stack are updated (except those with nop flag set).

The first six of these parameters are interpreted as specified by mode. For example, if ilDelVal is specified, then all six parameters are interpreted as changes from the current configuration. In addition, the parameters are used as specified. However, if ilClip is passed in mode, then the view position, size, and image position are clipped. After the view has been updated, it is painted unless ilDefer is passed in mode. The update function combines the functionality of moveView(), wipe(), and moveImg().

Using setMouse()

A display support function that you might find useful as you apply display operators is setMouse():

void setMouse(int x, int y, int mode = 0);

This function is typically used in an interactive loop to initialize the starting x and y coordinate values that the ilDisplay keeps track of. The coordinates passed to any function with ilRelVal or ilOldRel are interpreted relative to the current mouse position. If ilRelVal is specified, the old start values are updated; however, if ilOldRel is specified, the start values are not updated. This is useful if several operations are needed and you do not want to update the start values until you are finished. This model is used in the program presented in “A Simple Interactive Display Program”. To retrieve the previously set start values, use getMouse(). This function returns the start values by reference:

void getMouse(int& x, int& y);

You can achieve many different effects by judiciously deferring drawing while you apply a combination of these and/or any of the other display operators.

A More Complicated Interactive Display Program

The ilview interactive display program (which is installed in /usr/sbin when you install the Image Tools) allows a user to drag, roam, and wipe several images in a display window. (See ilview's reference page for more information.) A simplified version of ilview's source code is provided online in:

/usr/share/src/il/ilview.c++

The C version of this program named ilcview.c is located in the same directory.

Example 5-2 shows the portion of this program that processes events and calls display operators. It uses an ilViewer to handle events. The ilViewer class is a higher-level object derived from ilDisplay. It calls ilDisplay functions and operators based on X events. It calls moveView() for left mouse button movement and moveImg() for middle mouse button movement. The cursor changes shape near the edges and corners to indicate that wipe mode is enabled on the left mouse button. If you press the left mouse button and perform a wipe, this changed cursor remains for the duration of the wipe. See the ilViewer reference page and the header file il/ilViewer.h for details.

Example 5-2. A More Complicated Interactive Display Program


    // Create X window viewer
    ilViewer viewer(dpy, winsize.x, winsize.y, attr);
    
    for (idx = 0; idx < nimg; idx++) 
        viewer.addView(img[idx], ilLast, ilClip|ilCenter|ilDefer);
    viewer.setStop(TRUE);
    
    // Execute the UI event loop
    // XXX need event call back on ilViewer to make this easier to do
    int done=FALSE;

    while (!done) {

        XEvent e;
        XNextEvent(dpy, &e);
        switch (e.type) {

        case KeyPress:
            switch(XLookupKeysym(&e.xkey, 0)) {
                // center the selected view(s) in the viewer
                case XK_Home:
                    viewer.display(NULL, ilCenter|ilClip);
                    break;

                // control-Q and escape exit the program
                case XK_q:
                    if (!(e.xkey.state&ControlMask))
                        break;
                    /*FALLTHROUGH*/
                case XK_Escape:
                    done = TRUE;
                    break;

                // raise and lower the current view(s)
                case XK_Up:
                    viewer.raise();
                    break;
                case XK_Down:
                    viewer.lower();
                    break;

                // enable/disable paint pipelining
                case XK_p:
                    viewer.enableQueueing(!viewer.isQueueingEnabled());
                    break;
            }
            break;

        case DestroyNotify: 
            viewer.destroyNotify();
            done = TRUE; 
            break;
        
        default: 
            viewer.event(&e);
            break;
        }
    }
    exit(EXIT_SUCCESS);
}