Package GChartWrapper :: Module GChart'
[hide private]
[frames] | no frames]

Source Code for Module GChartWrapper.GChart'

  1  # -*- coding: utf-8 -*- 
  2  ################################################################################ 
  3  #  GChartWrapper - v0.8 
  4  #  Copyright (C) 2009  Justin Quick <justquick@gmail.com> 
  5  # 
  6  #  This program is free software; you can redistribute it and/or modify 
  7  #  it under the terms of the GNU General Public License version 3 as published 
  8  #  by the Free Software Foundation. 
  9  # 
 10  #  Thanks to anyone who does anything for this project. 
 11  #  If you have even the smallest revision, please email me at above address. 
 12  ################################################################################ 
 13  """ 
 14  GChartWrapper - Google Chart API Wrapper 
 15   
 16  The wrapper can render the URL of the Google chart based on your parameters. 
 17  With the chart you can render an HTML img tag to insert into webpages on the fly, 
 18  show it directly in a webbrowser, or save the chart PNG to disk. New versions 
 19  can generate PIL PngImage instances. 
 20   
 21  Example 
 22   
 23      >>> G = GChart('lc',['simpleisbetterthancomplexcomplexisbetterthancomplicated']) 
 24      >>> G.title('The Zen of Python','00cc00',36) 
 25      >>> G.color('00cc00') 
 26      >>> str(G) 
 27      'http://chart.apis.google.com/chart? 
 28          chd=e:simpleisbetterthancomplexcomplexisbetterthancomplicated 
 29          &chs=300x150 
 30          &cht=lc 
 31          &chtt=The+Zen+of+Python'     
 32      >>> G.image() # PIL instance 
 33      <PngImagePlugin.PngImageFile instance at ...> 
 34      >>> 1#G.show() # Webbrowser open 
 35      True 
 36      >>> G.save('tmp.png') # Save to disk 
 37      'tmp.png' 
 38   
 39  See tests.py for unit test and other examples 
 40  """ 
 41  from GChartWrapper.constants import * 
 42  from GChartWrapper.encoding import Encoder 
 43  from copy import copy 
