Rendering SVG in SDL2 using Rust
This posts explains how to display SVG picture using Rust sdl2 crate.
Disclaimer: while what is written does work, I am new to both Rust and SDL2, so the suggested approach might be not the best or even plain wrong. I am also developing on Windows (I do not have Linux, and I have failed to get sdl2 crate working on FreeBSD). I also did not try on Mac.
Pre-requisites
- You have to have a working Rust installation and cargo. If you do not have it, I suggest that you look here.
- Install
cargo-vcpkg
usingcargo install cargo-vcpkg
.
Basic Rust SDL2 project
There will be no SVG in this section. It just explains how to create a basic SDL2 project in Rust.
Start by creating Rust application: cargo new svgexample
. Now cargo run
in
the created directory should print Hello, world!
.
Adding sdl2 crate
To use SDL2 from Rust we are going to use sdl2
crate. To connect it to our
project put the following snippet into Cargo.toml
:
[dependencies.sdl2]
version = "0.36"
default-features = false
features = ["ttf", "image", "gfx", "mixer", "static-link", "use-vcpkg"]
[package.metadata.vcpkg]
dependencies = [
"sdl2",
"sdl2-image[libjpeg-turbo,tiff,libwebp]",
"sdl2-ttf",
"sdl2-gfx",
"sdl2-mixer",
]
git = "https://github.com/microsoft/vcpkg"
rev = "c869686"
[package.metadata.vcpkg.target]
x86_64-pc-windows-msvc = { triplet = "x64-windows-static-md" }
You might need to change vcpkg revision. Check Microsoft’s page to find it.
Now make sure that VCPKG_ROOT
environment variable is not set (on Windows in
PowerShell: $env:VCPKG_ROOT=""
) and run cargo vcpkg build
. Now it is a good
time to go drink some coffee, as it will take time. When it (hopefully)
successfully finishes, cargo run
should compile sdl2-sys
and sdl2
and
still print Hello, world!
.
Now let us create a window. Put the following code in src/main.rs
:
use sdl2::{event::Event, keyboard::Keycode};
fn main() {
let sdl_context = sdl2::init().unwrap();
let video_subsystem = sdl_context.video().unwrap();
let _window = video_subsystem
.window("SVG example", 800, 600)
.build()
.unwrap();
let mut events = sdl_context.event_pump().unwrap();
'main: loop {
for event in events.poll_iter() {
match event {
Event::Quit { .. }
| Event::KeyDown {
keycode: Some(Keycode::Escape),
..
} => {
break 'main;
}
_ => {}
}
}
}
}
Now cargo run
should open 800x600 window and wait for escape or until the
windows is closed.
Note: there are multiple unwrap()
calls. Do not do it in anything
resembling production code.
Drawing SVG image
The plan is to show SVG image on the central third of the screen.
To render SVG image, we will use resvg
crate, so run cargo add resvg
(the text
below is applicable to version 0.38.0). Unless you have your own SVG image,
download test image from
Wikipedia
and put it next to main.rs
, naming it test.svg
.
Let us start by rendering SVG. To keep things simple, SVG file will not be read from disk but instead included in the code as string. For starters, the image will be shown in the top left corner. The full code is:
use resvg::usvg::{fontdb, PostProcessingSteps, TreeParsing, TreePostProc};
use sdl2::{
event::Event, keyboard::Keycode, pixels::PixelFormatEnum, rect::Rect, render::TextureAccess,
};
fn main() {
let sdl_context = sdl2::init().unwrap();
let video_subsystem = sdl_context.video().unwrap();
let window = video_subsystem
.window("SVG example", 800, 600)
.build()
.unwrap();
// use resvg to render the SVG image to pixmap
let mut tree =
resvg::usvg::Tree::from_str(include_str!("test.svg"), &resvg::usvg::Options::default())
.unwrap();
let mut fdb = fontdb::Database::new();
fdb.load_system_fonts();
tree.postprocess(
PostProcessingSteps {
convert_text_into_paths: true,
},
&fdb,
);
let svg_width = tree.size.to_int_size().width();
let svg_height = tree.size.to_int_size().height();
let mut pixmap = resvg::tiny_skia::Pixmap::new(svg_width, svg_height).unwrap();
resvg::render(
&tree,
resvg::tiny_skia::Transform::default(),
&mut pixmap.as_mut(),
);
// render pixmap to SDL window
let mut canvas = window.into_canvas().build().unwrap();
let creator = canvas.texture_creator();
let mut texture = creator
.create_texture(
Some(PixelFormatEnum::RGBA32),
TextureAccess::Target,
svg_width,
svg_height,
)
.unwrap();
let svg_data = pixmap.data();
texture
.update(None, svg_data, 4 * svg_width as usize)
.unwrap();
canvas
.copy(&texture, None, Some(Rect::new(0, 0, svg_width, svg_height)))
.unwrap();
canvas.present();
let mut events = sdl_context.event_pump().unwrap();
'main: loop {
for event in events.poll_iter() {
match event {
Event::Quit { .. }
| Event::KeyDown {
keycode: Some(Keycode::Escape),
..
} => {
break 'main;
}
_ => {}
}
}
}
}
Let’s go through this code.
Rendering SVG to pixmap
First we need to parse svg image into usvg Tree:
let mut tree =
resvg::usvg::Tree::from_str(include_str!("test.svg"), &resvg::usvg::Options::default())
.unwrap();
As said, SVG file name is hardcoded and the file’s content is included in the
code just as string. Then we just call unwrap()
on the result we obtained. Do
not do this!
Now we need to postprocess the tree – if it is not done, you will see the
black screen. Also resvg cannot render text, so we will need to convert it into
paths. We start by creating an empty font database, loading system fonts into
it, and then postprocessing the tree, asking to convert fonts into paths (if it
is not done – you use PostProcessingSteps::default()
and empty font database
– you will see the picture but there will be no text):
let mut fdb = fontdb::Database::new();
fdb.load_system_fonts();
tree.postprocess(
PostProcessingSteps {
convert_text_into_paths: true,
},
&fdb,
);
Now the tree can be rendered to tiny_skia pixmap. To do that we need to create a pixmap of an appropriate size:
let svg_width = tree.size.to_int_size().width();
let svg_height = tree.size.to_int_size().height();
let mut pixmap = resvg::tiny_skia::Pixmap::new(svg_width, svg_height).unwrap();
Now we have both postprocessed SVG tree and a pixmap it can be rendered to, so let’s render it:
resvg::render(
&tree,
resvg::tiny_skia::Transform::default(),
&mut pixmap.as_mut(),
);
Showing the pixmap in SDL window
Now everything is fine and dandy; SVG is rendered to a pixmap. The only small remaining problem is that pixmap is unused, and nothing is shown on the screen. To overcome this, let’s draw the pixmap to SDL window.
We start by obtaining a texture corresponding to our window:
let mut canvas = window.into_canvas().build().unwrap();
let creator = canvas.texture_creator();
let mut texture = creator
.create_texture(
Some(PixelFormatEnum::RGBA32),
TextureAccess::Target,
svg_width,
svg_height,
)
.unwrap();
Notice PixelFormatEnum::RGBA32
– it is important, as it is the format we
will get data from the pixmap later.
Now let us get the pixmap’s data and copy it to the texture:
let svg_data = pixmap.data();
texture
.update(None, svg_data, 4 * svg_width as usize)
.unwrap();
The arguments to the update method mean the following:
None
: we want to update the whole texture, not justSome
rectangular area of it.svg_data
is the data we are using for the update.- The last parameter is so called pitch: it is the length of one row in
bytes. We have 4 8-bit channels (red, green, blue, alpha) so it is four
times the SVG width (why, oh why, should it be bytes not pixels – the
texture already knows it is in RGBA32 format, so 4 bytes per pixel…). The
number expected also should be of
usize
type, hence the cast.
The final step left is to copy texture to the canvas and to present it on the screen:
canvas
.copy(&texture, None, Some(Rect::new(0, 0, svg_width, svg_height)))
.unwrap();
canvas.present();
The arguments to copy
method (besides obvious &texture
) mean the following:
None
: we want to copy the whole texture, not justSome
subrectangle of it.Some(Rect::new(0, 0, svg_width, svg_height))
: we copy the texture to the rectangle in top left corner (0, 0) having the width ofsvg_width
and the height ofsvg_height
(i.e. the same dimensions as the texture created). You can put hereNone
to stretch the texture to fill the whole window, or specify any other destination rectangle, and SDL will do the scaling. However, SVG is a vector format and we do not want to do pixel-based scaling; we will do a proper scaling below.
Results
Now use cargo run
and you will see the window below. I also show the results
of some errors.

