~activity/openobject-server/trunk

« back to all changes in this revision

Viewing changes to bin/pychart/svgcanvas.py

  • Committer: Dainius Malachovskis
  • Date: 2009-06-11 21:01:55 UTC
  • mfrom: (1235.2.5 server)
  • Revision ID: dainius.malachovskis@sandas.eu-20090611210155-ql9k1pd0dnr9avr0
merged

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
1
#
2
2
# Copyright (C) 2000-2005 by Yasushi Saito (yasushi.saito@gmail.com)
3
 
 
3
#
4
4
# Jockey is free software; you can redistribute it and/or modify it
5
5
# under the terms of the GNU General Public License as published by the
6
6
# Free Software Foundation; either version 2, or (at your option) any
24
24
 
25
25
_comment_p = 0                           # whether comment() writes output
26
26
 
27
 
# Convert a PyChart color object to an SVG rgb() value
28
 
def _svgcolor(color):                   # see color.py
 
27
def _svgcolor(color):
 
28
    """
 
29
    Convert a PyChart color object to an SVG rgb() value.
 
30
    See color.py.
 
31
    """
29
32
    return 'rgb(%d,%d,%d)' % tuple(map(lambda x:int(255*x),
30
33
                                       [color.r,color.g,color.b]))
31
34
 
32
 
# Take an SVG 'style' attribute string like 'stroke:none;fill:black'
33
 
# and parse it into a dictionary like {'stroke' : 'none', 'fill' : 'black'}
34
 
def _parseStyleStr(s):
 
35
def _parse_style_str(s):
 
36
    """
 
37
    Take an SVG 'style' attribute string like 'stroke:none;fill:black'
 
38
    and parse it into a dictionary like {'stroke' : 'none', 'fill' : 'black'}.
 
39
    """
35
40
    styledict = {}
36
41
    if s :
37
42
        # parses L -> R so later keys overwrite earlier ones
40
45
            if l and len(l) == 2: styledict[l[0].strip()] = l[1].strip()
41
46
    return styledict
42
47
 
43
 
# Make an SVG style string from the dictionary described above
44
 
def _makeStyleStr(styledict):
 
48
def _make_style_str(styledict):
 
49
    """
 
50
    Make an SVG style string from the dictionary. See also _parse_style_str also.
 
51
    """
45
52
    s = ''
46
53
    for key in styledict.keys():
47
 
        s += "%s:%s;"%(key,styledict[key])
 
54
        s += "%s:%s;"%(key, styledict[key])
48
55
    return s
49
56
 
50
 
def _protectCurrentChildren(elt):
 
57
def _protect_current_children(elt):
51
58
    # If elt is a group, check to see whether there are any non-comment
52
59
    # children, and if so, create a new group to hold attributes
53
60
    # to avoid affecting previous children.  Return either the current
64
71
                elt = g
65
72
                break
66
73
    return elt
67
 
            
 
74
 
68
75
class T(basecanvas.T):
69
76
    def __init__(self, fname):
70
77
        basecanvas.T.__init__(self)
77
84
        self.__doc.appendChild(self.__svg)
78
85
        self.__defs = self.__doc.createElement('defs') # for clip paths
79
86
        self.__svg.appendChild(self.__defs)
80
 
        self.__currElt = self.__svg
 
87
        self.__cur_element = self.__svg
81
88
        self.gsave()       # create top-level group for dflt styles
82
 
        self._updateStyle(font_family = theme.default_font_family,
83
 
                          font_size = theme.default_font_size,
84
 
                          font_style = 'normal',
85
 
                          font_weight = 'normal',
86
 
                          font_stretch = 'normal',
87
 
                          fill = 'none',
88
 
                          stroke = 'rgb(0,0,0)', #SVG dflt none, PS dflt blk
89
 
                          stroke_width = theme.default_line_width,
90
 
                          stroke_linejoin = 'miter',
91
 
                          stroke_linecap = 'butt',
92
 
                          stroke_dasharray = 'none')
93
 
        
94
 
    def _updateStyle(self, **addstyledict): 
95
 
        elt = _protectCurrentChildren(self.__currElt)
 
