1""" 2An agg http://antigrain.com/ backend 3 4Features 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, optionally JPEG and TIFF 12 * alpha blending 13 * DPI scaling properly - everything scales properly (dashes, linewidths, etc) 14 * draw polygon 15 * freetype2 w/ ft2font 16 17TODO: 18 19 * integrate screen dpi w/ ppi and text 20 21""" 22from __future__ import (absolute_import, division, print_function, 23 unicode_literals) 24 25import six 26 27try: 28 import threading 29except ImportError: 30 import dummy_threading as threading 31 32import numpy as np 33from collections import OrderedDict 34from math import radians, cos, sin 35from matplotlib import cbook, rcParams, __version__ 36from matplotlib.backend_bases import ( 37 _Backend, FigureCanvasBase, FigureManagerBase, RendererBase, cursors) 38from matplotlib.cbook import maxdict 39from matplotlib.figure import Figure 40from matplotlib.font_manager import findfont, get_font 41from matplotlib.ft2font import (LOAD_FORCE_AUTOHINT, LOAD_NO_HINTING, 42 LOAD_DEFAULT, LOAD_NO_AUTOHINT) 43from matplotlib.mathtext import MathTextParser 44from matplotlib.path import Path 45from matplotlib.transforms import Bbox, BboxBase 46from matplotlib import colors as mcolors 47 48from matplotlib.backends._backend_agg import RendererAgg as _RendererAgg 49from matplotlib import _png 50 51try: 52 from PIL import Image 53 _has_pil = True 54except ImportError: 55 _has_pil = False 56 57backend_version = 'v2.2' 58 59def get_hinting_flag(): 60 mapping = { 61 True: LOAD_FORCE_AUTOHINT, 62 False: LOAD_NO_HINTING, 63 'either': LOAD_DEFAULT, 64 'native': LOAD_NO_AUTOHINT, 65 'auto': LOAD_FORCE_AUTOHINT, 66 'none': LOAD_NO_HINTING 67 } 68 return mapping[rcParams['text.hinting']] 69 70 71class RendererAgg(RendererBase): 72 """ 73 The renderer handles all the drawing primitives using a graphics 74 context instance that controls the colors/styles 75 """ 76 77 @property 78 @cbook.deprecated("2.2") 79 def debug(self): 80 return 1 81 82 # we want to cache the fonts at the class level so that when 83 # multiple figures are created we can reuse them. This helps with 84 # a bug on windows where the creation of too many figures leads to 85 # too many open file handles. However, storing them at the class 86 # level is not thread safe. The solution here is to let the 87 # FigureCanvas acquire a lock on the fontd at the start of the 88 # draw, and release it when it is done. This allows multiple 89 # renderers to share the cached fonts, but only one figure can 90 # draw at time and so the font cache is used by only one 91 # renderer at a time. 92 93 lock = threading.RLock() 94 95 def __init__(self, width, height, dpi): 96 RendererBase.__init__(self) 97 98 self.dpi = dpi 99 self.width = width 100 self.height = height 101 self._renderer = _RendererAgg(int(width), int(height), dpi) 102 self._filter_renderers = [] 103 104 self._update_methods() 105 self.mathtext_parser = MathTextParser('Agg') 106 107 self.bbox = Bbox.from_bounds(0, 0, self.width, self.height) 108 109 def __getstate__(self): 110 # We only want to preserve the init keywords of the Renderer. 111 # Anything else can be re-created. 112 return {'width': self.width, 'height': self.height, 'dpi': self.dpi} 113 114 def __setstate__(self, state): 115 self.__init__(state['width'], state['height'], state['dpi']) 116 117 def _get_hinting_flag(self): 118 if rcParams['text.hinting']: 119 return LOAD_FORCE_AUTOHINT 120 else: 121 return LOAD_NO_HINTING 122 123 # for filtering to work with rasterization, methods needs to be wrapped. 124 # maybe there is better way to do it. 125 def draw_markers(self, *kl, **kw): 126 return self._renderer.draw_markers(*kl, **kw) 127 128 def draw_path_collection(self, *kl, **kw): 129 return self._renderer.draw_path_collection(*kl, **kw) 130 131 def _update_methods(self): 132 self.draw_quad_mesh = self._renderer.draw_quad_mesh 133 self.draw_gouraud_triangle = self._renderer.draw_gouraud_triangle 134 self.draw_gouraud_triangles = self._renderer.draw_gouraud_triangles 135 self.draw_image = self._renderer.draw_image 136 self.copy_from_bbox = self._renderer.copy_from_bbox 137 self.get_content_extents = self._renderer.get_content_extents 138 139 def tostring_rgba_minimized(self): 140 extents = self.get_content_extents() 141 bbox = [[extents[0], self.height - (extents[1] + extents[3])], 142 [extents[0] + extents[2], self.height - extents[1]]] 143 region = self.copy_from_bbox(bbox) 144 return np.array(region), extents 145 146 def draw_path(self, gc, path, transform, rgbFace=None): 147 """ 148 Draw the path 149 """ 150 nmax = rcParams['agg.path.chunksize'] # here at least for testing 151 npts = path.vertices.shape[0] 152 153 if (nmax > 100 and npts > nmax and path.should_simplify and 154 rgbFace is None and gc.get_hatch() is None): 155 nch = np.ceil(npts / nmax) 156 chsize = int(np.ceil(npts / nch)) 157 i0 = np.arange(0, npts, chsize) 158 i1 = np.zeros_like(i0) 159 i1[:-1] = i0[1:] - 1 160 i1[-1] = npts 161 for ii0, ii1 in zip(i0, i1): 162 v = path.vertices[ii0:ii1, :] 163 c = path.codes 164 if c is not None: 165 c = c[ii0:ii1] 166 c[0] = Path.MOVETO # move to end of last chunk 167 p = Path(v, c) 168 try: 169 self._renderer.draw_path(gc, p, transform, rgbFace) 170 except OverflowError: 171 raise OverflowError("Exceeded cell block limit (set " 172 "'agg.path.chunksize' rcparam)") 173 else: 174 try: 175 self._renderer.draw_path(gc, path, transform, rgbFace) 176 except OverflowError: 177 raise OverflowError("Exceeded cell block limit (set " 178 "'agg.path.chunksize' rcparam)") 179 180 181 def draw_mathtext(self, gc, x, y, s, prop, angle): 182 """ 183 Draw the math text using matplotlib.mathtext 184 """ 185 ox, oy, width, height, descent, font_image, used_characters = \ 186 self.mathtext_parser.parse(s, self.dpi, prop) 187 188 xd = descent * sin(radians(angle)) 189 yd = descent * cos(radians(angle)) 190 x = np.round(x + ox + xd) 191 y = np.round(y - oy + yd) 192 self._renderer.draw_text_image(font_image, x, y + 1, angle, gc) 193 194 def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): 195 """ 196 Render the text 197 """ 198 if ismath: 199 return self.draw_mathtext(gc, x, y, s, prop, angle) 200 201 flags = get_hinting_flag() 202 font = self._get_agg_font(prop) 203 204 if font is None: 205 return None 206 if len(s) == 1 and ord(s) > 127: 207 font.load_char(ord(s), flags=flags) 208 else: 209 # We pass '0' for angle here, since it will be rotated (in raster 210 # space) in the following call to draw_text_image). 211 font.set_text(s, 0, flags=flags) 212 font.draw_glyphs_to_bitmap(antialiased=rcParams['text.antialiased']) 213 d = font.get_descent() / 64.0 214 # The descent needs to be adjusted for the angle. 215 xo, yo = font.get_bitmap_offset() 216 xo /= 64.0 217 yo /= 64.0 218 xd = -d * sin(radians(angle)) 219 yd = d * cos(radians(angle)) 220 221 self._renderer.draw_text_image( 222 font, np.round(x - xd + xo), np.round(y + yd + yo) + 1, angle, gc) 223 224 def get_text_width_height_descent(self, s, prop, ismath): 225 """ 226 Get the width, height, and descent (offset from the bottom 227 to the baseline), in display coords, of the string *s* with 228 :class:`~matplotlib.font_manager.FontProperties` *prop* 229 """ 230 if ismath in ["TeX", "TeX!"]: 231 # todo: handle props 232 size = prop.get_size_in_points() 233 texmanager = self.get_texmanager() 234 fontsize = prop.get_size_in_points() 235 w, h, d = texmanager.get_text_width_height_descent( 236 s, fontsize, renderer=self) 237 return w, h, d 238 239 if ismath: 240 ox, oy, width, height, descent, fonts, used_characters = \ 241 self.mathtext_parser.parse(s, self.dpi, prop) 242 return width, height, descent 243 244 flags = get_hinting_flag() 245 font = self._get_agg_font(prop) 246 font.set_text(s, 0.0, flags=flags) 247 w, h = font.get_width_height() # width and height of unrotated string 248 d = font.get_descent() 249 w /= 64.0 # convert from subpixels 250 h /= 64.0 251 d /= 64.0 252 return w, h, d 253 254 def draw_tex(self, gc, x, y, s, prop, angle, ismath='TeX!', mtext=None): 255 # todo, handle props, angle, origins 256 size = prop.get_size_in_points() 257 258 texmanager = self.get_texmanager() 259 260 Z = texmanager.get_grey(s, size, self.dpi) 261 Z = np.array(Z * 255.0, np.uint8) 262 263 w, h, d = self.get_text_width_height_descent(s, prop, ismath) 264 xd = d * sin(radians(angle)) 265 yd = d * cos(radians(angle)) 266 x = np.round(x + xd) 267 y = np.round(y + yd) 268 269 self._renderer.draw_text_image(Z, x, y, angle, gc) 270 271 def get_canvas_width_height(self): 272 'return the canvas width and height in display coords' 273 return self.width, self.height 274 275 def _get_agg_font(self, prop): 276 """ 277 Get the font for text instance t, cacheing for efficiency 278 """ 279 fname = findfont(prop) 280 font = get_font(fname) 281 282 font.clear() 283 size = prop.get_size_in_points() 284 font.set_size(size, self.dpi) 285 286 return font 287 288 def points_to_pixels(self, points): 289 """ 290 convert point measures to pixes using dpi and the pixels per 291 inch of the display 292 """ 293 return points*self.dpi/72.0 294 295 def tostring_rgb(self): 296 return self._renderer.tostring_rgb() 297 298 def tostring_argb(self): 299 return self._renderer.tostring_argb() 300 301 def buffer_rgba(self): 302 return self._renderer.buffer_rgba() 303 304 def clear(self): 305 self._renderer.clear() 306 307 def option_image_nocomposite(self): 308 # It is generally faster to composite each image directly to 309 # the Figure, and there's no file size benefit to compositing 310 # with the Agg backend 311 return True 312 313 def option_scale_image(self): 314 """ 315 agg backend doesn't support arbitrary scaling of image. 316 """ 317 return False 318 319 def restore_region(self, region, bbox=None, xy=None): 320 """ 321 Restore the saved region. If bbox (instance of BboxBase, or 322 its extents) is given, only the region specified by the bbox 323 will be restored. *xy* (a tuple of two floasts) optionally 324 specifies the new position (the LLC of the original region, 325 not the LLC of the bbox) where the region will be restored. 326 327 >>> region = renderer.copy_from_bbox() 328 >>> x1, y1, x2, y2 = region.get_extents() 329 >>> renderer.restore_region(region, bbox=(x1+dx, y1, x2, y2), 330 ... xy=(x1-dx, y1)) 331 332 """ 333 if bbox is not None or xy is not None: 334 if bbox is None: 335 x1, y1, x2, y2 = region.get_extents() 336 elif isinstance(bbox, BboxBase): 337 x1, y1, x2, y2 = bbox.extents 338 else: 339 x1, y1, x2, y2 = bbox 340 341 if xy is None: 342 ox, oy = x1, y1 343 else: 344 ox, oy = xy 345 346 # The incoming data is float, but the _renderer type-checking wants 347 # to see integers. 348 self._renderer.restore_region(region, int(x1), int(y1), 349 int(x2), int(y2), int(ox), int(oy)) 350 351 else: 352 self._renderer.restore_region(region) 353 354 def start_filter(self): 355 """ 356 Start filtering. It simply create a new canvas (the old one is saved). 357 """ 358 self._filter_renderers.append(self._renderer) 359 self._renderer = _RendererAgg(int(self.width), int(self.height), 360 self.dpi) 361 self._update_methods() 362 363 def stop_filter(self, post_processing): 364 """ 365 Save the plot in the current canvas as a image and apply 366 the *post_processing* function. 367 368 def post_processing(image, dpi): 369 # ny, nx, depth = image.shape 370 # image (numpy array) has RGBA channels and has a depth of 4. 371 ... 372 # create a new_image (numpy array of 4 channels, size can be 373 # different). The resulting image may have offsets from 374 # lower-left corner of the original image 375 return new_image, offset_x, offset_y 376 377 The saved renderer is restored and the returned image from 378 post_processing is plotted (using draw_image) on it. 379 """ 380 381 # WARNING: For agg_filter to work, the renderer's method need to 382 # overridden in the class. See draw_markers and draw_path_collections. 383 384 width, height = int(self.width), int(self.height) 385 386 buffer, bounds = self.tostring_rgba_minimized() 387 388 l, b, w, h = bounds 389 390 self._renderer = self._filter_renderers.pop() 391 self._update_methods() 392 393 if w > 0 and h > 0: 394 img = np.frombuffer(buffer, np.uint8) 395 img, ox, oy = post_processing(img.reshape((h, w, 4)) / 255., 396 self.dpi) 397 gc = self.new_gc() 398 if img.dtype.kind == 'f': 399 img = np.asarray(img * 255., np.uint8) 400 img = img[::-1] 401 self._renderer.draw_image( 402 gc, l + ox, height - b - h + oy, img) 403 404 405class FigureCanvasAgg(FigureCanvasBase): 406 """ 407 The canvas the figure renders into. Calls the draw and print fig 408 methods, creates the renderers, etc... 409 410 Attributes 411 ---------- 412 figure : `matplotlib.figure.Figure` 413 A high-level Figure instance 414 415 """ 416 417 def copy_from_bbox(self, bbox): 418 renderer = self.get_renderer() 419 return renderer.copy_from_bbox(bbox) 420 421 def restore_region(self, region, bbox=None, xy=None): 422 renderer = self.get_renderer() 423 return renderer.restore_region(region, bbox, xy) 424 425 def draw(self): 426 """ 427 Draw the figure using the renderer 428 """ 429 self.renderer = self.get_renderer(cleared=True) 430 # acquire a lock on the shared font cache 431 RendererAgg.lock.acquire() 432 433 toolbar = self.toolbar 434 try: 435 # if toolbar: 436 # toolbar.set_cursor(cursors.WAIT) 437 self.figure.draw(self.renderer) 438 # A GUI class may be need to update a window using this draw, so 439 # don't forget to call the superclass. 440 super(FigureCanvasAgg, self).draw() 441 finally: 442 # if toolbar: 443 # toolbar.set_cursor(toolbar._lastCursor) 444 RendererAgg.lock.release() 445 446 def get_renderer(self, cleared=False): 447 l, b, w, h = self.figure.bbox.bounds 448 key = w, h, self.figure.dpi 449 try: self._lastKey, self.renderer 450 except AttributeError: need_new_renderer = True 451 else: need_new_renderer = (self._lastKey != key) 452 453 if need_new_renderer: 454 self.renderer = RendererAgg(w, h, self.figure.dpi) 455 self._lastKey = key 456 elif cleared: 457 self.renderer.clear() 458 return self.renderer 459 460 def tostring_rgb(self): 461 '''Get the image as an RGB byte string 462 463 `draw` must be called at least once before this function will work and 464 to update the renderer for any subsequent changes to the Figure. 465 466 Returns 467 ------- 468 bytes 469 ''' 470 return self.renderer.tostring_rgb() 471 472 def tostring_argb(self): 473 '''Get the image as an ARGB byte string 474 475 `draw` must be called at least once before this function will work and 476 to update the renderer for any subsequent changes to the Figure. 477 478 Returns 479 ------- 480 bytes 481 482 ''' 483 return self.renderer.tostring_argb() 484 485 def buffer_rgba(self): 486 '''Get the image as an RGBA byte string 487 488 `draw` must be called at least once before this function will work and 489 to update the renderer for any subsequent changes to the Figure. 490 491 Returns 492 ------- 493 bytes 494 ''' 495 return self.renderer.buffer_rgba() 496 497 def print_raw(self, filename_or_obj, *args, **kwargs): 498 FigureCanvasAgg.draw(self) 499 renderer = self.get_renderer() 500 original_dpi = renderer.dpi 501 renderer.dpi = self.figure.dpi 502 if isinstance(filename_or_obj, six.string_types): 503 fileobj = open(filename_or_obj, 'wb') 504 close = True 505 else: 506 fileobj = filename_or_obj 507 close = False 508 try: 509 fileobj.write(renderer._renderer.buffer_rgba()) 510 finally: 511 if close: 512 fileobj.close() 513 renderer.dpi = original_dpi 514 print_rgba = print_raw 515 516 def print_png(self, filename_or_obj, *args, **kwargs): 517 FigureCanvasAgg.draw(self) 518 renderer = self.get_renderer() 519 original_dpi = renderer.dpi 520 renderer.dpi = self.figure.dpi 521 522 version_str = 'matplotlib version ' + __version__ + \ 523 ', http://matplotlib.org/' 524 metadata = OrderedDict({'Software': version_str}) 525 user_metadata = kwargs.pop("metadata", None) 526 if user_metadata is not None: 527 metadata.update(user_metadata) 528 529 try: 530 with cbook.open_file_cm(filename_or_obj, "wb") as fh: 531 _png.write_png(renderer._renderer, fh, 532 self.figure.dpi, metadata=metadata) 533 finally: 534 renderer.dpi = original_dpi 535 536 def print_to_buffer(self): 537 FigureCanvasAgg.draw(self) 538 renderer = self.get_renderer() 539 original_dpi = renderer.dpi 540 renderer.dpi = self.figure.dpi 541 try: 542 result = (renderer._renderer.buffer_rgba(), 543 (int(renderer.width), int(renderer.height))) 544 finally: 545 renderer.dpi = original_dpi 546 return result 547 548 if _has_pil: 549 # add JPEG support 550 def print_jpg(self, filename_or_obj, *args, **kwargs): 551 """ 552 Other Parameters 553 ---------------- 554 quality : int 555 The image quality, on a scale from 1 (worst) to 556 95 (best). The default is 95, if not given in the 557 matplotlibrc file in the savefig.jpeg_quality parameter. 558 Values above 95 should be avoided; 100 completely 559 disables the JPEG quantization stage. 560 561 optimize : bool 562 If present, indicates that the encoder should 563 make an extra pass over the image in order to select 564 optimal encoder settings. 565 566 progressive : bool 567 If present, indicates that this image 568 should be stored as a progressive JPEG file. 569 """ 570 buf, size = self.print_to_buffer() 571 if kwargs.pop("dryrun", False): 572 return 573 # The image is "pasted" onto a white background image to safely 574 # handle any transparency 575 image = Image.frombuffer('RGBA', size, buf, 'raw', 'RGBA', 0, 1) 576 rgba = mcolors.to_rgba(rcParams['savefig.facecolor']) 577 color = tuple([int(x * 255.0) for x in rgba[:3]]) 578 background = Image.new('RGB', size, color) 579 background.paste(image, image) 580 options = {k: kwargs[k] 581 for k in ['quality', 'optimize', 'progressive', 'dpi'] 582 if k in kwargs} 583 options.setdefault('quality', rcParams['savefig.jpeg_quality']) 584 if 'dpi' in options: 585 # Set the same dpi in both x and y directions 586 options['dpi'] = (options['dpi'], options['dpi']) 587 588 return background.save(filename_or_obj, format='jpeg', **options) 589 print_jpeg = print_jpg 590 591 # add TIFF support 592 def print_tif(self, filename_or_obj, *args, **kwargs): 593 buf, size = self.print_to_buffer() 594 if kwargs.pop("dryrun", False): 595 return 596 image = Image.frombuffer('RGBA', size, buf, 'raw', 'RGBA', 0, 1) 597 dpi = (self.figure.dpi, self.figure.dpi) 598 return image.save(filename_or_obj, format='tiff', 599 dpi=dpi) 600 print_tiff = print_tif 601 602 603@_Backend.export 604class _BackendAgg(_Backend): 605 FigureCanvas = FigureCanvasAgg 606 FigureManager = FigureManagerBase 607