Logo Search packages:      
Sourcecode: matplotlib version File versions


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

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

_debug = False
#_debug = True

# Image formats that this backend supports - for print_figure()
IMAGE_FORMAT          = ['eps', 'pdf', 'png', 'ps', 'svg']

# Image::color_conv(format) for draw_image()
if sys.byteorder == 'little':

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


        if fill_c:
            ctx.set_source_rgb (*fill_c)
            #if stroke_c:   # always (implicitly) set at the moment
            #    ctx.fill()

        #if stroke_c:                      # always stroke
            #ctx.set_source_rgb (stroke_c) # is already set

        #_.ctx.restore() # revert to the default attributes

    def draw_arc(self, gc, rgbFace, x, y, width, height, angle1, angle2,
        if _debug: print '%s.%s()' % (self.__class__.__name__, _fn_name())
        # this implementation draws circular arcs where width=height
        # FIXME
        # to get a proper arc of width/height you can use translate() and
        # scale(), see cairo_arc() manual page

        ctx = gc.ctx
        ctx.new_path()  # prevent cairo_arc() adding to current path
        radius = (height + width) / 4
        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())


        rows, cols, buf = im.color_conv (BYTE_FORMAT)
        surface = cairo.ImageSurface.create_for_data (
                      buf, cairo.FORMAT_ARGB32, rows, cols, rows*4)

        # function does not pass a 'gc' so use renderer.ctx
        ctx = self.ctx
        y = self.height - y - rows
        ctx.set_source_surface (surface, x, y)


    def draw_line(self, gc, x1, y1, x2, y2):
        if _debug: print '%s.%s()' % (self.__class__.__name__, _fn_name())
        ctx = gc.ctx
        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.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:

        for x,y in izip(x,y):
            ctx.translate(x, self.height - y)
            generate_path (path_list)

            self._fill_and_stroke (ctx, rgbFace)

            ctx.restore() # undo translate()


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

        x, y = points[0]
        ctx.move_to (x, y)
        for x,y in points[1:]:
            ctx.line_to (x, y)

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

           ctx = gc.ctx
           ctx.move_to (x, y)
           ctx.select_font_face (prop.get_name(),
                                 self.fontangles [prop.get_style()],

           # size = prop.get_size_in_points() * self.dpi.get() / 96.0
           size = prop.get_size_in_points() * self.dpi.get() / 72.0

           if angle:
              ctx.rotate (-angle * numx.pi / 180)
           ctx.set_font_size (size)
           ctx.show_text (s)

    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)
        surface = cairo.ImageSurface.create_for_data (pa, cairo.FORMAT_ARGB32,
                                                      imw, imh, imw*4)
        gc.ctx.set_source_surface (surface, x, y)
        #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.select_font_face (prop.get_name(),
                              self.fontangles [prop.get_style()],

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

        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()
        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):
        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])
            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.rectangle (x, self.renderer.height - h - y, w, h)
        ctx.clip ()
        # Alternative: just set _cliprect here and actually set cairo clip rect
        # in fill_and_stroke() inside ctx.save() ... ctx.restore()

    def set_dashes(self, offset, dashes):
        self._dashes = offset, dashes
        if dashes == None:
            self.ctx.set_dash([], 0)  # switch dashes off
            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)

    def set_graylevel(self, frac):
        GraphicsContextBase.set_graylevel(self, frac)

    def set_joinstyle(self, js):
        if js in ('miter', 'round', 'bevel'):
            self._joinstyle = js
            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))

00502 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

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

        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)
           warnings.warn ("unknown format: %s" % format)

        # 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.rectangle(0, 0, width_in_points, height_in_points)
            ctx.select_font_face ('sans-serif')
            ctx.show_text('Origin corner')


Generated by  Doxygen 1.6.0   Back to index