Choosing a Cairo Surface Backend: Image, PDF, SVG, or Recording
Cairo fixes the output backend when you create the drawing context. Compare image, PDF, SVG, PostScript, recording and similar surfaces, then verify the choice with status checks and a zoom test.
22 Jul 2026, 04:10 UTC

The decision is made before you draw anything
In cairo, the surface is the destination, and you commit to it when you create the cairo_t drawing context. Path construction, fills, text and compositing are all written against that context. Swapping an image surface for a PDF surface afterwards is not a flag change — it means re-running the entire scene against a new context.
So the useful question is not "which cairo backend is best" but "what does this output have to survive?" If the answer is "screen pixels at a known size", an image surface is appropriate. If it is "someone will zoom in or print it", a vector backend is appropriate. If it is "the same scene at three sizes or on three targets", a recording surface in the middle lets you keep one copy of the drawing code.
This article assumes Cairo means the cairo 2D graphics library (cairographics), not the Starknet smart-contract language or OS hibernation. It also assumes a C API in the cairo 1.16/1.18 era; confirm your own version with cairo_version_string(), because backend feature coverage and API availability vary by release.
Constraints that narrow the choice
- Surface type is fixed at context creation. The backend determines the output format, whether output is raster or vector, and which drawing operations map cleanly.
- Raster memory scales with area. An ARGB32 image surface costs roughly
width × height × 4bytes, so a very large canvas is expensive regardless of how simple the drawing is. - Vector file size scales with content complexity, not canvas dimensions — but feature coverage differs between PDF, SVG and PostScript, and text fidelity depends on font embedding.
- Errors are often deferred. Cairo may return a surface object that carries a failure status rather than returning null, so status checks matter more than null checks.
Comparing the supported options
| Backend | Output | Raster or vector | Cost scales with | Reasonable fit | Watch out for |
|---|---|---|---|---|---|
| Image surface | Pixel buffer (PNG via cairo_surface_write_to_png) | Raster | Width × height | On-screen display, compositing, thumbnails | Fixed resolution; enlarging after render loses quality |
| PDF surface | PDF file | Vector | Content complexity | Print, documents, archival output | Font embedding and page setup decisions |
| SVG surface | SVG file | Vector | Content complexity | Web delivery, diagram export | Some operations may degrade or be approximated |
| PostScript surface | PS/EPS file | Vector | Content complexity | Legacy print pipelines | Narrower feature coverage than PDF |
| Recording surface | Stored drawing operations | Neither (deferred) | Operation count | Replaying one scene to several targets or sizes | CPU cost on every replay, plus indirection |
| Similar surface | Matches an existing target's type | Depends on source | Depends on source | Portable off-screen buffers and group compositing | Not a delivery format — it is a helper |
Trade-offs worth naming
Raster: predictable, resolution-fixed
An image surface is fast to composite and blit, and its behaviour is easy to reason about. The price is that resolution is baked in. If you render at logical size and then scale up, the result is blurry; if you render at a high device-pixel ratio to compensate, memory grows with the square of that ratio.
Vector: scalable, backend-dependent
PDF, SVG and PostScript keep strokes, fills and text resolution-independent. The catch is that a scene which renders correctly on an image surface may silently degrade or drop elements on SVG or PostScript. If your scene uses unusual compositing or pattern features, test the actual target backend rather than assuming parity.
Recording and similar: indirection with a payoff
A recording surface stores operations instead of pixels, which suits replaying one scene onto several targets or at several resolutions. A similar surface creates a surface matching the type and format of an existing target, which is the portable way to build off-screen buffers instead of hard-coding an image surface. The practical rule: do intermediate work on a similar or recording surface, then paint the result onto the delivery target chosen for the job.
A concrete draw-and-validate path
The program below draws one scene into a recording surface, then replays it onto an image surface, a similar off-screen buffer, a PDF surface and an SVG surface. Run it from a directory you can write to; no special privileges are required.
/* build: cc -o backend_check backend_check.c $(pkg-config --cflags --libs cairo) */
#include <cairo.h>
#include <cairo-pdf.h>
#include <cairo-svg.h>
#include <stdio.h>
#define W 800
#define H 600
static void draw_scene(cairo_t *cr)
{
cairo_set_source_rgb(cr, 0.1, 0.1, 0.1);
cairo_set_line_width(cr, 2.0);
cairo_rectangle(cr, 0.1 * W, 0.1 * H, 0.8 * W, 0.8 * H);
cairo_stroke(cr);
cairo_set_source_rgb(cr, 0.2, 0.4, 0.9);
cairo_arc(cr, 0.5 * W, 0.5 * H, 0.25 * H, 0.0, 2.0 * 3.14159265358979);
cairo_fill(cr);
cairo_set_source_rgb(cr, 0.0, 0.0, 0.0);
cairo_select_font_face(cr, "sans-serif",
CAIRO_FONT_SLANT_NORMAL,
CAIRO_FONT_WEIGHT_NORMAL);
cairo_set_font_size(cr, 0.08 * H);
cairo_move_to(cr, 0.15 * W, 0.9 * H);
cairo_show_text(cr, "cairo backend check");
}
static int report(const char *label, cairo_status_t st)
{
printf("%-22s %s\n", label, cairo_status_to_string(st));
return st == CAIRO_STATUS_SUCCESS ? 0 : 1;
}
int main(void)
{
int failures = 0;
/* Draw once into a recording surface. */
cairo_surface_t *rec =
cairo_recording_surface_create(CAIRO_CONTENT_COLOR_ALPHA, NULL);
failures += report("recording surface", cairo_surface_status(rec));
cairo_t *cr = cairo_create(rec);
draw_scene(cr);
failures += report("recording context", cairo_status(cr));
cairo_destroy(cr);
/* Target A: image surface, written to PNG. */
cairo_surface_t *img =
cairo_image_surface_create(CAIRO_FORMAT_ARGB32, W, H);
failures += report("image surface", cairo_surface_status(img));
cr = cairo_create(img);
cairo_set_source_surface(cr, rec, 0, 0);
cairo_paint(cr);
failures += report("image context", cairo_status(cr));
cairo_destroy(cr);
failures += report("png write",
cairo_surface_write_to_png(img, "out-image.png"));
/* Target B: off-screen buffer matching the image surface type. */
cairo_surface_t *buf = cairo_surface_create_similar(
img, CAIRO_CONTENT_COLOR_ALPHA, W, H);
failures += report("similar surface", cairo_surface_status(buf));
cr = cairo_create(buf);
cairo_set_source_surface(cr, rec, 0, 0);
cairo_paint(cr);
cairo_destroy(cr);
failures += report("buffer write",
cairo_surface_write_to_png(buf, "out-buffer.png"));
cairo_surface_destroy(buf);
cairo_surface_destroy(img);
/* Target C: PDF. */
cairo_surface_t *pdf = cairo_pdf_surface_create("out.pdf", W, H);
failures += report("pdf surface", cairo_surface_status(pdf));
cr = cairo_create(pdf);
cairo_set_source_surface(cr, rec, 0, 0);
cairo_paint(cr);
cairo_show_page(cr);
failures += report("pdf context", cairo_status(cr));
cairo_destroy(cr);
cairo_surface_finish(pdf);
failures += report("pdf finish", cairo_surface_status(pdf));
cairo_surface_destroy(pdf);
/* Target D: SVG. */
cairo_surface_t *svg = cairo_svg_surface_create("out.svg", W, H);
failures += report("svg surface", cairo_surface_status(svg));
cr = cairo_create(svg);
cairo_set_source_surface(cr, rec, 0, 0);
cairo_paint(cr);
cairo_show_page(cr);
failures += report("svg context", cairo_status(cr));
cairo_destroy(cr);
cairo_surface_finish(svg);
failures += report("svg finish", cairo_surface_status(svg));
cairo_surface_destroy(svg);
cairo_surface_destroy(rec);
printf("failures: %d\n", failures);
return failures == 0 ? 0 : 1;
}
Two details in that code are the point of the exercise. First, cairo_surface_finish() is called on the file-backed surfaces before checking status, because write errors surface at finish time rather than at creation. Second, the off-screen buffer is built with cairo_surface_create_similar() from a live target, so the drawing code never names a concrete backend.
Validation you can run
- Compile and run the program. Every status line should read
successand the final failure count should be zero. A non-success status means a backend failed to initialise or a write failed. - Open
out.svgorout.pdfand zoom in on the circle's edge. Vector output should stay sharp; compare againstout-image.png, which should pixelate at the same zoom. - Force a failure deliberately: pass an unwritable path such as
/nonexistent-dir/out.pdf, or create an image surface with a negative dimension. Confirm your code reports the problem instead of assuming success. - Compare file sizes for the identical scene across backends. Vector sizes should track content complexity, not canvas dimensions — enlarge the canvas and re-check.
Limitations and local verification
This guide describes the general shape of the decision, not a guarantee about your toolchain. Language bindings differ in naming and error-handling conventions, so the concepts transfer but exact call names do not. Vector backends vary in feature coverage, and a scene that renders correctly on an image surface may silently degrade on SVG or PostScript. Surface format and device-pixel-ratio handling also affect crispness.
Before committing to a backend, confirm which cairo release and binding your environment provides, and run the zoom and file-size checks above against a representative scene rather than a toy one. If your output must match a specific print or web pipeline, treat the backend choice as reviewable until that pipeline has accepted a sample.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.