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