svgcanvas.py :  » Chart-Report » Pychart » PyChart-1.39 » pychart » Python Open Source

Home
Python Open Source
1.3.1.2 Python
2.Ajax
3.Aspect Oriented
4.Blog
5.Build
6.Business Application
7.Chart Report
8.Content Management Systems
9.Cryptographic
10.Database
11.Development
12.Editor
13.Email
14.ERP
15.Game 2D 3D
16.GIS
17.GUI
18.IDE
19.Installer
20.IRC
21.Issue Tracker
22.Language Interface
23.Log
24.Math
25.Media Sound Audio
26.Mobile
27.Network
28.Parser
29.PDF
30.Project Management
31.RSS
32.Search
33.Security
34.Template Engines
35.Test
36.UML
37.USB Serial
38.Web Frameworks
39.Web Server
40.Web Services
41.Web Unit
42.Wiki
43.Windows
44.XML
Python Open Source » Chart Report » Pychart 
Pychart » PyChart 1.39 » pychart » svgcanvas.py
#
# Copyright (C) 2000-2005 by Yasushi Saito (yasushi.saito@gmail.com)
#
# Jockey is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the
# Free Software Foundation; either version 2, or (at your option) any
# later version.
#
# Jockey is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
# for more details.
#
import sys,string,re,math
from xml.dom.minidom import Document,Comment
import theme
import basecanvas
import version
from scaling import *

# Note we flip all y-coords and negate all angles because SVG's coord
# system is inverted wrt postscript/PDF - note it's not enough to
# scale(1,-1) since that turns text into mirror writing with wrong origin

_comment_p = 0                           # whether comment() writes output

def _svgcolor(color):
    """
    Convert a PyChart color object to an SVG rgb() value.
    See color.py.
    """
    return 'rgb(%d,%d,%d)' % tuple(map(lambda x:int(255*x),
                                       [color.r,color.g,color.b]))

def _parse_style_str(s):
    """
    Take an SVG 'style' attribute string like 'stroke:none;fill:black'
    and parse it into a dictionary like {'stroke' : 'none', 'fill' : 'black'}.
    """
    styledict = {}
    if s :
        # parses L -> R so later keys overwrite earlier ones
        for keyval in s.split(';'):
            l = keyval.strip().split(':')
            if l and len(l) == 2: styledict[l[0].strip()] = l[1].strip()
    return styledict

def _make_style_str(styledict):
    """
    Make an SVG style string from the dictionary. See also _parse_style_str also.
    """
    s = ''
    for key in styledict.keys():
        s += "%s:%s;"%(key, styledict[key])
    return s

def _protect_current_children(elt):
    # If elt is a group, check to see whether there are any non-comment
    # children, and if so, create a new group to hold attributes
    # to avoid affecting previous children.  Return either the current
    # elt or the newly generated group.
    if (elt.nodeName == 'g') :
        for kid in elt.childNodes :
            if kid.nodeType != Comment.nodeType:
                g = elt.ownerDocument.createElement('g')
                g.setAttribute('auto','')
                if _comment_p:
                    g.appendChild(g.ownerDocument.createComment
                                  ('auto-generated group'))
                elt.appendChild(g)
                elt = g
                break
    return elt

