Introduction to SDL3_image
Learn to load, manipulate, and save various image formats using SDL3_image
.
In this lesson, we'll start using the SDL3_image
extension library. We'll cover three main topics:
- 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 handle transparency when blitting.
- Creating image files from our surfaces using
IMG_SaveJPG()
andIMG_SavePNG()
.
We'll be building upon the basic application loop and surface-blitting concepts we covered in the previous lessons.
Starting Point
We'll start with a simplified version of the code from our previous lesson. To focus on SDL3_image
, we have removed the aspect ratio logic from the Image
class.
Our main()
function is now passing a .png
image path to our Image
constructor. The example image for this lesson is a PNG file, which is available here. Remember to place this image in the same directory as your application's executable if you want to follow along.
Files
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(const std::string& File)
: ImageSurface{SDL_LoadBMP(File.c_str())} {
if (!ImageSurface) {
std::cout << "Failed to load image: " <<
File << ":\n " << SDL_GetError() << '\n';
}
// ...
}
// ...
};
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
Using IMG_Load()
Now that we have SDL3_image
set up, 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-style string and returns a pointer to the SDL_Surface
where the image data was loaded.
src/Image.h
#pragma once
#include <SDL3/SDL.h>
#include <SDL3_image/SDL_image.h>
#include <iostream>
#include <string>
class Image {
public:
Image(const std::string& File)
: ImageSurface{IMG_Load(File.c_str())} {
if (!ImageSurface) {
std::cout << "Failed to load image: "
<< File << ":\n " << SDL_GetError() << '\n';
}
}
// ...
};
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 SDL_BlitSurfaceScaled()
instead, and update our DestinationRectangle
to set the size and position of our image:
src/Image.h
#pragma once
#include <SDL3/SDL.h>
#include <SDL3_image/SDL_image.h>
#include <iostream>
#include <string>
class Image {
public:
Image(const std::string& File)
: ImageSurface{IMG_Load(File.c_str())} {
if (!ImageSurface) {
std::cout << "Failed to load image: "
<< File << ":\n " << SDL_GetError() << '\n';
}
}
void Render(SDL_Surface* DestinationSurface) {
if (!ImageSurface) return;
SDL_BlitSurfaceScaled(
ImageSurface, nullptr,
DestinationSurface, &DestinationRectangle,
SDL_SCALEMODE_LINEAR
);
}
~Image() {
if (ImageSurface) {
SDL_DestroySurface(ImageSurface);
}
}
Image(const Image&) = delete;
Image& operator=(const Image&) = delete;
private:
SDL_Surface* ImageSurface{nullptr};
SDL_Rect DestinationRectangle{200, 50, 200, 200};
};

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 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:
src/Image.h
// ...
class Image {
public:
Image(const std::string& File)
: ImageSurface{IMG_Load(File.c_str())} {
if (!ImageSurface) {
std::cout << "Failed to load image: "
<< File << ":\n " << SDL_GetError() << '\n';
}
SDL_SetSurfaceBlendMode(
ImageSurface, SDL_BLENDMODE_BLEND
);
}
// ...
};
We can disable blending and return to the basic blitting algorithm using SDL_BLENDMODE_NONE
:
src/Image.h
// ...
class Image {
public:
Image(const std::string& File)
: ImageSurface{IMG_Load(File.c_str())} {
if (!ImageSurface) {
std::cout << "Failed to load image: "
<< File << ":\n " << SDL_GetError() << '\n';
}
SDL_SetSurfaceBlendMode(
ImageSurface, SDL_BLENDMODE_NONE
);
}
// ...
};

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, the ability to blend our surface onto others.
We can check if a pixel format includes transparency (also called alpha) by passing its format
to the SDL_ISPIXELFORMAT_ALPHA()
macro:
src/main.cpp
#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include <SDL3_image/SDL_image.h>
#include <iostream>
#include "Window.h"
int main(int, char**) {
SDL_Init(SDL_INIT_VIDEO);
Window GameWindow;
SDL_Surface* Surface{IMG_Load("example.png")};
if (SDL_ISPIXELFORMAT_ALPHA(Surface->format)) {
std::cout << "Surface has alpha";
}
Surface = SDL_ConvertSurface(
Surface,
GameWindow.GetSurface()->format
);
if (!SDL_ISPIXELFORMAT_ALPHA(Surface->format)) {
std::cout << "...but not any more";
}
SDL_Quit();
return 0;
}
Surface has alpha...but not any more
Taking Screenshots
SDL3_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 where we want the file saved.
The location where the file will be created is relative to the location of our executable:
src/Image.h
//...
class Image {
public:
// ...
void SaveToFile(const std::string& Location) const {
if (!ImageSurface) return;
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:
src/Window.h
#pragma once
#include <SDL3/SDL.h>
#include <SDL3_image/SDL_image.h>
class Window {
public:
//...
void TakeScreenshot() const {
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:
src/Image.h
// 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()
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_DestroySurface()
function in the Image
destructor:
src/Image.h
// Image.h
// ...
class Image {
public:
// ...
~Image() {
if (ImageSurface) {
SDL_DestroySurface(ImageSurface);
}
}
// ...
};
Allowing Image
objects to be copied using the default copy constructor and operator would be problematic. 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. 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 second Image
is later destroyed, its destructor will call SDL_DestroySurface()
again with that same address, causing a double-free error.
To allow our Image
objects to be copied without creating these memory issues, we need to ensure that when an Image
is copied, both the original and the new object gets their own copy of the underlying SDL_Surface
data, each in a distinct memory address.
Using SDL_ConvertSurface()
To create a copy of a surface, including all of its pixel data, we can use the SDL_ConvertSurface()
function again. As a reminder, it requires two arguments:
- A pointer to the surface to copy.
- 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.
Below, we create an SDL_Surface
called Copy
, based on data from an SDL_Surface
called Source
:
SDL_Surface* Copy{SDL_ConvertSurface(
Source,
Source->format
)};
Implementing Copy Semantics
To implement copy semantics, let's first update our header file to declare a copy constructor and a copy assignment operator, instead of deleting them. This will let us define them in a new Image.cpp
file.
src/Image.h
// ...
class Image {
public:
Image(const std::string& File);
// ...
~Image();
Image(const Image&);
Image& operator=(const Image&);
// ...
};
Next, let's create Image.cpp
and move our constructor and destructor logic there.
src/Image.cpp
#include "Image.h"
Image::Image(const std::string& File)
: ImageSurface{IMG_Load(File.c_str())} {
if (!ImageSurface) {
std::cout << "Failed to load image: "
<< File << ":\n " << SDL_GetError() << '\n';
}
}
Image::~Image() {
if (ImageSurface) {
SDL_DestroySurface(ImageSurface);
}
}
src/Image.h
#pragma once
#include <SDL3/SDL.h>
#include <SDL3_image/SDL_image.h>
#include <iostream>
#include <string>
class Image {
public:
Image(const std::string& File);
: ImageSurface{IMG_Load(File.c_str())} {
if (!ImageSurface) {
std::cout << "Failed to load image: "
<< File << ":\n " << SDL_GetError() << '\n';
}
}
Image(const std::string& File);
void Render(SDL_Surface* DestinationSurface);
~Image();
// ...
};
Now, in Image.cpp
, we can implement the copy constructor to duplicate the SDL_Surface
from the source object using SDL_ConvertSurface()
. We also need to copy our other member variables.
src/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
);
}
}
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 first.
src/Image.cpp
//...
Image& Image::operator=(const Image& Source) {
// Early return for self-assignment
if (this == &Source) {
return *this;
}
// Free current resources
if (ImageSurface) {
SDL_DestroySurface(ImageSurface);
}
// Copy the SDL_Surface using SDL_ConvertSurface
if (Source.ImageSurface) {
ImageSurface = SDL_ConvertSurface(
Source.ImageSurface,
Source.ImageSurface->format
);
} else {
ImageSurface = nullptr;
}
// Copy the other member variables too
DestinationRectangle = Source.DestinationRectangle;
return *this;
}
With these changes, we can now safely copy our Image
instances as needed. Managing external resources like images is an important topic, so we'll revisit it several times throughout the course with increasingly advanced techniques.
If this section didn't entirely make sense, I'd recommend reviewing our on the topic, where we walk through this process in more detail.
Complete Code
Here is the complete code after all the changes in this lesson. We've added the SDL3_image
header, switched to IMG_Load
, and implemented proper memory management for our Image
class.
Files
Summary
In this lesson, we've introduced the SDL3_image
extension, 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. Finally, we implemented safe copy semantics for our Image
class, making it more flexible.
In the next lesson, we'll move on to SDL3_ttf
, another extension library that allows us to load font files and render text.
Rendering Text with SDL3_ttf
Learn to render and manipulate text in SDL3 applications using the SDL3_ttf
extension.