Logo Search packages:      
Sourcecode: mascyma version File versions  Download package

backend_ps.py

# A postscript backend

# postscript for mathmeatical drawing:
# http://www.math.ubc.ca/people/faculty/cass/graphics/text/www/
from __future__ import division
from cStringIO import StringIO
import sys, os


from matplotlib.cbook import iterable, is_string_like, flatten, enumerate
from matplotlib.backend_bases import RendererBase, GraphicsContextBase,\
     AxisTextBase, FigureBase, FigureManagerBase, _process_text_args,\
     arg_to_rgb
from matplotlib._matlab_helpers import Gcf

from matplotlib.afm import AFM
from matplotlib.cbook import is_string_like, get_recursive_filelist, True, False
from matplotlib.transforms import Bound1D, Bound2D, Transform
from matplotlib import get_data_path

defaultPaperSize = 8.5,11

def error_msg_ps(msg, *args):
    print >>sys.stderr, 'Error:', msg
    sys.exit()


def _nums_to_str(seq, fmt='%1.3f'):
    return ' '.join([_int_or_float(val, fmt) for val in seq])

def _int_or_float(val, fmt='%1.3f'):
    "return val as %d if it's equal to an int, otherwise return fmt%val"
    if is_string_like(val): return val
    ival = int(val)
    if val==ival: return str(ival)
    else: return fmt%val

class RendererPS(RendererBase):
    def __init__(self, pswriter):
        self._pswriter = pswriter

    def draw_postscript(self, ps):
        self._pswriter.write(ps)
        
    def draw_arc(self, gc, faceColor, x, y, width, height, angle1, angle2):

        ps = 'newpath %s ellipse' % _nums_to_str( (x,y,0.5*width,0.5*height,angle1,angle2))
        self._draw_ps(ps, gc, faceColor)
        
    def draw_line(self, gc, x1, y1, x2, y2):
        """
        Draw a single line from x1,y1 to x2,y2
        """
        ps = '%s l' % _nums_to_str( (x1,y1,x2,y2) )
        self._draw_ps(ps, gc, None)

    def draw_lines(self, gc, x, y):
        if len(x)==0: return
        if len(x)!=len(y): error_msg_ps('x and y must be the same length')
        ps =  []
        ps.append('newpath %s moveto' % _nums_to_str((x[0], y[0])))
        for tup in zip(x[1:], y[1:]):
            ps.append('%s lineto' % _nums_to_str(tup))
        self._draw_ps('\n'.join(ps), gc, None)
        
    def draw_rectangle(self, gc, faceColor, x, y, width, height):
        ps = '%s box' % _nums_to_str( (x, y, width, height) )
        self._draw_ps(ps, gc, faceColor)


    def draw_polygon(self, gc, faceColor, points):
        verts = [_nums_to_str(xy) for xy in points]
        # build a 2D postscript array
        ps = 'newpath [ [ %s ] ] make-polygon' % ' ] [  '.join(verts)
        self._draw_ps(ps, gc, faceColor)

    def draw_point(self, gc, x, y):
        # todo: is there a better way to draw points in postscript?
        self.draw_line(gc, x, y, x+1, y+1)
        

    def get_ps(self):
        return self._pswriter.getvalue()
    
    def finish(self):
        self._pswriter.write('showpage\n')

    def new_gc(self):
        return GraphicsContextPS()

    def _draw_ps(self, ps, gc, faceColor):
        if faceColor is not None:
            fill = '%1.3f %1.3f %1.3f setrgbcolor fill' % arg_to_rgb(faceColor)
        else: fill = ''
        gcprops = self._get_gc_props_ps(gc)
        clip = self._get_gc_clip_ps(gc)
        s = """
gsave
 %(clip)s
 %(ps)s
 %(gcprops)s
 gsr
 %(fill)s
grestore
""" % locals()
        self._pswriter.write(s)

    
    def _get_gc_clip_ps(self, gc):
        cliprect = gc.get_clip_rectangle()
        if cliprect is not None:
            box = ['%1.5f'%val for val in cliprect]
            return  '%s clipbox' % ' '.join(box)
        return ''
        
    def _get_gc_props_ps(self, gc):
        setcolor = '%1.3f %1.3f %1.3f setrgbcolor' % gc.get_rgb()
        linewidth  = '%1.5f setlinewidth' % gc.get_linewidth()

        jint = {'miter':0, 'round':1, 'bevel':2}[gc.get_joinstyle()]
        join = '%d setlinejoin' % jint
        cint = {'butt':0, 'round':1, 'projecting':2}[gc.get_capstyle()]
        cap = '%d setlinecap' % cint
        offset, seq = gc.get_dashes()
        #print offset, seq
        if seq is not None:
            seq = ' '.join(['%d'%val for val in  seq])
            dashes = '[%s] %d setdash' % (seq, offset)            
        else:
            dashes = None

        args = (setcolor, linewidth, join, cap, dashes)
        return '\n'.join([s for s in args if s is not None])


