cairocffi: CFFI-based Python bindings for cairo
Simon Sapin,
Note: cairocffi is kinda old news, but I was asked recently about it. This is the anwser, made public.
Cairo is a 2D vector graphics library with support for multiple backends including image buffers, PNG, PostScript, PDF, and SVG file output.
pycairo is a set of Python bindings for cairo that has been around for a long time. Unfortunately, it also seems abandonned. I’ve sent a couple of patches more than a year ago and haven’t heard since.
I’ve considered taking over maintainership of pycairo or forking it but to be honest, working on it is kind of a pain. pycairo is a CPython extension written in C, which means it has to manually increment and decrement reference counts of Python objects. Failure to do so correctly means leaking memory or crashing with a segmentation fault. Even with reference counting aside, every little thing is tedious when interacting with CPython from C code.
Now, the only reason pycairo is written in C is to be able to call functions from cairo, a C library. Enter CFFI, a Python library for calling C functions from Python code.
Writing a new set of bindings using CFFI seemed way easier1
than maintining pycairo and fixing a bunch of its issues.
Thus, cairocffi was born.
It implements the same Python API as pycairo
and so is a “drop-in” replacement.
For example, CairoSVG can use either one,
without code change other than import
statements.
CFFI’s dlopen()
method allows loading shared libraries dynamically.
Users can therefore get a pre-compiled cairo from somewhere and use cairocffi from source,
without a working C compiler being required (which is a pain on Windows).
And I don’t need to maintain binaries for various plateforms either.
From the users’ point of view:
- cairocffi uses standard Python packaging tools, and thus can easily be installed in a virtualenv. Doing so with pycairo requires some tricks.
- The same code base runs on Python 2.x and 3.x (whereas py2cairo is separate from pycairo).
- It runs on PyPy (and anywhere CFFI does).
- It has bindings for some cairo features that were added after the last pycairo release. Just tell me if you need more.
pycairo is dead, long live cairocffi!
-
Compare cairocffi’s
context.py
:def set_dash(self, dashes, offset=0): """ ... (32 lines of docstring) """ cairo.cairo_set_dash( self._pointer, ffi.new('double[]', dashes), len(dashes), offset) self._check_status()
… to pycairo’s
context.c
:static PyObject * pycairo_set_dash (PycairoContext *o, PyObject *args) { double *dashes, offset = 0; int num_dashes, i; PyObject *py_dashes; if (!PyArg_ParseTuple (args, "O|d:Context.set_dash", &py_dashes, &offset)) return NULL; py_dashes = PySequence_Fast (py_dashes, "first argument must be a sequence"); if (py_dashes == NULL) return NULL; num_dashes = PySequence_Fast_GET_SIZE(py_dashes); dashes = PyMem_Malloc (num_dashes * sizeof(double)); if (dashes == NULL) { Py_DECREF(py_dashes); return PyErr_NoMemory(); } for (i = 0; i < num_dashes; i++) { dashes[i] = PyFloat_AsDouble(PySequence_Fast_GET_ITEM(py_dashes, i)); if (PyErr_Occurred()) { PyMem_Free (dashes); Py_DECREF(py_dashes); return NULL; } } cairo_set_dash (o->ctx, dashes, num_dashes, offset); PyMem_Free (dashes); Py_DECREF(py_dashes); RETURN_NULL_IF_CAIRO_CONTEXT_ERROR(o->ctx); Py_RETURN_NONE; }