Introduction to SDL_Image

Learn to load, manipulate, and save various image formats using SDL_Image.
This lesson is part of the course:

Game Dev with SDL2

Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games

Get Started for Free
Abstract art representing computer programming
Ryan McCombe
Ryan McCombe
Updated

In this lesson, we’ll start using the SDL_Image extension we installed earlier. We’ll cover 3 main topics:

  • Initializing and closing SDL_Image
  • Using the IMG_Load() function to load and render a wide variety of image types, rather than being restricted to the basic bitmap (.bmp) format.
  • Using surface blending modes to use transparency and other techniques when blitting
  • Creating image files from our surfaces using IMG_SaveJPG() and IMG_SavePNG()

We’ll be building upon the basic application loop and surface-blitting concepts we covered earlier in the course:

#include <SDL.h>
#include "Image.h"

class Window {
public:
  Window() {
    SDLWindow = SDL_CreateWindow(
      "My Program", SDL_WINDOWPOS_UNDEFINED,
      SDL_WINDOWPOS_UNDEFINED, 600, 300, 0);
  }

  void Render() {
    SDL_FillRect(
      GetSurface(), nullptr, SDL_MapRGB(
        GetSurface()->format, 50, 50, 50
      )
    );
  }

  void Update() {
    SDL_UpdateWindowSurface(SDLWindow);
  }

  SDL_Surface* GetSurface() {
    return SDL_GetWindowSurface(SDLWindow);
  }

  ~Window() { SDL_DestroyWindow(SDLWindow); }
  
  Window(const Window&) = delete;
  Window& operator=(const Window&) = delete;

private:
  SDL_Window* SDLWindow;
};

int main(int argc, char** argv) {
  SDL_Init(SDL_INIT_VIDEO);
  SDL_Event Event;
  bool shouldQuit{false};

  Window GameWindow;
  Image ExampleImg{"example.png"};

  while (!shouldQuit) {
    while (SDL_PollEvent(&Event)) {
      if (Event.type == SDL_QUIT) {
        shouldQuit = true;
      }
    }
    GameWindow.Render();
    ExampleImg.Render(GameWindow.GetSurface());
    GameWindow.Update();
  }

  SDL_Quit();
  return 0;
}
#pragma once
#include <SDL.h>

#include <iostream>
#include <string>

class Image {
public:
  Image(std::string File) : ImageSurface{
    SDL_LoadBMP(File.c_str())} {
    if (!ImageSurface) {
      std::cout << "Failed to load image: " <<
        File << ":\n" << SDL_GetError();
    }
    SourceRectangle.w = ImageSurface->w;
    SourceRectangle.h = ImageSurface->h;
  }

  void Render(SDL_Surface* DestinationSurface) {
    SDL_BlitSurface(ImageSurface,
                    nullptr,
                    DestinationSurface,
                    &DestinationRectangle);
  }

  ~Image() { SDL_FreeSurface(ImageSurface); }
  Image(const Image&) = delete;
  Image& operator=(const Image&) = delete;

private:
  SDL_Surface* ImageSurface{nullptr};
  SDL_Rect DestinationRectangle{0, 0, 0, 0};
};

One notable change from the previous lesson is that our main function is now passing a .png image path to our Image constructor:

Image ExampleImg{"example.png"};

This image should be located in the same directory that our executable file is created in. The examples and screenshots from this lesson are using a .png file that is available by clicking here.

However, our Image class does not currently support .png images. It’s using SDL_LoadBMP() to load image files and, as the name suggests, this function only supports .bmp images.

// Image.h
class Image {
public:
  Image(std::string File)
  : ImageSurface{SDL_LoadBMP(File.c_str())} {
    if (!ImageSurface) {
      std::cout << "Failed to load image: " <<
        File << ":\n" << SDL_GetError();
    }
    // ...
  }
  // ...
};

As such, our program can’t render the image, and we have an error being reported in the terminal:

Failed to load image: example.png:
File is not a Windows BMP file

Initializing and Quitting SDL_Image

The SDL_Image extension allows us to load many more image formats. It includes the code that can understand formats like PNG and JPG, and load their pixels into an SDL_Surface that we can work with as before.

To use SDL_Image, we should first initialize it. This is done using the IMG_Init() function, available after including SDL_Image.h. We pass it initialization flags indicating what formats we need to support:

// main.cpp
#include <SDL.h>
#include <SDL_image.h>

int main(int argc, char** argv) {
  SDL_Init(SDL_INIT_VIDEO);
  IMG_Init(IMG_INIT_PNG);
  // ...
}

A list of the available IMG_Init flags is available here. We can initialize multiple libraries at once using the | operator:

IMG_Init(IMG_INIT_PNG | IMG_INIT_JPG);

We should also call IMG_Quit() before our application ends, allowing the extension to clean up at the appropriate time. Let’s add both of these to our main() function:

// main.cpp
#include <SDL.h>
#include <SDL_image.h>

#include "Image.h"