class GraphicsContextPS(GraphicsContextBase):

    def set_linestyle(self, style):
        GraphicsContextBase.set_linestyle(self, style)
        offset, dashes = self._dashd[style]
        self.set_dashes(offset, dashes)

              
class FigurePS(FigureBase):
    
    def __init__(self, *args, **kwargs):
        FigureBase.__init__(self, *args, **kwargs)
        self._figurePatch.set_facecolor('w')
        self._figurePatch.set_edgecolor('w')
        self._flush()
        
    def _flush(self):
        self._pswriter = StringIO()
        self._pswriter.write(_psHeader)
               
    def add_axis(self, a):
        FigureBase.add_axis(self, a)

    def draw(self, drawable=None, *args, **kwargs):
        if drawable is None: drawable=self.drawable
        if drawable is None: return 

        self._figurePatch.draw(drawable)

        for a in self.axes:
            a.resize()
            a.draw(drawable)

        for t in self._text:
            t.draw(drawable)

        self._drawable = drawable

    def print_figure(self, filename, dpi):
        'dpi is ignored for PS output, it is set depends on the output device'
        # ignore dpi for ps
        xo = self.dpi.get()*0.5*(defaultPaperSize[0] - self.figsize[0])
        yo = self.dpi.get()*0.5*(defaultPaperSize[1] - self.figsize[1])

        self._pswriter.write('%1.3f %1.3f translate\n' % (xo,yo))
        self.drawable = RendererPS(self._pswriter)
        self.draw(self.drawable)
        self.drawable.finish()

        basename, ext = os.path.splitext(filename)
        if not len(ext): filename += '.ps'
            
        try: fh = file(filename, 'w')
        except IOError:
            error_msg_ps('Could not open %s for writing' % filename)
            return
        print >>fh, self._drawable.get_ps()
        self._flush()
        
    def realize(self, *args):
        pass

    def text(self, x, y, s, *args, **kwargs):
        """
        Add text to figure at location x,y (relative 0-1 coords) See
        the help for Axis text for the meaning of the other arguments
        """

        override = _process_text_args({}, *args, **kwargs)
        t = AxisTextPS(
            self.dpi, self.bbox,
            x=x, y=y, text=s,
            transx = Transform(Bound1D(0,1), self.bbox.x),
            transy = Transform(Bound1D(0,1), self.bbox.y),
            **override)
        self._text.append(t)
        return t


def draw_if_interactive():
    pass

def show():
    """
    Show all the figures and enter the gtk mainloop

    This should be the last line of your script
    """
    for manager in Gcf.get_all_fig_managers():
        manager.figure.realize()


def new_figure_manager(num, figsize, dpi):
    thisFig = FigurePS(figsize, 72)
    figwin = FigureManagerPS(thisFig, num)
    return figwin

class FigureManagerPS(FigureManagerBase):
    pass

    
00237 class AxisTextPS(AxisTextBase):
    """
    Handle storing and drawing of text in window or data coordinates
    """

    def __init__(self, *args, **kwargs):
        AxisTextBase.__init__(self, *args, **kwargs)
        self._afm = _fontManager.findfont(
            self._fontname,
            self._fontweight,
            self._fontangle)

00249     def _compute_offsets(self):
        """
        Return the (x,y) offsets to adjust for the alignment
        specifications
        """
        #w, h = self._afm.string_width_height(self._text)
        l, b, w, h = self._afm.get_str_bbox(self._text)        
        w *= 0.001*self._fontsize
        h *= 0.001*self._fontsize

        if self._rotation=='vertical':
            w, h = h, w
            if self._horizontalalignment=='center': offsetx = -w/2
            elif self._horizontalalignment=='right': offsetx = -w
            else: offsetx = 0

            if self._verticalalignment=='center': offsety = -h/2
            elif self._verticalalignment=='top': offsety = -h
            else: offsety = 0
        else:
            if self._horizontalalignment=='center': offsetx = -w/2
            elif self._horizontalalignment=='right': offsetx = -w
            else: offsetx = 0

            if self._verticalalignment=='center': offsety = -h/2
            elif self._verticalalignment=='top': offsety = -h
            else: offsety = 0

        return (offsetx, offsety)
        
