00001 """ A Cairo backend for matplotlib Author: Steve Chaplin Cairo is a vector graphics library with cross-device output support. Features of Cairo: * anti-aliasing * alpha channel * saves image files as PNG, PostScript, PDF http://cairographics.org Requires (in order, all available from Cairo website): cairo, pycairo Naming Conventions * classes MixedUpperCase * varables lowerUpper * functions underscore_separated """ from __future__ import division import os import sys import warnings def _fn_name(): return sys._getframe(1).f_code.co_name import cairo _version_required = (1,2,0) if cairo.version_info < _version_required: raise SystemExit ("Pycairo %d.%d.%d is installed\n" "Pycairo %d.%d.%d or later is required" % (cairo.version_info + _version_required)) backend_version = cairo.version del _version_required from matplotlib.backend_bases import RendererBase, GraphicsContextBase,\ FigureManagerBase, FigureCanvasBase from matplotlib.cbook import enumerate, izip from matplotlib.figure import Figure from matplotlib.mathtext import math_parse_s_ft2font import matplotlib.numerix as numx from matplotlib.transforms import Bbox if hasattr (cairo.ImageSurface, 'create_for_array'): HAVE_CAIRO_NUMPY = True else: HAVE_CAIRO_NUMPY = False _debug = False #_debug = True # Image formats that this backend supports - for print_figure() IMAGE_FORMAT = ['eps', 'pdf', 'png', 'ps', 'svg'] IMAGE_FORMAT_DEFAULT = 'png' class RendererCairo(RendererBase): fontweights = { 100 : cairo.FONT_WEIGHT_NORMAL, 200 : cairo.FONT_WEIGHT_NORMAL, 300 : cairo.FONT_WEIGHT_NORMAL, 400 : cairo.FONT_WEIGHT_NORMAL, 500 : cairo.FONT_WEIGHT_NORMAL, 600 : cairo.FONT_WEIGHT_BOLD, 700 : cairo.FONT_WEIGHT_BOLD, 800 : cairo.FONT_WEIGHT_BOLD, 900 : cairo.FONT_WEIGHT_BOLD, 'ultralight' : cairo.FONT_WEIGHT_NORMAL, 'light' : cairo.FONT_WEIGHT_NORMAL, 'normal' : cairo.FONT_WEIGHT_NORMAL, 'medium' : cairo.FONT_WEIGHT_NORMAL, 'semibold' : cairo.FONT_WEIGHT_BOLD, 'bold' : cairo.FONT_WEIGHT_BOLD, 'heavy' : cairo.FONT_WEIGHT_BOLD, 'ultrabold' : cairo.FONT_WEIGHT_BOLD, 'black' : cairo.FONT_WEIGHT_BOLD, } fontangles = { 'italic' : cairo.FONT_SLANT_ITALIC, 'normal' : cairo.FONT_SLANT_NORMAL, 'oblique' : cairo.FONT_SLANT_OBLIQUE, } def __init__(self, dpi): """ """ if _debug: print '%s.%s()' % (self.__class__.__name__, _fn_name()) self.dpi = dpi self.text_ctx = cairo.Context ( cairo.ImageSurface (cairo.FORMAT_ARGB32,1,1)) def set_ctx_from_surface (self, surface): self.ctx = cairo.Context (surface) self.ctx.save() # restore, save - when call new_gc() def set_width_height(self, width, height): self.width = width self.height = height self.matrix_flipy = cairo.Matrix (yy=-1, y0=self.height) # use matrix_flipy for ALL rendering? # - problem with text? - will need to switch matrix_flipy off, or do a # font transform? def _fill_and_stroke (self, ctx, fill_c): #assert fill_c or stroke_c #_.ctx.save() if fill_c: ctx.save() ctx.set_source_rgb (*fill_c) #if stroke_c: # always (implicitly) set at the moment ctx.fill_preserve() #else: # ctx.fill() ctx.restore() #if stroke_c: # always stroke #ctx.set_source_rgb (stroke_c) # is already set ctx.stroke() #_.ctx.restore() # revert to the default attributes def draw_arc(self, gc, rgbFace, x, y, width, height, angle1, angle2, rotation): if _debug: print '%s.%s()' % (self.__class__.__name__, _fn_name()) # draws circular arcs where width=height # FIXME # to get a proper arc of width/height you can use translate() and # scale(), see draw_arc() manual page #radius = (height + width) / 4 ctx = gc.ctx ctx.save() ctx.rotate(rotation) ctx.scale(width / 2.0, height / 2.0) ctx.arc(0.0, 0.0, 1.0, 0.0, 2*numx.pi) ctx.restore() #ctx.new_path() #ctx.arc (x, self.height - y, radius, # angle1 * numx.pi/180.0, angle2 * numx.pi/180.0) self._fill_and_stroke (ctx, rgbFace) def draw_image(self, x, y, im, bbox): # bbox - not currently used if _debug: print '%s.%s()' % (self.__class__.__name__, _fn_name()) if numx.which[0] == "numarray": warnings.warn("draw_image() currently works for numpy, but not " "numarray") return if not HAVE_CAIRO_NUMPY: warnings.warn("cairo with Numeric support is required for " "draw_image()") return im.flipud_out() rows, cols, buf = im.buffer_argb32() # ARGB32, but colors still wrong X = numx.fromstring (buf, numx.UInt8) X.shape = rows, cols, 4 # function does not pass a 'gc' so use renderer.ctx ctx = self.ctx surface = cairo.ImageSurface.create_for_array (X) ctx.set_source_surface (surface, x, y) ctx.paint() im.flipud_out() def draw_line(self, gc, x1, y1, x2, y2): if _debug: print '%s.%s()' % (self.__class__.__name__, _fn_name()) ctx = gc.ctx ctx.new_path() ctx.move_to (x1, self.height - y1) ctx.line_to (x2, self.height - y2) self._fill_and_stroke (ctx, None) def draw_lines(self, gc, x, y, transform=None): if _debug: print '%s.%s()' % (self.__class__.__name__, _fn_name()) if transform: if transform.need_nonlinear(): x, y = transform.nonlinear_only_numerix(x, y) x, y = transform.numerix_x_y(x, y) ctx = gc.ctx matrix_old = ctx.get_matrix() ctx.set_matrix (self.matrix_flipy) points = izip(x,y) x, y = points.next() ctx.new_path() ctx.move_to (x, y) for x,y in points: ctx.line_to (x, y) self._fill_and_stroke (ctx, None) ctx.set_matrix (matrix_old) def draw_markers_OLD(self, gc, path, rgbFace, x, y, transform): if _debug: print '%s.%s()' % (self.__class__.__name__, _fn_name()) ctx = gc.ctx if transform.need_nonlinear(): x,y = transform.nonlinear_only_numerix(x, y) x, y = transform.numerix_x_y(x, y) # do nonlinear and affine transform # TODO - use cairo transform # matrix worked for dotted lines, but not markers in line_styles.py # it upsets/transforms generate_path() ? # need to flip y too, and update generate_path() ? # the a,b,c,d,tx,ty affine which transforms x and y #vec6 = transform.as_vec6_val() # not used (yet) #matrix_old = ctx.get_matrix() #ctx.set_matrix (cairo.Matrix (*vec6)) path_list = [path.vertex() for i in range(path.total_vertices())] def generate_path (path_list): for code, xp, yp in path_list: if code == agg.path_cmd_move_to: ctx.move_to (xp, -yp) elif code == agg.path_cmd_line_to: ctx.line_to (xp, -yp) elif code == agg.path_cmd_end_poly: ctx.close_path() for x,y in izip(x,y): ctx.save() ctx.new_path() ctx.translate(x, self.height - y) generate_path (path_list) self._fill_and_stroke (ctx, rgbFace) ctx.restore() # undo translate() #ctx.set_matrix(matrix_old) def draw_point(self, gc, x, y): if _debug: print '%s.%s()' % (self.__class__.__name__, _fn_name()) # render by drawing a 0.5 radius circle ctx = gc.ctx ctx.new_path() ctx.arc (x, self.height - y, 0.5, 0, 2*numx.pi) self._fill_and_stroke (ctx, gc.get_rgb()) def draw_polygon(self, gc, rgbFace, points): if _debug: print '%s.%s()' % (self.__class__.__name__, _fn_name()) ctx = gc.ctx matrix_old = ctx.get_matrix() ctx.set_matrix (self.matrix_flipy) ctx.new_path() x, y = points[0] ctx.move_to (x, y) for x,y in points[1:]: ctx.line_to (x, y) ctx.close_path() self._fill_and_stroke (ctx, rgbFace) ctx.set_matrix (matrix_old) def draw_rectangle(self, gc, rgbFace, x, y, width, height): if _debug: print '%s.%s()' % (self.__class__.__name__, _fn_name()) ctx = gc.ctx ctx.new_path() ctx.rectangle (x, self.height - y - height, width, height) self._fill_and_stroke (ctx, rgbFace) def draw_text(self, gc, x, y, s, prop, angle, ismath=False): # Note: x,y are device/display coords, not user-coords, unlike other # draw_* methods if _debug: print '%s.%s()' % (self.__class__.__name__, _fn_name()) if ismath: self._draw_mathtext(gc, x, y, s, prop, angle) else: ctx = gc.ctx ctx.new_path() ctx.move_to (x, y) ctx.select_font_face (prop.get_name(), self.fontangles [prop.get_style()], self.fontweights[prop.get_weight()]) # size = prop.get_size_in_points() * self.dpi.get() / 96.0 size = prop.get_size_in_points() * self.dpi.get() / 72.0 ctx.save() if angle: ctx.rotate (-angle * numx.pi / 180) ctx.set_font_size (size) ctx.show_text (s) ctx.restore() def _draw_mathtext(self, gc, x, y, s, prop, angle): if _debug: print '%s.%s()' % (self.__class__.__name__, _fn_name()) # mathtext using the gtk/gdk method if numx.which[0] == "numarray": warnings.warn("_draw_mathtext() currently works for numpy, but " "not numarray") return if not HAVE_CAIRO_NUMPY: warnings.warn("cairo with Numeric support is required for " "_draw_mathtext()") return size = prop.get_size_in_points() width, height, fonts = math_parse_s_ft2font( s, self.dpi.get(), size) if angle==90: width, height = height, width x -= width y -= height imw, imh, s = fonts[0].image_as_str() N = imw*imh # a numpixels by num fonts array Xall = numx.zeros((N,len(fonts)), typecode=numx.UInt8) for i, font in enumerate(fonts): if angle == 90: font.horiz_image_to_vert_image() # <-- Rotate imw, imh, s = font.image_as_str() Xall[:,i] = numx.fromstring(s, numx.UInt8) # get the max alpha at each pixel Xs = numx.mlab.max (Xall,1) # convert it to it's proper shape Xs.shape = imh, imw pa = numx.zeros(shape=(imh,imw,4), typecode=numx.UInt8) rgb = gc.get_rgb() pa[:,:,0] = int(rgb[0]*255) pa[:,:,1] = int(rgb[1]*255) pa[:,:,2] = int(rgb[2]*255) pa[:,:,3] = Xs # works for numpy pa, not a numarray pa surface = cairo.ImageSurface.create_for_array (pa) gc.ctx.set_source_surface (surface, x, y) gc.ctx.paint() #gc.ctx.show_surface (surface, imw, imh) def flipy(self): if _debug: print '%s.%s()' % (self.__class__.__name__, _fn_name()) return True #return False # tried - all draw objects ok except text (and images?) # which comes out mirrored! def get_canvas_width_height(self): if _debug: print '%s.%s()' % (self.__class__.__name__, _fn_name()) return self.width, self.height def get_text_width_height(self, s, prop, ismath): if _debug: print '%s.%s()' % (self.__class__.__name__, _fn_name()) if ismath: width, height, fonts = math_parse_s_ft2font( s, self.dpi.get(), prop.get_size_in_points()) return width, height ctx = self.text_ctx ctx.save() ctx.select_font_face (prop.get_name(), self.fontangles [prop.get_style()], self.fontweights[prop.get_weight()]) # Cairo (says it) uses 1/96 inch user space units, ref: cairo_gstate.c # but if /96.0 is used the font is too small #size = prop.get_size_in_points() * self.dpi.get() / 96.0 size = prop.get_size_in_points() * self.dpi.get() / 72.0 # problem - scale remembers last setting and font can become # enormous causing program to crash # save/restore prevents the problem ctx.set_font_size (size) w, h = ctx.text_extents (s)[2:4] ctx.restore() return w, h def new_gc(self): if _debug: print '%s.%s()' % (self.__class__.__name__, _fn_name()) self.ctx.restore() # matches save() in set_ctx_from_surface() self.ctx.save() return GraphicsContextCairo (renderer=self) def points_to_pixels(self, points): if _debug: print '%s.%s()' % (self.__class__.__name__, _fn_name()) return points/72.0 * self.dpi.get() class GraphicsContextCairo(GraphicsContextBase): _joind = { 'bevel' : cairo.LINE_JOIN_BEVEL, 'miter' : cairo.LINE_JOIN_MITER, 'round' : cairo.LINE_JOIN_ROUND, } _capd = { 'butt' : cairo.LINE_CAP_BUTT, 'projecting' : cairo.LINE_CAP_SQUARE, 'round' : cairo.LINE_CAP_ROUND, } def __init__(self, renderer): GraphicsContextBase.__init__(self) self.renderer = renderer self.ctx = renderer.ctx def set_alpha(self, alpha): self._alpha = alpha rgb = self._rgb self.ctx.set_source_rgba (rgb[0], rgb[1], rgb[2], alpha) #def set_antialiased(self, b): # enable/disable anti-aliasing is not (yet) supported by Cairo def set_capstyle(self, cs): if cs in ('butt', 'round', 'projecting'): self._capstyle = cs self.ctx.set_line_cap (self._capd[cs]) else: raise ValueError('Unrecognized cap style. Found %s' % cs) def set_clip_rectangle(self, rectangle): self._cliprect = rectangle x,y,w,h = rectangle # pixel-aligned clip-regions are faster x,y,w,h = round(x), round(y), round(w), round(h) ctx = self.ctx ctx.new_path() ctx.rectangle (x, self.renderer.height - h - y, w, h) # enabline ctx.clip() causes problems: # line_styles.py - only see first axes # simple_plot.py - lose text #ctx.clip () def set_dashes(self, offset, dashes): self._dashes = offset, dashes if dashes == None: self.ctx.set_dash([], 0) # switch dashes off else: self.ctx.set_dash ( self.renderer.points_to_pixels (numx.asarray(dashes)), offset) def set_foreground(self, fg, isRGB=None): GraphicsContextBase.set_foreground(self, fg, isRGB) self.ctx.set_source_rgb(*self._rgb) def set_graylevel(self, frac): GraphicsContextBase.set_graylevel(self, frac) self.ctx.set_source_rgb(*self._rgb) def set_joinstyle(self, js): if js in ('miter', 'round', 'bevel'): self._joinstyle = js self.ctx.set_line_join(self._joind[js]) else: raise ValueError('Unrecognized join style. Found %s' % js) def set_linewidth(self, w): self._linewidth = w self.ctx.set_line_width (self.renderer.points_to_pixels(w)) 00514 def new_figure_manager(num, *args, **kwargs): # called by backends/__init__.py """ Create a new figure manager instance """ if _debug: print '%s.%s()' % (self.__class__.__name__, _fn_name()) FigureClass = kwargs.pop('FigureClass', Figure) thisFig = FigureClass(*args, **kwargs) canvas = FigureCanvasCairo(thisFig) manager = FigureManagerBase(canvas, num) return manager class FigureCanvasCairo (FigureCanvasBase): def print_figure(self, fo, dpi=150, facecolor='w', edgecolor='w', orientation='portrait', format=None, **kwargs): if _debug: print '%s.%s()' % (self.__class__.__name__, _fn_name()) # settings for printing self.figure.dpi.set(dpi) self.figure.set_facecolor(facecolor) self.figure.set_edgecolor(edgecolor) if format is None and isinstance (fo, basestring): # get format from filename extension format = os.path.splitext(fo)[1][1:] if format is not None: format = format.lower() if format == 'png': self._save_png (fo) elif format in ('pdf', 'ps', 'svg'): self._save (fo, format, orientation, **kwargs) elif format == 'eps': # backend_ps for eps from backend_ps import FigureCanvasPS as FigureCanvas fc = FigureCanvas(self.figure) fc.print_figure (filename, dpi, facecolor, edgecolor, orientation, **kwargs) else: warnings.warn('Format "%s" is not supported.\n' 'Supported formats: ' '%s.' % (format, ', '.join(IMAGE_FORMAT))) def _save_png (self, fobj): width, height = self.get_width_height() renderer = RendererCairo (self.figure.dpi) renderer.set_width_height (width, height) surface = cairo.ImageSurface (cairo.FORMAT_ARGB32, width, height) renderer.set_ctx_from_surface (surface) self.figure.draw (renderer) surface.write_to_png (fobj) def _save (self, fo, format, orientation, **kwargs): # save PDF/PS/SVG orientation = kwargs.get('orientation', 'portrait') dpi = 72 self.figure.dpi.set (dpi) w_in, h_in = self.figure.get_size_inches() width_in_points, height_in_points = w_in * dpi, h_in * dpi if orientation == 'landscape': width_in_points, height_in_points = (height_in_points, width_in_points) if format == 'ps': if not cairo.HAS_PS_SURFACE: raise RuntimeError ('cairo has not been compiled with PS ' 'support enabled') surface = cairo.PSSurface (fo, width_in_points, height_in_points) elif format == 'pdf': if not cairo.HAS_PDF_SURFACE: raise RuntimeError ('cairo has not been compiled with PDF ' 'support enabled') surface = cairo.PDFSurface (fo, width_in_points, height_in_points) elif format == 'svg': if not cairo.HAS_SVG_SURFACE: raise RuntimeError ('cairo has not been compiled with SVG ' 'support enabled') surface = cairo.SVGSurface (fo, width_in_points, height_in_points) else: warnings.warn ("unknown format: %s" % format) return # surface.set_dpi() can be used renderer = RendererCairo (self.figure.dpi) renderer.set_width_height (width_in_points, height_in_points) renderer.set_ctx_from_surface (surface) ctx = renderer.ctx if orientation == 'landscape': ctx.rotate (numx.pi/2) ctx.translate (0, -height_in_points) # cairo/src/cairo_ps_surface.c # '%%Orientation: Portrait' is always written to the file header # '%%Orientation: Landscape' would possibly cause problems # since some printers would rotate again ? # TODO: # add portrait/landscape checkbox to FileChooser self.figure.draw (renderer) show_fig_border = False # for testing figure orientation and scaling if show_fig_border: ctx.new_path() ctx.rectangle(0, 0, width_in_points, height_in_points) ctx.set_line_width(4.0) ctx.set_source_rgb(1,0,0) ctx.stroke() ctx.move_to(30,30) ctx.select_font_face ('sans-serif') ctx.set_font_size(20) ctx.show_text('Origin corner') ctx.show_page() surface.finish()