/usr/share/pyshared/pychart/svgcanvas.py is in python-pychart 1.39-7.
This file is owned by root:root, with mode 0o644.
The actual contents of the file can be viewed below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 | #
# 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()
|