00279     def _draw(self, drawable, *args, **kwargs):
        """
        Render the text to the drawable (or defaul drawable is drawable is None
        """

        if self._text=='': return
        if self._reset: self._set_font()

        x, y = self._get_xy_display()
        ox, oy = self._compute_offsets()
        fontsize = _int_or_float(self._fontsize)
        pos = _nums_to_str((x+ox,y+oy))


        thetext = '(%s)'%self._text

        fontname = self._afm.get_fontname()
        if self._rotation=='vertical':
            rotate = '90 rotate'
        else:
            rotate = ''

        gc = drawable.new_gc()
        gc.set_foreground(self._color)        
        setcolor = '%1.3f %1.3f %1.3f setrgbcolor' % gc.get_rgb()
            
        ps = """\
gsave
/%(fontname)s findfont
%(fontsize)s scalefont
setfont
%(pos)s moveto
%(rotate)s
%(thetext)s
%(setcolor)s
show
grestore
""" % locals()
        drawable.draw_postscript(ps)
        
        
00320     def draw_rotated(self, drawable):
        """
        Draw the text rotated 90 degrees
        """

        pass

    def get_window_extent(self):
        x, y = self._get_xy_display()
        l,b,w,h = self._afm.get_str_bbox(self._text)

        b *= 0.001*self._fontsize
        w *= 0.001*self._fontsize
        h *= 0.001*self._fontsize
        ox, oy = self._compute_offsets()
        left = x+ox+l
        bottom = y+oy+b
        if self._rotation=='vertical':
            l -= h
            w,h = h,w
        return Bound2D(left, bottom, w, h)

    def _get_window_extent(self):
        x, y = self._get_xy_display()
        l,b,w,h = self._afm.get_bbox_str(self._text).get_bounds()
        w *= 0.001*self._fontsize
        h *= 0.001*self._fontsize
        ox, oy = self._compute_offsets()
        l = x+ox
        b = y+oy
        if self._rotation=='vertical':
            l -= h
            w,h = h,w
        return Bound2D(l, b, w, h)
        
    
    def _set_font(self):
        "Update the font object"
        self._afm = _fontManager.findfont(
            self._fontname,
            self._fontweight,
            self._fontangle)
        self._reset = False






FigureManager = FigureManagerPS
AxisText = AxisTextPS
Figure = FigurePS
error_msg = error_msg_ps

class FontManager:

    def __init__(self):
        self._build_afm_dict()
        self._cached = {}  
        #keys = self._dname.keys()
        #keys.sort()
        #print keys

    def _get_afm_filenames(self):
        print 'get afm'
        paths = [get_data_path()]
        if os.environ.has_key('AFMPATH'):
            afmpath = os.environ['AFMPATH']
            paths.extend(afmpath.split(os.pathsep))

        fnames = [fname for fname in get_recursive_filelist(paths) 
                  if fname.lower().find('.afm')>0 and
                  os.path.exists(fname)]

        return fnames

    def _build_afm_dict(self):
        """
        Builds two dicts, dname maps FontName to afm instance and a
        dfamily maps from family name to a dict mapping weight to afm
        instance

        Eg,
        dname['Times-Bold'] = amf
        dfamily['Times']['Bold'] = amf
        
        """
        fnames = self._get_afm_filenames()
        dname = {}
        dfamily = {}
        for fname in fnames:
            try: fh = file(fname, 'r')
            except OSError, msg:
                error_msg_ps('Could not open AFM file %s for reading'%f)
                continue
            afm = AFM(fh)
            name = afm.get_fullname()
            family = afm.get_familyname()
            weight = afm.get_weight()
            dname[name.lower()] = afm
            dfamily.setdefault(family.lower(), []).append(afm)
        if not len(dname):
            error_msg_ps('Could not find any AFM files; please set AFMPATH to point to some readable adobe font metric files')
            sys.exit()
        self._dname = dname
        self._dfamily = dfamily

        
    def findfont(self, fontstr, fontweight='normal', fontangle='normal'):

        if self._cached.has_key( (fontstr, fontweight, fontangle) ):
            return self._cached[ (fontstr, fontweight, fontangle) ]
        
        
        fontstr = fontstr.lower()
        if self._dfamily.has_key(fontstr):
            matches = self._dfamily[fontstr]
            afm = self._get_best(matches, fontweight, fontangle)
        else:
            afm = self.get_default_font()

        self._cached[ (fontstr, fontweight, fontangle) ] = afm
        return afm

    def _get_best(self, candidates, fontweight, fontangle):

        LIGHT, NORMALW, BOLD = -1, 0, 1
        ITALIC, NORMALA, OBLIQUE = -1, 0, 1

        if fontweight.find('light')>=0: targetw = LIGHT
        elif fontweight.find('bold')>=0: targetw = BOLD
        else: targetw = NORMALW
        
        if fontangle.find('italic')>=0: targeta = ITALIC
        elif fontangle.find('oblique')>=0: targeta = OBLIQUE
        else: targeta = NORMALA

        target = (targetw, targeta)
        #print 'target', target
        if len(candidates)==0:
            return self.get_default_font()

        for c in candidates:
            w = c.get_weight()
            a = c.get_angle()
            name = c.get_fullname().lower()

            # test on the basis of font names
            if name.find('light')>=0: thisw = LIGHT
            elif name.find('bold')>=0: thisw = BOLD
            else: thisw = NORMALW
        
            if name.find('italic')>=0: thisa = ITALIC
            elif name.find('oblique')>=0: thisa = OBLIQUE
            else: thisa = NORMALA

            if target == (thisw, thisa): return c

        # failed that, try, try again
        for c in candidates:
            w = c.get_weight().lower()
            a = c.get_angle()
            name = a.get_fullname().lower()

            # test on the basis of font weight and angle
            if w.find('bold')>=0: thisw = BOLD
            else: thisw = NORMALW
            
            if a<0: thisa = ITALIC
            else: thisa = NORMALA

            if target == (thisw, thisa): return c

        return self.get_default_font()        

    def get_default_font(self):
        print >>sys.stderr, 'Falling back on default font'
        # if you've ever returned one, return it for always
        try: return self._defaultFont
        except AttributeError: pass
        keys = ('times roman', 'courier',
                'helvetica', 'utopia regular',
                'new century schoolbook roman')
        for k in keys:
            if self._dname.has_key(k):
                self._defaultFont = self._dname[k]
                return self._defaultFont
        # OK, I give up, returning a random font!
        self._defaultFont = self._dname.popitem()[1]
        return self._defaultFont

