SDL_Image
SDL_Image
.In this lesson, we’ll start using the SDL_Image
extension we installed earlier. We’ll cover 3 main topics:
SDL_Image
IMG_Load()
function to load and render a wide variety of image types, rather than being restricted to the basic bitmap (.bmp
) format.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
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;
}
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:
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};
};
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
);
}
// ...
};
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
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
);
}
// ...
}
// ...
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:
format
member variable0
, which is required for legacy backward compatibility reasonsBelow, 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
)};
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:
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.
SDL_Image
Learn to load, manipulate, and save various image formats using SDL_Image
.
Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games