1from __future__ import (absolute_import, division, print_function,
2                        unicode_literals)
3
4import six
5
6import warnings
7
8import gobject
9import gtk; gdk = gtk.gdk
10import pango
11pygtk_version_required = (2,2,0)
12if gtk.pygtk_version < pygtk_version_required:
13    raise ImportError ("PyGTK %d.%d.%d is installed\n"
14                      "PyGTK %d.%d.%d or later is required"
15                      % (gtk.pygtk_version + pygtk_version_required))
16del pygtk_version_required
17
18import numpy as np
19
20import matplotlib
21from matplotlib import rcParams
22from matplotlib._pylab_helpers import Gcf
23from matplotlib.backend_bases import (
24    _Backend, FigureCanvasBase, FigureManagerBase, GraphicsContextBase,
25    RendererBase)
26from matplotlib.cbook import warn_deprecated
27from matplotlib.mathtext import MathTextParser
28from matplotlib.transforms import Affine2D
29from matplotlib.backends._backend_gdk import pixbuf_get_pixels_array
30
31backend_version = "%d.%d.%d" % gtk.pygtk_version
32
33# Image formats that this backend supports - for FileChooser and print_figure()
34IMAGE_FORMAT = sorted(['bmp', 'eps', 'jpg', 'png', 'ps', 'svg']) # 'raw', 'rgb'
35IMAGE_FORMAT_DEFAULT = 'png'
36
37
38class RendererGDK(RendererBase):
39    fontweights = {
40        100          : pango.WEIGHT_ULTRALIGHT,
41        200          : pango.WEIGHT_LIGHT,
42        300          : pango.WEIGHT_LIGHT,
43        400          : pango.WEIGHT_NORMAL,
44        500          : pango.WEIGHT_NORMAL,
45        600          : pango.WEIGHT_BOLD,
46        700          : pango.WEIGHT_BOLD,
47        800          : pango.WEIGHT_HEAVY,
48        900          : pango.WEIGHT_ULTRABOLD,
49        'ultralight' : pango.WEIGHT_ULTRALIGHT,
50        'light'      : pango.WEIGHT_LIGHT,
51        'normal'     : pango.WEIGHT_NORMAL,
52        'medium'     : pango.WEIGHT_NORMAL,
53        'semibold'   : pango.WEIGHT_BOLD,
54        'bold'       : pango.WEIGHT_BOLD,
55        'heavy'      : pango.WEIGHT_HEAVY,
56        'ultrabold'  : pango.WEIGHT_ULTRABOLD,
57        'black'      : pango.WEIGHT_ULTRABOLD,
58                   }
59
60    # cache for efficiency, these must be at class, not instance level
61    layoutd = {}  # a map from text prop tups to pango layouts
62    rotated = {}  # a map from text prop tups to rotated text pixbufs
63
64    def __init__(self, gtkDA, dpi):
65        # widget gtkDA is used for:
66        #  '<widget>.create_pango_layout(s)'
67        #  cmap line below)
68        self.gtkDA = gtkDA
69        self.dpi   = dpi
70        self._cmap = gtkDA.get_colormap()
71        self.mathtext_parser = MathTextParser("Agg")
72
73    def set_pixmap (self, pixmap):
74        self.gdkDrawable = pixmap
75
76    def set_width_height (self, width, height):
77        """w,h is the figure w,h not the pixmap w,h
78        """
79        self.width, self.height = width, height
80
81    def draw_path(self, gc, path, transform, rgbFace=None):
82        transform = transform + Affine2D(). \
83            scale(1.0, -1.0).translate(0, self.height)
84        polygons = path.to_polygons(transform, self.width, self.height)
85        for polygon in polygons:
86            # draw_polygon won't take an arbitrary sequence -- it must be a list
87            # of tuples
88            polygon = [(int(np.round(x)), int(np.round(y))) for x, y in polygon]
89            if rgbFace is not None:
90                saveColor = gc.gdkGC.foreground
91                gc.gdkGC.foreground = gc.rgb_to_gdk_color(rgbFace)
92                self.gdkDrawable.draw_polygon(gc.gdkGC, True, polygon)
93                gc.gdkGC.foreground = saveColor
94            if gc.gdkGC.line_width > 0:
95                self.gdkDrawable.draw_lines(gc.gdkGC, polygon)
96
97    def draw_image(self, gc, x, y, im):
98        bbox = gc.get_clip_rectangle()
99
100        if bbox != None:
101            l,b,w,h = bbox.bounds
102            #rectangle = (int(l), self.height-int(b+h),
103            #             int(w), int(h))
104            # set clip rect?
105
106        rows, cols = im.shape[:2]
107
108        pixbuf = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB,
109                                has_alpha=True, bits_per_sample=8,
110                                width=cols, height=rows)
111
112        array = pixbuf_get_pixels_array(pixbuf)
113        array[:, :, :] = im[::-1]
114
115        gc = self.new_gc()
116
117
118        y = self.height-y-rows
119
120        try: # new in 2.2
121            # can use None instead of gc.gdkGC, if don't need clipping
122            self.gdkDrawable.draw_pixbuf (gc.gdkGC, pixbuf, 0, 0,
123                                          int(x), int(y), cols, rows,
124                                          gdk.RGB_DITHER_NONE, 0, 0)
125        except AttributeError:
126            # deprecated in 2.2
127            pixbuf.render_to_drawable(self.gdkDrawable, gc.gdkGC, 0, 0,
128                                  int(x), int(y), cols, rows,
129                                  gdk.RGB_DITHER_NONE, 0, 0)
130
131    def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
132        x, y = int(x), int(y)
133
134        if x < 0 or y < 0: # window has shrunk and text is off the edge
135            return
136
137        if angle not in (0,90):
138            warnings.warn('backend_gdk: unable to draw text at angles ' +
139                          'other than 0 or 90')
140        elif ismath:
141            self._draw_mathtext(gc, x, y, s, prop, angle)
142
143        elif angle==90:
144            self._draw_rotated_text(gc, x, y, s, prop, angle)
145
146        else:
147            layout, inkRect, logicalRect = self._get_pango_layout(s, prop)
148            l, b, w, h = inkRect
149            if (x + w > self.width or y + h > self.height):
150                return
151
152            self.gdkDrawable.draw_layout(gc.gdkGC, x, y-h-b, layout)
153
154    def _draw_mathtext(self, gc, x, y, s, prop, angle):
155        ox, oy, width, height, descent, font_image, used_characters = \
156            self.mathtext_parser.parse(s, self.dpi, prop)
157
158        if angle == 90:
159            width, height = height, width
160            x -= width
161        y -= height
162
163        imw = font_image.get_width()
164        imh = font_image.get_height()
165
166        pixbuf = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, has_alpha=True,
167                                bits_per_sample=8, width=imw, height=imh)
168
169        array = pixbuf_get_pixels_array(pixbuf)
170
171        rgb = gc.get_rgb()
172        array[:,:,0] = int(rgb[0]*255)
173        array[:,:,1] = int(rgb[1]*255)
174        array[:,:,2] = int(rgb[2]*255)
175        array[:,:,3] = (
176            np.fromstring(font_image.as_str(), np.uint8).reshape((imh, imw)))
177
178        # can use None instead of gc.gdkGC, if don't need clipping
179        self.gdkDrawable.draw_pixbuf(gc.gdkGC, pixbuf, 0, 0,
180                                     int(x), int(y), imw, imh,
181                                     gdk.RGB_DITHER_NONE, 0, 0)
182
183    def _draw_rotated_text(self, gc, x, y, s, prop, angle):
184        """
185        Draw the text rotated 90 degrees, other angles are not supported
186        """
187        # this function (and its called functions) is a bottleneck
188        # Pango 1.6 supports rotated text, but pygtk 2.4.0 does not yet have
189        # wrapper functions
190        # GTK+ 2.6 pixbufs support rotation
191
192        gdrawable = self.gdkDrawable
193        ggc = gc.gdkGC
194
195        layout, inkRect, logicalRect = self._get_pango_layout(s, prop)
196        l, b, w, h = inkRect
197        x = int(x-h)
198        y = int(y-w)
199
200        if (x < 0 or y < 0 or # window has shrunk and text is off the edge
201            x + w > self.width or y + h > self.height):
202            return
203
204        key = (x,y,s,angle,hash(prop))
205        imageVert = self.rotated.get(key)
206        if imageVert != None:
207            gdrawable.draw_image(ggc, imageVert, 0, 0, x, y, h, w)
208            return
209
210        imageBack = gdrawable.get_image(x, y, w, h)
211        imageVert = gdrawable.get_image(x, y, h, w)
212        imageFlip = gtk.gdk.Image(type=gdk.IMAGE_FASTEST,
213                                  visual=gdrawable.get_visual(),
214                                  width=w, height=h)
215        if imageFlip == None or imageBack == None or imageVert == None:
216            warnings.warn("Could not renderer vertical text")
217            return
218        imageFlip.set_colormap(self._cmap)
219        for i in range(w):
220            for j in range(h):
221                imageFlip.put_pixel(i, j, imageVert.get_pixel(j,w-i-1) )
222
223        gdrawable.draw_image(ggc, imageFlip, 0, 0, x, y, w, h)
224        gdrawable.draw_layout(ggc, x, y-b, layout)
225
226        imageIn  = gdrawable.get_image(x, y, w, h)
227        for i in range(w):
228            for j in range(h):
229                imageVert.put_pixel(j, i, imageIn.get_pixel(w-i-1,j) )
230
231        gdrawable.draw_image(ggc, imageBack, 0, 0, x, y, w, h)
232        gdrawable.draw_image(ggc, imageVert, 0, 0, x, y, h, w)
233        self.rotated[key] = imageVert
234
235    def _get_pango_layout(self, s, prop):
236        """
237        Create a pango layout instance for Text 's' with properties 'prop'.
238        Return - pango layout (from cache if already exists)
239
240        Note that pango assumes a logical DPI of 96
241        Ref: pango/fonts.c/pango_font_description_set_size() manual page
242        """
243        # problem? - cache gets bigger and bigger, is never cleared out
244        # two (not one) layouts are created for every text item s (then they
245        # are cached) - why?
246
247        key = self.dpi, s, hash(prop)
248        value = self.layoutd.get(key)
249        if value != None:
250            return value
251
252        size = prop.get_size_in_points() * self.dpi / 96.0
253        size = np.round(size)
254
255        font_str = '%s, %s %i' % (prop.get_name(), prop.get_style(), size,)
256        font = pango.FontDescription(font_str)
257
258        # later - add fontweight to font_str
259        font.set_weight(self.fontweights[prop.get_weight()])
260
261        layout = self.gtkDA.create_pango_layout(s)
262        layout.set_font_description(font)
263        inkRect, logicalRect = layout.get_pixel_extents()
264
265        self.layoutd[key] = layout, inkRect, logicalRect
266        return layout, inkRect, logicalRect
267
268    def flipy(self):
269        return True
270
271    def get_canvas_width_height(self):
272        return self.width, self.height
273
274    def get_text_width_height_descent(self, s, prop, ismath):
275        if ismath:
276            ox, oy, width, height, descent, font_image, used_characters = \
277                self.mathtext_parser.parse(s, self.dpi, prop)
278            return width, height, descent
279
280        layout, inkRect, logicalRect = self._get_pango_layout(s, prop)
281        l, b, w, h = inkRect
282        ll, lb, lw, lh = logicalRect
283
284        return w, h + 1, h - lh
285
286    def new_gc(self):
287        return GraphicsContextGDK(renderer=self)
288
289    def points_to_pixels(self, points):
290        return points/72.0 * self.dpi
291
292
293class GraphicsContextGDK(GraphicsContextBase):
294    # a cache shared by all class instances
295    _cached = {}  # map: rgb color -> gdk.Color
296
297    _joind = {
298        'bevel' : gdk.JOIN_BEVEL,
299        'miter' : gdk.JOIN_MITER,
300        'round' : gdk.JOIN_ROUND,
301        }
302
303    _capd = {
304        'butt'       : gdk.CAP_BUTT,
305        'projecting' : gdk.CAP_PROJECTING,
306        'round'      : gdk.CAP_ROUND,
307        }
308
309
310    def __init__(self, renderer):
311        GraphicsContextBase.__init__(self)
312        self.renderer = renderer
313        self.gdkGC    = gtk.gdk.GC(renderer.gdkDrawable)
314        self._cmap    = renderer._cmap
315
316
317    def rgb_to_gdk_color(self, rgb):
318        """
319        rgb - an RGB tuple (three 0.0-1.0 values)
320        return an allocated gtk.gdk.Color
321        """
322        try:
323            return self._cached[tuple(rgb)]
324        except KeyError:
325            color = self._cached[tuple(rgb)] = \
326                    self._cmap.alloc_color(
327                        int(rgb[0]*65535),int(rgb[1]*65535),int(rgb[2]*65535))
328            return color
329
330
331    #def set_antialiased(self, b):
332        # anti-aliasing is not supported by GDK
333
334    def set_capstyle(self, cs):
335        GraphicsContextBase.set_capstyle(self, cs)
336        self.gdkGC.cap_style = self._capd[self._capstyle]
337
338
339    def set_clip_rectangle(self, rectangle):
340        GraphicsContextBase.set_clip_rectangle(self, rectangle)
341        if rectangle is None:
342            return
343        l,b,w,h = rectangle.bounds
344        rectangle = (int(l), self.renderer.height-int(b+h)+1,
345                     int(w), int(h))
346        #rectangle = (int(l), self.renderer.height-int(b+h),
347        #             int(w+1), int(h+2))
348        self.gdkGC.set_clip_rectangle(rectangle)
349
350    def set_dashes(self, dash_offset, dash_list):
351        GraphicsContextBase.set_dashes(self, dash_offset, dash_list)
352
353        if dash_list == None:
354            self.gdkGC.line_style = gdk.LINE_SOLID
355        else:
356            pixels = self.renderer.points_to_pixels(np.asarray(dash_list))
357            dl = [max(1, int(np.round(val))) for val in pixels]
358            self.gdkGC.set_dashes(dash_offset, dl)
359            self.gdkGC.line_style = gdk.LINE_ON_OFF_DASH
360
361
362    def set_foreground(self, fg, isRGBA=False):
363        GraphicsContextBase.set_foreground(self, fg, isRGBA)
364        self.gdkGC.foreground = self.rgb_to_gdk_color(self.get_rgb())
365
366
367    def set_joinstyle(self, js):
368        GraphicsContextBase.set_joinstyle(self, js)
369        self.gdkGC.join_style = self._joind[self._joinstyle]
370
371
372    def set_linewidth(self, w):
373        GraphicsContextBase.set_linewidth(self, w)
374        if w == 0:
375            self.gdkGC.line_width = 0
376        else:
377            pixels = self.renderer.points_to_pixels(w)
378            self.gdkGC.line_width = max(1, int(np.round(pixels)))
379
380
381class FigureCanvasGDK (FigureCanvasBase):
382    def __init__(self, figure):
383        FigureCanvasBase.__init__(self, figure)
384        if self.__class__ == matplotlib.backends.backend_gdk.FigureCanvasGDK:
385            warn_deprecated('2.0', message="The GDK backend is "
386                            "deprecated. It is untested, known to be "
387                            "broken and will be removed in Matplotlib 3.0. "
388                            "Use the Agg backend instead. "
389                            "See Matplotlib usage FAQ for"
390                            " more info on backends.",
391                            alternative="Agg")
392        self._renderer_init()
393
394    def _renderer_init(self):
395        self._renderer = RendererGDK (gtk.DrawingArea(), self.figure.dpi)
396
397    def _render_figure(self, pixmap, width, height):
398        self._renderer.set_pixmap (pixmap)
399        self._renderer.set_width_height (width, height)
400        self.figure.draw (self._renderer)
401
402    filetypes = FigureCanvasBase.filetypes.copy()
403    filetypes['jpg'] = 'JPEG'
404    filetypes['jpeg'] = 'JPEG'
405
406    def print_jpeg(self, filename, *args, **kwargs):
407        return self._print_image(filename, 'jpeg')
408    print_jpg = print_jpeg
409
410    def print_png(self, filename, *args, **kwargs):
411        return self._print_image(filename, 'png')
412
413    def _print_image(self, filename, format, *args, **kwargs):
414        width, height = self.get_width_height()
415        pixmap = gtk.gdk.Pixmap (None, width, height, depth=24)
416        self._render_figure(pixmap, width, height)
417
418        # jpg colors don't match the display very well, png colors match
419        # better
420        pixbuf = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, 0, 8,
421                                width, height)
422        pixbuf.get_from_drawable(pixmap, pixmap.get_colormap(),
423                                 0, 0, 0, 0, width, height)
424
425        # set the default quality, if we are writing a JPEG.
426        # http://www.pygtk.org/docs/pygtk/class-gdkpixbuf.html#method-gdkpixbuf--save
427        options = {k: kwargs[k] for k in ['quality'] if k in kwargs}
428        if format in ['jpg', 'jpeg']:
429            options.setdefault('quality', rcParams['savefig.jpeg_quality'])
430            options['quality'] = str(options['quality'])
431
432        pixbuf.save(filename, format, options=options)
433
434
435@_Backend.export
436class _BackendGDK(_Backend):
437    FigureCanvas = FigureCanvasGDK
438    FigureManager = FigureManagerBase
439