Cairo Pattern Coordinate Spaces: Why Your Gradient Looks Wrong
Cairo's pattern system unifies painting via cairo_pattern_t, but gradient stops live in pattern space (0–1) while geometry uses user space. The pattern matrix maps user→pattern — scaling it up shrinks the gradient. Fix: invert the CTM into the pattern matrix so gradients track your layout.
28 Sept 2025, 13:42 UTC

The Problem: Gradients That Don't Match Your Layout
You define a linear gradient from left to right across a 200-pixel button. In code you set stops at 0.0 and 1.0, expecting the color change to span the button width. Instead the gradient compresses into a sliver or stretches across the whole window. The culprit isn't the gradient API — it's the coordinate space Cairo uses for pattern stops.
How Cairo Patterns Actually Work
Every drawing operation in Cairo — fill, stroke, text, mask — pulls color from a cairo_pattern_t. There are three pattern types: solid (cairo_pattern_create_rgba), gradient (linear/radial via cairo_pattern_create_linear/radial), and surface patterns (cairo_pattern_create_for_surface) for tiled images. Unlike libraries with separate brush and pen objects, Cairo funnels everything through this single pattern abstraction.
Gradient stops are defined in pattern space, a normalized 0.0–1.0 coordinate system that exists before the pattern matrix is applied. The current transformation matrix (CTM) affects geometry, but the pattern matrix (cairo_pattern_set_matrix) maps user space to pattern space independently. This separation is powerful — you can rotate a gradient without rotating the shape — but it's the source of the most common bugs.
The Coordinate Space Trap
When you call cairo_pattern_create_linear(x0, y0, x1, y1), those coordinates are in user space at the moment of creation. However, the stop offsets you add with cairo_pattern_add_color_stop_rgba are always in pattern space (0.0–1.0 along the gradient vector). If you later change the CTM — say by scaling the context for a high-DPI display — the gradient vector scales with it, but the stop offsets stay fixed at 0.0–1.0. The result: your gradient stretches or compresses relative to the drawn shape.
The pattern matrix compounds this. It multiplies on the right: user_to_pattern = pattern_matrix * user_space. Setting an identity matrix then scaling by 2 halves the apparent pattern size — the opposite of CTM intuition. Many developers expect the pattern matrix to work like the CTM (where scaling up makes things bigger), but it actually maps user coordinates into pattern coordinates, so a larger matrix value means a smaller pattern repeat.
Worked Example: Button Gradient That Stays Put
Suppose you want a horizontal gradient across a 200×40 button, regardless of any CTM scaling. The fix is to align pattern space with the button's local user space at draw time by setting the pattern matrix to the inverse of the current CTM.
cairo_t *cr = cairo_create(surface);
// Draw at (100, 100) with possible prior scaling
cairo_translate(cr, 100, 100);
// Assume unknown CTM from parent context
// Create gradient in button-local coordinates
cairo_pattern_t *pat = cairo_pattern_create_linear(0, 0, 200, 0);
cairo_pattern_add_color_stop_rgba(pat, 0.0, 0.2, 0.4, 0.8, 1.0);
cairo_pattern_add_color_stop_rgba(pat, 1.0, 0.6, 0.8, 0.3, 1.0);
// Critical: make pattern space track current user space
cairo_matrix_t ctm, inv;
cairo_get_matrix(cr, &ctm);
cairo_matrix_invert(&inv);
cairo_pattern_set_matrix(pat, &inv);
cairo_set_source(cr, pat);
cairo_rectangle(cr, 0, 0, 200, 40);
cairo_fill(cr);
cairo_pattern_destroy(pat);
cairo_destroy(cr);
The cairo_get_matrix/cairo_matrix_invert sequence captures the current user-to-device transform, then inverts it so the pattern matrix maps user space back to the normalized 0–1 gradient vector. Now the gradient spans exactly 200 user-space units no matter what scaling, rotation, or translation the CTM carries.
Trade-offs and Limitations
- Performance: Computing and inverting the matrix per draw call adds overhead. For static UI, cache the inverted matrix when the CTM changes.
- Backend fidelity: PDF and SVG backends emit native gradient shadings when possible, but complex pattern matrices or unsupported extend modes (REFLECT on surface patterns) force rasterization fallback. Test vector output with
cairo_surface_write_to_pngfor raster reference. - Premultiplied alpha: Gradient stops interpolate in premultiplied alpha space. A stop at
rgba(1,0,0,0)torgba(0,0,1,1)darkens mid-gradient because transparent red becomes (0,0,0,0). Use opaque stops or pre-multiply components manually for linear-light blending. - Mesh gradients (Cairo 1.16+):
cairo_mesh_pattern_*APIs exist but backend support is incomplete. PDF emits Type 6/7 shadings; SVG and Win32/Quartz often rasterize. Verify withpdftk uncompresson PDF output before relying on them.
Quick Verification Checklist
- Create a linear gradient with stops at 0.0 and 1.0, set pattern matrix to scale by 2, draw a rectangle — the gradient should appear half-width.
- Render a surface pattern with
CAIRO_EXTEND_REPEATto PDF and SVG; compare tiling fidelity and vector vs raster fallback. - Run
valgrind --leak-check=fullon a test that creates a pattern, sets it as source, destroys the pattern, then continues drawing — should not crash or leak.
Actionable Takeaway
Treat the pattern matrix as a user-to-pattern transform, not a pattern-to-user transform. When you need a gradient or tiled image to stay locked to your geometry regardless of CTM, capture cairo_get_matrix, invert it, and assign it to the pattern before setting the source. This one pattern (pun intended) eliminates the majority of coordinate-space bugs in Cairo drawing code.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.