Proper result

During postprocessing text was not converted to paths

Wrong PixelFormat was given to create_texture()
(ARGB32)

The row length in texture.update()
was specified in pixels not in bytes

The destination in canvas.copy()
was specified as None

Finally: no tree postprocessing is done at all
Scaling the SVG
We want to scale SVG to the central rectangle of the window (one third of the window width, one third of the window height), keeping the aspect ratio.
Note: due to the different types required there will be quite a lot of casts.
The following changes to the code are needed.
Determine what is “one third” of the window
To define “one third of the window width” and “one third of the window height” we will use integer division:
let (window_width, window_height) = window.size();
let one_third_width = window_width / 3;
let one_third_height = window_height / 3;
We will also use a pixmap of the corresponding size:
let mut pixmap = resvg::tiny_skia::Pixmap::new(one_third_width, one_third_height).unwrap();
Determine how much SVG should be scaled
We will determine how much SVG should be scaled to fit into the designed area by width and by height. As we want to keep the aspect ratio, the final scale will be the minimal of the two:
let scale_width = one_third_width as f32 / svg_width as f32;
let scale_height = one_third_height as f32 / svg_height as f32;
let scale = f32::min(scale_width, scale_height);
Notice that we use f32
and not Rust default floating-point type (f64
)
because we will need f32
later.
Render SVG tree to the pixmap with scaling
Now we want to render the SVG tree to the pixmap, applying the found scale. It
means that resvg::tiny_skia::Transform::default()
is no longer good. Luckily
there is resvg::tiny_skia::Transform::from_scale()
, accepting horizontal and
vertical scales. As we want to keep the aspect ratio, we will use the same
value for both:
resvg::render(
&tree,
resvg::tiny_skia::Transform::from_scale(scale, scale),
&mut pixmap.as_mut(),
);
Show pixmap
To show the pixmap, we will do the following steps. First, we can create a (smaller) texture, representing the middle third (actually, ninth) of the window:
let mut texture = creator
.create_texture(
Some(PixelFormatEnum::RGBA32),
TextureAccess::Target,
one_third_width,
one_third_height,
)
.unwrap();
Second (extremely important), as the pixmap size has changed, the length of one row in it did too:
texture
.update(None, svg_data, 4 * one_third_width as usize)
.unwrap();
Third, the destination rectangle is different now too:
canvas
.copy(
&texture,
None,
Some(Rect::new(
one_third_width as i32,
one_third_height as i32,
one_third_width,
one_third_height,
)),
)
.unwrap();
Final result
This is the final code:
use resvg::usvg::{fontdb, PostProcessingSteps, TreeParsing, TreePostProc};
use sdl2::{
event::Event, keyboard::Keycode, pixels::PixelFormatEnum, rect::Rect, render::TextureAccess,
};
fn main() {
let sdl_context = sdl2::init().unwrap();
let video_subsystem = sdl_context.video().unwrap();
let window = video_subsystem
.window("SVG example", 800, 600)
.build()
.unwrap();
// use resvg to render the SVG image to pixmap
let mut tree =
resvg::usvg::Tree::from_str(include_str!("test.svg"), &resvg::usvg::Options::default())
.unwrap();
let mut fdb = fontdb::Database::new();
fdb.load_system_fonts();
tree.postprocess(
PostProcessingSteps {
convert_text_into_paths: true,
},
&fdb,
);
let svg_width = tree.size.to_int_size().width();
let svg_height = tree.size.to_int_size().height();
let (window_width, window_height) = window.size();
let one_third_width = window_width / 3;
let one_third_height = window_height / 3;
let mut pixmap = resvg::tiny_skia::Pixmap::new(one_third_width, one_third_height).unwrap();
let scale_width = one_third_width as f32 / svg_width as f32;
let scale_height = one_third_height as f32 / svg_height as f32;
let scale = f32::min(scale_width, scale_height);
resvg::render(
&tree,
resvg::tiny_skia::Transform::from_scale(scale, scale),
&mut pixmap.as_mut(),
);
// render pixmap to SDL window
let mut canvas = window.into_canvas().build().unwrap();
let creator = canvas.texture_creator();
let mut texture = creator
.create_texture(
Some(PixelFormatEnum::RGBA32),
TextureAccess::Target,
one_third_width,
one_third_height,
)
.unwrap();
let svg_data = pixmap.data();
texture
.update(None, svg_data, 4 * one_third_width as usize)
.unwrap();
canvas
.copy(
&texture,
None,
Some(Rect::new(
one_third_width as i32,
one_third_height as i32,
one_third_width,
one_third_height,
)),
)
.unwrap();
canvas.present();
let mut events = sdl_context.event_pump().unwrap();
'main: loop {
for event in events.poll_iter() {
match event {
Event::Quit { .. }
| Event::KeyDown {
keycode: Some(Keycode::Escape),
..
} => {
break 'main;
}
_ => {}
}
}
}
}
When running this with cargo run
, you will see the SVG in the middle of the
window.

Final result