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