_fontManager = FontManager()  # one module instance
        

_psHeader = """\
%!
%% Created by matplotlib http://matplotlib.sourceforge.net

/inch {72 mul} def      % Convert inches->points (1/72 inch)
/gsr {gsave stroke grestore} def

/l { newpath moveto lineto } def

% http://www.mactech.com/articles/mactech/Vol.09/09.04/PostscriptTutorial/

/box %called as: leftx bottomy xdimension ydimension box
{
/yval exch def 
/xval exch def
newpath
moveto
0 yval rlineto
xval 0 rlineto
0 yval neg rlineto
closepath
}
bind def

/clipbox %called as: leftx bottomy xdimension ydimension box
{
/yval exch def
/xval exch def
newpath
moveto
0 yval rlineto
xval 0 rlineto
0 yval neg rlineto
closepath
clip
}
bind def




/make-polygon
{
3 dict
begin
  /a exch def
  /n a length def
  n 1 gt
    {
    a 0 get 0 get a 0 get 1 get moveto
    1 1 n 1 sub \
    {
     /i exch def
     a i get 0 get   a i get 1 get lineto
    }
    for
    }
  if
 closepath
end

}
def

/mtrx matrix def                            % Allocate a matrix for the save
                                  % matrix operation below.
/ellipse                                    % ellipse adds a counter-clockwise
  { /endangle exch def                      % segment of an elliptical arc to
    /startangle exch def                    % the current path. The ellipse
    /yrad exch def                          % procedure takes six operands:
    /xrad exch def                          % the x and y coordinates of the
    /y exch def                             % center of the ellipse (the
    /x exch def                             % center is defined as the point
                                  % of intersection of the major and
                                  % minor axes), the ``radius'' of
                                  % the ellipse in the x direction,
                                  % the ``radius'' of the ellipse in
                                  % the y direction, the starting
                                  % angle of the elliptical arc and
                                  % the ending angle of the
                                  % elliptical arc.
                                               
                                  % The basic strategy used in
                                  % drawing the ellipse is to
                                  % translate to the center of the
                                  % ellipse, scale the user
                                  % coordinate system by the x and y
                                  % radius values, and then add a
                                  % circular arc, centered at the
                                  % origin with a 1 unit radius to
                                  % the current path. We will be
                                  % transforming the user coordinate
                                  % system with the translate and
                                  % rotate operators to add the
                                  % elliptical arc segment but we
                                  % don't want these transformations
                                  % to affect other parts of the
                                  % program. In other words, we
                                  % would like to localize the
                                  % effect of the transformations.
                                  % Usually the gsave and grestore
                                  % operators would be ideal
                                  % candidates for this task.
                                  % Unfortunately gsave and grestore
                                  % are inappropriate for this
                                  % situation because we cannot save
                                  % the arc segment that we have
                                  % added to the path. Instead we
                                  % will localize the effect of the
                                  % transformations by saving the
                                  % current transformation matrix
                                  % and restoring it explicitly
                                  % after we have added the
                                  % elliptical arc to the path.
                                                       
    /savematrix mtrx currentmatrix def      % Save the current transformation.
    x y translate                           % Translate to the center of the
                                  % ellipse.
    xrad yrad scale                         % Scale by the x and y radius
                                  % values.
    0 0 1 startangle endangle arc           % Add the arc segment to the path.
    savematrix setmatrix                    % Restore the transformation.
  } def

"""
         

Generated by  Doxygen 1.6.0   Back to index