Exyr.org Subscribe

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:

pycairo is dead, long live cairocffi!


  1. 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;
    }