In the past few weeks, I’ve been itching to learn some new stuff. Those who know me even just a little bit know that “new stuff” is generally going include “new ways of creating graphics and/or animations”. And so it does this time.
I’ve been creating graphics in HTML5 Canvas for quite a few years now, and that’s cool. But there are a few pain points with its workflow:
1. It sometimes seems silly to pipe everything through the browser all the time. I’d like to just be able to run a program and have it create a file and display that file.
2. Running in the browser via the file system introduces various security issues, so you need to run some kind of local server, and even then you run into issues with saving files to disk automatically, so you need to right click and save or set up some kind of button to save an image.
3. At work, I’m using React with Webpack and Babel, so I get to use the latest and greatest ECMAScript with classes and all the other syntactic sugar. It’s really nice, but setting all of that up is not trivial. For a one-time large project, it’s not a huge deal to spend some time on creating all that config. But if you’re going to be doing random experiments all the time, it’s a ton of overhead. I get stuck with whatever the current browsers implement.
So I’ve been looking for some other language / drawing api I could learn.
After a short amount of searching, I came across Cairo Graphics.
https://cairographics.org/
It’s a 2D graphics library, written in C, but has bindings for several other languages. Well, that sounds pretty cool. Looking over the tutorials and api documents (https://cairographics.org/manual/), I see that you create a surface, then get a context from that surface, then call various commands on the context such as move_to
and line_to
, and then follow up with stroke
and/or fill
. Well that sounds awfully familiar. In fact, I’m going to guess that HTML5’s Canvas is either backed up by Cairo at some level, or is at the very least inspired by Cairo’s api. At any rate, it sounded worth looking into.
I wasn’t about to dive into C programming, but as I mentioned, there are bindings for several other languages. The first I tried was Python, and that worked great. I’ll probably do another tutorial on pyCairo
at some point, but I wanted to take it a little further and really dive into something new. I’ve had my eye on a couple of languages – Rust and Go. Both have Cairo bindings, so I flipped a coin and decided to try out Rust.
https://www.rust-lang.org/en-US/
Rust is a pretty interesting programming language. It’s built around speed and safety – preventing you from doing bad things. As such, it has some programming paradigms that seem really strange at first. But there’s a lot of logic behind those decisions. I’m not going to do a Rust tutorial here. But the documentation on how to learn the language is some of the best learning material I’ve seen on any site for any language or system.
Just start reading “The Book”:
https://doc.rust-lang.org/book/
I’m going through the Second edition, which is still listed as “under construction”, but I haven’t run into any flaws or incompleteness as of yet.
What I do want to cover in this tutorial is using Cairo in Rust. There’s a lot resources on how to use Cairo, lots of stuff on how to use Rust. But the center part of the Venn diagram is rather thin.
So, here’s what you need to do:
1. Install Rust and learn how to program in it. At least the basics. I had to read maybe 1/3 of “The Book” before I felt I was ready to start trying to pull in the Cairo bindings and create some graphics.
2. Install Cairo. This is the C library itself. It needs to be on your system so that the Rust bindings for Cairo have something to bind to.
https://cairographics.org/download/
I’m on Ubuntu, so this was really easy, just:
sudo apt install libcairo2-dev
On MacOS, it seems you’re supposed to use MacPorts, which I’m not really familiar with, or fink, which I’ve never heard of.
On Windows, it looks like you just have to add a few dlls to your path.
OK, we’ll carry on, assuming you got Cairo installed.
3. Create a project and set up the dependencies.
Again, this is not a Rust tutorial, but to create a Rust project, you can use cargo new
.
cargo new cairo_stuff --bin
This creates a new project named cairo_stuff
, which will be a binary executable, (bin) as opposed to a library project.
You’ll see a new folder with the project name. Inside that will be a src
folder and a .toml
file. It also sets up the project to use git by default, so you get a .git
folder and .gitignore
file. The .toml
file is the project configuration file. It should look something like this:
[package]
name = "cairo_stuff"
version = "0.1.0"
authors = ["Your Name <you@domain.com>"]
[dependencies]
The authors data is taken from your .gitconfig
file if you have one. We need to add our Cairo binding as dependencies for this project. To do that, we list them under dependencies. We’ll need two packages or “crates” as they are called in Rust.
We’ll need the png
crate so we can save images, and the cairo-rs
crate which contains the actual Cairo bindings. We also need to add png
as a feature to cairo-rs
, so that cairo-rs
itself knows how to save pngs.
Whenever you’re doing dependency configuration in any language, it’s important to have a way to know what versions of different dependencies are available. In Rust, you can go to https://crates.io and search for the package you want to add, in this case, “png”.
The first match tells us that we want version 0.11.0
.
And for cairo-rs
, it’s 0.2.0
.
Note, these will definitely and continuously change, so actually go to the site and check the numbers.
For png
, we can just add a line in the dependencies section that says
[dependencies]
png = "0.11.0"
Simple.
But the cairo-rs
line is a bit more complicated because we need to pass png
into it as a feature. So we say
[dependencies]
png = "0.11.0"
cairo-rs = { version = "0.2.0", features = ["png"] }
Again, read “The Book” to fully understand all the syntax here. But this will get you up and running.
Now, if you look at the main source file in the project, it’s src/main.rs
and it looks like this:
fn main() {
println!("Hello, world!");
}
If you’ve done any kind of programming, all that should be pretty self-explanatory. You have a function called main
. Rust follows the pattern of many other languages where main
is the function that is called when the program is executed.
In main
, we call println
, passing a string. Other than the exclamation point there, it should be straightforward. (The !
indicates that println
is implemented as a macro, not a function, but … whatever.)
At this point you can call cargo run
from the main project directory. This will satisfy all your dependencies, build the executable, and run it. It should print out the message “Hello, world!”. Great! It probably took a while to download everything it needed. But if you run it again, it should complete almost instantly.
If you now look in your project folder, you’ll see you have a Cargo.lock
file and a target
folder. The target
is where your final executable and all the build artifacts are. The lock file lists all the other dependencies that got pulled in when you added png
and cairo-rs
. For me, this file wound up at 171 lines, so there were quite a few other libraries it needed to pull down to satisfy those two that we added.
Let’s Draw Some Stuff
Now that we’ve set things up, let’s draw something.
Here are the basic steps:
1. Create an image surface.
2. Create a 2D drawing context that references that surface.
3. Draw things.
4. Create a file.
5. Write the surface to the file as a png.
For most of your projects, steps 1, 2, 4 and 5 are going to be boilerplate. The meat is going to be step 3 where you draw the actual content you want to draw.
First, we’re going to need to import the cairo-rs
bindings. That’s an external crate that we downloaded in our dependencies. So up at the top of the file we say
extern crate cairo;
Note that although the crate name was cairo-rs
, we just link to it as cairo
.
We’ll be using three modules from the cairo library: ImageSurface
, Format
, and Context
. We could access these like so:
cairo::ImageSurface
cairo::Format
cairo::Context
But repeating that leading cairo::
every time is unnecessary. Instead we can say:
use cairo::{ ImageSurface, Format, Context };
This would be equivalent to something like the following in the latest versions of JavaScript:
import { ImageSurface, Format, Context } from "cairo";
Now we can use those modules directly. Next, let’s make our surface. Delete the println
line inside of main
and add this instead:
let surface = ImageSurface::create(Format::ARgb32, 600, 600);
So ImageSurface
has a method named create
which gets a format, width and height and creates a new surface. The format will have four channels – a, r, g, b – each 32 bits.
But actually, this is not quite right as it stands. The ImageSurface::create
method does not directly return a surface, it returns a result
because this call may fail. Maybe you don’t have enough memory to make a surface the size that you requested for example. So you need to check the result
to make sure that it’s valid before proceeding.
There are a few ways to handle that result
and get at the surface. Rust provides a quick shortcut to let us do this in one step: just call expect
, passing in a message string. If the call succeeds, it will pass through the newly created surface. If it fails, the program will crash, displaying your message.
let surface = ImageSurface::create(Format::ARgb32, 600, 600)
.expect("Couldn't create a surface!");
Now, obviously, this is not great error handling for a program you’re going to release into the world, but it’s good enough for a first pass of something you’re testing out.
Now we create a context based on this surface, assuming it gets created.
let context = Context::new(&surface);
Context
has a new
method which takes a reference to a surface, hence the ampersand. (Read “The Book”!)
Now we have a context to draw stuff on! Let’s start by painting it red.
context.set_source_rgb(1.0, 0.0, 0.0);
context.paint();
set_source_rgb
takes three normalized floating point values. paint
fills the context with the current source value.
Where is My Image?
So how do we see this thing? The program we are creating has no UI. It just runs on the command line. We can’t just display the image in a window, because we have no window. We can save it to a file though.
First we need to create a file object. That would be std::fs::File
. We can use use
up at the top to shorten that in our code.
use std::fs::File;
After the context.paint()
line, create the file:
let mut file = File::create("output.png")
.expect("Couldn't create file.");
The mut
there means this is a mutable variable. It can be changed. By default, variables are immutable in Rust. Our next line will require a mutable reference though, so we make file
mutable. Again, this call may fail, so we tack on expect
with a message string so that we get a file not a result, and we’ll crash if this fails.
Now we can write the surface as a png to this file:
surface.write_to_png(&mut file)
.expect("Couldn't write to png");
The write_to_png
method needs a mutable reference to a file object. &mut file
is how we specify that (see “The Book”). And once again, we check the result.
And that’s that. Here’s the whole program:
extern crate cairo;
use cairo::{ ImageSurface, Format, Context };
use std::fs::File;
fn main() {
let surface = ImageSurface::create(Format::ARgb32, 600, 600)
.expect("Couldn't create surface");
let context = Context::new(&surface);
context.set_source_rgb(1.0, 0.0, 0.0);
context.paint();
let mut file = File::create("output.png")
.expect("Couldn't create file");
surface.write_to_png(&mut file)
.expect("Couldn't write to png");
}
When you run that, you should wind up with a file called ouput.png
in the main project directory. It should be a 600×600 red square.
Wait, That’s Not Drawing!
OK, OK, filling a png with a single color is not really drawing. Let’s get a taste of the drawing api itself. I’m just going to throw the whole file at you and let you figure it out.
extern crate cairo;
extern crate rand;
use cairo::{ ImageSurface, Format, Context };
use std::fs::File;
fn main() {
let surface = ImageSurface::create(Format::ARgb32, 600, 600)
.expect("Couldn't create surface");
let context = Context::new(&surface);
// paint canvas white
context.set_source_rgb(1.0, 1.0, 1.0);
context.paint();
// draw 100 random black lines
context.set_source_rgb(0.0, 0.0, 0.0);
for _i in 0..100 {
let x = rand::random::<f64>() * 600.0;
let y = rand::random::<f64>() * 600.0;
context.line_to(x, y);
}
context.stroke();
let mut file = File::create("output.png")
.expect("Couldn't create file");
surface.write_to_png(&mut file)
.expect("Couldn't write to png");
}
Again, the top and bottom of this file is mostly boilerplate. After we create the surface and context, we paint the surface white this time, and use a for loop to draw 100 lines. This is done with context.line_to
.
Note that we’re using a random number generator here. That’s another crate. Up top I’ve imported that with extern crate rand;
I’ll leave it up to you to search https://crates.io to find the current version and add it to your .toml
file.
Now the random functionality gets a bit wordy here, with specifying the type each time (f64 = 64-bit floating point number). But you’re probably going to want to create your own random range function anyway, so you can say random(min, max)
and get a result in that range. When you do that, you can just abstract the actual random call within that function.
What Next?
I’ve already said it enough, but read “The Book” if you want to learn more about Rust. It’s really that good.
In terms of cairo-rs
, all the api docs are here: http://gtk-rs.org/docs/cairo/
And all the actual drawing commands are on the context object here: http://gtk-rs.org/docs/cairo/
A lot of those will look really familiar if you’ve been working in HTML5 Canvas. move_to
, line_to
, curve_to
, arc
, rectangle
, etc.
And once you’re familiar with Rust and cairo-rs
, you should be able to go to any C-specific Cairo documentation and figure out how that can be translated over to Rust.
One point I want to address briefly is that I said in the beginning that setting up a Webpack/Babel project in order to use the latest and greatest JavaScript features is cumbersome. After reading this article, you might be thinking that setting up a Rust project is just as cumbersome, maybe more so.
But it’s not really. Installing Rust and Cairo itself may have taken a bit, but those are one time actions. Once they’re on your system, you’re done. From there, a new project is as simple as running cargo new and adding two lines for the dependencies. For the source file, you can create some kind of snippet or template for all that boilerplate stuff and you’re off to the races, writing drawing commands in under a minute.
Anyway, hopefully this starts you on some kind of journey of learning something new.