A Cairo wrapper I created in 2018 to programmatically draw vector graphics, inspired by Gizeh. As its name indicates, its main use is to simplify Cairo's markup for paths, by using SVG-style single-letter commands and allowing method chaining. It also supports transformations, clips and gradients.
While working on a Cairopath function to parse SVG path strings, I came across an existing vector graphics library named CairoSVG. My script has since invoked CairoSVG's path module for string input, albeit in a rather hacky way; because CairoSVG's purpose is SVG file conversion, it isn't designed to support direct input of vector data or individual use of its component scripts. My unfinished fork of the project is intended to allow easier use of CairoSVG from a script, and to use its existing code to extend Cairopath's functionality to include other SVG features like text, cloning and groups. In the meantime, though, Cairopath continues to be used in some of my other projects.
Requirements:
Installation using Pip (includes all requirements except the Cairo DLL):
pip install git+https://github.com/SilverCardioid/cairopath.git
The module's main class, and the only that needs to be instantiated directly, is Canvas
. It corresponds to a Cairo Surface
and Context
(which are accessible as the object properties canvas.surface
and canvas.context
respectively).
In the constructor, the surfacetype
parameter specifies one of the five Cairo surface types ('Image'
, 'SVG'
, 'PDF'
, 'PS'
or 'Recording'
), and filename
the destination filename. Both are optional, as a canvas's .export()
method can be used to save the canvas in any of the corresponding file types, and will convert the surface type if necessary. Wrappers for exporting to specific formats also exist: .pdf()
, .png()
, .ps()
and .svg()
. The .data()
method returns the image as a standard pixel array.
canvas = cairopath.Canvas(600, 600, bgcolor='#fff')
canvas.png('file.png') # or: canvas.export('Image', 'file.png')
The shape drawing methods of a canvas are .circle()
, .ellipse()
, .rect()
and .path()
. The latter, used for general polylines and curves, can optionally take a data string corresponding to the d=""
SVG attribute. It returns a Path
object, which provides the same commands a data string does, but in function form. The following two lines are thus equivalent ways of drawing a rhombus:
canvas.path('M0,-100 L 100,0 L 0,100 L -100,0z')
canvas.path().M(0, -100).L(100, 0).L(0, 100).L(-100, 0).z()
As in Cairo, calling a shape function adds the shape to the "current path", which is kept in memory and only made visible when it is used by a function like .fill()
, .stroke()
or .clip()
. Calling one of these functions empties the current path buffer, unless the parameter keep=True
is set (which corresponds to the _preserve
versions of these functions in Cairo). For example, to draw a circle with both a fill and a stroke, use canvas.circle(10).fill('#ff0', keep=True).stroke('#000')
.
Colours may be one of several data types: a list or tuple of RGB values (in the range 0-1 for floats or 0-255 for ints), a hex colour code as a string (e.g. '#ffffff'
, '#fff'
) or integer (0xffffff
), or a Gradient
object (created using .lineargradient()
or .radialgradient()
).
Transform methods include .translate()
, .scale()
, .rotate()
and .matrix()
, which mostly work the same way as the corresponding SVG functions. .clip()
applies the current path as a clip path.
Both transforms and clips only apply to objects drawn after the call to the transform or clip method, and can be undone by calling .resettransform()
or .resetclip()
. The returned Transform
object can also be used as a context manager, in order to apply transformations to a block of code and reset them automatically afterwards (as though it's an SVG <g>
element with a transform
or clip-path
attribute):
with canvas.scale(2):
# objects drawn here will be scaled
with canvas.circle(10).clip():
# objects drawn here will be clipped to a circle
which is equivalent to
canvas.scale(2)
# objects drawn here will be scaled
canvas.resettransform()
canvas.circle(10).clip()
# objects drawn here will be clipped to a circle
canvas.resetclip()
A circled five-pointed star:
import math
import cairopath
canvas = cairopath.Canvas(600, 600, bgcolor='#fff')
with canvas.translate(300, 300):
canvas.circle(120).fill(0)
path = canvas.path()
path.M(0, -100)
for i in range(1, 5):
path.L(100*math.sin(i*4*math.pi/5), -100*math.cos(i*4*math.pi/5))
path.z()
path.fill(0xffcc00)
canvas.png('star.png')
The given arguments are the default values. Arrows indicate the return type if the method returns a new object, or the variable name if it returns an existing object for chaining (note the case difference).
canvas = cairopath.Canvas(width, height, bgcolor=None, bgopacity=1, surfacetype='Image', filename=None)
canvas.clone(type='Image')
→Canvas
Create a new canvas of any type with the same contents.canvas.data(alpha=False)
→numpy.ndarray
Convert the canvas to an RGB(A) pixel array, of shape ''height''×''width''×3 ifalpha=False
or ''height''×''width''×4 ifalpha=True
.canvas.export(type='Image', filename=None)
Save the canvas in a file format corresponding to a surface type.canvas.pdf(filename)
Export to PDF filecanvas.png(filename)
Export to PNG filecanvas.ps(filename)
Export to PostScript filecanvas.svg(filename)
Export to SVG file
Shapes & colours:
canvas.path(d=None)
→Path
canvas.circle(r, cx=0, cy=0)
→canvas
canvas.ellipse(rx, ry, cx=0, cy=0)
→canvas
canvas.rect(width, height, x=0, y=0, center=False)
→canvas
x
andy
are the rectangle's centre ifcenter=True
, or its top left vertex otherwise.canvas.fill(color, opacity=1, evenodd=0, keep=False, affect=True)
→canvas
Draw the current path filled in.keep=True
preserves the current path for further drawing;affect
toggles whether transformations affect gradients.canvas.stroke(color, opacity=1, width=2, cap='butt', join='miter', miterlimit=10, dash=None, dashoffset=0, keep=False, affect=True)
→canvas
Draw the current path as an outline.canvas.lineargradient(x1=0, y1=0, x2=None, y2=None)
→Gradient
canvas.radialgradient(r1=0, x1=0, y1=0, r2=100, x2=None, y2=None)
→Gradient
Transforms (the Transform
object has identical methods, which return their parent Transform
object):
canvas.translate(tx, ty=0)
→Transform
canvas.scale(sx, sy=None)
→Transform
Omitsy
for uniform scaling.canvas.rotate(a, cx=0, cy=0, rad=False)
→Transform
Angles are in radians ifrad=True
, or in degrees otherwise.canvas.matrix(m, replace=False)
→Transform
Apply a transformation matrix of the form[xx, yx, xy, yy, x0, y0]
. Ifreplace=True
, replace the current transformation matrix, undoing any previous transformations.canvas.clip(keep=False)
→Transform
Set current path as clip pathcanvas.resettransform()
→Transform
canvas.resetclip()
→Transform
path = canvas.path(d=None)
Like in SVG, uppercase methods use absolute coordinates, and lowercase ones use coordinates relative to the previous vertex.
path.d(string)
→path
(parse data string)path.M(x, y)
→path
(start path)path.m(dx, dy)
→path
(start path)path.z()
→path
(close path)
Lines:
path.L(x, y)
→path
(line)path.l(dx, dy)
→path
(line)path.H(x)
→path
(horizontal line)path.h(dx)
→path
(horizontal line)path.V(y)
→path
(vertical line)path.v(dy)
→path
(vertical line)
Beziers:
path.C(x1, y1, x2, y2, x, y)
→path
(cubic)path.c(dx1, dy1, dx2, dy2, dx, dy)
→path
(cubic)path.S(x2, y2, x, y)
→path
(smooth cubic)path.s(dx2, dy2, dx, dy)
→path
(smooth cubic)path.Q(x1, y1, x, y)
→path
(quadratic)path.q(dx1, dy1, dx, dy)
→path
(quadratic)path.T(x, y)
→path
(smooth quadratic)path.t(dx, dy)
→path
(smooth quadratic)
Arcs:
path.A(r, x, y, large=1, sweep=1)
→path
(circular)path.a(r, dx, dy, large=1, sweep=1)
→path
(circular)path.Ae(rx, ry, x, y, large=1, sweep=1, angle=0, rad=False)
→path
(elliptical)path.ae(rx, ry, dx, dy, large=1, sweep=1, angle=0, rad=False)
→path
(elliptical)path.Ac(xc, yc, r, a1, a2, sweep=1, rad=False)
→path
(circular, Cairo syntax)
Supports chaining with fill
or stroke
(returning the path
) or with clip
(returning a Transform
).
grad = canvas.lineargradient(x1=0, y1=0, x2=None, y2=None)
grad = canvas.radialgradient(r1=0, x1=0, y1=0, r2=100, x2=None, y2=None)
grad.stop(offset, color, opacity=1)
→grad
Add a colour stop at an offset along the gradient.grad.fill(opacity=1, evenodd=0, keep=False, affect=True)
→canvas
Fill the current path with this gradient.grad.stroke(opacity=1, width=2, cap=0, join=0, miterlimit=10, dash=None, dashoffset=0, keep=False, affect=True)
→canvas
Outline the current path with this gradient.