Image Scaling and Aspect Ratios
Learn techniques for scaling images and working with aspect ratios
In this lesson, we'll learn how to scale our images up and down during the blitting process. Here's what we'll cover:
- The
SDL_BlitSurfaceScaled()
function, and how it differs fromSDL_BlitSurface()
. - What an aspect ratio is, why it matters, and how to calculate it.
- Using aspect ratios to prevent images from being stretched and deformed during scaling.
We'll be building upon the basic application loop and surface-blitting concepts we covered earlier in the course.
Starting Point
We'll start with the code from our previous lesson. To focus on image scaling, we have simplified the Image
class, removing the manual clipping detection logic. We've also reset the destination rectangle to position the image at (0, 0)
.
Our application currently loads an image named example.bmp
and displays it in a window. The image we're using in these examples is available here.
Files
Our program currently renders the following window:

Using SDL_BlitSurfaceScaled()
Previously, we've been using the SDL_BlitSurface()
function to copy color information from one surface to another. This performs a pixel-by-pixel copy - the image data on the destination surface will be the same size it was on the source surface.
If we want our image to appear larger or smaller on the destination surface, we can call SDL_BlitSurfaceScaled()
instead. In SDL3, this function receives a fifth argument for the scaling algorithm to use:
src/Image.h
// ...
class Image {
public:
// ...
void Render(SDL_Surface* DestinationSurface) {
if (!ImageSurface) return;
SDL_BlitSurfaceScaled(
ImageSurface, &SourceRectangle,
DestinationSurface, &DestinationRectangle,
SDL_SCALEMODE_LINEAR
);
}
// ...
};
There are three scaling modes available at the time of writing:
- The
SDL_SCALEMODE_LINEAR
option provides smooth, interpolated scaling, which is recommended for most use cases. SDL_SCALEMODE_NEAREST
provides "nearest-neighbor" scaling, which is less accurate but better preserves sharp edges.- If you're using SDL 3.4 or later,
SDL_SCALEMODE_PIXELART
is available, which further optimizes the nearest-neighbor algorithm for pixel art styles .
Unlike SDL_BlitSurface()
, the SDL_BlitSurfaceScaled()
function uses the w
and h
properties of the destination rectangle. These values will define the size of the image on the destination surface.
Let's update our DestinationRectangle
to cover the entire window surface, which is 600x300 in our example:
src/Image.h
// ...
class Image {
// ...
private:
SDL_Surface* ImageSurface{nullptr};
SDL_Rect SourceRectangle{};
SDL_Rect DestinationRectangle{0, 0, 600, 300};
};

Positioning and Missized Rectangles
As before, we can position the image within the output surface by setting the x
and y
values.
In this example, we move the image 50 pixels from the left edge and 100 pixels from the top edge. We have not reduced the destination rectangle's width and height, so it now extends beyond the bounds of the destination surface.
src/Image.h
// ...
class Image {
public:
// ...
void Render(SDL_Surface* DestinationSurface) {
if (!ImageSurface) return;
SDL_BlitSurfaceScaled(
ImageSurface, &SourceRectangle,
DestinationSurface, &DestinationRectangle,
SDL_SCALEMODE_LINEAR
);
}
// ...
private:
// ...
SDL_Rect DestinationRectangle{50, 100, 600, 300};
};

