123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622 |
- """
- An agg http://antigrain.com/ backend
- Features that are implemented
- * capstyles and join styles
- * dashes
- * linewidth
- * lines, rectangles, ellipses
- * clipping to a rectangle
- * output to RGBA and PNG, optionally JPEG and TIFF
- * alpha blending
- * DPI scaling properly - everything scales properly (dashes, linewidths, etc)
- * draw polygon
- * freetype2 w/ ft2font
- TODO:
- * integrate screen dpi w/ ppi and text
- """
- try:
- import threading
- except ImportError:
- import dummy_threading as threading
- try:
- from contextlib import nullcontext
- except ImportError:
- from contextlib import ExitStack as nullcontext # Py 3.6.
- from math import radians, cos, sin
- import numpy as np
- from matplotlib import cbook, rcParams, __version__
- from matplotlib.backend_bases import (
- _Backend, FigureCanvasBase, FigureManagerBase, RendererBase)
- from matplotlib.font_manager import findfont, get_font
- from matplotlib.ft2font import (LOAD_FORCE_AUTOHINT, LOAD_NO_HINTING,
- LOAD_DEFAULT, LOAD_NO_AUTOHINT)
- from matplotlib.mathtext import MathTextParser
- from matplotlib.path import Path
- from matplotlib.transforms import Bbox, BboxBase
- from matplotlib import colors as mcolors
- from matplotlib.backends._backend_agg import RendererAgg as _RendererAgg
- from matplotlib.backend_bases import _has_pil
- if _has_pil:
- from PIL import Image
- backend_version = 'v2.2'
- def get_hinting_flag():
- mapping = {
- True: LOAD_FORCE_AUTOHINT,
- False: LOAD_NO_HINTING,
- 'either': LOAD_DEFAULT,
- 'native': LOAD_NO_AUTOHINT,
- 'auto': LOAD_FORCE_AUTOHINT,
- 'none': LOAD_NO_HINTING
- }
- return mapping[rcParams['text.hinting']]
- class RendererAgg(RendererBase):
- """
- The renderer handles all the drawing primitives using a graphics
- context instance that controls the colors/styles
- """
- # we want to cache the fonts at the class level so that when
- # multiple figures are created we can reuse them. This helps with
- # a bug on windows where the creation of too many figures leads to
- # too many open file handles. However, storing them at the class
- # level is not thread safe. The solution here is to let the
- # FigureCanvas acquire a lock on the fontd at the start of the
- # draw, and release it when it is done. This allows multiple
- # renderers to share the cached fonts, but only one figure can
- # draw at time and so the font cache is used by only one
- # renderer at a time.
- lock = threading.RLock()
- def __init__(self, width, height, dpi):
- RendererBase.__init__(self)
- self.dpi = dpi
- self.width = width
- self.height = height
- self._renderer = _RendererAgg(int(width), int(height), dpi)
- self._filter_renderers = []
- self._update_methods()
- self.mathtext_parser = MathTextParser('Agg')
- self.bbox = Bbox.from_bounds(0, 0, self.width, self.height)
- def __getstate__(self):
- # We only want to preserve the init keywords of the Renderer.
- # Anything else can be re-created.
- return {'width': self.width, 'height': self.height, 'dpi': self.dpi}
- def __setstate__(self, state):
- self.__init__(state['width'], state['height'], state['dpi'])
- def _update_methods(self):
- self.draw_gouraud_triangle = self._renderer.draw_gouraud_triangle
- self.draw_gouraud_triangles = self._renderer.draw_gouraud_triangles
- self.draw_image = self._renderer.draw_image
- self.draw_markers = self._renderer.draw_markers
- self.draw_path_collection = self._renderer.draw_path_collection
- self.draw_quad_mesh = self._renderer.draw_quad_mesh
- self.copy_from_bbox = self._renderer.copy_from_bbox
- self.get_content_extents = self._renderer.get_content_extents
- def tostring_rgba_minimized(self):
- extents = self.get_content_extents()
- bbox = [[extents[0], self.height - (extents[1] + extents[3])],
- [extents[0] + extents[2], self.height - extents[1]]]
- region = self.copy_from_bbox(bbox)
- return np.array(region), extents
- def draw_path(self, gc, path, transform, rgbFace=None):
- # docstring inherited
- nmax = rcParams['agg.path.chunksize'] # here at least for testing
- npts = path.vertices.shape[0]
- if (nmax > 100 and npts > nmax and path.should_simplify and
- rgbFace is None and gc.get_hatch() is None):
- nch = np.ceil(npts / nmax)
- chsize = int(np.ceil(npts / nch))
- i0 = np.arange(0, npts, chsize)
- i1 = np.zeros_like(i0)
- i1[:-1] = i0[1:] - 1
- i1[-1] = npts
- for ii0, ii1 in zip(i0, i1):
- v = path.vertices[ii0:ii1, :]
- c = path.codes
- if c is not None:
- c = c[ii0:ii1]
- c[0] = Path.MOVETO # move to end of last chunk
- p = Path(v, c)
- try:
- self._renderer.draw_path(gc, p, transform, rgbFace)
- except OverflowError:
- raise OverflowError("Exceeded cell block limit (set "
- "'agg.path.chunksize' rcparam)")
- else:
- try:
- self._renderer.draw_path(gc, path, transform, rgbFace)
- except OverflowError:
- raise OverflowError("Exceeded cell block limit (set "
- "'agg.path.chunksize' rcparam)")
- def draw_mathtext(self, gc, x, y, s, prop, angle):
- """
- Draw the math text using matplotlib.mathtext
- """
- ox, oy, width, height, descent, font_image, used_characters = \
- self.mathtext_parser.parse(s, self.dpi, prop)
- xd = descent * sin(radians(angle))
- yd = descent * cos(radians(angle))
- x = round(x + ox + xd)
- y = round(y - oy + yd)
- self._renderer.draw_text_image(font_image, x, y + 1, angle, gc)
- def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
- # docstring inherited
- if ismath:
- return self.draw_mathtext(gc, x, y, s, prop, angle)
- flags = get_hinting_flag()
- font = self._get_agg_font(prop)
- if font is None:
- return None
- # We pass '0' for angle here, since it will be rotated (in raster
- # space) in the following call to draw_text_image).
- font.set_text(s, 0, flags=flags)
- font.draw_glyphs_to_bitmap(antialiased=rcParams['text.antialiased'])
- d = font.get_descent() / 64.0
- # The descent needs to be adjusted for the angle.
- xo, yo = font.get_bitmap_offset()
- xo /= 64.0
- yo /= 64.0
- xd = d * sin(radians(angle))
- yd = d * cos(radians(angle))
- x = round(x + xo + xd)
- y = round(y + yo + yd)
- self._renderer.draw_text_image(font, x, y + 1, angle, gc)
- def get_text_width_height_descent(self, s, prop, ismath):
- # docstring inherited
- if ismath in ["TeX", "TeX!"]:
- # todo: handle props
- texmanager = self.get_texmanager()
- fontsize = prop.get_size_in_points()
- w, h, d = texmanager.get_text_width_height_descent(
- s, fontsize, renderer=self)
- return w, h, d
- if ismath:
- ox, oy, width, height, descent, fonts, used_characters = \
- self.mathtext_parser.parse(s, self.dpi, prop)
- return width, height, descent
- flags = get_hinting_flag()
- font = self._get_agg_font(prop)
- font.set_text(s, 0.0, flags=flags)
- w, h = font.get_width_height() # width and height of unrotated string
- d = font.get_descent()
- w /= 64.0 # convert from subpixels
- h /= 64.0
- d /= 64.0
- return w, h, d
- def draw_tex(self, gc, x, y, s, prop, angle, ismath='TeX!', mtext=None):
- # docstring inherited
- # todo, handle props, angle, origins
- size = prop.get_size_in_points()
- texmanager = self.get_texmanager()
- Z = texmanager.get_grey(s, size, self.dpi)
- Z = np.array(Z * 255.0, np.uint8)
- w, h, d = self.get_text_width_height_descent(s, prop, ismath)
- xd = d * sin(radians(angle))
- yd = d * cos(radians(angle))
- x = round(x + xd)
- y = round(y + yd)
- self._renderer.draw_text_image(Z, x, y, angle, gc)
- def get_canvas_width_height(self):
- # docstring inherited
- return self.width, self.height
- def _get_agg_font(self, prop):
- """
- Get the font for text instance t, caching for efficiency
- """
- fname = findfont(prop)
- font = get_font(fname)
- font.clear()
- size = prop.get_size_in_points()
- font.set_size(size, self.dpi)
- return font
- def points_to_pixels(self, points):
- # docstring inherited
- return points * self.dpi / 72
- def buffer_rgba(self):
- return memoryview(self._renderer)
- def tostring_argb(self):
- return np.asarray(self._renderer).take([3, 0, 1, 2], axis=2).tobytes()
- def tostring_rgb(self):
- return np.asarray(self._renderer).take([0, 1, 2], axis=2).tobytes()
- def clear(self):
- self._renderer.clear()
- def option_image_nocomposite(self):
- # docstring inherited
- # It is generally faster to composite each image directly to
- # the Figure, and there's no file size benefit to compositing
- # with the Agg backend
- return True
- def option_scale_image(self):
- # docstring inherited
- return False
- def restore_region(self, region, bbox=None, xy=None):
- """
- Restore the saved region. If bbox (instance of BboxBase, or
- its extents) is given, only the region specified by the bbox
- will be restored. *xy* (a pair of floats) optionally
- specifies the new position (the LLC of the original region,
- not the LLC of the bbox) where the region will be restored.
- >>> region = renderer.copy_from_bbox()
- >>> x1, y1, x2, y2 = region.get_extents()
- >>> renderer.restore_region(region, bbox=(x1+dx, y1, x2, y2),
- ... xy=(x1-dx, y1))
- """
- if bbox is not None or xy is not None:
- if bbox is None:
- x1, y1, x2, y2 = region.get_extents()
- elif isinstance(bbox, BboxBase):
- x1, y1, x2, y2 = bbox.extents
- else:
- x1, y1, x2, y2 = bbox
- if xy is None:
- ox, oy = x1, y1
- else:
- ox, oy = xy
- # The incoming data is float, but the _renderer type-checking wants
- # to see integers.
- self._renderer.restore_region(region, int(x1), int(y1),
- int(x2), int(y2), int(ox), int(oy))
- else:
- self._renderer.restore_region(region)
- def start_filter(self):
- """
- Start filtering. It simply create a new canvas (the old one is saved).
- """
- self._filter_renderers.append(self._renderer)
- self._renderer = _RendererAgg(int(self.width), int(self.height),
- self.dpi)
- self._update_methods()
- def stop_filter(self, post_processing):
- """
- Save the plot in the current canvas as a image and apply
- the *post_processing* function.
- def post_processing(image, dpi):
- # ny, nx, depth = image.shape
- # image (numpy array) has RGBA channels and has a depth of 4.
- ...
- # create a new_image (numpy array of 4 channels, size can be
- # different). The resulting image may have offsets from
- # lower-left corner of the original image
- return new_image, offset_x, offset_y
- The saved renderer is restored and the returned image from
- post_processing is plotted (using draw_image) on it.
- """
- width, height = int(self.width), int(self.height)
- buffer, (l, b, w, h) = self.tostring_rgba_minimized()
- self._renderer = self._filter_renderers.pop()
- self._update_methods()
- if w > 0 and h > 0:
- img = np.frombuffer(buffer, np.uint8)
- img, ox, oy = post_processing(img.reshape((h, w, 4)) / 255.,
- self.dpi)
- gc = self.new_gc()
- if img.dtype.kind == 'f':
- img = np.asarray(img * 255., np.uint8)
- img = img[::-1]
- self._renderer.draw_image(gc, l + ox, height - b - h + oy, img)
- class FigureCanvasAgg(FigureCanvasBase):
- """
- The canvas the figure renders into. Calls the draw and print fig
- methods, creates the renderers, etc...
- Attributes
- ----------
- figure : `matplotlib.figure.Figure`
- A high-level Figure instance
- """
- def copy_from_bbox(self, bbox):
- renderer = self.get_renderer()
- return renderer.copy_from_bbox(bbox)
- def restore_region(self, region, bbox=None, xy=None):
- renderer = self.get_renderer()
- return renderer.restore_region(region, bbox, xy)
- def draw(self):
- """
- Draw the figure using the renderer.
- """
- self.renderer = self.get_renderer(cleared=True)
- # Acquire a lock on the shared font cache.
- with RendererAgg.lock, \
- (self.toolbar._wait_cursor_for_draw_cm() if self.toolbar
- else nullcontext()):
- self.figure.draw(self.renderer)
- # A GUI class may be need to update a window using this draw, so
- # don't forget to call the superclass.
- super().draw()
- def get_renderer(self, cleared=False):
- l, b, w, h = self.figure.bbox.bounds
- key = w, h, self.figure.dpi
- reuse_renderer = (hasattr(self, "renderer")
- and getattr(self, "_lastKey", None) == key)
- if not reuse_renderer:
- self.renderer = RendererAgg(w, h, self.figure.dpi)
- self._lastKey = key
- elif cleared:
- self.renderer.clear()
- return self.renderer
- def tostring_rgb(self):
- """Get the image as an RGB byte string.
- `draw` must be called at least once before this function will work and
- to update the renderer for any subsequent changes to the Figure.
- Returns
- -------
- bytes
- """
- return self.renderer.tostring_rgb()
- def tostring_argb(self):
- """Get the image as an ARGB byte string.
- `draw` must be called at least once before this function will work and
- to update the renderer for any subsequent changes to the Figure.
- Returns
- -------
- bytes
- """
- return self.renderer.tostring_argb()
- def buffer_rgba(self):
- """Get the image as a memoryview to the renderer's buffer.
- `draw` must be called at least once before this function will work and
- to update the renderer for any subsequent changes to the Figure.
- Returns
- -------
- memoryview
- """
- return self.renderer.buffer_rgba()
- def print_raw(self, filename_or_obj, *args, **kwargs):
- FigureCanvasAgg.draw(self)
- renderer = self.get_renderer()
- with cbook.open_file_cm(filename_or_obj, "wb") as fh:
- fh.write(renderer.buffer_rgba())
- print_rgba = print_raw
- def print_png(self, filename_or_obj, *args,
- metadata=None, pil_kwargs=None,
- **kwargs):
- """
- Write the figure to a PNG file.
- Parameters
- ----------
- filename_or_obj : str or PathLike or file-like object
- The file to write to.
- metadata : dict, optional
- Metadata in the PNG file as key-value pairs of bytes or latin-1
- encodable strings.
- According to the PNG specification, keys must be shorter than 79
- chars.
- The `PNG specification`_ defines some common keywords that may be
- used as appropriate:
- - Title: Short (one line) title or caption for image.
- - Author: Name of image's creator.
- - Description: Description of image (possibly long).
- - Copyright: Copyright notice.
- - Creation Time: Time of original image creation
- (usually RFC 1123 format).
- - Software: Software used to create the image.
- - Disclaimer: Legal disclaimer.
- - Warning: Warning of nature of content.
- - Source: Device used to create the image.
- - Comment: Miscellaneous comment;
- conversion from other image format.
- Other keywords may be invented for other purposes.
- If 'Software' is not given, an autogenerated value for matplotlib
- will be used.
- For more details see the `PNG specification`_.
- .. _PNG specification: \
- https://www.w3.org/TR/2003/REC-PNG-20031110/#11keywords
- pil_kwargs : dict, optional
- If set to a non-None value, use Pillow to save the figure instead
- of Matplotlib's builtin PNG support, and pass these keyword
- arguments to `PIL.Image.save`.
- If the 'pnginfo' key is present, it completely overrides
- *metadata*, including the default 'Software' key.
- """
- from matplotlib import _png
- if metadata is None:
- metadata = {}
- default_metadata = {
- "Software":
- f"matplotlib version{__version__}, http://matplotlib.org/",
- }
- FigureCanvasAgg.draw(self)
- if pil_kwargs is not None:
- from PIL import Image
- from PIL.PngImagePlugin import PngInfo
- # Only use the metadata kwarg if pnginfo is not set, because the
- # semantics of duplicate keys in pnginfo is unclear.
- if "pnginfo" in pil_kwargs:
- if metadata:
- cbook._warn_external("'metadata' is overridden by the "
- "'pnginfo' entry in 'pil_kwargs'.")
- else:
- pnginfo = PngInfo()
- for k, v in {**default_metadata, **metadata}.items():
- pnginfo.add_text(k, v)
- pil_kwargs["pnginfo"] = pnginfo
- pil_kwargs.setdefault("dpi", (self.figure.dpi, self.figure.dpi))
- (Image.fromarray(np.asarray(self.buffer_rgba()))
- .save(filename_or_obj, format="png", **pil_kwargs))
- else:
- renderer = self.get_renderer()
- with cbook.open_file_cm(filename_or_obj, "wb") as fh:
- _png.write_png(renderer._renderer, fh, self.figure.dpi,
- metadata={**default_metadata, **metadata})
- def print_to_buffer(self):
- FigureCanvasAgg.draw(self)
- renderer = self.get_renderer()
- return (bytes(renderer.buffer_rgba()),
- (int(renderer.width), int(renderer.height)))
- if _has_pil:
- # Note that these methods should typically be called via savefig() and
- # print_figure(), and the latter ensures that `self.figure.dpi` already
- # matches the dpi kwarg (if any).
- @cbook._delete_parameter("3.2", "dryrun")
- def print_jpg(self, filename_or_obj, *args, dryrun=False,
- pil_kwargs=None, **kwargs):
- """
- Write the figure to a JPEG file.
- Parameters
- ----------
- filename_or_obj : str or PathLike or file-like object
- The file to write to.
- Other Parameters
- ----------------
- quality : int
- The image quality, on a scale from 1 (worst) to 100 (best).
- The default is :rc:`savefig.jpeg_quality`. Values above
- 95 should be avoided; 100 completely disables the JPEG
- quantization stage.
- optimize : bool
- If present, indicates that the encoder should
- make an extra pass over the image in order to select
- optimal encoder settings.
- progressive : bool
- If present, indicates that this image
- should be stored as a progressive JPEG file.
- pil_kwargs : dict, optional
- Additional keyword arguments that are passed to
- `PIL.Image.save` when saving the figure. These take precedence
- over *quality*, *optimize* and *progressive*.
- """
- FigureCanvasAgg.draw(self)
- if dryrun:
- return
- # The image is pasted onto a white background image to handle
- # transparency.
- image = Image.fromarray(np.asarray(self.buffer_rgba()))
- background = Image.new('RGB', image.size, "white")
- background.paste(image, image)
- if pil_kwargs is None:
- pil_kwargs = {}
- for k in ["quality", "optimize", "progressive"]:
- if k in kwargs:
- pil_kwargs.setdefault(k, kwargs[k])
- pil_kwargs.setdefault("quality", rcParams["savefig.jpeg_quality"])
- pil_kwargs.setdefault("dpi", (self.figure.dpi, self.figure.dpi))
- return background.save(
- filename_or_obj, format='jpeg', **pil_kwargs)
- print_jpeg = print_jpg
- @cbook._delete_parameter("3.2", "dryrun")
- def print_tif(self, filename_or_obj, *args, dryrun=False,
- pil_kwargs=None, **kwargs):
- FigureCanvasAgg.draw(self)
- if dryrun:
- return
- if pil_kwargs is None:
- pil_kwargs = {}
- pil_kwargs.setdefault("dpi", (self.figure.dpi, self.figure.dpi))
- return (Image.fromarray(np.asarray(self.buffer_rgba()))
- .save(filename_or_obj, format='tiff', **pil_kwargs))
- print_tiff = print_tif
- @_Backend.export
- class _BackendAgg(_Backend):
- FigureCanvas = FigureCanvasAgg
- FigureManager = FigureManagerBase
|