89
        self.__update_style(font_family = theme.default_font_family,
 
90
                            font_size = theme.default_font_size,
 
91
                            font_style = 'normal',
 
92
                            font_weight = 'normal',
 
93
                            font_stretch = 'normal',
 
94
                            fill = 'none',
 
95
                            stroke = 'rgb(0,0,0)', #SVG dflt none, PS dflt blk
 
96
                            stroke_width = theme.default_line_width,
 
97
                            stroke_linejoin = 'miter',
 
98
                            stroke_linecap = 'butt',
 
99
                            stroke_dasharray = 'none')
 
100
 
 
101
    def __update_style(self, **addstyledict):
 
102
        elt = _protect_current_children(self.__cur_element)
96
103
 
97
104
        # fetch the current styles for this node
98
 
        mystyledict = _parseStyleStr(elt.getAttribute('style'))
 
105
        my_style_dict = _parse_style_str(elt.getAttribute('style'))
99
106
 
100
 
        # concat all parent style strings to get dflt styles for this node
101
 
        parent,s = elt.parentNode,''
 
107
        # concat all ancestor style strings to get default styles for this node
 
108
        parent, s = elt.parentNode, ''
102
109
        while parent.nodeType != Document.nodeType :
103
110
            # prepend parent str so later keys will override earlier ones
104
111
            s = parent.getAttribute('style') + s
105
112
            parent = parent.parentNode
106
 
        dfltstyledict = _parseStyleStr(s)
 
113
        default_style_dict = _parse_style_str(s)
107
114
 
108
115
        # Do some pre-processing on the caller-supplied add'l styles
109
116
        # Convert '_' to '-' so caller can specify style tags as python
110
117
        # variable names, eg. stroke_width => stroke-width.
111
 
        # Also convert all RHS values to strs 
 
118
        # Also convert all RHS values to strs
112
119
        for key in addstyledict.keys():
113
120
            k = re.sub('_','-',key)
114
121
            addstyledict[k] = str(addstyledict[key]) # all vals => strs
115
122
            if (k != key) : del addstyledict[key]
116
123
 
117
124
        for k in addstyledict.keys() :
118
 
            if (mystyledict.has_key(k) or # need to overwrite it
119
 
                (not dfltstyledict.has_key(k)) or # need to set it
120
 
                dfltstyledict[k] != addstyledict[k]) : # need to override it
121
 
                mystyledict[k] = addstyledict[k]
122
 
        
123
 
        s = _makeStyleStr(mystyledict)
 
125
            if (my_style_dict.has_key(k) or # need to overwrite it
 
126
                (not default_style_dict.has_key(k)) or # need to set it
 
127
                default_style_dict[k] != addstyledict[k]) : # need to override it
 
128
                my_style_dict[k] = addstyledict[k]
 
129
 
 
130
        s = _make_style_str(my_style_dict)
124
131
        if s : elt.setAttribute('style',s)
125
132
 
126
 
        self.__currElt = elt
 
133
        self.__cur_element = elt
127
134
 
128
135
    ####################################################################
129
136
    # methods below define the pychart backend device API
130
137
 
131
138
    # First are a set of methods to start, construct and finalize a path
132
 
    
 
139
 
133
140
    def newpath(self):                  # Start a new path
134
 
        if (self.__currElt.nodeName != 'g') :
 
141
        if (self.__cur_element.nodeName != 'g') :
135
142
            raise OverflowError, "No containing group for newpath"
136
143
        # Just insert a new 'path' element into the document
137
144
        p = self.__doc.createElement('path')
138
 
        self.__currElt.appendChild(p)
139
 
        self.__currElt = p
 
145
        self.__cur_element.appendChild(p)
 
146
        self.__cur_element = p
140
147
 
141
148
    # This set of methods add data to an existing path element,
142
149
    # simply add to the 'd' (data) attribute of the path elt
143
 
    
144
 
    def moveto(self, x, y):             # 
145
 
        if (self.__currElt.nodeName != 'path') :
 
150
 
 
151
    def moveto(self, x, y):             #
 
152
        if (self.__cur_element.nodeName != 'path') :
146
153
            raise OverflowError, "No path for moveto"
147
 
        d = ' '.join([self.__currElt.getAttribute('d'),'M',`x`,`-y`]).strip()
