Coverage for /home/martinb/.local/share/virtualenvs/camcops/lib/python3.6/site-packages/matplotlib/backends/backend_agg.py : 22%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1"""
2An agg http://antigrain.com/ backend
4Features that are implemented
6 * capstyles and join styles
7 * dashes
8 * linewidth
9 * lines, rectangles, ellipses
10 * clipping to a rectangle
11 * output to RGBA and PNG, optionally JPEG and TIFF
12 * alpha blending
13 * DPI scaling properly - everything scales properly (dashes, linewidths, etc)
14 * draw polygon
15 * freetype2 w/ ft2font
17TODO:
19 * integrate screen dpi w/ ppi and text
21"""
22try:
23 import threading
24except ImportError:
25 import dummy_threading as threading
26try:
27 from contextlib import nullcontext
28except ImportError:
29 from contextlib import ExitStack as nullcontext # Py 3.6.
30from math import radians, cos, sin
32import numpy as np
34from matplotlib import cbook, rcParams, __version__
35from matplotlib.backend_bases import (
36 _Backend, FigureCanvasBase, FigureManagerBase, RendererBase)
37from matplotlib.font_manager import findfont, get_font
38from matplotlib.ft2font import (LOAD_FORCE_AUTOHINT, LOAD_NO_HINTING,
39 LOAD_DEFAULT, LOAD_NO_AUTOHINT)
40from matplotlib.mathtext import MathTextParser
41from matplotlib.path import Path
42from matplotlib.transforms import Bbox, BboxBase
43from matplotlib import colors as mcolors
45from matplotlib.backends._backend_agg import RendererAgg as _RendererAgg
47from matplotlib.backend_bases import _has_pil
49if _has_pil:
50 from PIL import Image
52backend_version = 'v2.2'
55def get_hinting_flag():
56 mapping = {
57 True: LOAD_FORCE_AUTOHINT,
58 False: LOAD_NO_HINTING,
59 'either': LOAD_DEFAULT,
60 'native': LOAD_NO_AUTOHINT,
61 'auto': LOAD_FORCE_AUTOHINT,
62 'none': LOAD_NO_HINTING
63 }
64 return mapping[rcParams['text.hinting']]
67class RendererAgg(RendererBase):
68 """
69 The renderer handles all the drawing primitives using a graphics
70 context instance that controls the colors/styles
71 """
73 # we want to cache the fonts at the class level so that when
74 # multiple figures are created we can reuse them. This helps with
75 # a bug on windows where the creation of too many figures leads to
76 # too many open file handles. However, storing them at the class
77 # level is not thread safe. The solution here is to let the
78 # FigureCanvas acquire a lock on the fontd at the start of the
79 # draw, and release it when it is done. This allows multiple
80 # renderers to share the cached fonts, but only one figure can
81 # draw at time and so the font cache is used by only one
82 # renderer at a time.
84 lock = threading.RLock()
86 def __init__(self, width, height, dpi):
87 RendererBase.__init__(self)
89 self.dpi = dpi
90 self.width = width
91 self.height = height
92 self._renderer = _RendererAgg(int(width), int(height), dpi)
93 self._filter_renderers = []
95 self._update_methods()
96 self.mathtext_parser = MathTextParser('Agg')
98 self.bbox = Bbox.from_bounds(0, 0, self.width, self.height)
100 def __getstate__(self):
101 # We only want to preserve the init keywords of the Renderer.
102 # Anything else can be re-created.
103 return {'width': self.width, 'height': self.height, 'dpi': self.dpi}
105 def __setstate__(self, state):
106 self.__init__(state['width'], state['height'], state['dpi'])
108 def _update_methods(self):
109 self.draw_gouraud_triangle = self._renderer.draw_gouraud_triangle
110 self.draw_gouraud_triangles = self._renderer.draw_gouraud_triangles
111 self.draw_image = self._renderer.draw_image
112 self.draw_markers = self._renderer.draw_markers
113 self.draw_path_collection = self._renderer.draw_path_collection
114 self.draw_quad_mesh = self._renderer.draw_quad_mesh
115 self.copy_from_bbox = self._renderer.copy_from_bbox
116 self.get_content_extents = self._renderer.get_content_extents
118 def tostring_rgba_minimized(self):
119 extents = self.get_content_extents()
120 bbox = [[extents[0], self.height - (extents[1] + extents[3])],
121 [extents[0] + extents[2], self.height - extents[1]]]
122 region = self.copy_from_bbox(bbox)
123 return np.array(region), extents
125 def draw_path(self, gc, path, transform, rgbFace=None):
126 # docstring inherited
127 nmax = rcParams['agg.path.chunksize'] # here at least for testing
128 npts = path.vertices.shape[0]
130 if (nmax > 100 and npts > nmax and path.should_simplify and
131 rgbFace is None and gc.get_hatch() is None):
132 nch = np.ceil(npts / nmax)
133 chsize = int(np.ceil(npts / nch))
134 i0 = np.arange(0, npts, chsize)
135 i1 = np.zeros_like(i0)
136 i1[:-1] = i0[1:] - 1
137 i1[-1] = npts
138 for ii0, ii1 in zip(i0, i1):
139 v = path.vertices[ii0:ii1, :]
140 c = path.codes
141 if c is not None:
142 c = c[ii0:ii1]
143 c[0] = Path.MOVETO # move to end of last chunk
144 p = Path(v, c)
145 try:
146 self._renderer.draw_path(gc, p, transform, rgbFace)
147 except OverflowError:
148 raise OverflowError("Exceeded cell block limit (set "
149 "'agg.path.chunksize' rcparam)")
150 else:
151 try:
152 self._renderer.draw_path(gc, path, transform, rgbFace)
153 except OverflowError:
154 raise OverflowError("Exceeded cell block limit (set "
155 "'agg.path.chunksize' rcparam)")
157 def draw_mathtext(self, gc, x, y, s, prop, angle):
158 """
159 Draw the math text using matplotlib.mathtext
160 """
161 ox, oy, width, height, descent, font_image, used_characters = \
162 self.mathtext_parser.parse(s, self.dpi, prop)
164 xd = descent * sin(radians(angle))
165 yd = descent * cos(radians(angle))
166 x = round(x + ox + xd)
167 y = round(y - oy + yd)
168 self._renderer.draw_text_image(font_image, x, y + 1, angle, gc)
170 def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
171 # docstring inherited
173 if ismath:
174 return self.draw_mathtext(gc, x, y, s, prop, angle)
176 flags = get_hinting_flag()
177 font = self._get_agg_font(prop)
179 if font is None:
180 return None
181 # We pass '0' for angle here, since it will be rotated (in raster
182 # space) in the following call to draw_text_image).
183 font.set_text(s, 0, flags=flags)
184 font.draw_glyphs_to_bitmap(antialiased=rcParams['text.antialiased'])
185 d = font.get_descent() / 64.0
186 # The descent needs to be adjusted for the angle.
187 xo, yo = font.get_bitmap_offset()
188 xo /= 64.0
189 yo /= 64.0
190 xd = d * sin(radians(angle))
191 yd = d * cos(radians(angle))
192 x = round(x + xo + xd)
193 y = round(y + yo + yd)
194 self._renderer.draw_text_image(font, x, y + 1, angle, gc)
196 def get_text_width_height_descent(self, s, prop, ismath):
197 # docstring inherited
199 if ismath in ["TeX", "TeX!"]:
200 # todo: handle props
201 texmanager = self.get_texmanager()
202 fontsize = prop.get_size_in_points()
203 w, h, d = texmanager.get_text_width_height_descent(
204 s, fontsize, renderer=self)
205 return w, h, d
207 if ismath:
208 ox, oy, width, height, descent, fonts, used_characters = \
209 self.mathtext_parser.parse(s, self.dpi, prop)
210 return width, height, descent
212 flags = get_hinting_flag()
213 font = self._get_agg_font(prop)
214 font.set_text(s, 0.0, flags=flags)
215 w, h = font.get_width_height() # width and height of unrotated string
216 d = font.get_descent()
217 w /= 64.0 # convert from subpixels
218 h /= 64.0
219 d /= 64.0
220 return w, h, d
222 def draw_tex(self, gc, x, y, s, prop, angle, ismath='TeX!', mtext=None):
223 # docstring inherited
224 # todo, handle props, angle, origins
225 size = prop.get_size_in_points()
227 texmanager = self.get_texmanager()
229 Z = texmanager.get_grey(s, size, self.dpi)
230 Z = np.array(Z * 255.0, np.uint8)
232 w, h, d = self.get_text_width_height_descent(s, prop, ismath)
233 xd = d * sin(radians(angle))
234 yd = d * cos(radians(angle))
235 x = round(x + xd)
236 y = round(y + yd)
237 self._renderer.draw_text_image(Z, x, y, angle, gc)
239 def get_canvas_width_height(self):
240 # docstring inherited
241 return self.width, self.height
243 def _get_agg_font(self, prop):
244 """
245 Get the font for text instance t, caching for efficiency
246 """
247 fname = findfont(prop)
248 font = get_font(fname)
250 font.clear()
251 size = prop.get_size_in_points()
252 font.set_size(size, self.dpi)
254 return font
256 def points_to_pixels(self, points):
257 # docstring inherited
258 return points * self.dpi / 72
260 def buffer_rgba(self):
261 return memoryview(self._renderer)
263 def tostring_argb(self):
264 return np.asarray(self._renderer).take([3, 0, 1, 2], axis=2).tobytes()
266 def tostring_rgb(self):
267 return np.asarray(self._renderer).take([0, 1, 2], axis=2).tobytes()
269 def clear(self):
270 self._renderer.clear()
272 def option_image_nocomposite(self):
273 # docstring inherited
275 # It is generally faster to composite each image directly to
276 # the Figure, and there's no file size benefit to compositing
277 # with the Agg backend
278 return True
280 def option_scale_image(self):
281 # docstring inherited
282 return False
284 def restore_region(self, region, bbox=None, xy=None):
285 """
286 Restore the saved region. If bbox (instance of BboxBase, or
287 its extents) is given, only the region specified by the bbox
288 will be restored. *xy* (a pair of floats) optionally
289 specifies the new position (the LLC of the original region,
290 not the LLC of the bbox) where the region will be restored.
292 >>> region = renderer.copy_from_bbox()
293 >>> x1, y1, x2, y2 = region.get_extents()
294 >>> renderer.restore_region(region, bbox=(x1+dx, y1, x2, y2),
295 ... xy=(x1-dx, y1))
297 """
298 if bbox is not None or xy is not None:
299 if bbox is None:
300 x1, y1, x2, y2 = region.get_extents()
301 elif isinstance(bbox, BboxBase):
302 x1, y1, x2, y2 = bbox.extents
303 else:
304 x1, y1, x2, y2 = bbox
306 if xy is None:
307 ox, oy = x1, y1
308 else:
309 ox, oy = xy
311 # The incoming data is float, but the _renderer type-checking wants
312 # to see integers.
313 self._renderer.restore_region(region, int(x1), int(y1),
314 int(x2), int(y2), int(ox), int(oy))
316 else:
317 self._renderer.restore_region(region)
319 def start_filter(self):
320 """
321 Start filtering. It simply create a new canvas (the old one is saved).
322 """
323 self._filter_renderers.append(self._renderer)
324 self._renderer = _RendererAgg(int(self.width), int(self.height),
325 self.dpi)
326 self._update_methods()
328 def stop_filter(self, post_processing):
329 """
330 Save the plot in the current canvas as a image and apply
331 the *post_processing* function.
333 def post_processing(image, dpi):
334 # ny, nx, depth = image.shape
335 # image (numpy array) has RGBA channels and has a depth of 4.
336 ...
337 # create a new_image (numpy array of 4 channels, size can be
338 # different). The resulting image may have offsets from
339 # lower-left corner of the original image
340 return new_image, offset_x, offset_y
342 The saved renderer is restored and the returned image from
343 post_processing is plotted (using draw_image) on it.
344 """
346 width, height = int(self.width), int(self.height)
348 buffer, (l, b, w, h) = self.tostring_rgba_minimized()
350 self._renderer = self._filter_renderers.pop()
351 self._update_methods()
353 if w > 0 and h > 0:
354 img = np.frombuffer(buffer, np.uint8)
355 img, ox, oy = post_processing(img.reshape((h, w, 4)) / 255.,
356 self.dpi)
357 gc = self.new_gc()
358 if img.dtype.kind == 'f':
359 img = np.asarray(img * 255., np.uint8)
360 img = img[::-1]
361 self._renderer.draw_image(gc, l + ox, height - b - h + oy, img)
364class FigureCanvasAgg(FigureCanvasBase):
365 """
366 The canvas the figure renders into. Calls the draw and print fig
367 methods, creates the renderers, etc...
369 Attributes
370 ----------
371 figure : `matplotlib.figure.Figure`
372 A high-level Figure instance
374 """
376 def copy_from_bbox(self, bbox):
377 renderer = self.get_renderer()
378 return renderer.copy_from_bbox(bbox)
380 def restore_region(self, region, bbox=None, xy=None):
381 renderer = self.get_renderer()
382 return renderer.restore_region(region, bbox, xy)
384 def draw(self):
385 """
386 Draw the figure using the renderer.
387 """
388 self.renderer = self.get_renderer(cleared=True)
389 # Acquire a lock on the shared font cache.
390 with RendererAgg.lock, \
391 (self.toolbar._wait_cursor_for_draw_cm() if self.toolbar
392 else nullcontext()):
393 self.figure.draw(self.renderer)
394 # A GUI class may be need to update a window using this draw, so
395 # don't forget to call the superclass.
396 super().draw()
398 def get_renderer(self, cleared=False):
399 l, b, w, h = self.figure.bbox.bounds
400 key = w, h, self.figure.dpi
401 reuse_renderer = (hasattr(self, "renderer")
402 and getattr(self, "_lastKey", None) == key)
403 if not reuse_renderer:
404 self.renderer = RendererAgg(w, h, self.figure.dpi)
405 self._lastKey = key
406 elif cleared:
407 self.renderer.clear()
408 return self.renderer
410 def tostring_rgb(self):
411 """Get the image as an RGB byte string.
413 `draw` must be called at least once before this function will work and
414 to update the renderer for any subsequent changes to the Figure.
416 Returns
417 -------
418 bytes
419 """
420 return self.renderer.tostring_rgb()
422 def tostring_argb(self):
423 """Get the image as an ARGB byte string.
425 `draw` must be called at least once before this function will work and
426 to update the renderer for any subsequent changes to the Figure.
428 Returns
429 -------
430 bytes
431 """
432 return self.renderer.tostring_argb()
434 def buffer_rgba(self):
435 """Get the image as a memoryview to the renderer's buffer.
437 `draw` must be called at least once before this function will work and
438 to update the renderer for any subsequent changes to the Figure.
440 Returns
441 -------
442 memoryview
443 """
444 return self.renderer.buffer_rgba()
446 def print_raw(self, filename_or_obj, *args, **kwargs):
447 FigureCanvasAgg.draw(self)
448 renderer = self.get_renderer()
449 with cbook.open_file_cm(filename_or_obj, "wb") as fh:
450 fh.write(renderer.buffer_rgba())
452 print_rgba = print_raw
454 def print_png(self, filename_or_obj, *args,
455 metadata=None, pil_kwargs=None,
456 **kwargs):
457 """
458 Write the figure to a PNG file.
460 Parameters
461 ----------
462 filename_or_obj : str or PathLike or file-like object
463 The file to write to.
465 metadata : dict, optional
466 Metadata in the PNG file as key-value pairs of bytes or latin-1
467 encodable strings.
468 According to the PNG specification, keys must be shorter than 79
469 chars.
471 The `PNG specification`_ defines some common keywords that may be
472 used as appropriate:
474 - Title: Short (one line) title or caption for image.
475 - Author: Name of image's creator.
476 - Description: Description of image (possibly long).
477 - Copyright: Copyright notice.
478 - Creation Time: Time of original image creation
479 (usually RFC 1123 format).
480 - Software: Software used to create the image.
481 - Disclaimer: Legal disclaimer.
482 - Warning: Warning of nature of content.
483 - Source: Device used to create the image.
484 - Comment: Miscellaneous comment;
485 conversion from other image format.
487 Other keywords may be invented for other purposes.
489 If 'Software' is not given, an autogenerated value for matplotlib
490 will be used.
492 For more details see the `PNG specification`_.
494 .. _PNG specification: \
495 https://www.w3.org/TR/2003/REC-PNG-20031110/#11keywords
497 pil_kwargs : dict, optional
498 If set to a non-None value, use Pillow to save the figure instead
499 of Matplotlib's builtin PNG support, and pass these keyword
500 arguments to `PIL.Image.save`.
502 If the 'pnginfo' key is present, it completely overrides
503 *metadata*, including the default 'Software' key.
504 """
505 from matplotlib import _png
507 if metadata is None:
508 metadata = {}
509 default_metadata = {
510 "Software":
511 f"matplotlib version{__version__}, http://matplotlib.org/",
512 }
514 FigureCanvasAgg.draw(self)
515 if pil_kwargs is not None:
516 from PIL import Image
517 from PIL.PngImagePlugin import PngInfo
518 # Only use the metadata kwarg if pnginfo is not set, because the
519 # semantics of duplicate keys in pnginfo is unclear.
520 if "pnginfo" in pil_kwargs:
521 if metadata:
522 cbook._warn_external("'metadata' is overridden by the "
523 "'pnginfo' entry in 'pil_kwargs'.")
524 else:
525 pnginfo = PngInfo()
526 for k, v in {**default_metadata, **metadata}.items():
527 pnginfo.add_text(k, v)
528 pil_kwargs["pnginfo"] = pnginfo
529 pil_kwargs.setdefault("dpi", (self.figure.dpi, self.figure.dpi))
530 (Image.fromarray(np.asarray(self.buffer_rgba()))
531 .save(filename_or_obj, format="png", **pil_kwargs))
533 else:
534 renderer = self.get_renderer()
535 with cbook.open_file_cm(filename_or_obj, "wb") as fh:
536 _png.write_png(renderer._renderer, fh, self.figure.dpi,
537 metadata={**default_metadata, **metadata})
539 def print_to_buffer(self):
540 FigureCanvasAgg.draw(self)
541 renderer = self.get_renderer()
542 return (bytes(renderer.buffer_rgba()),
543 (int(renderer.width), int(renderer.height)))
545 if _has_pil:
547 # Note that these methods should typically be called via savefig() and
548 # print_figure(), and the latter ensures that `self.figure.dpi` already
549 # matches the dpi kwarg (if any).
551 @cbook._delete_parameter("3.2", "dryrun")
552 def print_jpg(self, filename_or_obj, *args, dryrun=False,
553 pil_kwargs=None, **kwargs):
554 """
555 Write the figure to a JPEG file.
557 Parameters
558 ----------
559 filename_or_obj : str or PathLike or file-like object
560 The file to write to.
562 Other Parameters
563 ----------------
564 quality : int
565 The image quality, on a scale from 1 (worst) to 100 (best).
566 The default is :rc:`savefig.jpeg_quality`. Values above
567 95 should be avoided; 100 completely disables the JPEG
568 quantization stage.
570 optimize : bool
571 If present, indicates that the encoder should
572 make an extra pass over the image in order to select
573 optimal encoder settings.
575 progressive : bool
576 If present, indicates that this image
577 should be stored as a progressive JPEG file.
579 pil_kwargs : dict, optional
580 Additional keyword arguments that are passed to
581 `PIL.Image.save` when saving the figure. These take precedence
582 over *quality*, *optimize* and *progressive*.
583 """
584 FigureCanvasAgg.draw(self)
585 if dryrun:
586 return
587 # The image is pasted onto a white background image to handle
588 # transparency.
589 image = Image.fromarray(np.asarray(self.buffer_rgba()))
590 background = Image.new('RGB', image.size, "white")
591 background.paste(image, image)
592 if pil_kwargs is None:
593 pil_kwargs = {}
594 for k in ["quality", "optimize", "progressive"]:
595 if k in kwargs:
596 pil_kwargs.setdefault(k, kwargs[k])
597 pil_kwargs.setdefault("quality", rcParams["savefig.jpeg_quality"])
598 pil_kwargs.setdefault("dpi", (self.figure.dpi, self.figure.dpi))
599 return background.save(
600 filename_or_obj, format='jpeg', **pil_kwargs)
602 print_jpeg = print_jpg
604 @cbook._delete_parameter("3.2", "dryrun")
605 def print_tif(self, filename_or_obj, *args, dryrun=False,
606 pil_kwargs=None, **kwargs):
607 FigureCanvasAgg.draw(self)
608 if dryrun:
609 return
610 if pil_kwargs is None:
611 pil_kwargs = {}
612 pil_kwargs.setdefault("dpi", (self.figure.dpi, self.figure.dpi))
613 return (Image.fromarray(np.asarray(self.buffer_rgba()))
614 .save(filename_or_obj, format='tiff', **pil_kwargs))
616 print_tiff = print_tif
619@_Backend.export
620class _BackendAgg(_Backend):
621 FigureCanvas = FigureCanvasAgg
622 FigureManager = FigureManagerBase