44 45 -def lookup_color(color):
46 """ 47 Returns the hex color for any valid css color name 48 49 >>> lookup_color('aliceblue') 50 'F0F8FF' 51 """ 52 if color is None: return 53 color = color.lower() 54 if color in COLOR_MAP: 55 return COLOR_MAP[color] 56 return color
57
58 -def color_args(args, *indexes):
59 """ 60 Color a list of arguments on particular indexes 61 62 >>> c = color_args([None,'blue'], 1) 63 >>> c.next() 64 None 65 >>> c.next() 66 '0000FF' 67 """ 68 for i,arg in enumerate(args): 69 if i in indexes: 70 yield lookup_color(arg) 71 else: 72 yield arg
73
74 75 -class Axes(dict):
76 """ 77 Axes attribute dictionary storage 78 79 Use this class via GChart(...).axes 80 Methods are taken one at a time, like so: 81 82 >>> G = GChart() 83 >>> G.axes.type('xy') 84 {} 85 >>> G.axes.label(1,'Label1') # X Axis 86 {} 87 >>> G.axes.label(2,'Label2') # Y Axis 88 {} 89 """
90 - def __init__(self, parent):
91 self.parent = parent 92 self.data = {'ticks':[],'labels':[],'positions':[], 93 'ranges':[],'styles':[]} 94 dict.__init__(self)
95
96 - def tick(self, index, length):
97 """ 98 Add tick marks in order of axes by width 99 APIPARAM: chxtc <axis index>,<length of tick mark> 100 """ 101 assert int(length) <= 25, 'Width cannot be more than 25' 102 self.data['ticks'].append('%s,%d'%(index,length)) 103 return self.parent
104
105 - def type(self, atype):
106 """ 107 Define the type of axes you wish to use 108 atype must be one of x,t,y,r 109 APIPARAM: chxt 110 """ 111 for char in atype: 112 assert char in 'xtyr', 'Invalid axes type: %s'%char 113 if not ',' in atype: 114 atype = ','.join(atype) 115 self['chxt'] = atype 116 return self.parent
117 __call__ = type 118
119 - def label(self, index, *args):
120 """ 121 Label each axes one at a time 122 args are of the form <label 1>,...,<label n> 123 APIPARAM: chxl 124 """ 125 self.data['labels'].append( 126 str('%s:|%s'%(index, '|'.join(map(str,args)) )).replace('None','') 127 ) 128 return self.parent
129
130 - def position(self, index, *args):
131 """ 132 Set the label position of each axis, one at a time 133 args are of the form <label position 1>,...,<label position n> 134 APIPARAM: chxp 135 """ 136 self.data['positions'].append( 137 str('%s,%s'%(index, ','.join(map(str,args)))).replace('None','') 138 ) 139 return self.parent
140
141 - def range(self, index, *args):
142 """ 143 Set the range of each axis, one at a time 144 args are of the form <start of range>,<end of range>,<interval> 145 APIPARAM: chxr 146 """ 147 self.data['ranges'].append('%s,%s'%(index, 148 ','.join(map(smart_str, args)))) 149 return self.parent
150
151 - def style(self, index, *args):
152 """ 153 Add style to your axis, one at a time 154 args are of the form:: 155 <axis color>, 156 <font size>, 157 <alignment>, 158 <drawing control>, 159 <tick mark color> 160 APIPARAM: chxs 161 """ 162 args = color_args(args, 0) 163 self.data['styles'].append( 164 ','.join([str(index)]+list(map(str,args))) 165 ) 166 return self.parent
167
168 - def render(self):
169 """Render the axes data into the dict data""" 170 for opt,values in self.data.items(): 171 if opt == 'ticks': 172 self['chxtc'] = '|'.join(values) 173 else: 174 self['chx%s'%opt[0]] = '|'.join(values) 175 return self
176
177 -class GChart(dict):
178 """Main chart class 179 180 Chart type must be valid for cht parameter 181 Dataset can be any python iterable and be multi dimensional 182 Kwargs will be put into chart API params if valid"""
183 - def __init__(self, ctype=None, dataset=[], **kwargs):
184 self._series = kwargs.pop('series',None) 185 self.lines,self.fills,self.markers,self.scales = [],[],[],[] 186 self._geo,self._ld = '','' 187 self._dataset = dataset 188 dict.__init__(self) 189 if ctype: 190 self['cht'] = self.check_type(ctype) 191 self._encoding = kwargs.pop('encoding', None) 192 self._scale = kwargs.pop('scale', None) 193 self._apiurl = kwargs.pop('apiurl', APIURL) 194 for k in kwargs: 195 assert k in APIPARAMS, 'Invalid chart parameter: %s' % k 196 self.update(kwargs) 197 self.axes = Axes(self)
198 199 @classmethod
200 - def fromurl(cls, qs):
201 """ 202 Reverse a chart URL or dict into a GChart instance 203 204 >>> G = GChart.fromurl('http://chart.apis.google.com/chart?...') 205 >>> G 206 <GChartWrapper.GChart instance at...> 207 >>> G.image().save('chart.jpg','JPEG') 208 """ 209 if isinstance(qs, dict): 210 return cls(**qs) 211 return cls(**dict(parse_qsl(qs[qs.index('?')+1:])))
212 213 ################### 214 # Callables 215 ###################
216 - def map(self, geo, country_codes):
217 """ 218 Creates a map of the defined geography with the given country/state codes 219 Geography choices are africa, asia, europe, middle_east, south_america, and world 220 ISO country codes can be found at http://code.google.com/apis/chart/isocodes.html 221 US state codes can be found at http://code.google.com/apis/chart/statecodes.html 222 APIPARAMS: chtm & chld 223 """ 224 assert geo in GEO, 'Geograpic area %s not recognized'%geo 225 self._geo = geo 226 self._ld = country_codes 227 return self
228
229 - def level_data(self, *args):
230 """ 231 Just used in QRCode for the moment 232 args are error_correction,margin_size 233 APIPARAM: chld 234 """ 235 assert args[0].lower() in 'lmqh', 'Unknown EC level %s'%level 236 self['chld'] = '%s|%s'%args 237 return self
238
239 - def bar(self, *args):
240 """ 241 For bar charts, specify bar thickness and spacing with the args 242 args are <bar width>,<space between bars>,<space between groups> 243 bar width can be relative or absolute, see the official doc 244 APIPARAM: chbh 245 """ 246 self['chbh'] = ','.join(map(str,args)) 247 return self
248
249 - def encoding(self, arg):
250 """ 251 Specifies the encoding to be used for the Encoder 252 Must be one of 'simple','text', or 'extended' 253 """ 254 self._encoding = arg 255 return self
256
257 - def output_encoding(self, encoding):
258 """ 259 Output encoding to use for QRCode encoding 260 Must be one of 'Shift_JIS','UTF-8', or 'ISO-8859-1' 261 APIPARAM: choe 262 """ 263 assert encoding in ('Shift_JIS','UTF-8','ISO-8859-1'),\ 264 'Unknown encoding %s'%encoding 265 self['choe'] = encoding 266 return self
267
268 - def scale(self, *args):
269 """ 270 Scales the data down to the given size 271 args must be of the form:: 272 <data set 1 minimum value>, 273 <data set 1 maximum value>, 274 <data set n minimum value>, 275 <data set n maximum value> 276 will only work with text encoding! 277 APIPARAM: chds 278 """ 279 self._scale = [','.join(map(smart_str, args))] 280 return self
281
282 - def dataset(self, data, series=''):
283 """ 284 Update the chart's dataset, can be two dimensional or contain string data 285 """ 286 self._dataset = data 287 self._series = series 288 return self
289
290 - def marker(self, *args):
291 """ 292 Defines markers one at a time for your graph 293 args are of the form:: 294 <marker type>, 295 <color>, 296 <data set index>, 297 <data point>, 298 <size>, 299 <priority> 300 see the official developers doc for the complete spec 301 APIPARAM: chm 302 """ 303 if len(args[0]) == 1: 304 assert args[0] in MARKERS, 'Invalid marker type: %s'%args[0] 305 assert len(args) <= 6, 'Incorrect arguments %s'%str(args) 306 args = color_args(args, 1) 307 self.markers.append(','.join(map(str,args)) ) 308 return self
309
310 - def margin(self, left, right, top, bottom, lwidth=0, lheight=0):
311 """ 312 Set margins for chart area 313 args are of the form:: 314 <left margin>, 315 <right margin>, 316 <top margin>, 317 <bottom margin>| 318 <legend width>, 319 <legend height> 320 321 APIPARAM: chma 322 """ 323 self['chma'] = '%d,%d,%d,%d' % (left, right, top, bottom) 324 if lwidth or lheight: 325 self['chma'] += '|%d,%d' % (lwidth, lheight) 326 return self
327
328 - def line(self, *args):
329 """ 330 Called one at a time for each dataset 331 args are of the form:: 332 <data set n line thickness>, 333 <length of line segment>, 334 <length of blank segment> 335 APIPARAM: chls 336 """ 337 self.lines.append(','.join(['%.1f'%x for x in map(float,args)])) 338 return self
339
340 - def fill(self, *args):
341 """ 342 Apply a solid fill to your chart 343 args are of the form <fill type>,<fill style>,... 344 fill type must be one of c,bg,a 345 fill style must be one of s,lg,ls 346 the rest of the args refer to the particular style 347 APIPARAM: chf 348 """ 349 a,b = args[:2] 350 assert a in ('c','bg','a'), 'Fill type must be bg/c/a not %s'%a 351 assert b in ('s','lg','ls'), 'Fill style must be s/lg/ls not %s'%b 352 if len(args) == 3: 353 args = color_args(args, 2) 354 else: 355 args = color_args(args, 3,5) 356 self.fills.append(','.join(map(str,args))) 357 return self
358
359 - def grid(self, *args):
360 """ 361 Apply a grid to your chart 362 args are of the form:: 363 <x axis step size>, 364 <y axis step size>, 365 <length of line segment>, 366 <length of blank segment> 367 <x offset>, 368 <y offset> 369 APIPARAM: chg 370 """ 371 grids = map(str,map(float,args)) 372 self['chg'] = ','.join(grids).replace('None','') 373 return self
374
375 - def color(self, *args):
376 """ 377 Add a color for each dataset 378 args are of the form <color 1>,...<color n> 379 APIPARAM: chco 380 """ 381 args = color_args(args, *range(len(args))) 382 self['chco'] = ','.join(args) 383 return self
384
385 - def type(self, type):
386 """ 387 Set the chart type, either Google API type or regular name 388 APIPARAM: cht 389 """ 390 self['cht'] = self.check_type(str(type)) 391 return self
392
393 - def label(self, *args):
394 """ 395 Add a simple label to your chart 396 call each time for each dataset 397 APIPARAM: chl 398 """ 399 if self['cht'] == 'qr': 400 self['chl'] = ''.join(map(str,args)) 401 else: 402 self['chl'] = '|'.join(map(str,args)) 403 return self
404
405 - def legend(self, *args):
406 """ 407 Add a legend to your chart 408 call each time for each dataset 409 APIPARAM: chdl 410 """ 411 self['chdl'] = '|'.join(args) 412 return self
413
414 - def legend_pos(self, pos):
415 """ 416 Define a position for your legend to occupy 417 APIPARAM: chdlp 418 """ 419 assert pos in LEGEND_POSITIONS, 'Unknown legend position: %s'%pos 420 self['chdlp'] = str(pos) 421 return self
422
423 - def title(self, title, *args):
424 """ 425 Add a title to your chart 426 args are optional style params of the form <color>,<font size> 427 APIPARAMS: chtt,chts 428 """ 429 self['chtt'] = title 430 if args: 431 args = color_args(args, 0) 432 self['chts'] = ','.join(map(str,args)) 433 return self
434
435 - def size(self,*args):
436 """ 437 Set the size of the chart, args are width,height and can be tuple 438 APIPARAM: chs 439 """ 440 if len(args) == 2: 441 x,y = map(int,args) 442 else: 443 x,y = map(int,args[0]) 444 self.check_size(x,y) 445 self['chs'] = '%dx%d'%(x,y) 446 return self
447
448 - def orientation(self, angle):
449 """ 450 Set the chart dataset orientation 451 angle is <angle in radians> 452 APIPARAM: chp 453 """ 454 self['chp'] = '%f'%angle 455 return self
456 position = orientation 457
458 - def render(self):
459 """ 460 Renders the chart context and axes into the dict data 461 """ 462 self.update(self.axes.render()) 463 encoder = Encoder(self._encoding, None, self._series) 464 if not 'chs' in self: 465 self['chs'] = '300x150' 466 else: 467 size = self['chs'].split('x') 468 assert len(size) == 2, 'Invalid size, must be in the format WxH' 469 self.check_size(*map(int,size)) 470 assert 'cht' in self, 'No chart type defined, use type method' 471 self['cht'] = self.check_type(self['cht']) 472 if ('any' in dir(self._dataset) and self._dataset.any()) or self._dataset: 473 self['chd'] = encoder.encode(self._dataset) 474 elif not 'choe' in self: 475 assert 'chd' in self, 'You must have a dataset, or use chd' 476 if self._scale: 477 assert self['chd'].startswith('t'),\ 478 'You must use text encoding with chds' 479 self['chds'] = ','.join(self._scale) 480 if self._geo and self._ld: 481 self['chtm'] = self._geo 482 self['chld'] = self._ld 483 if self.lines: 484 self['chls'] = '|'.join(self.lines) 485 if self.markers: 486 self['chm'] = '|'.join(self.markers) 487 if self.fills: 488 self['chf'] = '|'.join(self.fills)
489 490 ################### 491 # Checks 492 ###################
493 - def check_size(self,x,y):
494 """ 495 Make sure the chart size fits the standards 496 """ 497 assert x <= 1000, 'Width larger than 1,000' 498 assert y <= 1000, 'Height larger than 1,000' 499 assert x*y <= 300000, 'Resolution larger than 300,000'
500
501 - def check_type(self, type):
502 """Check to see if the type is either in TYPES or fits type name 503 504 Returns proper type 505 """ 506 if type in TYPES: 507 return type 508 tdict = dict(zip(TYPES,TYPES)) 509 tdict.update({ 510 'line': 'lc', 511 'bar': 'bvs', 512 'pie': 'p', 513 'venn': 'v', 514 'scater': 's', 515 'radar': 'r', 516 'meter': 'gom', 517 }) 518 assert type in tdict, 'Invalid chart type: %s'%type 519 return tdict[type]
520 521 ##################### 522 # Convience Functions 523 #####################
524 - def getname(self):
525 """ 526 Gets the name of the chart, if it exists 527 """ 528 return self.get('chtt','')
529
530 - def getdata(self):
531 """ 532 Returns the decoded dataset from chd param 533 """ 534 #XXX: Why again? not even sure decode works well 535 return Encoder(self._encoding).decode(self['chd'])
536
537 - def _parts(self):
538 return ('%s=%s'%(k,smart_str(v)) for k,v in self.items() if v)
539
540 - def __str__(self):
541 return self.url
542
543 - def __repr__(self):
544 return '<GChartWrapper.%s %s>'%(self.__class__.__name__,self)
545 546 @property
547 - def url(self):
548 """ 549 Returns the rendered URL of the chart 550 """ 551 self.render() 552 return self._apiurl + '&'.join(self._parts()).replace(' ','+')
553 554
555 - def show(self, *args, **kwargs):
556 """ 557 Shows the chart URL in a webbrowser 558 559 Other arguments passed to webbrowser.open 560 """ 561 from webbrowser import open as webopen 562 return webopen(str(self), *args, **kwargs)
563
564 - def save(self, fname=None):
565 """ 566 Download the chart from the URL into a filename as a PNG 567 568 The filename defaults to the chart title (chtt) if any 569 """ 570 if not fname: 571 fname = self.getname() 572 assert fname != None, 'You must specify a filename to save to' 573 if not fname.endswith('.png'): 574 fname += '.png' 575 try: 576 urlretrieve(self.url, fname) 577 except Exception: 578 raise IOError('Problem saving %s to file'%fname) 579 return fname
580
581 - def img(self, **kwargs):
582 """ 583 Returns an XHTML <img/> tag of the chart 584 585 kwargs can be other img tag attributes, which are strictly enforced 586 uses strict escaping on the url, necessary for proper XHTML 587 """ 588 safe = 'src="%s" ' % self.url.replace('&','&amp;').replace('<', '&lt;')\ 589 .replace('>', '&gt;').replace('"', '&quot;').replace( "'", '&#39;') 590 for item in kwargs.items(): 591 if not item[0] in IMGATTRS: 592 raise AttributeError('Invalid img tag attribute: %s'%item[0]) 593 safe += '%s="%s" '%item 594 return '<img %s/>'%safe
595
596 - def urlopen(self):
597 """ 598 Grabs readable PNG file pointer 599 """ 600 req = Request(str(self)) 601 try: 602 return urlopen(req) 603 except HTTPError: 604 _print('The server couldn\'t fulfill the request.') 605 except URLError: 606 _print('We failed to reach a server.')
607
608 - def image(self):
609 """ 610 Returns a PngImageFile instance of the chart 611 612 You must have PIL installed for this to work 613 """ 614 try: 615 try: 616 import Image 617 except ImportError: 618 from PIL import Image 619 except ImportError: 620 raise ImportError('You must install PIL to fetch image objects') 621 try: 622 from cStringIO import StringIO 623 except ImportError: 624 from StringIO import StringIO 625 return Image.open(StringIO(self.urlopen().read()))
626
627 - def write(self, fp):
628 """ 629 Writes out PNG image data in chunks to file pointer fp 630 631 fp must support w or wb 632 """ 633 urlfp = self.urlopen().fp 634 while 1: 635 try: 636 fp.write(urlfp.next()) 637 except StopIteration: 638 return
639
640 - def checksum(self):
641 """ 642 Returns the unique SHA1 hexdigest of the chart URL param parts 643 644 good for unittesting... 645 """ 646 self.render() 647 return new_sha(''.join(sorted(self._parts()))).hexdigest()
648
649 # Now a whole mess of convenience classes 650 # *for those of us who dont speak API* 651 -class QRCode(GChart):
652 - def __init__(self, content='', **kwargs):
653 kwargs['choe'] = 'UTF-8' 654 if isinstance(content, str): 655 kwargs['chl'] = quote(content).replace('%0A','\n') 656 else: 657 kwargs['chl'] = quote(content[0]).replace('%0A','\n') 658 GChart.__init__(self, 'qr', None, **kwargs)
659
660 -class _AbstractGChart(GChart):
661 o,t = {},None
662 - def __init__(self, dataset, **kwargs):
663 kwargs.update(self.o) 664 GChart.__init__(self, self.t, dataset, **kwargs)
665
666 667 -class Meter(_AbstractGChart): o,t = {'encoding':'text'},'gom'
668 -class Line(_AbstractGChart): t = 'lc'
669 -class LineXY(_AbstractGChart): t = 'lxy'
670 -class HorizontalBarStack(_AbstractGChart): t = 'bhs'
671 -class VerticalBarStack(_AbstractGChart): t = 'bvs'
672 -class HorizontalBarGroup(_AbstractGChart): t = 'bhg'
673 -class VerticalBarGroup(_AbstractGChart): t = 'bvg'
674 -class Pie(_AbstractGChart): t = 'p'
675 -class Pie3D(_AbstractGChart): t = 'p3'
676 -class Venn(_AbstractGChart): t = 'v'
677 -class Scatter(_AbstractGChart): t = 's'
678 -class Sparkline(_AbstractGChart): t = 'ls'
679 -class Radar(_AbstractGChart): t = 'r'
680 -class RadarSpline(_AbstractGChart): t = 'rs'
681 -class Map(_AbstractGChart): t = 't'
682 -class PieC(_AbstractGChart): t = 'pc'
683
684 ######################################## 685 # Now for something completely different 686 ######################################## 687 -class Text(GChart):
688 - def render(self): pass
689 - def __init__(self, *args):
690 GChart.__init__(self) 691 self['chst'] = 'd_text_outline' 692 args = list(map(str, color_args(args, 0, 3))) 693 assert args[2] in 'lrh', 'Invalid text alignment' 694 assert args[4] in '_b', 'Invalid font style' 695 self['chld'] = '|'.join(args).replace('\r\n','|')\ 696 .replace('\r','|').replace('\n','|').replace(' ','+')
697
698 -class Pin(GChart):
699 - def render(self): pass
700 - def __init__(self, ptype, *args):
701 GChart.__init__(self) 702 assert ptype in PIN_TYPES, 'Invalid type' 703 if ptype == "pin_letter": 704 args = color_args(args, 1,2) 705 elif ptype == 'pin_icon': 706 args = list(color_args(args, 1)) 707 assert args[0] in PIN_ICONS, 'Invalid icon name' 708 elif ptype == 'xpin_letter': 709 args = list(color_args(args, 2,3,4)) 710 assert args[0] in PIN_SHAPES, 'Invalid pin shape' 711 if not args[0].startswith('pin_'): 712 args[0] = 'pin_%s'%args[0] 713 elif ptype == 'xpin_icon': 714 args = list(color_args(args, 2,3)) 715 assert args[0] in PIN_SHAPES, 'Invalid pin shape' 716 if not args[0].startswith('pin_'): 717 args[0] = 'pin_%s'%args[0] 718 assert args[1] in PIN_ICONS, 'Invalid icon name' 719 elif ptype == 'spin': 720 args = color_args(args, 2) 721 self['chst'] = 'd_map_%s'%ptype 722 self['chld'] = '|'.join(map(str, args)).replace('\r\n','|')\ 723 .replace('\r','|').replace('\n','|').replace(' ','+')
724 - def shadow(self):
725 image = copy(self) 726 chsts = self['chst'].split('_') 727 chsts[-1] = 'shadow' 728 image.data['chst'] = '_'.join(chsts) 729 return image
730
731 -class Note(GChart):
732 - def render(self): pass
733 - def __init__(self, *args):
734 GChart.__init__(self) 735 assert args[0] in NOTE_TYPES,'Invalid note type' 736 assert args[1] in NOTE_IMAGES,'Invalid note image' 737 if args[0].find('note')>-1: 738 self['chst'] = 'd_f%s'%args[0] 739 args = list(color_args(args, 3)) 740 else: 741 self['chst'] = 'd_%s'%args[0] 742 assert args[2] in NOTE_WEATHERS,'Invalid weather' 743 args = args[1:] 744 self['chld'] = '|'.join(map(str, args)).replace('\r\n','|')\ 745 .replace('\r','|').replace('\n','|').replace(' ','+')
746
747 -class Bubble(GChart):
748 - def render(self): pass
749 - def __init__(self, btype, *args):
750 GChart.__init__(self) 751 assert btype in BUBBLE_TYPES, 'Invalid type' 752 if btype in ('icon_text_small','icon_text_big'): 753 args = list(color_args(args, 3,4)) 754 assert args[0] in BUBBLE_SICONS,'Invalid icon type' 755 elif btype == 'icon_texts_big': 756 args = list(color_args(args, 2,3)) 757 assert args[0] in BUBBLE_LICONS,'Invalid icon type' 758 elif btype == 'texts_big': 759 args = color_args(args, 1,2) 760 self['chst'] = 'd_bubble_%s'%btype 761 self['chld'] = '|'.join(map(str, args)).replace('\r\n','|')\ 762 .replace('\r','|').replace('\n','|').replace(' ','+')
763 - def shadow(self):
764 image = copy(self) 765 image.data['chst'] = '%s_shadow'%self['chst'] 766 return image
767