148
 
        self.__currElt.setAttribute('d', d)
 
154
        d = ' '.join([self.__cur_element.getAttribute('d'),'M',`x`,`-y`]).strip()
 
155
        self.__cur_element.setAttribute('d', d)
149
156
    def lineto(self, x, y):
150
 
        if (self.__currElt.nodeName != 'path') :
 
157
        if (self.__cur_element.nodeName != 'path') :
151
158
            raise OverflowError, "No path for lineto"
152
 
        d = ' '.join([self.__currElt.getAttribute('d'),'L',`x`,`-y`]).strip()
153
 
        self.__currElt.setAttribute('d', d)
 
159
        d = ' '.join([self.__cur_element.getAttribute('d'),'L',`x`,`-y`]).strip()
 
160
        self.__cur_element.setAttribute('d', d)
154
161
    def path_arc(self, x, y, radius, ratio, start_angle, end_angle):
155
162
        # mimic PS 'arc' given radius, yr/xr (=eccentricity), start and
156
163
        # end angles.  PS arc draws from CP (if exists) to arc start,
160
167
        # A xr yr rotate majorArcFlag counterclockwiseFlag xe ye
161
168
        # We don't use rotate(=0) and flipped axes => all arcs are clockwise
162
169
 
163
 
        if (self.__currElt.nodeName != 'path') :
 
170
        if (self.__cur_element.nodeName != 'path') :
164
171
            raise OverflowError, "No path for path_arc"
165
172
 
