Cairo PDF Surface: Vector Output, Trust Boundaries, and Failure Checks
Generate selectable vector PDFs with Cairo, check surface and context status at every stage, and switch libraries when encryption, forms, or complex text shaping are required.
28 Jan 2026, 10:11 UTC

Problem and takeaway
You need to emit a multi-page PDF from application data, and the text and shapes must remain selectable instead of being baked into a bitmap. Cairo's PDF surface backend can do that, but it produces a static, unencrypted vector stream and parses fonts and images inside your process. The practical design is therefore: bind one cairo_t to a PDF surface, draw per page, check status after every state-changing call, finish and destroy the surface, and treat any untrusted string or image as a potential memory-safety risk.
Requirements
- Vector output: text and vector shapes stay selectable in the PDF.
- Multiple pages: separate page content with
cairo_show_page. - Error detection: surface and context status must be checked before the file is considered complete.
- No forms, digital signatures, encryption, or incremental updates are required.
Smallest suitable design
The minimal call sequence is:
- Create the surface with
cairo_pdf_surface_create(path, width, height). - Create a context with
cairo_create(surface). - Draw page content, then call
cairo_show_page(cr)to end the page. - Repeat for additional pages.
- Call
cairo_surface_finish(surface)to close the PDF stream. - Destroy the context and surface.
Cairo 1.x is assumed; the PDF surface backend is stable in that series.
Example C program
Run this on a build host with libcairo development headers installed. The user needs write permission to the output directory. Placeholders: /tmp/output.pdf is the destination path; Hello PDF is the sample text.
#include <cairo.h>
#include <stdio.h>
#include <stdlib.h>
int main(void) {
const char *output = "/tmp/output.pdf";
cairo_surface_t *surface = cairo_pdf_surface_create(output, 595.0, 842.0);
if (cairo_surface_status(surface) != CAIRO_STATUS_SUCCESS) {
fprintf(stderr, "failed to create PDF surface\n");
cairo_surface_destroy(surface);
return 1;
}
cairo_t *cr = cairo_create(surface);
cairo_set_source_rgb(cr, 0, 0, 0);
cairo_rectangle(cr, 72, 72, 100, 100);
cairo_fill(cr);
cairo_move_to(cr, 72, 200);
cairo_show_text(cr, "Hello PDF");
if (cairo_status(cr) != CAIRO_STATUS_SUCCESS) {
fprintf(stderr, "drawing operation entered an error state\n");
}
cairo_show_page(cr);
cairo_destroy(cr);
cairo_surface_finish(surface);
cairo_status_t finish_status = cairo_surface_status(surface);
cairo_surface_destroy(surface);
if (finish_status != CAIRO_STATUS_SUCCESS) {
/* The operation changed state by creating a file; remove a partial result. */
remove(output);
return 1;
}
return 0;
}
Compile with:
cc -o pdf_example pdf_example.c $(pkg-config --cflags --libs cairo)
Expected checks after running:
ls -l /tmp/output.pdfshows a non-empty file.pdftotext /tmp/output.pdf -extractsHello PDF.qpdf --check /tmp/output.pdfreports no structural errors.
Risk: if cairo_surface_finish fails because the disk is full or the directory is not writable, a partial PDF may already exist. The example removes it before exiting, but only if the program reaches that point; a crash earlier can leave a truncated file.
Trust and data boundaries
Untrusted strings enter through text-showing operations, and untrusted pixels enter through image-source operations. Cairo parses fonts and decodes images inside the calling process, so a malformed font file or crafted image can affect host memory. Treat this as a process boundary: validate or sandbox untrusted input before handing it to Cairo, or isolate the renderer in a separate process with limited filesystem access.
Cairo's PDF backend does not encrypt, sign, or password-protect output. Confidentiality must be enforced at the filesystem or transport layer, for example by writing to a directory with restricted permissions and transmitting the file over TLS.
Operational checks
Cairo error status is sticky: once a surface or context enters an error state, later calls may be ignored until the status is inspected. Check:
cairo_surface_statusimmediately aftercairo_pdf_surface_create.cairo_statuson the context after drawing operations.cairo_surface_statusaftercairo_surface_finish.
All three must return CAIRO_STATUS_SUCCESS. A non-empty file alone is not sufficient evidence of a valid PDF.
Failure modes
| Condition | Behavior | Detection |
|---|---|---|
| Disk full or permission denied | Surface status reports an error; output may be truncated | cairo_surface_status after create and finish |
| Missing font | Substitution occurs without error; text may look wrong | Visual inspection or font-config audit before rendering |
| Unsupported transparency or blend mode | Silent fallback to a raster image for that region | Inspect page operators with qpdf or mutool; look for image operators instead of vector paths |
| Sticky context error | Subsequent drawing calls are ignored | cairo_status after each drawing group |
Conditions that change the design
Switch to a different library or add a pre-processing layer if you need:
- PDF forms, digital signatures, encryption, or incremental saving.
- Line wrapping, complex script shaping, or advanced typography; use Pango, Harfbuzz, or ICU to lay out text before passing positioned glyphs to Cairo.
- Strong isolation for untrusted fonts or images; run Cairo in a sandboxed subprocess and pass the finished PDF through a pipe or temporary file with restricted permissions.
Limitations and practical verification
This design produces only a static vector stream. It does not validate PDF structure beyond Cairo's own status codes, and it does not protect the output after it leaves the process. Verify the result by extracting text with pdftotext and inspecting page operators with qpdf or mutool to confirm that drawing commands are present rather than a single full-page bitmap.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.