1# drawsvg
2
3A Python 3 library for programmatically generating SVG images (vector drawings) and rendering them or displaying them in a Jupyter notebook.
4
5Most common SVG tags are supported and others can easily be added by writing a small subclass of `DrawableBasicElement` or `DrawableParentElement`.
6
7An interactive [Jupyter notebook](https://jupyter.org) widget, `drawsvg.widgets.DrawingWidget`, is included that can update drawings based on mouse events.
8
9# Install
10
11drawsvg is available on PyPI:
12
13```bash
14$ pip3 install "drawsvg[all]"
15```
16
17## Prerequisites
18
19Cairo needs to be installed separately. When Cairo is installed, drawsvg can output PNG or other image formats in addition to SVG. See platform-specific [instructions for Linux, Windows, and macOS from Cairo](https://www.cairographics.org/download/). Below are some examples for installing Cairo on Linux distributions and macOS.
20
21**Ubuntu**
22
23```bash
24$ sudo apt-get install libcairo2
25```
26
27**macOS**
28
29Using [homebrew](https://brew.sh/):
30
31```bash
32$ brew install cairo
33```
34
35# Examples
36
37### Basic drawing elements
38```python
39import drawsvg as draw
40
41d = draw.Drawing(200, 100, origin='center')
42
43# Draw an irregular polygon
44d.append(draw.Lines(-80, 45,
45 70, 49,
46 95, -49,
47 -90, -40,
48 close=False,
49 fill='#eeee00',
50 stroke='black'))
51
52# Draw a rectangle
53r = draw.Rectangle(-80, -50, 40, 50, fill='#1248ff')
54r.append_title("Our first rectangle") # Add a tooltip
55d.append(r)
56
57# Draw a circle
58d.append(draw.Circle(-40, 10, 30,
59 fill='red', stroke_width=2, stroke='black'))
60
61# Draw an arbitrary path (a triangle in this case)
62p = draw.Path(stroke_width=2, stroke='lime', fill='black', fill_opacity=0.2)
63p.M(-10, -20) # Start path at point (-10, -20)
64p.C(30, 10, 30, -50, 70, -20) # Draw a curve to (70, -20)
65d.append(p)
66
67# Draw text
68d.append(draw.Text('Basic text', 8, -10, -35, fill='blue')) # 8pt text at (-10, -35)
69d.append(draw.Text('Path text', 8, path=p, text_anchor='start', line_height=1))
70d.append(draw.Text(['Multi-line', 'text'], 8, path=p, text_anchor='end', center=True))
71
72# Draw multiple circular arcs
73d.append(draw.ArcLine(60, 20, 20, 60, 270,
74 stroke='red', stroke_width=5, fill='red', fill_opacity=0.2))
75d.append(draw.Arc(60, 20, 20, 60, 270, cw=False,
76 stroke='green', stroke_width=3, fill='none'))
77d.append(draw.Arc(60, 20, 20, 270, 60, cw=True,
78 stroke='blue', stroke_width=1, fill='black', fill_opacity=0.3))
79
80# Draw arrows
81arrow = draw.Marker(-0.1, -0.51, 0.9, 0.5, scale=4, orient='auto')
82arrow.append(draw.Lines(-0.1, 0.5, -0.1, -0.5, 0.9, 0, fill='red', close=True))
83p = draw.Path(stroke='red', stroke_width=2, fill='none',
84 marker_end=arrow) # Add an arrow to the end of a path
85p.M(20, 40).L(20, 27).L(0, 20) # Chain multiple path commands
86d.append(p)
87d.append(draw.Line(30, 20, 0, 10,
88 stroke='red', stroke_width=2, fill='none',
89 marker_end=arrow)) # Add an arrow to the end of a line
90
91d.set_pixel_scale(2) # Set number of pixels per geometry unit
92#d.set_render_size(400, 200) # Alternative to set_pixel_scale
93d.save_svg('example.svg')
94d.save_png('example.png')
95
96# Display in Jupyter notebook
97d.rasterize() # Display as PNG
98d # Display as SVG
99```
100
101[](https://github.com/cduck/drawsvg/blob/master/examples/example1.svg)
102
103### Gradients
104```python
105import drawsvg as draw
106
107d = draw.Drawing(1.5, 0.8, origin='center')
108
109d.draw(draw.Rectangle(-0.75, -0.5, 1.5, 1, fill='#ddd'))
110
111# Create gradient
112gradient = draw.RadialGradient(0, 0.35, 0.7*10)
113gradient.add_stop(0.5/0.7/10, 'green', 1)
114gradient.add_stop(1/10, 'red', 0)
115
116# Draw a shape to fill with the gradient
117p = draw.Path(fill=gradient, stroke='black', stroke_width=0.002)
118p.arc(0, 0.35, 0.7, 30, 120)
119p.arc(0, 0.35, 0.5, 120, 30, cw=True, include_l=True)
120p.Z()
121d.append(p)
122
123# Draw another shape to fill with the same gradient
124p = draw.Path(fill=gradient, stroke='red', stroke_width=0.002)
125p.arc(0, 0.35, 0.75, 130, 160)
126p.arc(0, 0.35, 0, 160, 130, cw=True, include_l=True)
127p.Z()
128d.append(p)
129
130# Another gradient
131gradient2 = draw.LinearGradient(0.1, 0.35, 0.1+0.6, 0.35+0.2)
132gradient2.add_stop(0, 'green', 1)
133gradient2.add_stop(1, 'red', 0)
134d.append(draw.Rectangle(0.1, 0.15, 0.6, 0.2,
135 stroke='black', stroke_width=0.002,
136 fill=gradient2))
137
138# Display
139d.set_render_size(w=600)
140d
141```
142
143[](https://github.com/cduck/drawsvg/blob/master/examples/example2.svg)
144
145### Duplicate geometry and clip paths
146```python
147import drawsvg as draw
148
149d = draw.Drawing(1.4, 1.4, origin='center')
150
151# Define clip path
152clip = draw.ClipPath()
153clip.append(draw.Rectangle(-.25, -.25, 1, 1))
154
155# Draw a cropped circle
156circle = draw.Circle(0, 0, 0.5,
157 stroke_width='0.01', stroke='black',
158 fill_opacity=0.3, clip_path=clip)
159d.append(circle)
160
161# Make a transparent copy, cropped again
162g = draw.Group(opacity=0.5, clip_path=clip)
163g.append(draw.Use(circle, 0.25, -0.1))
164d.append(g)
165
166# Display
167d.set_render_size(400)
168d.rasterize()
169```
170
171[](https://github.com/cduck/drawsvg/blob/master/examples/example3.svg)
172
173### Implementing other SVG tags
174```python
175import drawsvg as draw
176
177# Subclass DrawingBasicElement if it cannot have child nodes
178# Subclass DrawingParentElement otherwise
179# Subclass DrawingDef if it must go between <def></def> tags in an SVG
180class Hyperlink(draw.DrawingParentElement):
181 TAG_NAME = 'a'
182 def __init__(self, href, target=None, **kwargs):
183 # Other init logic...
184 # Keyword arguments to super().__init__() correspond to SVG node
185 # arguments: stroke_width=5 -> <a stroke-width="5" ...>...</a>
186 super().__init__(href=href, target=target, **kwargs)
187
188d = draw.Drawing(1, 1.2, origin='center')
189
190# Create hyperlink
191hlink = Hyperlink('https://www.python.org', target='_blank',
192 transform='skewY(-30)')
193# Add child elements
194hlink.append(draw.Circle(0, 0, 0.5, fill='green'))
195hlink.append(draw.Text('Hyperlink', 0.2, 0, 0, center=0.6, fill='white'))
196
197# Draw and display
198d.append(hlink)
199d.set_render_size(200)
200d
201```
202
203[](https://github.com/cduck/drawsvg/blob/master/examples/example4.svg)
204
205### Animation with the SVG Animate Tag
206```python
207import drawsvg as draw
208
209d = draw.Drawing(200, 200, origin='center')
210
211# Animate the position and color of circle
212c = draw.Circle(0, 0, 20, fill='red')
213# See for supported attributes:
214# https://developer.mozilla.org/en-US/docs/Web/SVG/Element/animate
215c.append_anim(draw.Animate('cy', '6s', '-80;80;-80',
216 repeatCount='indefinite'))
217c.append_anim(draw.Animate('cx', '6s', '0;80;0;-80;0',
218 repeatCount='indefinite'))
219c.append_anim(draw.Animate('fill', '6s', 'red;green;blue;yellow',
220 calc_mode='discrete',
221 repeatCount='indefinite'))
222d.append(c)
223
224# Animate a black circle around an ellipse
225ellipse = draw.Path()
226ellipse.M(-90, 0)
227ellipse.A(90, 40, 360, True, True, 90, 0) # Ellipse path
228ellipse.A(90, 40, 360, True, True, -90, 0)
229ellipse.Z()
230c2 = draw.Circle(0, 0, 10)
231# See for supported attributes:
232# https://developer.mozilla.org/en-US/docs/Web/SVG/Element/animate_motion
233c2.append_anim(draw.AnimateMotion(ellipse, '3s',
234 repeatCount='indefinite'))
235# See for supported attributes:
236# https://developer.mozilla.org/en-US/docs/Web/SVG/Element/animate_transform
237c2.append_anim(draw.AnimateTransform('scale', '3s', '1,2;2,1;1,2;2,1;1,2',
238 repeatCount='indefinite'))
239d.append(c2)
240
241d.save_svg('animated.svg') # Save to file
242d # Display in Jupyter notebook
243```
244
245[](https://github.com/cduck/drawsvg/blob/master/examples/animated.svg)
246
247### Interactive Widget
248```python
249import drawsvg as draw
250from drawsvg.widgets import DrawingWidget
251import hyperbolic.poincare.shapes as hyper # pip3 install hyperbolic
252from hyperbolic import euclid
253
254# Patch the hyperbolic package for drawsvg version 2
255patch = lambda m: lambda self, **kw: m(self, draw, **kw)
256hyper.Circle.to_drawables = patch(hyper.Circle.toDrawables)
257hyper.Line.to_drawables = patch(hyper.Line.toDrawables)
258euclid.Arc.Arc.drawToPath = lambda self, path, includeM=True, includeL=False: path.arc(self.cx, self.cy, self.r, self.startDeg, self.endDeg, cw=not self.cw, include_m=includeM, include_l=includeL)
259
260# Create drawing
261d = draw.Drawing(2, 2, origin='center', context=draw.Context(invert_y=True))
262d.set_render_size(500)
263d.append(draw.Circle(0, 0, 1, fill='orange'))
264group = draw.Group()
265d.append(group)
266
267# Update the drawing based on user input
268click_list = []
269def redraw(points):
270 group.children.clear()
271 for x1, y1 in points:
272 for x2, y2 in points:
273 if (x1, y1) == (x2, y2): continue
274 p1 = hyper.Point.fromEuclid(x1, y1)
275 p2 = hyper.Point.fromEuclid(x2, y2)
276 if p1.distanceTo(p2) <= 2:
277 line = hyper.Line.fromPoints(*p1, *p2, segment=True)
278 group.draw(line, hwidth=0.2, fill='white')
279 for x, y in points:
280 p = hyper.Point.fromEuclid(x, y)
281 group.draw(hyper.Circle.fromCenterRadius(p, 0.1),
282 fill='green')
283redraw(click_list)
284
285# Create interactive widget and register mouse events
286widget = DrawingWidget(d)
287@widget.mousedown
288def mousedown(widget, x, y, info):
289 if (x**2 + y**2) ** 0.5 + 1e-5 < 1:
290 click_list.append((x, y))
291 redraw(click_list)
292 widget.refresh()
293@widget.mousemove
294def mousemove(widget, x, y, info):
295 if (x**2 + y**2) ** 0.5 + 1e-5 < 1:
296 redraw(click_list + [(x, y)])
297 widget.refresh()
298widget
299```
300
301
302
303Note: The above example currently only works in `jupyter notebook`, not `jupyter lab`.
304
305### Animation with Python
306```python
307import drawsvg as draw
308
309# Draw a frame of the animation
310def draw_frame(t):
311 d = draw.Drawing(2, 6.05, origin=(-1, -5))
312 d.set_render_size(h=300)
313 d.append(draw.Rectangle(-2, -6, 4, 8, fill='white'))
314 d.append(draw.Rectangle(-1, 1, 2, 0.05, fill='brown'))
315 t = (t + 1) % 2 - 1
316 y = t**2 * 4 - 4
317 d.append(draw.Circle(0, y, 1, fill='lime'))
318 return d
319
320with draw.frame_animate_jupyter(draw_frame, delay=0.05) as anim:
321# Or:
322#with draw.animate_video('example6.gif', draw_frame, duration=0.05
323# ) as anim:
324 # Add each frame to the animation
325 for i in range(20):
326 anim.draw_frame(i/10)
327 for i in range(20):
328 anim.draw_frame(i/10)
329 for i in range(20):
330 anim.draw_frame(i/10)
331```
332
333
334
335### Asynchronous Animation in Jupyter
336```python
337# Jupyter cell 1:
338import drawsvg as draw
339from drawsvg.widgets import AsyncAnimation
340widget = AsyncAnimation(fps=10)
341widget
342# [Animation is displayed here (click to pause)]
343
344# Jupyter cell 2:
345global_variable = 'a'
346@widget.set_draw_frame # Animation above is automatically updated
347def draw_frame(secs=0):
348 # Draw something...
349 d = draw.Drawing(100, 40)
350 d.append(draw.Text(global_variable, 20, 0, 30))
351 d.append(draw.Text('{:0.1f}'.format(secs), 20, 30, 30))
352 return d
353
354# Jupyter cell 3:
355global_variable = 'b' # Animation above now displays 'b'
356```
357
358
359
360Note: The above example currently only works in `jupyter notebook`, not `jupyter lab`.