class Window {/*...*/}; int main(int argc, char** argv) { SDL_Init(SDL_INIT_VIDEO); IMG_Init(IMG_INIT_PNG); SDL_Event Event; bool shouldQuit{false}; Window GameWindow; Image ExampleImg{"example.png"};
while (!shouldQuit) {/*...*/} // Cleanup IMG_Quit(); SDL_Quit(); return 0; }

Using IMG_Load()

Once we’ve initialized SDL_Image, we can replace SDL_LoadBMP() in our Image class with IMG_Load(). This function is a drop-in replacement - it has the same API.

It accepts the file we want to load as a c-string, and returns a pointer to the SDL_Surface where the image data was loaded:

// Image.h
#pragma once
#include <SDL.h>
#include <SDL_image.h>

#include <iostream>
#include <string>

class Image {
public:
  Image(std::string File) : ImageSurface{
    IMG_Load(File.c_str())} {
    if (!ImageSurface) {
      std::cout << "Failed to load image: " <<
        File << ":\n" << SDL_GetError();
    }
  }
  // ...
};

Our program should now render our image:

Screenshot of the program running

We can treat the resulting surface in the same way we did before, such as blitting it onto other surfaces.

For example, let’s update our Render() function to use BlitScaled() instead, and update our DestinationRectangle to set the size and position of our image:

// Image.h
class Image {
public:
  // ...

  void Render(SDL_Surface* DestinationSurface) {
    SDL_BlitScaled(ImageSurface,
                   nullptr,
                   DestinationSurface,
                   &DestinationRectangle);
  }

private:
  SDL_Rect DestinationRectangle{200, 50, 200, 200};
};
Screenshot of the program running

Transparency and Blend Modes

Our PNG image has transparent sections, and we see our program is respecting that by default. In areas where our PNG was transparent, the blitting operation skipped those pixels, keeping the existing color on the destination surface.

In this example, the Window::Render() method fills the window surface with a solid gray color. The Image::Render() method then blends our image on top of it.

This blending strategy to use when blitting is configured on the source surface, which is the ImageSurface created by IMG_Load() in our example.

The surface created by IMG_Load() has blending enabled by default if the image it loads includes transparency. However, we can also enable it explicitly using the SDL_SetSurfaceBlendMode() function.

We pass a pointer to the surface we’re configuring, and the blend mode we want to use. SDL_BLENDMODE_BLEND is the mode that uses transparency:

// Image.h
class Image {
public:
  Image(std::string File) : ImageSurface{
    IMG_Load(File.c_str())} {
    if (!ImageSurface) {
      std::cout << "Failed to load image: " <<
        File << ":\n" << SDL_GetError();
    }
    SDL_SetSurfaceBlendMode(
      ImageSurface, SDL_BLENDMODE_BLEND
    );
  }
  // ...
};

We can disable blending and return to the basic blitting algorithm using SDL_BLENDMODE_NONE:

// Image.h
class Image {
public:
  Image(std::string File) : ImageSurface{
    IMG_Load(File.c_str())} {
    if (!ImageSurface) {
      std::cout << "Failed to load image: " <<
        File << ":\n" << SDL_GetError();
    }
    SDL_SetSurfaceBlendMode(
      ImageSurface, SDL_BLENDMODE_NONE
    );
  }
  // ...
};
Screenshot of the program running

Transparency and Pixel Formats

For blending to work, our surface must also use a pixel format that includes transparency data. IMG_Load() uses an appropriate pixel format by default.

However, if we change it - using SDL_ConvertSurface() for example - we may lose the transparency data, and therefore lose the ability to blend our surface onto others.

We can check if a pixel format includes transparency (also called alpha) by passing it to the SDL_ISPIXELFORMAT_ALPHA() macro:

// main.cpp

// ...

int main(int argc, char** argv) {
  SDL_Init(SDL_INIT_VIDEO);
  Window GameWindow;
  SDL_Surface* Surface{
    IMG_Load("example.png")};

  if (SDL_ISPIXELFORMAT_ALPHA(
    Surface->format->format)) {
    std::cout << "Surface has alpha";
  }

  Surface = SDL_ConvertSurface(
    Surface,
    GameWindow.GetSurface()->format, 0
  );

  if (!SDL_ISPIXELFORMAT_ALPHA(
    Surface->format->format)) {
    std::cout << "...but not any more";
  }

  SDL_Quit();
  return 0;
}
Surface has alpha...but not any more

Taking Screenshots

SDL_Image includes two functions that let us save a surface to an image file on our hard drive. To save a PNG file, we use IMG_SavePNG(), passing a pointer to the surface, and the location we want the file saved to.

The location where the file will be created is relative to the location of our executable:

// Image.h
class Image {
public:
  // ...
  void SaveToFile(std::string Location) {
    IMG_SavePNG(ImageSurface, Location.c_str());
  }
  // ...
};

IMG_SaveJPG() works similarly but has an additional integer parameter. This integer ranges from 0 to 100 and affects the quality of the saved file. Higher values prioritize quality, while lower values prioritize compression, thereby reducing file size.

We can use either of these functions to take screenshots of our program. We simply pass our window surface as the first argument:

// main.cpp
class Window {
public:
  // ...
  void TakeScreenshot() {
    IMG_SaveJPG(
      GetSurface(), "Screenshot.jpg", 90
    );
  }
  // ...
}

// ...

Memory Management

Currently, our Image class does not allow its instances to be copied in a memory-safe way, so we’ve deleted the copy constructor and copy assignment operator:

// Image.h
// ...

class Image {
public:
  // ...
  Image(const Image&) = delete;
  Image& operator=(const Image&) = delete;
  // ...
};

Let’s finally address this. The root cause is that IMG_Load() (and SDL_LoadBMP() previously) is one of the cases where SDL allocates dynamic memory, and requires us to tell it when that memory is safe to deallocate. We’ve been doing this through the SDL_FreeSurface() function in the Image destructor:

// Image.h
// ...

class Image {
public:
  // ...
  ~Image() {
    SDL_FreeSurface(ImageSurface);
  }
  // ...
};

Allowing Image objects to be copied using the default copy constructor and operator would be problematic when we have this destructor. If we copy one of our Image objects, the ImageSurface variable in both the original object and the copy will point to the same underlying SDL_Surface.

When one of our Image copies is destroyed, its destructor will delete that surface using the SDL_FreeSurface() function. This leaves the other copy with a dangling pointer, which will cause a use-after-free memory issue the next time the Image tries to use it. Also, when that Image later gets destroyed, it’s destructor will call SDL_FreeSurface() again with that same address. This introduces a second memory problem called a double-free error.

To allow our Image objects to be copied without creating these memory issues, we need to intervene in the copying process. Specifically, we need to ensure that, when an Image is copied, each copy gets its own copy of the underlying SDL_Surface data, in a distinct memory address.

SDL_ConvertSurface()

To create a copy of a surface, including all of its pixel data, we can use the SDL_ConvertSurface() function. It requires three arguments:

