Module Drawing
[hide private]
[frames] | no frames]

Source Code for Module Drawing

  1  #!/usr/bin/python2.5 
  2  """ 
  3  Drawing is a module to generate Encapsulated PostScript or SVG images. 
  4   
  5  Drawing exports a simple procedural API which can be used to generate paths 
  6  which are then stroked or filled.  Drawings created using this API can be 
  7  exported in any one of the supported formats, although some features (such 
  8  as those for scriptability) may be ignored for some formats. 
  9  """ 
 10  from __future__ import division, with_statement 
 11  import StringIO, math, warnings, re 
 12   
13 -class BBox (object):
14 """BBox is a utility class which encapsulates a bounding box."""
15 - def __init__ (self):
16 self.border = 0 17 """We can also track maximum linewidths within the bounding box, and 18 expand it by the appropriate amount.""" 19 self.minx, self.miny, self.maxx, self.maxy = (None,None,None,None)
20 - def update(self, x, y):
21 """Make sure the bounding box is big enough to include the given x,y 22 point.""" 23 if self.minx is None: 24 self.minx, self.miny, self.maxx, self.maxy = (x,y,x,y) 25 else: 26 self.minx = min (x, self.minx) 27 self.miny = min (y, self.miny) 28 self.maxx = max (x, self.maxx) 29 self.maxy = max (y, self.maxy)
30 - def updateBBox(self, bbox):
31 """Make sure the bounding box is big enough to include the given 32 bounding box. Note that the border is not increased by this method; 33 instead the given bounding box is expanded to include its border 34 before being added to this.""" 35 if isinstance(bbox, BBox): 36 bbox = bbox.bbox() 37 # now bbox is a tuple 38 self.update(bbox[0], bbox[1]) 39 self.update(bbox[2], bbox[3])
40 - def updateBorder(self, linewidth):
41 """Keep track of the largest linewidth we've seen.""" 42 self.border = max(self.border, linewidth)
43 - def __nonzero__ (self):
44 """Return `True` if there's been at least one update to this `BBox`.""" 45 return self.minx is not None
46 - def __str__ (self):
47 return str(self.bbox())
48 - def bbox (self):
49 """Return a bounding box (as a 4-tuple) guaranteed to encompass this 50 entire object.""" 51 if not self: return None 52 return (self.minx - self.border, self.miny - self.border, 53 self.maxx + self.border, self.maxy + self.border)
54
55 -class OutputLanguage:
56 """OutputLanguage abstracts the drawing backend by providing abstract 57 canvas operations. 58 59 This is the parent class for the specific backends; it should not be 60 instantiated directly."""
61 - def __init__ (self):
62 self.symbols = StringIO.StringIO() # procedure definitions 63 self.patterns = StringIO.StringIO()# pattern definitions 64 self.main = StringIO.StringIO() # main drawing code 65 self.out = self.main # current output target
66 - def translate (self, x, y):
67 """Translate a block of drawing commands by the given x and y offsets. 68 69 For example:: 70 71 with out.translate(10,10): 72 with out.stroke() as path: 73 path.moveto(0,0) 74 path.lineto(5,5) 75 """ 76 return self.group().translate(x, y)
77 - def rotate (self, theta):
78 """Rotate a block of drawing commands by the given angle CCW 79 around the origin. 80 81 For example:: 82 83 with out.rotate(45): 84 with out.stroke() as path: 85 path.moveto(0,0) 86 path.lineto(5,5) 87 """ 88 return self.group().rotate(theta)
89 - def stroke (self):
90 """Return a path which will be stroked. 91 92 For example:: 93 94 out.setgray(.5) 95 out.setlinewidth(2) 96 with out.stroke() as path: 97 path.moveto(0, 0) 98 path.lineto(5, 5) 99 """ 100 return self.path().stroke()
101 - def fill (self):
102 """Return a path which will be filled. 103 104 For example:: 105 106 out.setgray(.5) 107 with out.fill() as path: 108 path.moveto(0, 0) 109 path.lineto(5, 5) 110 path.lineto(10, 0) 111 path.closepath() 112 """ 113 return self.path().fill()
114
115 - def write (self, str):
116 """Write an uninterpreted raw string to the current output target.""" 117 self.out.write(str)
118
119 -class PostScript (OutputLanguage):
120 """PostScript backend."""
121 - def __init__ (self):
122 OutputLanguage.__init__ (self) 123 self._clearState() 124 self._bbox = BBox()
125 - def bbox (self):
126 return self._bbox.bbox()
127 - def _clearState (self):
128 self.state = { 'linewidth': None, 'linecap': None, 'font': None, 'color': None, 'transform': None }
129 - def view (self, bbox, **ignore):
130 pass # does nothing in postscript backend.
131 - def comment (self, text):
132 self.write ('%% %s\n' % text)
133 - def setlinewidth (self, width):
134 if not self.state['transform']: self._bbox.updateBorder(width) 135 if width != self.state['linewidth']: 136 self.write ('%s setlinewidth\n' % width) 137 self.state['linewidth'] = width
138 - def setrgbcolor (self, r, g, b):
139 if (r, g, b) != self.state['color']: 140 self.write ('%s %s %s setrgbcolor\n' % (r, g, b)) 141 self.state['color'] = (r, g, b)
142 - def setgray (self, gray):
143 if gray != self.state['color']: 144 self.write ('%s setgray\n' % gray) 145 self.state['color'] = gray
146 - def setgrayorrgb (self, x):
147 if isinstance (x, tuple): 148 self.setrgbcolor (*x) 149 else: 150 self.setgray (x)
151 - def setlinecap (self, linecap):
152 if linecap != self.state['linecap']: 153 mapping = { 'butt': 0, 'round': 1, 'square': 2 } 154 assert mapping.has_key(linecap) 155 self.state['linecap'] = linecap 156 self.write ('%s setlinecap\n' % mapping[linecap])
157 - def path (self, **ignore):
158 # in postscript we ignore the 'id' and 'classes' arguments 159 class PathMgr(object): 160 def __init__ (_self): 161 _self.cmd = ''
162 def __enter__ (_self): 163 return _self
164 def __exit__ (_self, type, value, traceback): 165 self.write (_self.cmd) 166 def fill (_self): 167 _self.cmd += 'fill\n' 168 return _self 169 def stroke (_self): 170 _self.cmd += 'stroke\n' 171 return _self 172 def moveto (_self, x, y): 173 self.write ('%s %s moveto\n' % (x, y)) 174 if not self.state['transform']: self._bbox.update (x, y) 175 def lineto (_self, x, y): 176 self.write ('%s %s lineto\n' % (x, y)) 177 if not self.state['transform']: self._bbox.update (x, y) 178 def closepath (_self): 179 self.write ('closepath\n') 180 def circle (_self, x, y, r): 181 self.write ('%s %s %s 0 360 arc\n' % (x, y, r)) 182 self._bbox.update (x - r, y - r) 183 self._bbox.update (x + r, y + r) 184 def arc (_self, x, y, r, s, t): 185 self.write ('%s %s %s %s %s arc\n' % (x, y, r, s, t)) 186 self._bbox.update (x - r, y - r) 187 self._bbox.update (x + r, y + r) 188 def arcn (_self, x, y, r, s, t): 189 self.write ('%s %s %s %s %s arcn\n' % (x, y, r, s, t)) 190 self._bbox.update (x - r, y - r) 191 self._bbox.update (x + r, y + r) 192 return PathMgr()
193 - def setfont (self, font, scale):
194 if (font, scale) != self.state['font']: 195 self.write ('/%s findfont %s scalefont setfont\n' % (font, scale)) 196 self.state['font'] = (font, scale)
197 - def text (self, x, y, text, **ignore):
198 self.write ('%s %s moveto (%s) show\n' % (x, y, self.esc(text))) 199 if not self.state['transform']: self._bbox.update (x,y)
200 - def group (self, **ignore):
201 # in postscript we ignore the 'id' and 'classes' arguments 202 class TransMgr(object): 203 def __init__(_self): 204 _self.cmd = ''
205 def __enter__(_self): 206 if _self.cmd: # don't write useless groups 207 self.write('gsave\n') 208 self.write(_self.cmd) 209 _self.state = self.state.copy() 210 self.state['transform'] = _self.cmd # or 'True' 211 def __exit__(_self, type, value, traceback): 212 if _self.cmd: 213 self.write('grestore\n') 214 self.state = _self.state 215 def translate (_self, x, y): 216 if x!=0 or y!=0: 217 _self.cmd += '%s %s translate\n' % (x, y) 218 return _self 219 def rotate(_self, theta): 220 if theta != 0: 221 _self.cmd += '%s rotate\n' % theta 222 return _self 223 def scale(_self, x, y): 224 if x != 1 or y != 1: 225 _self.cmd += '%s %s scale\n' % (x, y) 226 return _self 227 return TransMgr()
228 - def defineSymbol (self, name):
229 """Define a stored procedure named 'name'. 230 231 with out.defineSymbol('foo'): 232 out.moveto(0,0) 233 # etc 234 """ 235 class SymMgr(object): 236 def __enter__ (_self): 237 _self.old_out = self.out 238 _self.old_state = self.state 239 self._clearState() 240 self.out = StringIO.StringIO() 241 self.write('/%s {\n' % self.escId(name)) 242 self.write('gsave translate\n')
243 def __exit__ (_self, type, value, traceback): 244 self.write('grestore\n') 245 self.write('} def\n') 246 self.symbols.write(self.out.getvalue ()) 247 self.out = _self.old_out 248 self.state = _self.old_state 249 return SymMgr ()
250 - def refSymbol (self, name, x, y):
251 self.write('%s %s %s\n' % (x, y, self.escId(name)))
252 - def esc (self, str):
253 """Escape a string so that it is safe for postscript.""" 254 return str.replace ('\\', '\\\\').replace ('(', '\\(') \ 255 .replace (')', '\\)').replace ('\n', '\\n')
256 - def escId (self, str):
257 """Make a valid postscript entity from an arbitrary string.""" 258 def _esc (c): 259 assert len(c)==1 260 return '!%02X' % ord(c[0])
261 def _escMO (mo): return _esc(mo.group(0)) 262 r = re.sub(r'[\[\]()<>{}/%\000\011\012\014\015\040!]', _escMO, str) 263 if r and re.match(r'[-+0-9.]', r): # escape 1st char if not appropriate 264 r = _esc([r[0]]) + r[1:] 265 return r
266 - def __str__ (self):
267 bbox = self._bbox.bbox() 268 return '''\ 269 %%!PS-Adobe-3.0 270 %%%%BoundingBox: %d %d %d %d 271 %%%%HiResBoundingBox: %s %s %s %s 272 %s 273 %s 274 showpage 275 %%%%EOF 276 ''' % (bbox[0], bbox[1], bbox[2], bbox[3], 277 bbox[0], bbox[1], bbox[2], bbox[3], 278 self.symbols.getvalue (), 279 self.main.getvalue ())
280 281 # Sad that Python doesn't have this natively
282 -def _intToStr(num, radix=10):
283 """Convert an integer to a string using the given radix.""" 284 if num < 0: return '-' + _intToStr(-num, radix) 285 if num < radix: return "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"[num] 286 return _intToStr(num // radix,radix) + _intToStr(num % radix,radix)
287
288 -class SVG (OutputLanguage):
289 """SVG backend."""
290 - def __init__ (self, docTitle, version='1.1', useCSS=True):
291 OutputLanguage.__init__(self) 292 self.docTitle = docTitle 293 self.version = version 294 self.useCSS = useCSS 295 self.scriptOut = StringIO.StringIO() 296 self.styleOut = StringIO.StringIO() 297 self.styles = {} 298 self.nextStyle = 0 299 self._clearState()
300 - def _clearState (self):
301 self.state = { 'color': 'black', 'linewidth': 1, 'linecap': 'round', 302 'font-family': None, 'font-size': None, 'transform': None, 303 'bbox': BBox(), 'nesting': 0 }
304 - def bbox (self):
305 """Return a bounding box around the contents of the current active group. 306 """ 307 return self.state['bbox'].bbox()
308 - def _cachedAttribs(self, attribs, **addlAttribs):
309 all = attribs.copy() 310 all.update(addlAttribs) 311 if all.has_key('classes'): # work around python keyword 'class' 312 all['class'] = all['classes'] 313 del all['classes'] 314 # if we're not using CSS, then print these all out as a list. 315 if not self.useCSS: 316 return self._mkAttrib(**all) 317 # otherwise, let's use the style cache. 318 # first blacklist the attributes we don't want to cache. 319 blacklist = dict([ (k,v) for k,v in all.iteritems() 320 if k in ('id', 'class', 'x', 'y', 'transform', 321 'xlink:href' ) ]) 322 for k in blacklist.keys(): del all[k] 323 # okay, now construct a key w/ the attributes that are left. 324 key = frozenset(all.iteritems()) 325 if len(key)==0: 326 nclass = '' # no non-blacklisted attributes 327 elif self.styles.has_key(key): 328 nclass = self.styles[key] 329 else: 330 nclass = ':' + _intToStr(self.nextStyle, radix=36) 331 self.nextStyle += 1 332 self.styles[key] = nclass 333 self.styleOut.write('[class~="%s"] { %s }\n' % (nclass, '; '.join([('%s: %s'%(k,v)) for k,v in all.iteritems()]))) 334 # merge nclass with blacklist['class'] 335 if nclass: 336 blacklist['class'] = (blacklist.get('class','')+' '+nclass).strip() 337 return self._mkAttrib(**blacklist)
338
339 - def loadScript (self, scriptURL):
340 """SVG-specific method: generate a reference which will load the specified 341 script when this SVG drawing is viewed.""" 342 self.scriptOut.write('<script type="text/ecmascript" xlink:href="%s" />' 343 % scriptURL)
344 - def view (self, bbox, id=None, viewTarget=None):
345 """Emit an SVG 'view' element.""" 346 if isinstance(bbox, BBox): bbox = bbox.bbox() 347 self.indent('<view%s/>' % self._mkAttrib 348 (id=id, viewTarget=viewTarget, 349 viewBox=("%s %s %s %s" % 350 (bbox[0], bbox[1], 351 bbox[2]-bbox[0], bbox[3]-bbox[1]))))
352 - def comment (self, text):
353 self.indent('<!-- %s -->' % text)
354 - def setlinewidth (self, width, force = False):
355 self.state['linewidth'] = width
356 - def setrgbcolor (self, r, g, b):
357 self.state['color'] = 'rgb(%s%%,%s%%,%s%%)' % (100*r, 100*g, 100*b)
358 - def setgray (self, gray):
359 self.setrgbcolor( gray, gray, gray )
360 - def setgrayorrgb (self, x):
361 if isinstance (x, tuple): 362 self.setrgbcolor (*x) 363 else: 364 self.setgray (x)
365 - def setlinecap (self, linecap):
366 assert linecap in ('butt','round','square') 367 self.state['linecap'] = linecap
368 - def path (self, **attribs):
369 class PathMgr(object): 370 def __init__ (_self): 371 _self.cmd = {} 372 _self.path = StringIO.StringIO() 373 _self.curpos = None 374 _self.bbox = BBox()
375 def __enter__ (_self): 376 return _self
377 def __exit__ (_self, type, value, traceback): 378 if not _self.cmd.has_key('fill'): _self.cmd['fill'] = 'none' 379 if not _self.cmd.has_key('stroke'): _self.cmd['stroke'] = 'none' 380 self.indent ('<path%s d="%s"/>' 381 % (self._cachedAttribs(_self.cmd, **attribs), 382 _self.path.getvalue ())) 383 if not self.state['transform']: 384 self.state['bbox'].updateBBox(_self.bbox) 385 def fill (_self): 386 _self.cmd['fill'] = self.state['color'] 387 return _self 388 def stroke (_self): 389 _self.bbox.updateBorder(self.state['linewidth']) 390 _self.cmd.update({'stroke': self.state['color'], 391 'stroke-width': self.state['linewidth'], 392 'stroke-linecap': self.state['linecap'] }) 393 return _self 394 def moveto (_self, x, y, bbox = True): 395 _self.curpos = (x, y) 396 if not self.state['transform']: _self.bbox.update(x, y) 397 def lineto (_self, x, y, bbox = True): 398 if _self.curpos is not None: 399 _self.path.write('M %s %s ' % (_self.curpos[0], _self.curpos[1])) 400 _self.curpos = None 401 _self.path.write('L %s %s ' % (x, y)) 402 if not self.state['transform']: _self.bbox.update(x, y) 403 def closepath (_self): 404 _self.path.write ('Z ') 405 def psArc (_self, x, y, r, s, t, ccw): 406 _self.bbox.update (x - r, y - r) 407 _self.bbox.update (x + r, y + r) 408 # SVG arc command is rather different. A bit of compute needed here; 409 # see SVG 1.1 rec F.6.4 410 sx = x + r*math.cos(math.radians(s)) 411 sy = y + r*math.sin(math.radians(s)) 412 tx = x + r*math.cos(math.radians(t)) 413 ty = y + r*math.sin(math.radians(t)) 414 largeArc = 1 if (math.fabs(s-t) > 180) else 0 415 sweepFlag = 1 if ccw else 0 416 if _self.curpos is not None: 417 _self.path.write('M %s %s L ' % (_self.curpos[0], _self.curpos[1])) 418 _self.curpos = None 419 else: 420 _self.path.write('M ') 421 _self.path.write('%s %s A %s,%s %s %s,%s %s,%s ' % 422 (sx, sy, r, r, 0, largeArc, sweepFlag, tx, ty)) 423 def circle (_self, x, y, r): 424 _self.psArc(x, y, r, 0, 180, False) 425 _self.psArc(x, y, r, 180, 360, False) 426 def arc (_self, x, y, r, s, t): 427 while t < s: 428 t += 360 429 _self.psArc (x, y, r, s, t, True) 430 def arcn (_self, x, y, r, s, t): 431 while t > s: 432 t -= 360 433 _self.psArc (x, y, r, s, t, False) 434 return PathMgr() 435
436 - def setfont (self, font, scale):
437 if font == 'Times-Roman': # use web fonts 438 font = 'Verdana,sans-serif' 439 self.state['font-family'] = font 440 self.state['font-size'] = scale
441 - def text (self, x, y, text, bbox=True, **attribs):
442 with self.translate(x,y).scale(1,-1): 443 self.indent('<text%s>%s</text>' % 444 (self._cachedAttribs( 445 { 'x':0, 'y':0, 'font-family': self.state['font-family'], 446 # for some reason, we have to state 'px' here if we use CSS styles 447 'font-size': str(self.state['font-size'])+'px', 448 'fill': self.state['color'] }, 449 **attribs), self.esc(text))) 450 # xxx: this bbox could be more accurate 451 if not self.state['transform']: self.state['bbox'].update (x,y)
452 - def group (self, link=None, view_id=None, **attribs):
453 el = 'g' 454 if link: 455 el = 'a' 456 attribs['xlink:href']=link 457 if view_id: 458 view_target = attribs.get('id', 'view:'+view_id) 459 class TransMgr(object): 460 def __init__(_self): 461 _self.cmd = ''
462 def __enter__(_self): 463 _self.state = self.state.copy() 464 self.state['bbox'] = BBox() 465 if _self.cmd: 466 attribs['transform'] = _self.cmd 467 self.state['transform'] = _self.cmd 468 self.indent('<%s%s>' % (el, self._mkAttrib(**attribs))) 469 self.state['nesting'] += 1 470 def __exit__(_self, type, value, traceback): 471 bbox = self.state['bbox'].bbox() 472 # generate a view specification (in this coordinate system) 473 if view_id: 474 self.view(bbox, id=view_id, viewTarget=view_target) 475 self.state = _self.state 476 self.indent('</%s>' % el) 477 # merge this group's bounding box. 478 if bbox: self.state['bbox'].updateBBox(bbox) 479 def translate (_self, x, y): 480 if x!=0 or y!=0: 481 _self.cmd += 'translate(%s,%s) ' % (x, y) 482 return _self 483 def rotate(_self, theta): 484 if theta != 0: 485 _self.cmd += 'rotate(%s) ' % theta 486 return _self 487 def scale(_self, xScale, yScale): 488 if xScale != 1 or yScale != 1: 489 _self.cmd += 'scale(%s,%s) ' % (xScale,yScale) 490 return _self 491 return TransMgr() 492
493 - def defineSymbol (self, name):
494 """Define a symbol named 'name'. 495 496 with out.defineSymbol('foo'): 497 out.moveto(0,0) 498 # etc 499 """ 500 class SymMgr(object): 501 def __init__ (_self): 502 _self.group = self.group(id=self.escId(name))
503 def __enter__ (_self): 504 _self.old_out = self.out 505 _self.old_state = self.state 506 self._clearState() 507 self.out = StringIO.StringIO() 508 # emit a 'g' not a 'symbol' to avoid generating nested svgs, 509 # viewports, etc when this symbol is 'use'd. 510 _self.group.__enter__() 511 def __exit__ (_self, type, value, traceback): 512 _self.group.__exit__(type, value, traceback) 513 self.symbols.write(self.out.getvalue ()) 514 self.out = _self.old_out 515 # xxx: we could access the symbol's size in self.state['bbox'] here. 516 self.state = _self.old_state 517 return SymMgr ()
518 - def refSymbol (self, name, x, y):
519 # xxx: in theory, we ought to update the bounding box here. 520 self.indent('<use%s/>' % self._cachedAttribs( 521 {'x': x, 'y': y, 'xlink:href': '#'+self.escId(name) }))
522 - def _mkAttrib(self, **attribs):
523 r = '' 524 for key,value in attribs.iteritems(): 525 if key == 'classes': key = 'class' # work around python keyword 526 if value is not None: 527 r += ' %s="%s"' % (key, self.esc(str(value))) 528 return r
529 - def esc (self, str):
530 """Perform appropriate XML content escaping for the given string.""" 531 def _esc (mo): 532 c = mo.group(0) 533 assert len(c)==1 534 return '&#%d;' % ord(c[0])
535 return re.sub(r'[\"\'&<>\\]', _esc, str)
536 - def escId (self, str):
537 """Return a value suitable for an XML 'id' given an arbitrary string.""" 538 def _esc (c): 539 assert len(c)==1 540 return '_%02X' % ord(c[0])
541 def _escMO (mo): return _esc(mo.group(0)) 542 # note that : is legal, but we're reserving it for our style cache 543 r = re.sub(r'[^-A-Za-z0-9.]', _escMO, str) 544 if r and not re.match(r'[A-Za-z_:]', r): # escape 1st char if not good 545 r = _esc([r[0]]) + r[1:] 546 return r
547 - def indent (self, str):
548 self.write(' '*self.state['nesting']) 549 self.write(str) 550 self.write('\n')
551 - def __str__ (self):
552 """Return an SVG document.""" 553 assert self.state['nesting'] == 0 554 bbox = self.state['bbox'].bbox() 555 width = (bbox[2] - bbox[0]) 556 height = (bbox[3] - bbox[1]) 557 # Use either the SVG 1.1 full or the SVG 1.2 Tiny header 558 header = ('''\ 559 <?xml version="1.0" encoding="UTF-8"?> 560 <svg width="%s" height="%s" version="1.2" baseProfile="tiny" 561 xmlns="http://www.w3.org/2000/svg" 562 xmlns:xlink="http://www.w3.org/1999/xlink"> 563 ''' if self.version == '1.2 Tiny' else '''\ 564 <?xml version="1.0" standalone="no"?> 565 <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" 566 "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> 567 <svg width="%s" height="%s" version="1.1" 568 xmlns="http://www.w3.org/2000/svg" 569 xmlns:xlink="http://www.w3.org/1999/xlink"> 570 ''') % (width, height) 571 572 return header + '''\ 573 <title>Khipu %s</title> 574 %s 575 <style type="text/css"><![CDATA[ 576 %s 577 ]]></style> 578 <defs> 579 %s 580 </defs> 581 <g transform="scale(1,-1) translate(%s,%s)"> 582 %s 583 </g> 584 </svg> 585 ''' % (self.docTitle, 586 self.scriptOut.getvalue (), 587 self.styleOut.getvalue (), 588 self.symbols.getvalue (), 589 -bbox[0], -bbox[3], 590 self.main.getvalue())
591