Now that we’ve got the tools we need to create and save an image, and a base framework to build additional functionality on top of, let’s turn to the next important part of a graphics library: being able to draw something on to the image.


This post is part of a series:


Let’s start very simple, with the humble straight line.

cairo provides two functions for drawing a straight line:

  • cairo_line_to
  • cairo_rel_line_to

Each takes two arguments, floats representing the x and y coordinates of the destination point. The difference lies in how they translate those coordinates, and that difference is quite possibly revealed by the names of the functions. cairo_line_to draws a line to the given coordinates interpreted in absolute space, while cairo_rel_line_to interprets the given coordinates as deltas to be applied relative to the origin point.

Speaking of origin points, you’ll have noticed that each of these functions takes only one set of coordinates, the end point of the line. So how do we know where to start drawing?

As part of its state, the in-memory Cairo context keeps a record of its “current point”. This point starts out at (0, 0), and is updated every time a line (or other shape) is drawn on the image surface, or by using two related functions: cairo_move_to and cairo_rel_move_to. As you might imagine, these act much the same way as the _line_to functions above, but simply update the “current point” without rendering a line onto the surface.

Two other functions it will be useful to know about before diving into implementing this functionality in Xairo, are cairo_stroke and cairo_fill. These are used to complete a path built of _line_to, _move_to and other related functions, and then render it to the image surface either by rendering the path as a single line, or attempting to fill in the path, respectively. These functions are related to paint which we already saw implemented in a previous post. For now, though, since we’re only drawing straight lines, we’ll only worry about _stroke.

So let’s look at the functions we need to implement. For now, let’s leave aside the _rel_*_to functions, since their implementation will be very close to the absolute coordinate functions, and also _fill, since it will be a very small and predictable variation on _stroke. So that leaves us with:

  • line_to
  • move_to
  • stroke

On the Elixir side

In C and Rust, the line_to and move_to functions take 2 arguments, the x and y coordinates as floats. This is fine, and we could implement Xairo.line_to and Xairo.move_to to take 2 arguments (in addition to the image resource) without an problem. But in order to make Xairo as user-friendly as possible, wouldn’t it be nice to provide, through function signatures, some context for those two floats? What if we wrap them in a very simple Point struct?

defmodule Xairo.Point do
  defstruct [:x, :y]

  def new(x, y) do
    %__MODULE__{
      x: x * 1.0,
      y: y * 1.0
    }
  end
end

then we can write Xairo.line_to and Xairo.move_to, which take a single, contextualized, Point rather than two unmarked float values.

Now we have a choice. We can either have our NIF for line_to accept x and y, and unpack the Point in Xairo.line_to, or we could keep that context through the NIF into Rust, and unpack the coordinates only when it’s time to pass them into cairo. I prefer the second option, maintaining struct contexts as much as possible through the API code, both in Elixir and Rust, and unwrapping them only when it’s necessary to communicate with the original C API.

This means that for Xairo.line_to the code we write in Elixir, along with the Point struct definition from above, looks like

defmodule Xairo.Native do
  ...
  def line_to(_image, _point), do: :erlang.nif_error(:nif_not_loaded)
end

defmodule Xairo do
  ...
  def line_to(%Image{} = image, Point{} = point) do
    Xairo.Native.line_to(image.reference, point)
    image
  end
end

How cool is that? Barely any code at all. As we’ll see next, things are pretty much the same over in Rust

On the Rust side

In the Ruby code above, we saw that instead of passing two floats into Rust as the coordinates, we’re passing an Elixir struct that we’ve created. As I alluded to in an earlier post,1 rustler actually makes passing structured data between Elixir and Rust very simple by providing a NifStruct trait. So we define our Rust struct

#[derive(Copy,Clone,Debug,NifStruct)]
#[module = "Xairo.Point"]
pub struct Point {
    pub x: f64,
    pub y: f64
}