  1. A pointer to the surface to copy
  2. The format to use for the copy. If we want to use the same format as the source, we can retrieve it from the format member variable
  3. The integer value 0, which is required for legacy backward compatibility reasons

Below, we create an SDL_Surface called Copy, based on data from an SDL_Surface called Source:

SDL_Surface* Copy{SDL_ConvertSurface(
  Source.ImageSurface,
  Source.ImageSurface->format,
  0
)};

Implementing Copy Semantics

Let’s implement a copy constructor for our Image class that duplicates the SDL_Surface from the source object using SDL_ConvertSurface(). Our Image objects have a DestinationRectangle member variable, so we copy that in our constructor too:

// Image.cpp
// ...

Image::Image(const Image& Source)
  : DestinationRectangle(Source.DestinationRectangle)
{
  // Copy the SDL_Surface using SDL_ConvertSurface
  if (Source.ImageSurface) {
    ImageSurface = SDL_ConvertSurface(
      Source.ImageSurface,
      Source.ImageSurface->format,
      0
    );
  }
}

The copy assignment operator is similar but, in this scenario, we’re updating an existing Image instance. That instance might already have an SDL_Surface, so we need to free it:

// Image.cpp
// ...

Image& Image::operator=(const Image& Source) {
  // Early return for self-assignment
  if (this == &Source) {
    return *this;
  }
  
  // Free current resources
  SDL_FreeSurface(mImageSurface);
  
  // Copy the SDL_Surface using SDL_ConvertSurface
  if (Source.mImageSurface) {
    mImageSurface = SDL_ConvertSurface(
      Source.mImageSurface,
      Source.mImageSurface->format,
      0
    );
  } else {
    ImageSurface = nullptr;
  }
  
  // Copy the other member variables too
  DestinationRectangle = Source.DestinationRectangle;
  
  return *this;
}

We covered copy operations and the rule of three in more detail in our introductory course:

Summary

In this lesson, we’ve introduced the SDL_Image extension, greatly expanding the range of image formats our application can support.

We also saw how some formats include transparency, allowing us to blend surfaces whilst blitting.

In the next lesson, we’ll combine the topics we’ve covered so far in this lesson into a cohesive Image class. We’ll focus on how to provide the capabilities we learned to external code using a friendly public interface.

We’ll also see how to manage the complexities involved within our class in a way that keeps everything organized and easy to maintain.

Free and Unlimited Access

Professional C++

Unlock the true power of C++ by mastering complex features, optimizing performance, and learning expert workflows used in professional development

Screenshot from Warhammer: Total War
Screenshot from Tomb Raider
Screenshot from Jedi: Fallen Order
Ryan McCombe
Ryan McCombe
Updated
sdl2-promo.jpg
This lesson is part of the course:

Game Dev with SDL2

Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games

Get Started for Free
sdl2-promo.jpg
This lesson is part of the course:

Game Dev with SDL2

Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games

This course includes:

  • 96 Lessons
  • 92% Positive Reviews
  • Regularly Updated
  • Help and FAQs
Free and Unlimited Access

Professional C++

Unlock the true power of C++ by mastering complex features, optimizing performance, and learning expert workflows used in professional development

Screenshot from Warhammer: Total War
Screenshot from Tomb Raider
Screenshot from Jedi: Fallen Order
Contact|Privacy Policy|Terms of Use
Copyright © 2025 - All Rights Reserved