Tjelvar Olsson     About     Posts     Feed     Newsletter


I'm currently working on a computing book aimed at biologists. Check out the Biologist's Guide to Computing!

How to build a basic image viewer using FreeImage and SDL2

In this blog post we will use FreeImage and SDL2 to create a basic image viewer in C. FreeImage is an open source library for working with image files. It supports over 30 file formats, gives access to meta-data and provides basic image manipulation routines. SDL (Simple DirectMedia Layer) is a cross-platform library which provides low level access to things like the keyboard and mouse as well as graphics hardware using OpenGL and Direct3D. SDL provides official supports for Windows, Mac, Linux, iOS and Android.

By the end of this post we will have created a C program that can be used to view RGB and grayscale images from the command line.

Argument parsing

Let us start by adding some basic argument parsing. Add the code below to a file named see.c.

#include <stdlib.h>
#include <stdio.h>

char *parse_args_get_filename(int argc, char *argv[]) {
    if (argc != 2) {
        fprintf(stderr, "Usage: %s FILENAME\n", argv[0]);
        exit(2);
    }
    char *filename = argv[1];
    return filename;
}

There are a few things going on in the above. We include the <stdlib.h> and <stdio.h> header files. The former provides the exit() function and the latter the fprintf() function.

We also define the function parse_args_get_filename() that returns a pointer to a char array, the char array that will hold our input file name. Note that the argc variable is an integer that holds the number of arguments supplied from the command line and argc is a list of pointers to char arrays holding the values of the strings supplied from the command line. In our program argc will contain two such pointers, one to the name of the program and one to the filename of the image we want to view.

Reading in the image using FreeImage

We will now use FreeImage to read in an image. Let us start by including the FreeImage header.

#include <stdlib.h>
#include <stdio.h>

#include <FreeImage.h>

Now let us add a function for creating a FreeImage bitmap from an image file. The function will return a pointer to the bitmap.

/** Initialise a FreeImage bitmap and return a pointer to it. */
FIBITMAP *get_freeimage_bitmap(char *filename) {
    FREE_IMAGE_FORMAT filetype = FreeImage_GetFileType(filename, 0);
    FIBITMAP *freeimage_bitmap = FreeImage_Load(filetype, filename, 0);
    return freeimage_bitmap;
}

Here we use the FreeImage_GetFileType() function to determine the image file type by analysing the bitmap signature. According to the FreeImage documentation the second parameter (size) is currently not in use and can be set to 0.

We then use the FreeImage_Load() function to initialise the bitmap from the input file name. The third parameter (flags) can be used to change the loading behaviour for certain file types. As we are not interested in using this functionality we can set it to 0.

Note that the FreeImage bitmap is flipped vertically with respect to the coordinate system used by SDL. We will deal with this in the next section.

Creating a SDL surface from a FreeImage bitmap

It is time to start using SDL. Let us therefore include the SDL header.

#include <stdlib.h>
#include <stdio.h>

#include <FreeImage.h>

#include <SDL2/SDL.h>

Now we will create a function that takes our FreeImage bitmap and returns a SDL_Surface (a structure containing pixel information). To achieve this we will make use of the SDL_CreateRGBSurfaceFrom() function.

/** Initialise a SDL surface and return a pointer to it.
 *
 *  This function flips the FreeImage bitmap vertically to make it compatible
 *  with SDL's coordinate system.
 *
 *  If the input image is in grayscale a custom palette is created for the
 *  surface.
 */
SDL_Surface *get_sdl_surface(FIBITMAP *freeimage_bitmap, int is_grayscale) {

    // Loaded image is upside down, so flip it.
    FreeImage_FlipVertical(freeimage_bitmap);

    SDL_Surface *sdl_surface = SDL_CreateRGBSurfaceFrom(
        FreeImage_GetBits(freeimage_bitmap),
        FreeImage_GetWidth(freeimage_bitmap),
        FreeImage_GetHeight(freeimage_bitmap),
        FreeImage_GetBPP(freeimage_bitmap),
        FreeImage_GetPitch(freeimage_bitmap),
        FreeImage_GetRedMask(freeimage_bitmap),
        FreeImage_GetGreenMask(freeimage_bitmap),
        FreeImage_GetBlueMask(freeimage_bitmap),
        0 
    );

    if (sdl_surface == NULL) {
        fprintf(stderr, "Failed to create surface: %s\n", SDL_GetError());
        exit(1);
    }

    if (is_grayscale) {
        // To display a grayscale image we need to create a custom palette.
        SDL_Color colors[256];
        int i;
        for(i = 0; i < 256; i++) {
            colors[i].r = colors[i].g = colors[i].b = i;
        }
        SDL_SetPaletteColors(sdl_surface->format->palette, colors, 0, 256);
    }

    return sdl_surface;
}

We start off by flipping the bitmap vertically. Since this is done in memory it is a side-effect of the function.

We then create the SDL surface using the SDL_CreateRGBSurfaceFrom() function, which (amongst others) takes as input the red, green and blue masks of the FreeImage bitmap. The functions for accessing these masks (FreeImage_GetRedMask(), etc) work even if the FreeImage bitmap comes from a single channel input image (gray scale). If the input image is in gray scale we therefore need to create a custom palette for it and associate this with the SDL surface that we have created.