We derive the NifStruct trait and add an annotation matching it to the struct’s name in Elixir, and that is all we have to do to be able to use the struct directly in our NIF

#[rustler::nif]
fn line_to(context: WrapperRA, point: Point) -> WrapperRA {
    context.context.line_to(point.x, point.y);
    context
}

We implement move_to and stroke in the same way, and then we should be able to draw some lines on our image, right?

Well, just about. Except for paint, none of our functions deal with color. So how to do we tell cairo what color to use when it draws a line?

Our NIF for paint cheated just a little bit by wrapping two cairo functions togther. It calls set_source_rgb and paint. This was fine a little while ago because we only had one function that could render color on to the canvas. But now we’re adding more, so let’s surface set_source_rgb out into its own NIF.2 While we’re at it, let’s provide an RGB struct context for those values like we did with Point. The implementation of this struct in Elixir and Rust will be left as an exercise for the reader.

At the end of this, our Xairo Elixir module looks like

defmodule Xairo do
  ...
  def line_to(%Image{} = image, Point{} = point) do
    Xairo.Native.line_to(image.reference, point)
    image
  end

  def move_to(%Image{} = image, Point{} = point) do
    Xairo.Native.move_to(image.reference, point)
    image
  end

  def set_source_rgb(%Image{} = image, %RGB{} = rgb) do
    Xairo.Native.set_source_rgb(image.reference, rgb)
    image
  end

  def paint(%Image{} = image) do
    Xairo.Native.paint(image.reference)
    image
  end

  def stroke(%Image{} = image) do
    Xairo.Native.stroke(image.reference)
    image
  end

and Rust like

#[rustler::nif]
fn line_to(context: WrapperRA, point: Point) -> WrapperRA {
    context.context.line_to(point.x, point.y);
    context
}

#[rustler::nif]
fn move_to(context: WrapperRA, point: Point) -> WrapperRA {
    context.context.move_to(point.x, point.y);
    context
}

#[rustler::nif]
fn set_source_rgb(context: WrapperRA, rgb: RGB) -> WrapperRA {
    context.context.set_source_rgb(rgb.red, rgb.green, rgb.blue);
    context
}

#[rustler::nif]
fn paint(context: WrapperRa) -> WrapperRa {
    context.context.paint().unwrap();
    context
}

#[rustler::nif]
fn stroke(context: WrapperRa) -> WrapperRa {
    context.context.stroke().unwrap();
    context
}

A nice 1-to-1 mapping of the API, with our structured data kept contextualized until it needs to be destructed to pass to the native API.

Let’s end by putting it all together and drawing an empty white image with two horizontal black lines:

Xairo.Image.new(100, 100)
|> Xairo.set_source_rgb(RGB.new(255, 255, 255))
|> Xairo.paint()
|> Xairo.set_source_rgb(RGB.new(0, 0, 0))
|> Xairo.move_to(Point.new(20, 20))
|> Xairo.line_to(Point.new(80, 20))
|> Xairo.move_to(Point.new(20, 80))
|> Xairo.line_to(Point.new(80, 80))
|> Xairo.stroke()
|> Xairo.save_image("lines.png")

and that gives us

lines.png

just as we hoped

Next time we’ll look at some of that duplication creeping into Xairo, and talk about next steps for the library. Thanks for reading!


Footnotes

  1. In that previous post, I decided against using this option because of the complexity of the data we wanted to pass in the struct. Here, where we’re only passing two floating point numbers, it is much simpler. (for the Rust-ier among you, this approach is possible as long as the field types all implement the Copy trait.) 

  2. We could implement stroke (and later, fill) similarly, where they take the color and set it before rendering. Because of how cairo works under the hood, you can set the color at any point during a path’s creation/extension as long as it’s before you call stroke. But, from a workflow perspective, it is more natural when drawing to select the color first, so being able to separate these functions lends itself to a more familiar workflow when using Xairo