class T(basecanvas.T):
    def __init__(self, fname):
        basecanvas.T.__init__(self)
        self.__out_fname = fname
        self.__xmin, self.__xmax, self.__ymin, self.__ymax = 0,0,0,0
        self.__doc = Document()
        self.__doc.appendChild(self.__doc.createComment
             ('Created by PyChart ' + version.version + ' ' + version.copyright))
        self.__svg = self.__doc.createElement('svg') # the svg doc
        self.__doc.appendChild(self.__svg)
        self.__defs = self.__doc.createElement('defs') # for clip paths
        self.__svg.appendChild(self.__defs)
        self.__cur_element = self.__svg
        self.gsave()       # create top-level group for dflt styles
        self.__update_style(font_family = theme.default_font_family,
                            font_size = theme.default_font_size,
                            font_style = 'normal',
                            font_weight = 'normal',
                            font_stretch = 'normal',
                            fill = 'none',
                            stroke = 'rgb(0,0,0)', #SVG dflt none, PS dflt blk
                            stroke_width = theme.default_line_width,
                            stroke_linejoin = 'miter',
                            stroke_linecap = 'butt',
                            stroke_dasharray = 'none')

    def __update_style(self, **addstyledict):
        elt = _protect_current_children(self.__cur_element)

        # fetch the current styles for this node
        my_style_dict = _parse_style_str(elt.getAttribute('style'))

        # concat all ancestor style strings to get default styles for this node
        parent, s = elt.parentNode, ''
        while parent.nodeType != Document.nodeType :
            # prepend parent str so later keys will override earlier ones
            s = parent.getAttribute('style') + s
            parent = parent.parentNode
        default_style_dict = _parse_style_str(s)

        # Do some pre-processing on the caller-supplied add'l styles
        # Convert '_' to '-' so caller can specify style tags as python
        # variable names, eg. stroke_width => stroke-width.
        # Also convert all RHS values to strs
        for key in addstyledict.keys():
            k = re.sub('_','-',key)
            addstyledict[k] = str(addstyledict[key]) # all vals => strs
            if (k != key) : del addstyledict[key]

        for k in addstyledict.keys() :
            if (my_style_dict.has_key(k) or # need to overwrite it
                (not default_style_dict.has_key(k)) or # need to set it
                default_style_dict[k] != addstyledict[k]) : # need to override it
                my_style_dict[k] = addstyledict[k]

        s = _make_style_str(my_style_dict)
        if s : elt.setAttribute('style',s)

        self.__cur_element = elt

    ####################################################################
    # methods below define the pychart backend device API

    # First are a set of methods to start, construct and finalize a path

    def newpath(self):                  # Start a new path
        if (self.__cur_element.nodeName != 'g') :
            raise OverflowError, "No containing group for newpath"
        # Just insert a new 'path' element into the document
        p = self.__doc.createElement('path')
        self.__cur_element.appendChild(p)
        self.__cur_element = p

    # This set of methods add data to an existing path element,
    # simply add to the 'd' (data) attribute of the path elt

    def moveto(self, x, y):             #
        if (self.__cur_element.nodeName != 'path') :
            raise OverflowError, "No path for moveto"
        d = ' '.join([self.__cur_element.getAttribute('d'),'M',`x`,`-y`]).strip()
        self.__cur_element.setAttribute('d', d)
    def lineto(self, x, y):
        if (self.__cur_element.nodeName != 'path') :
            raise OverflowError, "No path for lineto"
        d = ' '.join([self.__cur_element.getAttribute('d'),'L',`x`,`-y`]).strip()
        self.__cur_element.setAttribute('d', d)
    def path_arc(self, x, y, radius, ratio, start_angle, end_angle):
        # mimic PS 'arc' given radius, yr/xr (=eccentricity), start and
        # end angles.  PS arc draws from CP (if exists) to arc start,
        # then draws arc in counterclockwise dir from start to end
        # SVG provides an arc command that draws a segment of an
        # ellipse (but not a full circle) given these args:
        # A xr yr rotate majorArcFlag counterclockwiseFlag xe ye
        # We don't use rotate(=0) and flipped axes => all arcs are clockwise

        if (self.__cur_element.nodeName != 'path') :
            raise OverflowError, "No path for path_arc"

        self.comment('x=%g, y=%g, r=%g, :=%g, %g-%g'
                     % (x,y,radius,ratio,start_angle,end_angle))

        xs = x+radius*math.cos(2*math.pi/360.*start_angle)
        ys = y+ratio*radius*math.sin(2*math.pi/360.*start_angle)
        xe = x+radius*math.cos(2*math.pi/360.*end_angle)
        ye = y+ratio*radius*math.sin(2*math.pi/360.*end_angle)
        if (end_angle < start_angle) :  # make end bigger than start
            while end_angle <= start_angle: # '<=' so 360->0 becomes 360->720
                end_angle += 360
        full_circ = (end_angle - start_angle >= 360) # draw a full circle?

        d = self.__cur_element.getAttribute('d')
        d += ' %s %g %g' % (d and 'L' or 'M',xs,-ys) # draw from CP, if exists
        if (radius > 0) : # skip, eg. 0-radius 'rounded' corners which blowup
            if (full_circ) :
                # If we're drawing a full circle, move to the end coord
                # and draw half a circle to the reflected xe,ye
                d += ' M %g %g A %g %g 0 1 0 %g %g'%(xe,-ye,
                                                     radius,radius*ratio,
                                                     2*x-xe,-(2*y-ye))
            # Draw arc from the CP (either reflected xe,ye for full circle else
            # xs,ys) to the end coord - note with full_circ the
            # 'bigArcFlag' value is moot, with exactly 180deg left to draw
            d += ' A %g %g 0 %d 0 %g %g' % (radius,radius*ratio,
                                            end_angle-start_angle>180,
                                            xe,-ye)
        self.__cur_element.setAttribute('d',d.strip())
    def curveto(self, x1,y1,x2,y2,x3,y3):
        # Equivalent of PostScript's x1 y1 x2 y2 x3 y3 curveto which
        # draws a cubic bezier curve from curr pt to x3,y3 with ctrl points
        # x1,y1, and x2,y2
        # In SVG this is just d='[M x0 y0] C x1 y1 x2 y2 x3 y3'
        #! I can't find an example of this being used to test it
        if (self.__cur_element.nodeNode != 'path') :
            raise OverflowError, "No path for curveto"
        d = ' '.join([self.__cur_element.getAttribute('d'),'C',
                      `x1`,`-y1`,`x2`,`-y2`,`x3`,`-y3`,]).strip()
        self.__cur_element.setAttribute('d', d)
    def closepath(self):                # close back to start of path
        if (self.__cur_element.nodeName != 'path') :
            raise OverflowError, "No path for closepath"
        d = ' '.join([self.__cur_element.getAttribute('d'),'Z']).strip()
        self.__cur_element.setAttribute('d', d)

    # Next we have three methods for finalizing a path element,
    # either fill it, clip to it, or draw it (stroke)
    # canvas.polygon() can generate fill/clip cmds with
    # no corresponding path so just ignore them
    def stroke(self):
        if (self.__cur_element.nodeName != 'path') :
            self.comment('No path - ignoring stroke')
            return
        self.__update_style(fill='none')
        self.__cur_element = self.__cur_element.parentNode
    def fill(self):
        if (self.__cur_element.nodeName != 'path') :
            self.comment('No path - ignoring fill')
            return
        self.__update_style(stroke='none')
        self.__cur_element = self.__cur_element.parentNode
    def clip_sub(self):
        if (self.__cur_element.nodeName != 'path') :
            self.comment('No path - ignoring clip')
            return

        # remove the current path from the tree ...
        p = self.__cur_element
        self.__cur_element=p.parentNode
        self.__cur_element.removeChild(p)

        # ... add it to a clipPath elt in the defs section
        clip = self.__doc.createElement('clipPath')
        clipid = 'clip'+`len(self.__defs.childNodes)`
        clip.setAttribute('id',clipid)
        clip.appendChild(p)
        self.__defs.appendChild(clip)

        # ... update the local style to point to it
        self.__update_style(clip_path = 'url(#%s)'%clipid)

    # The text_xxx routines specify the start/end and contents of text
    def text_begin(self):
        if (self.__cur_element.nodeName != 'g') :
            raise ValueError, "No group for text block"
        t = self.__doc.createElement('text')
        self.__cur_element.appendChild(t)
        self.__cur_element = t
    def text_moveto(self, x, y, angle):
        if (self.__cur_element.nodeName != 'text') :
            raise ValueError, "No text for moveto"
        self.__cur_element.setAttribute('x',`x`)
        self.__cur_element.setAttribute('y',`-y`)
        if (angle) :
            self.__cur_element.setAttribute('transform',
                                        'rotate(%g,%g,%g)' % (-angle,x,-y))
    def text_show(self, font_name, size, color, string):
        if (self.__cur_element.nodeName != 'text') :
            raise ValueError, "No text for show"

        # PyChart constructs a postscript font name, for example:
        #
        # Helvetica Helvetica-Bold Helvetica-Oblique Helvetica-BoldOblique
        # Helvetica-Narrow Times-Roman Times-Italic
        # Symbol Palatino-Roman Bookman-Demi Courier AvantGarde-Book
        #
        # We need to deconstruct this to get the font-family (the
        # piece before the '-'), and other characteristics.
        # Note that 'Courier' seems to correspond to SVGs 'CourierNew'
        # and that the SVG Symbol font is Unicode where the ascii text
        # 'Symbol' doesn't create greek characters like 'Sigma ...' -
        # should really pass a unicode string, or provide translation
        #
        # SVG defines:
        # font-style = normal (aka roman) | italic | oblique
        # font-weight = normal | bold (aka demi?)
        # font-stretch = normal | wider | narrower | ultra-condensed |
        #  extra-condensed | condensed | semi-condensed |
        #  semi-expanded | expanded | extra-expanded | ultra-expanded
        # ('narrow' seems to correspond to 'condensed')
        font_name, modifiers = re.match(r'([^-]*)(-.*)?', font_name).groups()
        if font_name == 'Courier' : font_name = 'CourierNew'
        font_style = font_weight = font_stretch = 'normal'
        if modifiers :
            if re.search('Italic',modifiers) : font_style = 'italic'
            elif re.search('Oblique',modifiers) : font_style = 'oblique'
            if re.search('Bold|Demi',modifiers) : font_weight = 'bold'
            if re.search('Narrow',modifiers) : font_stretch = 'condensed'
        #! translate ascii symbol font chars -> unicode (see www.unicode.org)
        #! http://www.unicode.org/Public/MAPPINGS/VENDORS/ADOBE/symbol.txt
        #! but xml Text element writes unicode chars as '?' to XML file...
        string = re.sub(r'\\([()])',r'\1',string) # unescape brackets
        self.__update_style(fill=_svgcolor(color),
                          stroke='none',
                          font_family=font_name,
                          font_size=size,
                          font_style=font_style,
                          font_weight=font_weight,
                          font_stretch=font_stretch)
        self.__cur_element.appendChild(self.__doc.createTextNode(string.encode('utf-8')))
    def text_end(self):
        if (self.__cur_element.nodeName != 'text') :
            raise ValueError, "No text for close"
        self.__cur_element = self.__cur_element.parentNode


    # Three methods that change the local style of elements
    # If applied to a group, they persist until the next grestore,
    # If applied within a path element, they only affect that path -
    # although this may not in general correspond to (say) PostScript
    # behavior, it appears to correspond to reflect mode of use of this API
    def set_fill_color(self, color):
        self.__update_style(fill=_svgcolor(color))
    def set_stroke_color(self, color):
        self.__update_style(stroke=_svgcolor(color))
    def set_line_style(self, style):  # see line_style.py
        linecap = {0:'butt', 1:'round', 2:'square'}
        linejoin = {0:'miter', 1:'round', 2:'bevel'}
        if style.dash: dash = ','.join(map(str,style.dash))
        else : dash = 'none'
        self.__update_style(stroke_width = style.width,
                          stroke = _svgcolor(style.color),
                          stroke_linecap = linecap[style.cap_style],
                          stroke_linejoin = linejoin[style.join_style],
                          stroke_dasharray = dash)

    # gsave & grestore respectively push & pop a new context to hold
    # new style and transform parameters.  push/pop transformation are
    # similar but explicitly specify a coordinate transform at the
    # same time
    def gsave(self):
        if (self.__cur_element.nodeName not in ['g','svg']) :
            raise ValueError, "No group for gsave"
        g = self.__doc.createElement('g')
        self.__cur_element.appendChild(g)
        self.__cur_element = g
    def grestore(self):
        if (self.__cur_element.nodeName != 'g'):
            raise ValueError, "No group for grestore"
        # first pop off any auto-generated groups (see protectCurrentChildren)
        while (self.__cur_element.hasAttribute('auto')) :
            self.__cur_element.removeAttribute('auto')
            self.__cur_element = self.__cur_element.parentNode
        # then pop off the original caller-generated group
        self.__cur_element = self.__cur_element.parentNode

    def push_transformation(self, baseloc, scale, angle, in_text=0):
        #? in_text arg appears to always be ignored

        # In some cases this gets called after newpath, with
        # corresonding pop_transformation called after the path is
        # finalized so we check specifically for that, and generate
        # an enclosing group to hold the incomplete path element
        # We could add the transform directly to the path element
        # (like we do with line-style etc) but that makes it harder
        # to handle the closing 'pop' and might lead to inconsitency
        # with PostScript if the closing pop doesn't come right after
        # the path element

        elt = self.__cur_element
        if elt.nodeName == 'g':
            elt = None
        elif (elt.nodeName == 'path' and not elt.hasAttribute('d')) :
            g = elt.parentNode
            g.removeChild(elt)
            self.__cur_element = g
        else:
            raise ValueError, "Illegal placement of push_transformation"

        t = ''
        if baseloc :
            t += 'translate(%g,%g) '%(baseloc[0],-baseloc[1])
        if angle :
            t += 'rotate(%g) '%-angle
        if scale :
            t += 'scale(%g,%g) '%tuple(scale)

        self.gsave()
        self.__cur_element.setAttribute('transform',t.strip())
        if elt:                         # elt has incomplete 'path' or None
            self.__cur_element.appendChild(elt)
            self.__cur_element = elt

    def pop_transformation(self, in_text=0): #? in_text unused?
        self.grestore()

    # If verbose, add comments to the output stream (helps debugging)
    def comment(self, string):
        if _comment_p :
            self.__cur_element.appendChild(self.__doc.createComment(string))

    # The verbatim method is currently not supported - presumably with
    # the SVG backend the user would require access to the DOM since
    # we're not directly outputting plain text here
    def verbatim(self, string):
        self.__cur_element.appendChild(self.__doc.createComment('verbatim not implemented: ' + string))

    # The close() method finalizes the SVG document and flattens the
    # DOM document to XML text to the specified file (or stdout)
    def close(self):
        basecanvas.T.close(self)
        self.grestore()           # matching the gsave in __init__
        if (self.__cur_element.nodeName != 'svg') :
            raise ValueError, "Incomplete document at close!"

        # Don't bother to output an empty document - this can happen
        # when we get close()d immediately by theme reinit
        if (len(self.__svg.childNodes[-1].childNodes) == 0) :
            return

        fp, need_close = self.open_output(self.__out_fname)
        bbox = theme.adjust_bounding_box([self.__xmin, self.__ymin,
                                          self.__xmax, self.__ymax])
        self.__svg.setAttribute('viewBox','%g %g %g %g'
                                % (xscale(bbox[0]),
                                   -yscale(bbox[3]),
                                   xscale(bbox[2])-xscale(bbox[0]),
                                   yscale(bbox[3])-yscale(bbox[1])))
        self.__svg.setAttribute('xmlns','http://www.w3.org/2000/svg')
        self.__svg.setAttribute('xmlns:xlink','http://www.w3.org/1999/xlink')

        self.__doc.writexml(fp,'','  ','\n')
        if need_close:
            fp.close()
www.java2java.com | Contact Us
Copyright 2009 - 12 Demo Source and Support. All rights reserved.
All other trademarks are property of their respective owners.