Hide keyboard shortcuts

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 

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""" 

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 

31 

32import numpy as np 

33 

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 

44 

45from matplotlib.backends._backend_agg import RendererAgg as _RendererAgg 

46 

47from matplotlib.backend_bases import _has_pil 

48 

49if _has_pil: 

50 from PIL import Image 

51 

52backend_version = 'v2.2' 

53 

54 

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']] 

65 

66 

67class RendererAgg(RendererBase): 

68 """ 

69 The renderer handles all the drawing primitives using a graphics 

70 context instance that controls the colors/styles 

71 """ 

72 

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. 

83 

84 lock = threading.RLock() 

85 

86 def __init__(self, width, height, dpi): 

87 RendererBase.__init__(self) 

88 

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 = [] 

94 

95 self._update_methods() 

96 self.mathtext_parser = MathTextParser('Agg') 

97 

98 self.bbox = Bbox.from_bounds(0, 0, self.width, self.height) 

99 

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} 

104 

105 def __setstate__(self, state): 

106 self.__init__(state['width'], state['height'], state['dpi']) 

107 

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 

117 

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 

124 

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] 

129 

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)") 

156 

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) 

163 

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) 

169 

170 def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): 

171 # docstring inherited 

172 

173 if ismath: 

174 return self.draw_mathtext(gc, x, y, s, prop, angle) 

175 

176 flags = get_hinting_flag() 

177 font = self._get_agg_font(prop) 

178 

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) 

195 

196 def get_text_width_height_descent(self, s, prop, ismath): 

197 # docstring inherited 

198 

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 

206 

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 

211 

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 

221 

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() 

226 

227 texmanager = self.get_texmanager() 

228 

229 Z = texmanager.get_grey(s, size, self.dpi) 

230 Z = np.array(Z * 255.0, np.uint8) 

231 

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) 

238 

239 def get_canvas_width_height(self): 

240 # docstring inherited 

241 return self.width, self.height 

242 

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) 

249 

250 font.clear() 

251 size = prop.get_size_in_points() 

252 font.set_size(size, self.dpi) 

253 

254 return font 

255 

256 def points_to_pixels(self, points): 

257 # docstring inherited 

258 return points * self.dpi / 72 

259 

260 def buffer_rgba(self): 

261 return memoryview(self._renderer) 

262 

263 def tostring_argb(self): 

264 return np.asarray(self._renderer).take([3, 0, 1, 2], axis=2).tobytes() 

265 

266 def tostring_rgb(self): 

267 return np.asarray(self._renderer).take([0, 1, 2], axis=2).tobytes() 

268 

269 def clear(self): 

270 self._renderer.clear() 

271 

272 def option_image_nocomposite(self): 

273 # docstring inherited 

274 

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 

279 

280 def option_scale_image(self): 

281 # docstring inherited 

282 return False 

283 

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. 

291 

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)) 

296 

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 

305 

306 if xy is None: 

307 ox, oy = x1, y1 

308 else: 

309 ox, oy = xy 

310 

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)) 

315 

316 else: 

317 self._renderer.restore_region(region) 

318 

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() 

327 

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. 

332 

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 

341 

342 The saved renderer is restored and the returned image from 

343 post_processing is plotted (using draw_image) on it. 

344 """ 

345 

346 width, height = int(self.width), int(self.height) 

347 

348 buffer, (l, b, w, h) = self.tostring_rgba_minimized() 

349 

350 self._renderer = self._filter_renderers.pop() 

351 self._update_methods() 

352 

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) 

362 

363 

364class FigureCanvasAgg(FigureCanvasBase): 

365 """ 

366 The canvas the figure renders into. Calls the draw and print fig 

367 methods, creates the renderers, etc... 

368 

369 Attributes 

370 ---------- 

371 figure : `matplotlib.figure.Figure` 

372 A high-level Figure instance 

373 

374 """ 

375 

376 def copy_from_bbox(self, bbox): 

377 renderer = self.get_renderer() 

378 return renderer.copy_from_bbox(bbox) 

379 

380 def restore_region(self, region, bbox=None, xy=None): 

381 renderer = self.get_renderer() 

382 return renderer.restore_region(region, bbox, xy) 

383 

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() 

397 

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 

409 

410 def tostring_rgb(self): 

411 """Get the image as an RGB byte string. 

412 

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. 

415 

416 Returns 

417 ------- 

418 bytes 

419 """ 

420 return self.renderer.tostring_rgb() 

421 

422 def tostring_argb(self): 

423 """Get the image as an ARGB byte string. 

424 

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. 

427 

428 Returns 

429 ------- 

430 bytes 

431 """ 

432 return self.renderer.tostring_argb() 

433 

434 def buffer_rgba(self): 

435 """Get the image as a memoryview to the renderer's buffer. 

436 

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. 

439 

440 Returns 

441 ------- 

442 memoryview 

443 """ 

444 return self.renderer.buffer_rgba() 

445 

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()) 

451 

452 print_rgba = print_raw 

453 

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. 

459 

460 Parameters 

461 ---------- 

462 filename_or_obj : str or PathLike or file-like object 

463 The file to write to. 

464 

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. 

470 

471 The `PNG specification`_ defines some common keywords that may be 

472 used as appropriate: 

473 

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. 

486 

487 Other keywords may be invented for other purposes. 

488 

489 If 'Software' is not given, an autogenerated value for matplotlib 

490 will be used. 

491 

492 For more details see the `PNG specification`_. 

493 

494 .. _PNG specification: \ 

495 https://www.w3.org/TR/2003/REC-PNG-20031110/#11keywords 

496 

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`. 

501 

502 If the 'pnginfo' key is present, it completely overrides 

503 *metadata*, including the default 'Software' key. 

504 """ 

505 from matplotlib import _png 

506 

507 if metadata is None: 

508 metadata = {} 

509 default_metadata = { 

510 "Software": 

511 f"matplotlib version{__version__}, http://matplotlib.org/", 

512 } 

513 

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)) 

532 

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}) 

538 

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))) 

544 

545 if _has_pil: 

546 

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). 

550 

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. 

556 

557 Parameters 

558 ---------- 

559 filename_or_obj : str or PathLike or file-like object 

560 The file to write to. 

561 

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. 

569 

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. 

574 

575 progressive : bool 

576 If present, indicates that this image 

577 should be stored as a progressive JPEG file. 

578 

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) 

601 

602 print_jpeg = print_jpg 

603 

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)) 

615 

616 print_tiff = print_tif 

617 

618 

619@_Backend.export 

620class _BackendAgg(_Backend): 

621 FigureCanvas = FigureCanvasAgg 

622 FigureManager = FigureManagerBase