Aspect Ratio
In the previous examples, we can see the SDL_BlitSurfaceScaled()
algorithm will squash and stretch our images to make them fill the destination rectangle. This deformation may not be desired. Instead, we might want to respect the relative proportions of the original image, or the proportions of the SourceRectangle
if it's smaller.
These proportions are often called the aspect ratio, which is the image's width divided by its height.
Width and height are typically integers, but aspect ratios are floating-point numbers, so we should cast at least one of the operands to a float before performing the division:
float SourceRatio{SourceRectangle.w
/ static_cast<float>(SourceRectangle.h)
};
Let's move our DestinationRectangle
initialization to a function so we can check our aspect ratios:
src/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';
} else {
SourceRectangle = {
0, 0, ImageSurface->w, ImageSurface->h
};
SetDestinationRectangle({0, 0, 600, 300});
}
}
void SetDestinationRectangle(
SDL_Rect Destination
) {
float SourceRatio{SourceRectangle.w
/ static_cast<float>(SourceRectangle.h)
};
float DestinationRatio{Destination.w
/ static_cast<float>(Destination.h)
};
DestinationRectangle = Destination;
// Non-functional code for logging
std::cout << "\n[Aspect Ratio] Source: "
<< SourceRatio
<< ", Destination: " << DestinationRatio;
}
// ...
private:
SDL_Surface* ImageSurface{nullptr};
SDL_Rect SourceRectangle{};
SDL_Rect DestinationRectangle{};
};
[Aspect Ratio] Source: 1.77778, Destination: 2
When the aspect ratios of our source and destination rectangles are different, as they are here, our image will be deformed if rendered using SDL_BlitSurfaceScaled()
.
Preventing Deformation
To prevent deformation, we should ensure our DestinationRectangle
has the same aspect ratio as our SourceRectangle
.
We'll rename our function parameter to be Requested
, indicating it might not be the settings we end up using. If the aspect ratio of the SDL_Rect
passed to SetDestinationRectangle
is wrong, we'll use different values.
We'll also rename our SourceRatio
to Target
, indicating it contains the value we'd like the aspect ratio of our rectangle to be.
The simplest way to ensure a rectangle has a specific aspect ratio is to choose one of its dimensions (width, in this example) and set it to a value that gives us the target ratio:
src/Image.h
// ...
class Image {
public:
//...
void SetDestinationRectangle(
SDL_Rect Requested
) {
float TargetRatio{SourceRectangle.w
/ static_cast<float>(SourceRectangle.h)
};
float RequestedRatio{Requested.w
/ static_cast<float>(Requested.h)
};
DestinationRectangle = Requested;
DestinationRectangle.w = static_cast<int>(
Requested.h * TargetRatio
);
// Non-functional code for logging
float AppliedRatio{DestinationRectangle.w /
static_cast<float>(DestinationRectangle.h)};
std::cout << "\n[Aspect Ratio] Requested: "
<< RequestedRatio
<< ", Target:" << TargetRatio
<< ", Applied: " << AppliedRatio;
}
// ...
};
[Aspect Ratio] Requested: 2, Target:1.77778, Applied: 1.77667
The highlighted logic may be confusing here. It uses the equation:
This comes from the definition of aspect ratio:
If we multiply both sides of this equation by , we get:
So, if we know the height and aspect ratio, we multiply them together to get the width.
Scaling to Fit
We can improve this implementation slightly. By restricting our resizing option to only one dimension (the width, in the previous example) we don't know if hitting the target aspect ratio requires us to make the rectangle smaller or larger.
In most scenarios, we don't want to render an image larger than was requested. Therefore, to hit a target ratio, we should only reduce a dimension. If the rectangle's aspect ratio is too large (that is, the rectangle is "too landscape"), we want to reduce its width.
However, if the aspect ratio is too small (that is, the rectangle is "too portrait"), we don't want to increase its width. Instead, we want to reduce its height.
Let's add an if
statement to figure out what strategy we need:
src/Image.h
// ...
class Image {
public:
//...
void SetDestinationRectangle(
SDL_Rect Requested
) {
float TargetRatio{SourceRectangle.w
/ static_cast<float>(SourceRectangle.h)
};
float RequestedRatio{Requested.w
/ static_cast<float>(Requested.h)
};
DestinationRectangle = Requested;
if (RequestedRatio < TargetRatio) {
// Reduce height
DestinationRectangle.h = static_cast<int>(
Requested.w / TargetRatio
);
} else {
// Reduce width as before
DestinationRectangle.w = static_cast<int>(
Requested.h * TargetRatio
);
}
// Non-functional code for logging
float AppliedRatio{DestinationRectangle.w /
static_cast<float>(DestinationRectangle.h)};
std::cout << "\n[Aspect Ratio] Requested: "
<< RequestedRatio
<< ", Target:" << TargetRatio
<< ", Applied: " << AppliedRatio;
}
// ...
};
[Aspect Ratio] Requested: 2, Target:1.77778, Applied: 1.77667
In this implementation, if the requested rectangle's aspect ratio is smaller than the target, that means it is "too portrait". Our previous implementation would address this by increasing the width, but now, we reduce the height instead.
The equation to calculate the height comes from the same process as before. We already worked out that:
Dividing both sides of the equation by , we get:
Later in this course, we'll create a more complex Image
class that will allow external code to configure Image
objects with many more options.
Complete Code
Here are the final versions of our files after applying the scaling and aspect ratio logic from this lesson.
Files
Summary
We've covered several concepts in this lesson:
- Using
SDL_BlitSurfaceScaled()
for scaling images up or down during the blitting process. - Selecting a scaling algorithm, like
SDL_SCALEMODE_LINEAR
for smooth scaling. - Calculating and maintaining aspect ratios.
- Preventing image deformation during scaling.
Later in this section, we'll expand our Image
class to give external code access to these new capabilities. However, before we do that, we'll introduce SDL_image
, which allows us to load more advanced image formats than the basic bitmaps we've been using so far.
This will include image formats that include transparency information, so we'll also learn how our blitting operations can use that data. This will allow us to blend our surface colors together in more advanced ways.
Introduction to SDL3_image
Learn to load, manipulate, and save various image formats using SDL3_image
.