-
Notifications
You must be signed in to change notification settings - Fork 7
/
Copy pathdrawing.py
163 lines (128 loc) · 4.92 KB
/
drawing.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
"""Helpers for drawing in Jupyter notebooks with PyCairo."""
import contextlib
import io
import math
import os.path
import cairo
import IPython.display
# Compass points for making circle arcs
DEG90 = math.pi / 2
DEG180 = math.pi
CE, CS, CW, CN = [i * DEG90 for i in range(4)]
class _CairoContext:
"""Base class for Cairo contexts that can display in Jupyter, or write to a file."""
def __init__(self, width: int, height: int, output: str | None = None):
self.width = width
self.height = height
if isinstance(output, str):
self.output = os.path.expandvars(os.path.expanduser(output))
else:
self.output = output
self.surface = None
self.ctx = None
def _repr_pretty_(self, p, cycle_unused):
"""Plain text repr for the context."""
# This is implemented just to limit needless changes in notebook files.
# This gets written to the .ipynb file, and the default includes the
# memory address, which changes each time. This string does not.
p.text(f"<{self.__class__.__module__}.{self.__class__.__name__}>")
def _repr_html_(self):
"""
HTML display in Jupyter.
If output went to a file, display a message saying so. If output
didn't go to a file, do nothing and the derived class will implement a
method to display the output in Jupyter.
"""
if self.output is not None:
return f"<b><i>Wrote to {self.output}</i></b>"
def __enter__(self):
return self
def __getattr__(self, name):
"""Proxy to the cairo context, so that we have all the same methods."""
return getattr(self.ctx, name)
# Drawing helpers
def circle(self, x, y, r):
"""Add a complete circle to the path."""
self.ctx.arc(x, y, r, 0, 2 * math.pi)
@contextlib.contextmanager
def save_restore(self):
self.ctx.save()
try:
yield
finally:
self.ctx.restore()
@contextlib.contextmanager
def flip_lr(self, wh):
with self.save_restore():
self.ctx.translate(wh, 0)
self.ctx.scale(-1, 1)
yield
@contextlib.contextmanager
def flip_tb(self, wh):
with self.save_restore():
self.ctx.translate(0, wh)
self.ctx.scale(1, -1)
yield
@contextlib.contextmanager
def rotated(self, wh, nturns):
with self.save_restore():
self.ctx.translate(wh / 2, wh / 2)
self.ctx.rotate(math.pi * nturns / 2)
self.ctx.translate(-wh / 2, -wh / 2)
yield
class _CairoSvg(_CairoContext):
"""For creating an SVG drawing in Jupyter."""
def __init__(self, width: int, height: int, output: str | None = None):
super().__init__(width, height, output)
self.svgio = io.BytesIO()
self.surface = cairo.SVGSurface(self.svgio, self.width, self.height)
self.surface.set_document_unit(cairo.SVGUnit.PX)
self.ctx = cairo.Context(self.surface)
def __exit__(self, typ, val, tb):
self.surface.finish()
if self.output is not None:
with open(self.output, "wb") as svgout:
svgout.write(self.svgio.getvalue())
def _repr_svg_(self):
if self.output is None:
return self.svgio.getvalue().decode()
class _CairoPng(_CairoContext):
"""For creating a PNG drawing in Jupyter."""
def __init__(self, width: int, height: int, output: str | None = None):
super().__init__(width, height, output)
self.pngio = None
self.surface = cairo.ImageSurface(cairo.Format.RGB24, self.width, self.height)
self.ctx = cairo.Context(self.surface)
def __exit__(self, typ, val, tb):
if self.output is not None:
self.surface.write_to_png(self.output)
else:
self.pngio = io.BytesIO()
self.surface.write_to_png(self.pngio)
self.surface.finish()
def _repr_png_(self):
if self.output is None:
return self.pngio.getvalue()
def cairo_context(
width: int, height: int, format: str = "svg", output: str | None = None
):
"""
Create a PyCairo context for use in Jupyter.
Arguments:
width (int), height (int): the size of the drawing in pixels.
format (str): either "svg" or "png".
output (optional str): if provided, the output will be written to this
file. If None, the output will be displayed in the Jupyter notebook.
Returns:
A PyCairo context proxy.
"""
if format == "svg":
cls = _CairoSvg
elif format == "png":
cls = _CairoPng
else:
raise ValueError(f"Unknown format: {format!r}")
return cls(width, height, output)
def svg_row(*svgs):
sbs = '<div style="display:flex; flex-direction: row; justify-content: space-evenly">{}</div>'
return IPython.display.HTML(sbs.format("".join(s._repr_svg_() for s in svgs)))