1 |
""" |
2 |
An agg http://antigrain.com/ backend |
3 |
|
4 |
Features that are implemented |
5 |
|
6 |
* capstyles and join styles |
7 |
* dashes |
8 |
* linewidth |
9 |
* lines, rectangles, ellipses |
10 |
* clipping to a rectangle |
11 |
* output to RGBA and PNG |
12 |
* alpha blending |
13 |
* DPI scaling properly - everything scales properly (dashes, linewidths, etc) |
14 |
* draw polygon |
15 |
* freetype2 w/ ft2font |
16 |
|
17 |
TODO: |
18 |
|
19 |
* integrate screen dpi w/ ppi and text |
20 |
|
21 |
""" |
22 |
from __future__ import absolute_import, division, print_function, unicode_literals |
23 |
|
24 |
import six |
25 |
|
26 |
import threading |
27 |
import numpy as np |
28 |
|
29 |
from matplotlib import verbose, rcParams |
30 |
from matplotlib.backend_bases import RendererBase,\ |
31 |
FigureManagerBase, FigureCanvasBase |
32 |
from matplotlib.cbook import is_string_like, maxdict |
33 |
from matplotlib.figure import Figure |
34 |
from matplotlib.font_manager import findfont |
35 |
from matplotlib.ft2font import FT2Font, LOAD_FORCE_AUTOHINT, LOAD_NO_HINTING, \ |
36 |
LOAD_DEFAULT, LOAD_NO_AUTOHINT |
37 |
from matplotlib.mathtext import MathTextParser |
38 |
from matplotlib.path import Path |
39 |
from matplotlib.transforms import Bbox, BboxBase |
40 |
|
41 |
from matplotlib.backends._backend_agg import RendererAgg as _RendererAgg |
42 |
from matplotlib import _png |
43 |
|
44 |
backend_version = 'v2.2' |
45 |
|
46 |
def get_hinting_flag(): |
47 |
mapping = { |
48 |
True: LOAD_FORCE_AUTOHINT, |
49 |
False: LOAD_NO_HINTING, |
50 |
'either': LOAD_DEFAULT, |
51 |
'native': LOAD_NO_AUTOHINT, |
52 |
'auto': LOAD_FORCE_AUTOHINT, |
53 |
'none': LOAD_NO_HINTING |
54 |
} |
55 |
return mapping[rcParams['text.hinting']] |
56 |
|
57 |
|
58 |
class RendererAgg(RendererBase): |
59 |
""" |
60 |
The renderer handles all the drawing primitives using a graphics |
61 |
context instance that controls the colors/styles |
62 |
""" |
63 |
debug=1 |
64 |
|
65 |
# we want to cache the fonts at the class level so that when |
66 |
# multiple figures are created we can reuse them. This helps with |
67 |
# a bug on windows where the creation of too many figures leads to |
68 |
# too many open file handles. However, storing them at the class |
69 |
# level is not thread safe. The solution here is to let the |
70 |
# FigureCanvas acquire a lock on the fontd at the start of the |
71 |
# draw, and release it when it is done. This allows multiple |
72 |
# renderers to share the cached fonts, but only one figure can |
73 |
# draw at at time and so the font cache is used by only one |
74 |
# renderer at a time |
75 |
|
76 |
lock = threading.RLock() |
77 |
_fontd = maxdict(50) |
78 |
def __init__(self, width, height, dpi): |
79 |
if __debug__: verbose.report('RendererAgg.__init__', 'debug-annoying') |
80 |
RendererBase.__init__(self) |
81 |
self.texd = maxdict(50) # a cache of tex image rasters |
82 |
|
83 |
self.dpi = dpi |
84 |
self.width = width |
85 |
self.height = height |
86 |
if __debug__: verbose.report('RendererAgg.__init__ width=%s, height=%s'%(width, height), 'debug-annoying') |
87 |
self._renderer = _RendererAgg(int(width), int(height), dpi, debug=False) |
88 |
self._filter_renderers = [] |
89 |
|
90 |
if __debug__: verbose.report('RendererAgg.__init__ _RendererAgg done', |
91 |
'debug-annoying') |
92 |
|
93 |
self._update_methods() |
94 |
self.mathtext_parser = MathTextParser('Agg') |
95 |
|
96 |
self.bbox = Bbox.from_bounds(0, 0, self.width, self.height) |
97 |
if __debug__: verbose.report('RendererAgg.__init__ done', |
98 |
'debug-annoying') |
99 |
|
100 |
def _get_hinting_flag(self): |
101 |
if rcParams['text.hinting']: |
102 |
return LOAD_FORCE_AUTOHINT |
103 |
else: |
104 |
return LOAD_NO_HINTING |
105 |
|
106 |
# for filtering to work with rasterization, methods needs to be wrapped. |
107 |
# maybe there is better way to do it. |
108 |
def draw_markers(self, *kl, **kw): |
109 |
return self._renderer.draw_markers(*kl, **kw) |
110 |
|
111 |
def draw_path_collection(self, *kl, **kw): |
112 |
return self._renderer.draw_path_collection(*kl, **kw) |
113 |
|
114 |
def _update_methods(self): |
115 |
#self.draw_path = self._renderer.draw_path # see below |
116 |
#self.draw_markers = self._renderer.draw_markers |
117 |
#self.draw_path_collection = self._renderer.draw_path_collection |
118 |
self.draw_quad_mesh = self._renderer.draw_quad_mesh |
119 |
self.draw_gouraud_triangle = self._renderer.draw_gouraud_triangle |
120 |
self.draw_gouraud_triangles = self._renderer.draw_gouraud_triangles |
121 |
self.draw_image = self._renderer.draw_image |
122 |
self.copy_from_bbox = self._renderer.copy_from_bbox |
123 |
self.tostring_rgba_minimized = self._renderer.tostring_rgba_minimized |
124 |
|
125 |
def draw_path(self, gc, path, transform, rgbFace=None): |
126 |
""" |
127 |
Draw the path |
128 |
""" |
129 |
nmax = rcParams['agg.path.chunksize'] # here at least for testing |
130 |
npts = path.vertices.shape[0] |
131 |
if (nmax > 100 and npts > nmax and path.should_simplify and |
132 |
rgbFace is None and gc.get_hatch() is None): |
133 |
nch = np.ceil(npts/float(nmax)) |
134 |
chsize = int(np.ceil(npts/nch)) |
135 |
i0 = np.arange(0, npts, chsize) |
136 |
i1 = np.zeros_like(i0) |
137 |
i1[:-1] = i0[1:] - 1 |
138 |
i1[-1] = npts |
139 |
for ii0, ii1 in zip(i0, i1): |
140 |
v = path.vertices[ii0:ii1,:] |
141 |
c = path.codes |
142 |
if c is not None: |
143 |
c = c[ii0:ii1] |
144 |
c[0] = Path.MOVETO # move to end of last chunk |
145 |
p = Path(v, c) |
146 |
self._renderer.draw_path(gc, p, transform, rgbFace) |
147 |
else: |
148 |
self._renderer.draw_path(gc, path, transform, rgbFace) |
149 |
|
150 |
def draw_mathtext(self, gc, x, y, s, prop, angle): |
151 |
""" |
152 |
Draw the math text using matplotlib.mathtext |
153 |
""" |
154 |
if __debug__: verbose.report('RendererAgg.draw_mathtext', |
155 |
'debug-annoying') |
156 |
ox, oy, width, height, descent, font_image, used_characters = \ |
157 |
self.mathtext_parser.parse(s, self.dpi, prop) |
158 |
|
159 |
xd = descent * np.sin(np.deg2rad(angle)) |
160 |
yd = descent * np.cos(np.deg2rad(angle)) |
161 |
x = np.round(x + ox + xd) |
162 |
y = np.round(y - oy + yd) |
163 |
self._renderer.draw_text_image(font_image, x, y + 1, angle, gc) |
164 |
|
165 |
def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): |
166 |
""" |
167 |
Render the text |
168 |
""" |
169 |
if __debug__: verbose.report('RendererAgg.draw_text', 'debug-annoying') |
170 |
|
171 |
if ismath: |
172 |
return self.draw_mathtext(gc, x, y, s, prop, angle) |
173 |
|
174 |
flags = get_hinting_flag() |
175 |
font = self._get_agg_font(prop) |
176 |
if font is None: return None |
177 |
if len(s) == 1 and ord(s) > 127: |
178 |
font.load_char(ord(s), flags=flags) |
179 |
else: |
180 |
# We pass '0' for angle here, since it will be rotated (in raster |
181 |
# space) in the following call to draw_text_image). |
182 |
font.set_text(s, 0, flags=flags) |
183 |
font.draw_glyphs_to_bitmap(antialiased=rcParams['text.antialiased']) |
184 |
d = font.get_descent() / 64.0 |
185 |
# The descent needs to be adjusted for the angle |
186 |
xd = -d * np.sin(np.deg2rad(angle)) |
187 |
yd = d * np.cos(np.deg2rad(angle)) |
188 |
|
189 |
#print x, y, int(x), int(y), s |
190 |
self._renderer.draw_text_image( |
191 |
font.get_image(), np.round(x - xd), np.round(y + yd) + 1, angle, gc) |
192 |
|
193 |
def get_text_width_height_descent(self, s, prop, ismath): |
194 |
""" |
195 |
get the width and height in display coords of the string s |
196 |
with FontPropertry prop |
197 |
|
198 |
# passing rgb is a little hack to make cacheing in the |
199 |
# texmanager more efficient. It is not meant to be used |
200 |
# outside the backend |
201 |
""" |
202 |
if rcParams['text.usetex']: |
203 |
# todo: handle props |
204 |
size = prop.get_size_in_points() |
205 |
texmanager = self.get_texmanager() |
206 |
fontsize = prop.get_size_in_points() |
207 |
w, h, d = texmanager.get_text_width_height_descent(s, fontsize, |
208 |
renderer=self) |
209 |
return w, h, d |
210 |
|
211 |
if ismath: |
212 |
ox, oy, width, height, descent, fonts, used_characters = \ |
213 |
self.mathtext_parser.parse(s, self.dpi, prop) |
214 |
return width, height, descent |
215 |
|
216 |
flags = get_hinting_flag() |
217 |
font = self._get_agg_font(prop) |
218 |
font.set_text(s, 0.0, flags=flags) # the width and height of unrotated string |
219 |
w, h = font.get_width_height() |
220 |
d = font.get_descent() |
221 |
w /= 64.0 # convert from subpixels |
222 |
h /= 64.0 |
223 |
d /= 64.0 |
224 |
return w, h, d |
225 |
|
226 |
|
227 |
def draw_tex(self, gc, x, y, s, prop, angle, ismath='TeX!', mtext=None): |
228 |
# todo, handle props, angle, origins |
229 |
size = prop.get_size_in_points() |
230 |
|
231 |
texmanager = self.get_texmanager() |
232 |
key = s, size, self.dpi, angle, texmanager.get_font_config() |
233 |
im = self.texd.get(key) |
234 |
if im is None: |
235 |
Z = texmanager.get_grey(s, size, self.dpi) |
236 |
Z = np.array(Z * 255.0, np.uint8) |
237 |
|
238 |
w, h, d = self.get_text_width_height_descent(s, prop, ismath) |
239 |
xd = d * np.sin(np.deg2rad(angle)) |
240 |
yd = d * np.cos(np.deg2rad(angle)) |
241 |
x = np.round(x + xd) |
242 |
y = np.round(y + yd) |
243 |
|
244 |
self._renderer.draw_text_image(Z, x, y, angle, gc) |
245 |
|
246 |
def get_canvas_width_height(self): |
247 |
'return the canvas width and height in display coords' |
248 |
return self.width, self.height |
249 |
|
250 |
def _get_agg_font(self, prop): |
251 |
""" |
252 |
Get the font for text instance t, cacheing for efficiency |
253 |
""" |
254 |
if __debug__: verbose.report('RendererAgg._get_agg_font', |
255 |
'debug-annoying') |
256 |
|
257 |
key = hash(prop) |
258 |
font = RendererAgg._fontd.get(key) |
259 |
|
260 |
if font is None: |
261 |
fname = findfont(prop) |
262 |
font = RendererAgg._fontd.get(fname) |
263 |
if font is None: |
264 |
font = FT2Font( |
265 |
str(fname), |
266 |
hinting_factor=rcParams['text.hinting_factor']) |
267 |
RendererAgg._fontd[fname] = font |
268 |
RendererAgg._fontd[key] = font |
269 |
|
270 |
font.clear() |
271 |
size = prop.get_size_in_points() |
272 |
font.set_size(size, self.dpi) |
273 |
|
274 |
return font |
275 |
|
276 |
def points_to_pixels(self, points): |
277 |
""" |
278 |
convert point measures to pixes using dpi and the pixels per |
279 |
inch of the display |
280 |
""" |
281 |
if __debug__: verbose.report('RendererAgg.points_to_pixels', |
282 |
'debug-annoying') |
283 |
return points*self.dpi/72.0 |
284 |
|
285 |
def tostring_rgb(self): |
286 |
if __debug__: verbose.report('RendererAgg.tostring_rgb', |
287 |
'debug-annoying') |
288 |
return self._renderer.tostring_rgb() |
289 |
|
290 |
def tostring_argb(self): |
291 |
if __debug__: verbose.report('RendererAgg.tostring_argb', |
292 |
'debug-annoying') |
293 |
return self._renderer.tostring_argb() |
294 |
|
295 |
def buffer_rgba(self): |
296 |
if __debug__: verbose.report('RendererAgg.buffer_rgba', |
297 |
'debug-annoying') |
298 |
return self._renderer.buffer_rgba() |
299 |
|
300 |
def clear(self): |
301 |
self._renderer.clear() |
302 |
|
303 |
def option_image_nocomposite(self): |
304 |
# It is generally faster to composite each image directly to |
305 |
# the Figure, and there's no file size benefit to compositing |
306 |
# with the Agg backend |
307 |
return True |
308 |
|
309 |
def option_scale_image(self): |
310 |
""" |
311 |
agg backend support arbitrary scaling of image. |
312 |
""" |
313 |
return True |
314 |
|
315 |
def restore_region(self, region, bbox=None, xy=None): |
316 |
""" |
317 |
Restore the saved region. If bbox (instance of BboxBase, or |
318 |
its extents) is given, only the region specified by the bbox |
319 |
will be restored. *xy* (a tuple of two floasts) optionally |
320 |
specifies the new position (the LLC of the original region, |
321 |
not the LLC of the bbox) where the region will be restored. |
322 |
|
323 |
>>> region = renderer.copy_from_bbox() |
324 |
>>> x1, y1, x2, y2 = region.get_extents() |
325 |
>>> renderer.restore_region(region, bbox=(x1+dx, y1, x2, y2), |
326 |
... xy=(x1-dx, y1)) |
327 |
|
328 |
""" |
329 |
if bbox is not None or xy is not None: |
330 |
if bbox is None: |
331 |
x1, y1, x2, y2 = region.get_extents() |
332 |
elif isinstance(bbox, BboxBase): |
333 |
x1, y1, x2, y2 = bbox.extents |
334 |
else: |
335 |
x1, y1, x2, y2 = bbox |
336 |
|
337 |
if xy is None: |
338 |
ox, oy = x1, y1 |
339 |
else: |
340 |
ox, oy = xy |
341 |
|
342 |
self._renderer.restore_region2(region, x1, y1, x2, y2, ox, oy) |
343 |
|
344 |
else: |
345 |
self._renderer.restore_region(region) |
346 |
|
347 |
def start_filter(self): |
348 |
""" |
349 |
Start filtering. It simply create a new canvas (the old one is saved). |
350 |
""" |
351 |
self._filter_renderers.append(self._renderer) |
352 |
self._renderer = _RendererAgg(int(self.width), int(self.height), |
353 |
self.dpi) |
354 |
self._update_methods() |
355 |
|
356 |
def stop_filter(self, post_processing): |
357 |
""" |
358 |
Save the plot in the current canvas as a image and apply |
359 |
the *post_processing* function. |
360 |
|
361 |
def post_processing(image, dpi): |
362 |
# ny, nx, depth = image.shape |
363 |
# image (numpy array) has RGBA channels and has a depth of 4. |
364 |
... |
365 |
# create a new_image (numpy array of 4 channels, size can be |
366 |
# different). The resulting image may have offsets from |
367 |
# lower-left corner of the original image |
368 |
return new_image, offset_x, offset_y |
369 |
|
370 |
The saved renderer is restored and the returned image from |
371 |
post_processing is plotted (using draw_image) on it. |
372 |
""" |
373 |
|
374 |
# WARNING. |
375 |
# For agg_filter to work, the rendere's method need |
376 |
# to overridden in the class. See draw_markers, and draw_path_collections |
377 |
|
378 |
from matplotlib._image import fromarray |
379 |
|
380 |
width, height = int(self.width), int(self.height) |
381 |
|
382 |
buffer, bounds = self._renderer.tostring_rgba_minimized() |
383 |
|
384 |
l, b, w, h = bounds |
385 |
|
386 |
|
387 |
self._renderer = self._filter_renderers.pop() |
388 |
self._update_methods() |
389 |
|
390 |
if w > 0 and h > 0: |
391 |
img = np.fromstring(buffer, np.uint8) |
392 |
img, ox, oy = post_processing(img.reshape((h, w, 4)) / 255., |
393 |
self.dpi) |
394 |
image = fromarray(img, 1) |
395 |
image.flipud_out() |
396 |
|
397 |
gc = self.new_gc() |
398 |
self._renderer.draw_image(gc, |
399 |
l+ox, height - b - h +oy, |
400 |
image) |
401 |
|
402 |
|
403 |
def new_figure_manager(num, *args, **kwargs): |
404 |
""" |
405 |
Create a new figure manager instance |
406 |
""" |
407 |
if __debug__: verbose.report('backend_agg.new_figure_manager', |
408 |
'debug-annoying') |
409 |
|
410 |
|
411 |
FigureClass = kwargs.pop('FigureClass', Figure) |
412 |
thisFig = FigureClass(*args, **kwargs) |
413 |
return new_figure_manager_given_figure(num, thisFig) |
414 |
|
415 |
|
416 |
def new_figure_manager_given_figure(num, figure): |
417 |
""" |
418 |
Create a new figure manager instance for the given figure. |
419 |
""" |
420 |
canvas = FigureCanvasAgg(figure) |
421 |
manager = FigureManagerBase(canvas, num) |
422 |
return manager |
423 |
|
424 |
|
425 |
class FigureCanvasAgg(FigureCanvasBase): |
426 |
""" |
427 |
The canvas the figure renders into. Calls the draw and print fig |
428 |
methods, creates the renderers, etc... |
429 |
|
430 |
Public attribute |
431 |
|
432 |
figure - A Figure instance |
433 |
""" |
434 |
|
435 |
def copy_from_bbox(self, bbox): |
436 |
renderer = self.get_renderer() |
437 |
return renderer.copy_from_bbox(bbox) |
438 |
|
439 |
def restore_region(self, region, bbox=None, xy=None): |
440 |
renderer = self.get_renderer() |
441 |
return renderer.restore_region(region, bbox, xy) |
442 |
|
443 |
def draw(self): |
444 |
""" |
445 |
Draw the figure using the renderer |
446 |
""" |
447 |
if __debug__: verbose.report('FigureCanvasAgg.draw', 'debug-annoying') |
448 |
|
449 |
self.renderer = self.get_renderer(cleared=True) |
450 |
# acquire a lock on the shared font cache |
451 |
RendererAgg.lock.acquire() |
452 |
|
453 |
try: |
454 |
self.figure.draw(self.renderer) |
455 |
finally: |
456 |
RendererAgg.lock.release() |
457 |
|
458 |
|
459 |
|
460 |
def get_renderer(self, cleared=False): |
461 |
l, b, w, h = self.figure.bbox.bounds |
462 |
key = w, h, self.figure.dpi |
463 |
try: self._lastKey, self.renderer |
464 |
except AttributeError: need_new_renderer = True |
465 |
else: need_new_renderer = (self._lastKey != key) |
466 |
|
467 |
if need_new_renderer: |
468 |
self.renderer = RendererAgg(w, h, self.figure.dpi) |
469 |
self._lastKey = key |
470 |
elif cleared: |
471 |
self.renderer.clear() |
472 |
return self.renderer |
473 |
|
474 |
def tostring_rgb(self): |
475 |
if __debug__: verbose.report('FigureCanvasAgg.tostring_rgb', |
476 |
'debug-annoying') |
477 |
return self.renderer.tostring_rgb() |
478 |
|
479 |
def tostring_argb(self): |
480 |
if __debug__: verbose.report('FigureCanvasAgg.tostring_argb', |
481 |
'debug-annoying') |
482 |
return self.renderer.tostring_argb() |
483 |
|
484 |
def buffer_rgba(self): |
485 |
if __debug__: verbose.report('FigureCanvasAgg.buffer_rgba', |
486 |
'debug-annoying') |
487 |
return self.renderer.buffer_rgba() |
488 |
|
489 |
def print_raw(self, filename_or_obj, *args, **kwargs): |
490 |
FigureCanvasAgg.draw(self) |
491 |
renderer = self.get_renderer() |
492 |
original_dpi = renderer.dpi |
493 |
renderer.dpi = self.figure.dpi |
494 |
if is_string_like(filename_or_obj): |
495 |
filename_or_obj = open(filename_or_obj, 'wb') |
496 |
close = True |
497 |
else: |
498 |
close = False |
499 |
try: |
500 |
renderer._renderer.write_rgba(filename_or_obj) |
501 |
finally: |
502 |
if close: |
503 |
filename_or_obj.close() |
504 |
renderer.dpi = original_dpi |
505 |
print_rgba = print_raw |
506 |
|
507 |
def print_png(self, filename_or_obj, *args, **kwargs): |
508 |
FigureCanvasAgg.draw(self) |
509 |
renderer = self.get_renderer() |
510 |
original_dpi = renderer.dpi |
511 |
renderer.dpi = self.figure.dpi |
512 |
if is_string_like(filename_or_obj): |
513 |
filename_or_obj = open(filename_or_obj, 'wb') |
514 |
close = True |
515 |
else: |
516 |
close = False |
517 |
try: |
518 |
_png.write_png(renderer._renderer.buffer_rgba(), |
519 |
renderer.width, renderer.height, |
520 |
filename_or_obj, self.figure.dpi) |
521 |
finally: |
522 |
if close: |
523 |
filename_or_obj.close() |
524 |
renderer.dpi = original_dpi |
525 |
|
526 |
def print_to_buffer(self): |
527 |
FigureCanvasAgg.draw(self) |
528 |
renderer = self.get_renderer() |
529 |
original_dpi = renderer.dpi |
530 |
renderer.dpi = self.figure.dpi |
531 |
result = (renderer._renderer.buffer_rgba(), |
532 |
(int(renderer.width), int(renderer.height))) |
533 |
renderer.dpi = original_dpi |
534 |
return result |