Cairo 2D Graphics — Deep Dive
Setting up
pip install pycairo
On Linux, pycairo links against the system libcairo. On macOS, install via Homebrew (brew install cairo) first. On Windows, binary wheels include the library.
Basic drawing workflow
import cairo
WIDTH, HEIGHT = 800, 600
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, WIDTH, HEIGHT)
ctx = cairo.Context(surface)
# White background
ctx.set_source_rgb(1, 1, 1)
ctx.paint()
# Draw a rounded rectangle
def rounded_rect(ctx, x, y, w, h, r):
ctx.new_sub_path()
ctx.arc(x + w - r, y + r, r, -0.5 * 3.14159, 0)
ctx.arc(x + w - r, y + h - r, r, 0, 0.5 * 3.14159)
ctx.arc(x + r, y + h - r, r, 0.5 * 3.14159, 3.14159)
ctx.arc(x + r, y + r, r, 3.14159, 1.5 * 3.14159)
ctx.close_path()
rounded_rect(ctx, 50, 50, 300, 200, 20)
ctx.set_source_rgb(0.2, 0.4, 0.8)
ctx.fill_preserve()
ctx.set_source_rgb(0.1, 0.2, 0.4)
ctx.set_line_width(3)
ctx.stroke()
surface.write_to_png("card.png")
fill_preserve() keeps the path after filling so you can stroke it immediately — a common pattern for shapes with both fill and outline.
Gradients and patterns
Linear gradient
gradient = cairo.LinearGradient(0, 0, WIDTH, HEIGHT)
gradient.add_color_stop_rgba(0, 0.1, 0.1, 0.3, 1.0)
gradient.add_color_stop_rgba(0.5, 0.4, 0.1, 0.6, 1.0)
gradient.add_color_stop_rgba(1, 0.1, 0.1, 0.3, 1.0)
ctx.rectangle(0, 0, WIDTH, HEIGHT)
ctx.set_source(gradient)
ctx.fill()
Radial gradient
radial = cairo.RadialGradient(400, 300, 10, 400, 300, 250)
radial.add_color_stop_rgba(0, 1, 1, 0.8, 1)
radial.add_color_stop_rgba(1, 0.8, 0.3, 0, 0)
ctx.set_source(radial)
ctx.arc(400, 300, 250, 0, 2 * 3.14159)
ctx.fill()
Surface patterns (tiling)
tile = cairo.ImageSurface.create_from_png("texture.png")
pattern = cairo.SurfacePattern(tile)
pattern.set_extend(cairo.Extend.REPEAT)
ctx.set_source(pattern)
ctx.paint()
Transformation matrix
Cairo maintains a current transformation matrix (CTM) that maps user coordinates to device coordinates. All drawing operations go through this matrix.
ctx.save()
ctx.translate(400, 300) # move origin to center
ctx.rotate(0.785) # rotate 45 degrees
ctx.scale(2, 0.5) # stretch horizontally
ctx.rectangle(-50, -50, 100, 100)
ctx.set_source_rgb(0.9, 0.2, 0.2)
ctx.fill()
ctx.restore() # undo all transformations
save() and restore() manage a stack of graphics states — transformation, clip region, source, line width. Always pair them to avoid state leaks.
Building complex scenes with transformations
import math
ctx.translate(400, 300)
for i in range(12):
ctx.save()
angle = i * (2 * math.pi / 12)
ctx.rotate(angle)
ctx.translate(0, -200)
# Draw a tick mark at "12 o'clock" position
ctx.rectangle(-3, 0, 6, 20)
ctx.set_source_rgb(0, 0, 0)
ctx.fill()
ctx.restore()
This clock-face pattern demonstrates how transformations eliminate manual trigonometry.
Compositing operators
Cairo supports Porter-Duff compositing operators that control how new drawing operations combine with existing content:
ctx.set_operator(cairo.Operator.MULTIPLY)
# Subsequent draws multiply colors with the background
ctx.set_operator(cairo.Operator.SCREEN)
# Lightening blend mode
ctx.set_operator(cairo.Operator.OVER)
# Default: standard alpha compositing
Useful operators for data visualization include OVER (layering), SOURCE (replace), and CLEAR (erase regions).
Text rendering with Pango
For production-quality text, use PangoCairo:
import gi
gi.require_version('Pango', '1.0')
gi.require_version('PangoCairo', '1.0')
from gi.repository import Pango, PangoCairo
layout = PangoCairo.create_layout(ctx)
layout.set_width(600 * Pango.SCALE) # wrap at 600 user units
layout.set_font_description(Pango.FontDescription("Inter 16"))
layout.set_text("Cairo handles complex text layout including "
"line wrapping, bidirectional text, and font fallback.", -1)
ctx.move_to(100, 400)
ctx.set_source_rgb(0, 0, 0)
PangoCairo.show_layout(ctx, layout)
Pango handles:
- Line breaking and wrapping
- Right-to-left scripts (Arabic, Hebrew)
- Font fallback for missing glyphs
- Rich text with
set_markup()(Pango markup, similar to HTML)
Multi-format output pipeline
The same drawing function can target different surfaces:
def draw_report(ctx, width, height):
# ... all your drawing logic ...
pass
# PNG for web
img = cairo.ImageSurface(cairo.FORMAT_ARGB32, 1200, 800)
draw_report(cairo.Context(img), 1200, 800)
img.write_to_png("report.png")
# PDF for print (dimensions in points: 72 pt = 1 inch)
pdf = cairo.PDFSurface("report.pdf", 842, 595) # A4 landscape
draw_report(cairo.Context(pdf), 842, 595)
pdf.finish()
# SVG for editing
svg = cairo.SVGSurface("report.svg", 1200, 800)
draw_report(cairo.Context(svg), 1200, 800)
svg.finish()
For PDF, call ctx.show_page() to emit a page and start a new one. Multi-page documents work by calling show_page() between pages.
Integration with NumPy
ImageSurface data can be accessed as a buffer and converted to NumPy:
import numpy as np
buf = surface.get_data()
arr = np.ndarray(shape=(HEIGHT, WIDTH, 4), dtype=np.uint8, buffer=buf)
# arr is BGRA on little-endian systems
# Convert to RGB for other libraries:
rgb = arr[:, :, [2, 1, 0]]
This enables mixing Cairo drawing with image processing (OpenCV, Pillow, scikit-image).
Performance considerations
- Path complexity — Thousands of small paths are slower than fewer complex paths. Batch similar shapes when possible.
- Surface format —
FORMAT_ARGB32supports transparency but uses 4 bytes per pixel.FORMAT_RGB24saves memory when transparency is not needed. - Recording surfaces —
cairo.RecordingSurfacecaptures drawing commands without rasterizing, then replays them onto a target surface. Useful for deferred rendering and caching reusable graphics. - Subpixel rendering — For screen output,
set_antialias(cairo.Antialias.SUBPIXEL)improves text clarity on LCD displays.
Real-world production patterns
Report generator — Companies use pycairo to generate branded PDF reports with charts, tables, and logos. Unlike HTML-to-PDF tools, Cairo gives pixel-precise control over layout.
Map tile rendering — Mapnik generates map tiles using Cairo as one of its rendering backends. A single Python script can re-style an entire map by changing colors, line widths, and label fonts.
Custom diagram DSL — Build a domain-specific language (parse a text description of a network diagram, circuit schematic, or org chart) and render it through Cairo to any format.
Tradeoffs
- No built-in chart types — Unlike Matplotlib, you draw everything from primitives. This offers maximum control but more code for standard charts.
- GTK dependency for Pango — PangoCairo pulls in GObject Introspection, which can be complex to install on Windows/macOS.
- No interactivity — Cairo produces static output. For interactive graphics, combine with GTK, Qt, or export to SVG for web embedding.
One thing to remember
Cairo gives you a professional-grade 2D rendering engine in Python — complete with anti-aliasing, gradients, compositing, and multi-format output — at the cost of working with primitives instead of high-level chart abstractions. When you need precise control over every pixel and path, Cairo delivers.
See Also
- Python Adaptive Learning Systems How Python builds learning apps that adjust to each student like a personal tutor who knows exactly what you need next.
- Python Airflow Learn Airflow as a timetable manager that makes sure data tasks run in the right order every day.
- Python Altair Learn Altair through the idea of drawing charts by describing rules, not by hand-placing every visual element.
- Python Automated Grading How Python grades homework and exams automatically, from simple answer keys to understanding written essays.
- Python Batch Vs Stream Processing Batch processing is like doing laundry once a week; stream processing is like a self-cleaning shirt that cleans itself constantly.