166
 
        self.comment('x=%g, y=%g, r=%g, :=%g, %g-%g' 
 
173
        self.comment('x=%g, y=%g, r=%g, :=%g, %g-%g'
167
174
                     % (x,y,radius,ratio,start_angle,end_angle))
168
175
 
169
176
        xs = x+radius*math.cos(2*math.pi/360.*start_angle)
174
181
            while end_angle <= start_angle: # '<=' so 360->0 becomes 360->720
175
182
                end_angle += 360
176
183
        full_circ = (end_angle - start_angle >= 360) # draw a full circle?
177
 
            
178
 
        d = self.__currElt.getAttribute('d')
 
184
 
 
185
        d = self.__cur_element.getAttribute('d')
179
186
        d += ' %s %g %g' % (d and 'L' or 'M',xs,-ys) # draw from CP, if exists
180
187
        if (radius > 0) : # skip, eg. 0-radius 'rounded' corners which blowup
181
188
            if (full_circ) :
190
197
            d += ' A %g %g 0 %d 0 %g %g' % (radius,radius*ratio,
191
198
                                            end_angle-start_angle>180,
192
199
                                            xe,-ye)
193
 
        self.__currElt.setAttribute('d',d.strip())
 
200
        self.__cur_element.setAttribute('d',d.strip())
194
201
    def curveto(self, x1,y1,x2,y2,x3,y3):
195
202
        # Equivalent of PostScript's x1 y1 x2 y2 x3 y3 curveto which
196
203
        # draws a cubic bezier curve from curr pt to x3,y3 with ctrl points
197
204
        # x1,y1, and x2,y2
198
205
        # In SVG this is just d='[M x0 y0] C x1 y1 x2 y2 x3 y3'
199
206
        #! I can't find an example of this being used to test it
200
 
        if (self.__currElt.nodeNode != 'path') :
 
207
        if (self.__cur_element.nodeNode != 'path') :
201
208
            raise OverflowError, "No path for curveto"
202
 
        d = ' '.join([self.__currElt.getAttribute('d'),'C',
 
209
        d = ' '.join([self.__cur_element.getAttribute('d'),'C',
203
210
                      `x1`,`-y1`,`x2`,`-y2`,`x3`,`-y3`,]).strip()
204
 
        self.__currElt.setAttribute('d', d)
 
211
        self.__cur_element.setAttribute('d', d)
205
212
    def closepath(self):                # close back to start of path
206
 
        if (self.__currElt.nodeName != 'path') :
 
213
        if (self.__cur_element.nodeName != 'path') :
207
214
            raise OverflowError, "No path for closepath"
208
 
        d = ' '.join([self.__currElt.getAttribute('d'),'Z']).strip()
209
 
        self.__currElt.setAttribute('d', d)
 
215
        d = ' '.join([self.__cur_element.getAttribute('d'),'Z']).strip()
 
216
        self.__cur_element.setAttribute('d', d)
210
217
 
211
218
    # Next we have three methods for finalizing a path element,
212
219
    # either fill it, clip to it, or draw it (stroke)
213
220
    # canvas.polygon() can generate fill/clip cmds with
214
221
    # no corresponding path so just ignore them
215
222
    def stroke(self):
216
 
        if (self.__currElt.nodeName != 'path') :
 
223
        if (self.__cur_element.nodeName != 'path') :
217
224
            self.comment('No path - ignoring stroke')
218
225
            return
219
 
        self._updateStyle(fill='none')
220
 
        self.__currElt = self.__currElt.parentNode
 
226
        self.__update_style(fill='none')
 
227
        self.__cur_element = self.__cur_element.parentNode
221
228
    def fill(self):
222
 
        if (self.__currElt.nodeName != 'path') :
 
229
        if (self.__cur_element.nodeName != 'path') :
223
230
            self.comment('No path - ignoring fill')
224
231
            return
225
 
        self._updateStyle(stroke='none')
226
 
        self.__currElt = self.__currElt.parentNode
 
232
        self.__update_style(stroke='none')
 
233
        self.__cur_element = self.__cur_element.parentNode
227
234
    def clip_sub(self):
228
 
        if (self.__currElt.nodeName != 'path') :
 
235
        if (self.__cur_element.nodeName != 'path') :
229
236
            self.comment('No path - ignoring clip')
230
237
            return
231
238
 
232
239
        # remove the current path from the tree ...
233
 
        p = self.__currElt
234
 
        self.__currElt=p.parentNode
235
 
        self.__currElt.removeChild(p)
 
240
        p = self.__cur_element
 
241
        self.__cur_element=p.parentNode
 
242
        self.__cur_element.removeChild(p)
236
243
 
237
244
        # ... add it to a clipPath elt in the defs section
238
245
        clip = self.__doc.createElement('clipPath')
242
249
        self.__defs.appendChild(clip)
243
250
 
244
251
        # ... update the local style to point to it
245
 
        self._updateStyle(clip_path = 'url(#%s)'%clipid)
 
252
        self.__update_style(clip_path = 'url(#%s)'%clipid)
246
253
 
247
254
    # The text_xxx routines specify the start/end and contents of text
248
255
    def text_begin(self):
249
 
        if (self.__currElt.nodeName != 'g') :
 
256
        if (self.__cur_element.nodeName != 'g') :
250
257
            raise ValueError, "No group for text block"
251
258
        t = self.__doc.createElement('text')
252
 
        self.__currElt.appendChild(t)
253
 
        self.__currElt = t
 
259
        self.__cur_element.appendChild(t)
 
260
        self.__cur_element = t
254
261
    def text_moveto(self, x, y, angle):
255
 
        if (self.__currElt.nodeName != 'text') :
 
262
        if (self.__cur_element.nodeName != 'text') :
256
263
            raise ValueError, "No text for moveto"
257
 
        self.__currElt.setAttribute('x',`x`)
258
 
        self.__currElt.setAttribute('y',`-y`)
 
264
        self.__cur_element.setAttribute('x',`x`)
 
265
        self.__cur_element.setAttribute('y',`-y`)
259
266
        if (angle) :
260
 
            self.__currElt.setAttribute('transform',
 
267
            self.__cur_element.setAttribute('transform',
261
268
                                        'rotate(%g,%g,%g)' % (-angle,x,-y))
262
 
    def text_show(self, font_name, size, color, str):
263
 
        if (self.__currElt.nodeName != 'text') :
 
269
    def text_show(self, font_name, size, color, string):
 
270
        if (self.__cur_element.nodeName != 'text') :
264
271
            raise ValueError, "No text for show"
265
272
 
266
273
        # PyChart constructs a postscript font name, for example:
283
290
        #       extra-condensed | condensed | semi-condensed |
284
291
        #       semi-expanded | expanded | extra-expanded | ultra-expanded
285
292
        # ('narrow' seems to correspond to 'condensed')
286
 
 
287
 
        m = re.match(r'([^-]*)(-.*)?',font_name)
288
 
        font_name,modifiers = m.groups()
 
293
        font_name, modifiers = re.match(r'([^-]*)(-.*)?', font_name).groups()
289
294
        if font_name == 'Courier' : font_name = 'CourierNew'
290
295
        font_style = font_weight = font_stretch = 'normal'
291
296
        if modifiers :
296
301
        #! translate ascii symbol font chars -> unicode (see www.unicode.org)
297
302
        #! http://www.unicode.org/Public/MAPPINGS/VENDORS/ADOBE/symbol.txt
298
303
        #! but xml Text element writes unicode chars as '?' to XML file...
299
 
        str = re.sub(r'\\([()])',r'\1',str) # unescape brackets
300
 
        self._updateStyle(fill=_svgcolor(color),
 
304
        string = re.sub(r'\\([()])',r'\1',string) # unescape brackets
 
305
        self.__update_style(fill=_svgcolor(color),
301
306
                          stroke='none',
302
307
                          font_family=font_name,
303
308
                          font_size=size,
304
309
                          font_style=font_style,
305
310
                          font_weight=font_weight,
306
311
                          font_stretch=font_stretch)
307
 
        self.__currElt.appendChild(self.__doc.createTextNode(str))
 
312
        self.__cur_element.appendChild(self.__doc.createTextNode(string.encode('utf-8')))
308
313
    def text_end(self):
309
 
        if (self.__currElt.nodeName != 'text') :
 
314
        if (self.__cur_element.nodeName != 'text') :
310
315
            raise ValueError, "No text for close"
311
 
        self.__currElt = self.__currElt.parentNode
 
316
        self.__cur_element = self.__cur_element.parentNode
312
317
 
313
318
 
314
319
    # Three methods that change the local style of elements
317
322
    # although this may not in general correspond to (say) PostScript
318
323
    # behavior, it appears to correspond to reflect mode of use of this API
319
324
    def set_fill_color(self, color):
320
 
        self._updateStyle(fill=_svgcolor(color))
 
325
        self.__update_style(fill=_svgcolor(color))
321
326
    def set_stroke_color(self, color):
322
 
        self._updateStyle(stroke=_svgcolor(color))
 
327
        self.__update_style(stroke=_svgcolor(color))
323
328
    def set_line_style(self, style):  # see line_style.py
324
329
        linecap = {0:'butt', 1:'round', 2:'square'}
325
330
        linejoin = {0:'miter', 1:'round', 2:'bevel'}
326
331
        if style.dash: dash = ','.join(map(str,style.dash))
327
332
        else : dash = 'none'
328
 
        self._updateStyle(stroke_width = style.width,
 
333
        self.__update_style(stroke_width = style.width,
329
334
                          stroke = _svgcolor(style.color),
330
335
                          stroke_linecap = linecap[style.cap_style],
331
336
                          stroke_linejoin = linejoin[style.join_style],
336
341
    # similar but explicitly specify a coordinate transform at the
337
342
    # same time
338
343
    def gsave(self):
339
 
        if (self.__currElt.nodeName not in ['g','svg']) :
 
344
        if (self.__cur_element.nodeName not in ['g','svg']) :
340
345
            raise ValueError, "No group for gsave"
341
346
        g = self.__doc.createElement('g')
342
 
        self.__currElt.appendChild(g)
343
 
        self.__currElt = g
 
347
        self.__cur_element.appendChild(g)
 
348
        self.__cur_element = g
344
349
    def grestore(self):
345
 
        if (self.__currElt.nodeName != 'g'):
 
350
        if (self.__cur_element.nodeName != 'g'):
346
351
            raise ValueError, "No group for grestore"
347
352
        # first pop off any auto-generated groups (see protectCurrentChildren)
348
 
        while (self.__currElt.hasAttribute('auto')) :
349
 
            self.__currElt.removeAttribute('auto')
350
 
            self.__currElt = self.__currElt.parentNode
 
353
        while (self.__cur_element.hasAttribute('auto')) :
 
354
            self.__cur_element.removeAttribute('auto')
 
355
            self.__cur_element = self.__cur_element.parentNode
351
356
        # then pop off the original caller-generated group
352
 
        self.__currElt = self.__currElt.parentNode
 
357
        self.__cur_element = self.__cur_element.parentNode
353
358
 
354
359
    def push_transformation(self, baseloc, scale, angle, in_text=0):
355
360
        #? in_text arg appears to always be ignored
364
369
        # with PostScript if the closing pop doesn't come right after
365
370
        # the path element
366
371
 
367
 
        elt = self.__currElt
 
372
        elt = self.__cur_element
368
373
        if elt.nodeName == 'g':
369
374
            elt = None
370
375
        elif (elt.nodeName == 'path' and not elt.hasAttribute('d')) :
371
376
            g = elt.parentNode
372
377
            g.removeChild(elt)
373
 
            self.__currElt = g
 
378
            self.__cur_element = g
374
379
        else:
375
380
            raise ValueError, "Illegal placement of push_transformation"
376
 
            
 
381
 
377
382
        t = ''
378
383
        if baseloc :
379
384
            t += 'translate(%g,%g) '%(baseloc[0],-baseloc[1])
381
386
            t += 'rotate(%g) '%-angle
382
387
        if scale :
383
388
            t += 'scale(%g,%g) '%tuple(scale)
384
 
            
 
389
 
385
390
        self.gsave()
386
 
        self.__currElt.setAttribute('transform',t.strip())
 
391
        self.__cur_element.setAttribute('transform',t.strip())
387
392
        if elt:                         # elt has incomplete 'path' or None
388
 
            self.__currElt.appendChild(elt)
389
 
            self.__currElt = elt
 
393
            self.__cur_element.appendChild(elt)
 
394
            self.__cur_element = elt
390
395
 
391
396
    def pop_transformation(self, in_text=0): #? in_text unused?
392
397
        self.grestore()
393
398
 
394
399
    # If verbose, add comments to the output stream (helps debugging)
395
 
    def comment(self, str):
396
 
        if _comment_p : 
397
 
            self.__currElt.appendChild(self.__doc.createComment(str))
 
400
    def comment(self, string):
 
401
        if _comment_p :
 
402
            self.__cur_element.appendChild(self.__doc.createComment(string))
398
403
 
399
404
    # The verbatim method is currently not supported - presumably with
400
405
    # the SVG backend the user would require access to the DOM since
401
406
    # we're not directly outputting plain text here
402
 
    def verbatim(self, str):
403
 
        self.__currElt.appendChild(self.__doc.createComment('verbatim not implemented: ' + str))
 
407
    def verbatim(self, string):
 
408
        self.__cur_element.appendChild(self.__doc.createComment('verbatim not implemented: ' + string))
404
409
 
405
410
    # The close() method finalizes the SVG document and flattens the
406
411
    # DOM document to XML text to the specified file (or stdout)
407
412
    def close(self):
408
413
        basecanvas.T.close(self)
409
414
        self.grestore()           # matching the gsave in __init__
410
 
        if (self.__currElt.nodeName != 'svg') :
 
415
        if (self.__cur_element.nodeName != 'svg') :
411
416
            raise ValueError, "Incomplete document at close!"
412
417
 
413
418
        # Don't bother to output an empty document - this can happen
414
419
        # when we get close()d immediately by theme reinit
415
420
        if (len(self.__svg.childNodes[-1].childNodes) == 0) :
416
421
            return
417
 
            
 
422
 
418
423
        fp, need_close = self.open_output(self.__out_fname)
419
424
        bbox = theme.adjust_bounding_box([self.__xmin, self.__ymin,
420
425
                                          self.__xmax, self.__ymax])
423
428
                                   -yscale(bbox[3]),
424
429
                                   xscale(bbox[2])-xscale(bbox[0]),
425
430
                                   yscale(bbox[3])-yscale(bbox[1])))
 
431
        self.__svg.setAttribute('xmlns','http://www.w3.org/2000/svg')
 
432
        self.__svg.setAttribute('xmlns:xlink','http://www.w3.org/1999/xlink')
 
433
 
426
434
        self.__doc.writexml(fp,'','  ','\n')
427
435
        if need_close:
428
436
            fp.close()