Creating a SDL window

Our image viewer will display the image in a SDL window. For sake of minimalism (and simplicity) this will be a border-less window displayed in the centre of the screen.

/** Initialise a SDL window and return a pointer to it. */
SDL_Window *get_sdl_window(int width, int height) {
    if (SDL_Init(SDL_INIT_VIDEO) < 0) {
        fprintf(stderr, "SDL couldn't initialise: %s.\n", SDL_GetError());
        exit(1);
    }

    SDL_Window *sdl_window;
    sdl_window = SDL_CreateWindow( "Image",
        SDL_WINDOWPOS_CENTERED,
        SDL_WINDOWPOS_CENTERED,
        width,
        height,
        SDL_WINDOW_BORDERLESS);

    return sdl_window;
}

Rendering the surface as a texture in the window (a.k.a. displaying the image)

We now need some code to render the surface as a texture in the window. In the code we will do this back to front, by using the window to generate a renderer and using the renderer to generate a texture. Finally, the renderer is cleared before adding the texture and presenting it.

It is worth noting that a SDL_Texture is a structure that contains an efficient, driver-specific representation of pixel data. Which means that, unlike a SDL_Surface, it can be processed by the GPU.

/** Display the image by rendering the surface as a texture in the window. */
void render_image(SDL_Window *window, SDL_Surface *surface) {
    SDL_Renderer* renderer = SDL_CreateRenderer(window, -1, 0);
    if ( renderer == NULL ) {
        fprintf(stderr, "Failed to create renderer: %s\n", SDL_GetError());
        exit(1);
    }

    SDL_Texture* texture = SDL_CreateTextureFromSurface(renderer, surface);
    if ( texture == NULL ) {
        fprintf(stderr, "Failed to load image as texture\n");
        exit(1);
    }

    SDL_RenderClear(renderer);
    SDL_RenderCopy(renderer, texture, NULL, NULL);
    SDL_RenderPresent(renderer);
}

Note that the third parameter of the SDL_RenderCopy() function (srcrect) is a pointer to the source rectangle and can be used to implement zooming. However, here we set it to NULL to display the entire texture.

Giving the user the chance to view the image

At this point we need some sort of event loop to make sure that the image does not vanish instantaneously after having been rendered in the window. Below is is a simple event loop that ends when the user presses a key on the keyboard.

/** Loop until a key is pressed. */
void event_loop() {
    int done = 0;
    SDL_Event e;
    while (!done) {
        while (SDL_PollEvent(&e)) {
            if (e.type == SDL_KEYDOWN) {
                done = 1;
            }
        }
    }
}

Putting it all together

Finally we add the main logic of the code. This includes some functionality for checking if the input image is gray scale. We achieve this by checking if the FreeImage colour type is FIC_MINISBLACK. Other colour types include FIC_RGB and FIC_CMYK.

As gray scale images can be more than 8-bits (quite common when dealing with microscopy images) we make sure that we compress the data using the FreeImage_ConvertToGreyscale() function.

int main(int argc, char *argv[]) {
    char *filename = parse_args_get_filename(argc, argv);
    FIBITMAP *freeimage_bitmap = get_freeimage_bitmap(filename);

    int is_grayscale = 0;
    if (FreeImage_GetColorType(freeimage_bitmap) == FIC_MINISBLACK) {
        // Single channel so ensure image is compressed to 8-bit.
        is_grayscale = 1;
        FIBITMAP *tmp_bitmap = FreeImage_ConvertToGreyscale(freeimage_bitmap);
        FreeImage_Unload(freeimage_bitmap);
        freeimage_bitmap = tmp_bitmap;
    }

    int width = FreeImage_GetWidth(freeimage_bitmap);
    int height = FreeImage_GetHeight(freeimage_bitmap);
    SDL_Window *sdl_window = get_sdl_window(width, height);
    SDL_Surface *sdl_surface = get_sdl_surface(freeimage_bitmap, is_grayscale);

    render_image(sdl_window, sdl_surface);
    event_loop();

    FreeImage_Unload(freeimage_bitmap);
    SDL_FreeSurface(sdl_surface);
    return 0;
}

Note that we free up the dynamically allocated bitmap and surface memory using FreeImage_Unload() and SDL_FreeSurface() before we exit the program.

Compiling and linking

Now we can compile the code.

$ gcc -c see.c

This creates the object file see.o which contains machine code as well as information that allows a linker to find out which symbols (global objects, functions, etc) it requires in order to work.

Let us link our object file.

$ gcc -o see see.o -lfreeimage -lsdl2

This produces the executable file see, which we can test using the command below.

$  ./see image.png

Conclusion

FreeImage and SDL are useful C libraries for working with images and graphical user interfaces, respectively. In this post we have used the two in combination to create a basic image viewer that can parse over 30 image file formats and display both RGB and gray scale images correctly.

Acknowledgements

This blog post was inspired by and based on some of the code in the github.com/JIC-CSB/eye project.