A (new) cairo backend for Matplotlib
- Test suite
- Known issues
- Possible optimizations
- What about the already existing cairo (gtk3/qt4/qt5/wx/tk/...cairo) backends?
This is a new, essentially complete implementation of a cairo backend for Matplotlib. It can be used in combination with a Qt5, GTK3, Tk, wx, or macOS UI, or non-interactively (i.e., to save figure to various file formats).
Noteworthy points include:
- Improved accuracy (e.g., with marker positioning, quad meshes, and text kerning).
- Support for a wider variety of font formats, such as otf and pfb, for vector (PDF, PS, SVG) backends (Matplotlib's Agg backend also supports such fonts).
- Optional support for complex text layout (right-to-left languages, etc.) using Raqm. Note that Raqm depends on Fribidi, which is licensed under the LGPLv2.1+.
- Support for embedding URLs in PDF (but not SVG) output (requires cairo≥1.15.4).
- Support for multi-page output both for PDF and PS (Matplotlib only supports multi-page PDF).
- Support for custom blend modes (see examples/operators.py).
- Python 3 (3.6 on Windows),
- Matplotlib≥2.2 (declared as
- pybind11≥2.2.4  (declared as
- on Linux and OSX, pycairo≥1.16.0  (declared as conditional
- on Windows, cairo≥1.11.4  (shipped with the wheel).
As usual, install using pip:
$ pip install mplcairo # from PyPI $ pip install git+https://github.com/anntzer/mplcairo # from Github
Note that wheels are not available for OSX, because no OSX version ships a recent-enough libc++ by default and vendoring of libc++ appears to be fragile. Help for packaging would be welcome.
mplcairo can use Raqm (≥0.2) for complex text layout if it is available. Refer to the instructions on that project's website for installation on Linux and OSX. You may want to look at https://github.com/HOST-Oman/libraqm-cmake for Windows build scripts.
|||pybind11 is actually only a build-time requirement, but doesn't play
well with |
pycairo 1.16.0 added
We do not actually rely on pycairo's Python bindings. Rather, specifying a dependency on pycairo is a convenient way to specify a dependency on cairo (≥1.13.1, for pycairo≥1.14.0) itself, and allows us to load cairo at runtime instead of linking to it (simplifying the build of self-contained wheels).
On Windows, this strategy is (AFAIK) not possible, so we explicitly link against the cairo DLL.
cairo 1.11.4 added mesh gradient support (used by
(cairo 1.15.4 added support for PDF metadata and links; the presence of this feature is detected at runtime.)
On Fedora, the package is available as python-mplcairo.
This section is only relevant if you wish to build mplcairo yourself. Otherwise, proceed to the Use section.
In all cases, once the dependencies described below are installed, mplcairo
can be built and installed using any of the standard commands (
pip install .,
pip install -e . and
build_ext -i being the most relevant ones).
The following additional dependencies are required:
a C++ compiler with C++17 support, e.g. GCC≥7.2 or Clang≥5.0.
cairo and FreeType headers, and pkg-config information to locate them.
If using conda, they can be installed using
conda install -y -c conda-forge pycairo pkg-config
as pycairo (also a dependency) depends on cairo, which depends on freetype. Note that cairo and pkg-config from the
anacondachannel will not work.
On Linux, they can also be installed with your distribution's package manager (Arch:
Raqm (≥0.2) headers are also needed, but will be automatically downloaded if not found.
conda's compilers (
gxx_linux-64 on the
anaconda channel) currently
interact poorly with installing cairo and pkg-config from conda-forge, so you are on your own to install a recent compiler
(e.g., using your distribution's package manager). You may want to set the
CXX environment variables to point to your C++ compiler if it is
nonstandard . In that case, be careful to set them to e.g.
gcc-7, otherwise the compilation will succeed but the shared object
will be mis-linked and fail to load.
The manylinux wheel is built using
NOTE: On Arch Linux, the python-pillow (Arch) package includes an invalid
raqm.h (https://bugs.archlinux.org/task/57492) and must not be
installed while building a Raqm-enabled version of mplcairo using the system
Python, even in a virtualenv (it can be installed when using mplcairo without
causing any problems). One solution is to temporarily uninstall the package;
another one is to package it yourself using e.g. pypi2pkgbuild.
Clang≥5.0 can be installed from
anaconda channel (
install -c anaconda clangxx_osx-64), or can also be installed with Homebrew
brew install llvm). Note that Homebrew's llvm formula is keg-only, i.e.
it requires manual modifications to the PATH and LDFLAGS (as documented by
brew info llvm).
The OSX wheel is built using
tools/build-osx-wheel.sh, which relies on
delocate-wheel (to vendor a recent version of libc++). Currently, it can only
be built from a Homebrew-clang wheel, not a conda-clang wheel (due to some path
The following additional dependencies are required:
MSVC≥19.14, which corresponds to VS2017≥15.7. (This is the reason for restricting support to Python 3.6 on Windows: distutils is able to use such a recent MSVC only since Python 3.6.4.)
cairo headers and import and dynamic libraries (
cairo.dll) with FreeType support. Note that this excludes, in particular, the Anaconda and conda-forge builds: they do not include FreeType support.
I am in fact not aware of any such build available online, with the exception of https://github.com/preshing/cairo-windows/releases; however, this specific build appears to misrender pdfs. Instead, a solution is to get the headers e.g. from a Linux distribution package, the DLL from Christoph Gohlke's cairocffi build, and generate the import library oneself using
FreeType headers and import and dynamic libraries (
freetype.dll), which can be retrieved from https://github.com/ubawurinna/freetype-windows-binaries, or alternatively using conda:
conda install -y freetype
set CL=/IC:\path\to\dir\containing\cairo.h /IC:\same\for\ft2build.h set LINK=/LIBPATH:C\path\to\dir\containing\cairo.lib /LIBPATH:C\same\for\freetype.lib
Moreover, we also need to find
freetype.dll and copy
them next to
mplcairo's extension module. As the dynamic libraries are
typically found next to import libraries, we search the
LINK environment variable and copy the first
freetype.dll found there.
tools/build-windows-wheel.py automates the retrieval of the
cairo (assuming that a Gohlke cairocffi is already installed) and FreeType and
the wheel build.
On Linux and Windows, mplcairo can be used as any normal Matplotlib backend:
matplotlib.use("module://mplcairo.qt") before importing pyplot,
backend: module://mplcairo.qt line in your
matplotlibrc, or set
MPLBACKEND environment variable to
specifically, the following backends are provided:
module://mplcairo.base(No GUI, but can output to EPS, PDF, PS, SVG, and SVGZ using cairo's implementation, rather than Matplotlib's),
module://mplcairo.gtk(GTK3 widget, copying data from a cairo image surface),
module://mplcairo.gtk_native(GTK3 widget, directly drawn onto as a native surface; does not and cannot support blitting),
module://mplcairo.qt(Qt4/5 widget, copying data from a cairo image surface -- select the binding to use by importing it before mplcairo, or by setting the
module://mplcairo.tk(Tk widget, copying data from a cairo image surface),
module://mplcairo.wx(wx widget, copying data from a cairo image surface),
module://mplcairo.macosx(macOS widget, copying data from a cairo image surface).
On OSX, it is necessary to explicitly import mplcairo before importing
Matplotlib due to incompatibilities associated with the use of a recent
libc++. As such, the most practical option is to import mplcairo, then call
Alternatively, set the
MPLCAIRO_PATCH_AGG environment variable to a
non-empty value to fully replace the Agg renderer by the cairo renderer
throughout Matplotlib. However, this approach is inefficient (due to the need
of copies and conversions between premultiplied ARGB32 and straight RGBA8888
buffers); additionally, it does not work with the wx and macosx backends due
to peculiarities of the corresponding canvas classes. On the other hand, this
is currently the only way in which the webagg-based backends (e.g., Jupyter's
inline widget) are supported.
At import-time, mplcairo will attempt to load Raqm. The use of that library
can be controlled and checked using the
The examples directory contains a few cases where the output of this renderer is arguably more accurate than the one of the default renderer, Agg:
- circle_markers.py and square_markers.py: more accurate and faster marker stamping.
- markevery.py: more accurate marker stamping.
- quadmesh.py: better antialiasing of quad meshes, fewer artefacts with masked data.
- text_kerning.py: improved text kerning.
Install (in the virtualenv)
pytest --benchmark-group-by=fullfunc --benchmark-timer=time.process_time
Keep in mind that conda-forge's cairo is (on my setup) ~2× slower than a "native" build of cairo.
run-mpl-test-suite.py (which depends on
pytest>=3.2.2) to run the
Matplotlib test suite with the Agg backend patched by the mplcairo backend.
Note that Matplotlib must be installed with its test data, which is not the
case when it is installed from conda or from most Linux distributions; instead,
it should be installed from PyPI or from source.
Nearly all image comparison tests "fail" as the renderers are fundamentally
different; currently, the intent is to manually check the diff images. Passing
--tolerance=inf marks these tests as "passed" (while still textually
reporting the image differences) so that one can spot issues not related to
rendering differences. In practice,
--tolerance=50 appears to be enough.
Some other (non-image-comparison) tests are also known to fail (they are listed
ISSUES.rst, with the relevant explanations), and automatically skipped.
The artist antialiasing property can be set to any of the
enum values, or
True (the default) or
False (which is synonym to
Setting antialiasing to
FAST antialiasing for lines thicker
than 1/3px and
BEST for lines thinner than that: for lines thinner
than 1/3px, the former leads to artefacts such as lines disappearing in
certain sections (see e.g.
forcing the antialiasing to
FAST). The threshold of 1/3px was determined
empirically, see examples/thin_line_antialiasing.py.
Note that in order to set the
rcparams to a
cairo_antialias_t enum value, it is necessary to bypass
rcparam validation, using, e.g.
dict.__setitem__(plt.rcParams, "lines.antialiased", antialias_t.FAST)
text.antialiased rcparam can likewise be set to any
cairo_antialias_t enum value, or
True (the default, which maps to
GRAY is not sufficient to benefit from Raqm's subpixel
positioning; see also cairo bug #152) or
False (which maps
For fast drawing of path with many segments, the
should be set to 1000 (see examples/time_drawing_per_element.py for the
determination of this value); this causes longer paths to be split into
individually rendered sections of 1000 segments each (directly rendering longer
paths appears to have slightly superlinear complexity).
path.simplify_threshold rcparam is used to control the accuracy of
marker stamping, down to an arbitrarily chosen threshold of 1/16px. If the
threshold is set to a lower value, the exact (slower) marker drawing path will
be used. Marker stamping is also implemented for scatter plots (which can have
multiple colors). Likewise, markers of different sizes get mapped into markers
of discretized sizes, with an error bounded by the threshold.
pcolor and mplot3d's
plot_surface display some artifacts
where the facets join each other. This is because these functions internally
PathCollection, thus triggering the approximate stamping.
pcolormesh (which internally uses a
QuadMesh) should generally be
plot_surface should likewise instead
represent the surface using
QuadMesh, which is drawn without such
In order to use a specific font that Matplotlib may be unable to use, pass a filename directly:
from matplotlib.font_manager import FontProperties ax.text(.5, .5, "hello, world", fontproperties=FontProperties(fname="..."))
mplcairo still relies on Matplotlib's font cache, so fonts unsupported by Matplotlib remain unavailable by other means.
For ttc fonts (and, more generally, font formats that include multiple font
faces in a single file), the nth font (n≥0) can be selected by appending
#n to the filename (e.g.,
Note that Matplotlib's (default) Agg backend will handle most (single-face) fonts equally well (ultimately, both backends relies on FreeType for rasterization). It is Matplotlib's vector backends (PS, PDF, and, for pfb fonts, SVG) that do not support these fonts, whereas mplcairo support these fonts in all output formats.
PdfPages class is deeply tied with the builtin
(in fact, it cannot even be used with Matplotlib's own cairo backend).
mplcairo.multipage.MultiPage for multi-page PDF and PS output.
The API is similar:
from mplcairo.multipage import MultiPage fig1 = ... fig2 = ... with MultiPage(path_or_stream, metadata=...) as mp: mp.savefig(fig1) mp.savefig(fig2)
See the class' docstring for additional information.
MPLCAIRO_SCRIPT_SURFACE environment variable before mplcairo
is imported to
raster allows one to save figures (with
savefig) in the
.cairoscript format, which is a "native script that
matches the cairo drawing model". The value of the variable determines the
rendering path used (e.g., whether marker stamping is used at all). This may
be helpful for troubleshooting purposes.
Note that this may crash the process after the file is written, due to cairo bug #277.
draw_markers draws a marker at each control point of the given path, which
is the documented behavior, even though all builtin renderers only draw markers
at straight or Bézier segment ends.
Support for the following features is missing:
- SVG output does not set URLs on any element, as cairo provides no support for doing so.
- PS output does not respect SOURCE_DATE_EPOCH.
- The following rcparams have no effect:
- Cache eviction policy and persistent cache for
- Path simplification (although cairo appears to use vertex reduction and Douglas-Peucker internally?).
- mathtext should probably hold onto a vector of
FT_Glyphs instead of reloading a
FT_Facefor each glyph, but that'll likely wait for the ft2 rewrite in Matplotlib itself.
- Use QtOpenGLWidget and the cairo-gl backend.
hexbincurrently falls back on the slow implementation due to its use of the
offset_positionparameter. This should be fixed on Matplotlib's side.
They are very slow (try running examples/mplot3d/wire3d_animation.py) and
render math poorly (try