Rust: PDFs — Text Rotation with Cairo and Pango
My fascination with Pango and CairoGraphics has led me to explore text rotation. I find it very interesting. It becomes straightforward once we understand a few key ideas. In this article, we focus on ±90° rotation for left‑to‑right text only.
🦀 Index of the Complete Series.
![]() |
|---|
| Rust: PDFs — Text Rotation with Cairo and Pango |
🚀 The code for this post is in the following GitHub repository: pdf_04_text_rotation.
Recall from our previous discussion,
Cairo Units and Coordinates — that
Cairo’s effective coordinate system (when used with PangoCairo)
is top‑left. We can visualise it as illustrated below:

For Latin‑based writing systems, as well as scripts such as Khmer, Lao, and Thai,
text flows horizontally from 0 toward X+, and vertically
from 0 toward Y+.
❷ Cairo Text Rotation
We perform text rotation through Cairo. We rotate the
cairo::Context, then draw the
text using
pangocairo::functions::show_layout() — the same function we used in the
previous article.
The official
cairo::Context method
pub fn rotate(&self, angle: f64)
has no documentation. In the underlying Cairo C library, the corresponding
cairo_rotate() function belongs to
the Transformations category, described as
“Transformations — Manipulating the current transformation matrix”.
I am reproducing the official cairo_rotate() documentation below:
cairo_rotate()
void
cairo_rotate (cairo_t *cr,
double angle);Parameters
cr: a cairo context
angle: angle (in radians) by which the user-space axes will be rotated
Since: 1.0
💡 Please note: the angle parameter to cairo_rotate() is in
radians.
Briefly, 360° = 2π radians and 180° = π radians.
To convert degrees to radians, multiply degrees by π/180.
To convert radians to degrees, multiply radians by 180/π. For example:
- 90° → radians: 90 × (π / 180) = 1.57 radians
- 1.57 radians → degrees: 1.57 × (180 / π) = 90°
I did high school in Vietnam back in the 1980s… radians felt foreign 😂 and suggested nothing. I have since confirmed that they are taught — and called radian — but I remember none of it. Degrees make far more sense to me, both visually and intuitively.
🦀 In Rust, converting 90° to radians is simple:
90.0_f64.to_radians() or 90.0_f32.to_radians().
Next, we will look at some Rust code that performs text rotation.
💡 Please note: on both Windows and Ubuntu, I’m running Rust version
rustc 1.90.0 (1159e78c4 2025-09-14).
This is once again a one‑off project—I don’t plan to update it in future development. I want to keep a log of progress exactly as it occurred. Future code may copy this and make changes to it. I’ve placed the project under the pdf_04_text_rotation directory. The structure is:
.
├── Cargo.toml
├── set_env.bat
└── src
├── main_01.rs
├── main_02.rs
├── main_03.rs
├── main_04.rs
├── main_05.rs
├── main.rs
└── page_geometry.rs
All main*.rs modules under src/ are self‑contained
Rust programs written in the listed order to help me understand text rotation.
We discuss these modules in the listed order.
The src/page_geometry.rs module is copied unchanged from the
last article.
👉 Changing any margin value in the A4_DEFAULT_MARGINS constant will
change the layout of the text in the PDF.
💡 The code requires the Pango, HarfBuzz, Cairo,
etc. libraries. 🐧 On Ubuntu, all required libraries are globally recognised. 🪟 On Windows,
I haven’t added the paths for the libraries’ DLLs to the PATH environment
variable. In each new Windows terminal session, I run the following once:
set PATH=C:\PF\harfbuzz\dist\bin\;%PATH%
set PATH=C:\PF\vcpkg\installed\x64-windows\bin\;%PATH%
set PATH=C:\PF\pango\dist\bin;C:\PF\cairo-1.18.4\dist\bin;C:\PF\fribidi\dist\bin;%PATH%
Alternatively, you can simply run
set_env.bat.
After that, cargo run works as expected.
In this section, we look at several small study programs. Our focus is on ±90° text rotation.
We start with the
pdf_04_text_rotation/src/main_01.rs module — most of the
code should already be familiar. The only new line is line 45:
context.rotate(90.0_f64.to_radians());
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
layout.set_text("Kỷ độ Long Tuyền đới nguyệt ma");
// Save the current state
context.save()?;
context.move_to(A4_DEFAULT_MARGINS.left, A4_DEFAULT_MARGINS.top);
// Both produce the same result.
// context.rotate(90.0 * PI / 180.0);
context.rotate(90.0_f64.to_radians());
show_layout(&context, &layout);
// Restore the context to the original matrix state for subsequent drawing operations
context.restore()?;
When I first began studying text rotation, I assumed that before calling
cairo_rotate(), we would need to
call cairo_translate().
However, cairo_move_to() is sufficient.
The output it produces is shown below:

In this output, both A4_DEFAULT_MARGINS.top and
A4_DEFAULT_MARGINS.left are set to 57.0 PostScript points.
However, visually, the left margin appears smaller than the top margin.
According to the documentation:
The rotation direction for positive angles is from the positive X axis toward the positive Y axis.
This means that after rotation, X+ now points down, and
Y+ now points left.
It follows that the rotated user‑space X origin effectively becomes
A4_DEFAULT_MARGINS.left − line height.
If we compensate the X origin by one line height (in PostScript
points), the visual origin aligns correctly with the intended
A4_DEFAULT_MARGINS.left.
We implement the above compensation in
pdf_04_text_rotation/src/main_02.rs. We added lines
38–39 and updated line 44:
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
layout.set_text("Kỷ độ Long Tuyền đới nguyệt ma");
let (_, logical) = layout.extents();
let line_height = logical.height() as f64 / pango::SCALE as f64;
// Save the current state
context.save()?;
context.move_to(A4_DEFAULT_MARGINS.left + line_height, A4_DEFAULT_MARGINS.top);
// Both produce the same result.
// context.rotate(90.0 * PI / 180.0);
context.rotate(90.0_f64.to_radians());
show_layout(&context, &layout);
// Restore the context to the original matrix state for subsequent drawing operations
context.restore()?;
The new code is not conceptually new—we have used these calls in previous articles. The user‑space axes are rotated, but the text itself is not; it retains the same metrics it would have in the default user‑space orientation. The output is now:

Both margin look visually identical. But let’s verify that by printing the same line
of text just below the rotated text, at the same X coordinate.
In the
pdf_04_text_rotation/src/main_03.rs module:
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
layout.set_text("Kỷ độ Long Tuyền đới nguyệt ma");
let (_, logical) = layout.extents();
let line_height = logical.height() as f64 / pango::SCALE as f64;
let line_width = logical.width() as f64 / pango::SCALE as f64;
// Save the current state
context.save()?;
context.move_to(A4_DEFAULT_MARGINS.left + line_height, A4_DEFAULT_MARGINS.top);
// Both produce the same result.
// context.rotate(90.0 * PI / 180.0);
context.rotate(90.0_f64.to_radians());
show_layout(&context, &layout);
// Restore the context to the original matrix state for subsequent drawing operations
context.restore()?;
context.move_to(A4_DEFAULT_MARGINS.left, A4_DEFAULT_MARGINS.top + line_width);
show_layout(&context, &layout);
We added line 40 — and after restore the original user-space in line 54
context.restore()?; — we added lines 56-57:
A4_DEFAULT_MARGINS.top + line_width gives the Y coordinate
just below the rotated text. The text is rotated vertically, line_width
can be treated as logical height since the rotated text actually occupies
line_width PostScript points veritically. The output looks like:

The compensation we have implemented appears to be correct.
The
pdf_04_text_rotation/src/main_04.rs module implements
-90° rotation. It is a copy of
pdf_04_text_rotation/src/main_01.rs
with only a single modification — the angle is negative, updating line 45 to:
43
44
45
// Both produce the same result.
// context.rotate(-90.0 * PI / 180.0);
context.rotate(-90.0_f64.to_radians());
The PDF looks like the screenshot below:

The X coordinate visually appears correct at A4_DEFAULT_MARGINS.left.
However, because the user‑space axes rotate upward with a negative angle, the text is clipped.
To position the “top” of the text at the A4_DEFAULT_MARGINS.top
Y coordinate, we must compensate by
A4_DEFAULT_MARGINS.top + line_width, similar to a
previous discussion.
The
pdf_04_text_rotation/src/main_05.rs module implements this
compensation. We added lines 38–39 and updated line 44:
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
layout.set_text("Kỷ độ Long Tuyền đới nguyệt ma");
let (_, logical) = layout.extents();
let line_width = logical.width() as f64 / pango::SCALE as f64;
// Save the current state
context.save()?;
context.move_to(A4_DEFAULT_MARGINS.left, A4_DEFAULT_MARGINS.top + line_width);
// Both produce the same result.
// context.rotate(-90.0 * PI / 180.0);
context.rotate(-90.0_f64.to_radians());
show_layout(&context, &layout);
// Restore the context to the original matrix state for subsequent drawing operations
context.restore()?;
There is no new logic here. The output PDF looks like the screenshot below; visually,
both margins now align with A4_DEFAULT_MARGINS.left and
A4_DEFAULT_MARGINS.top:

As a final example, we look at rotating by angles other than ±90°. The
pdf_04_text_rotation/src/main.rs module rotates the
user‑space axes in 10° steps until reaching 360°:
47
48
49
50
51
for degree in (10..370).step_by(10) {
context.rotate((degree as f64).to_radians());
show_layout(&context, &layout);
context.set_matrix(cairo::Matrix::identity());
}
Because Cairo transformations are cumulative, we must call
context.set_matrix() to reset the transformation matrix after each draw.
Instead of context.set_matrix(), we can use context.save()?;
and context.restore()?;, as shown in the snippet below.
Please refer to the module for full details:
context.save()?;
context.rotate((degree as f64).to_radians());
show_layout(&context, &layout);
context.restore()?;
Its output is shown in the screenshot below:

❺ The ±90° Rotation and cairo-rotate()’s Documentation Revisited
The cairo-rotate() documentation states:
Modifies the current transformation matrix (CTM) by rotating the user-space axes by angle radians. The rotation of the axes takes places after any existing transformation of user space. The rotation direction for positive angles is from the positive X axis toward the positive Y axis.
It took me a while to fully understand the above paragraph, even though through trial and error I did get the ±90° rotations working. We have already seen how the code behaves; now let’s unpack this visually to cement our understanding of this transformation function.
The image below illustrates how Cairo draws text in the default
user‑space axes:
![]() |
|---|
| Cairo Text in Default User-Space Axes |
The text simply flows toward X+ for left‑to‑right text.
“The rotation direction for positive angles is from the positive X axis toward the positive Y axis.”
For a +90° rotation, X+ points down and
Y+ points left. That is:
![]() |
|---|
| Cairo Text in +90° Rotated User-Space Axes |
For a -90° rotation, X+ points up and
Y+ points right. That is:
![]() |
|---|
| Cairo Text in -90° Rotated User-Space Axes |
💡 The key point is that the user‑space axes are rotated — not the text.
Following the directions of X+ and Y+ is the key to
understanding why the text appears rotated the way it does.
I initially tried to reason about this in terms of “clockwise” and
“counter‑clockwise”, but that approach failed; it does not map cleanly to
how Cairo rotates the coordinate system.
It was fun exploring this functionality. I also looked briefly into image rotation, though not as deeply as text rotation. I may study image rotation further and share my findings in another article.
Thanks for reading! I hope this post helps others who are looking to deepen their understanding of PDF technology. As always—stay curious, stay safe 🦊
✿✿✿
Feature image sources:
- https://www.omgubuntu.co.uk/2024/03/ubuntu-24-04-wallpaper
- https://in.pinterest.com/pin/337277459600111737/
- https://medium.com/analytics-vidhya/rust-adventures-from-java-class-to-rust-struct-1d63b66890cf/
- https://www.pngitem.com/download/ibmJoR_rust-language-hd-png-download/
- https://en.wikipedia.org/wiki/Cairo_%28graphics%29#/media/File:Cairo_banner_closeup.svg
- https://ur.wikipedia.org/wiki/%D9%81%D8%A7%D8%A6%D9%84:HarfBuzz.svg
- https://en.wikipedia.org/wiki/Pango



