# # Python SVG Library # coded by Kenar (Kenji Arisawa) # email: arisawa@aichi-u.ac.jp # import copy import math import re import sys from base import * version = "0.4" # style properties: # font,fill,direction,letter-spacing,text-decoration,unicode-bidi,word-spacing,clip,color, # cursor,overflow,visibility, # clip-path,clip-rule,mask,opacy,enable-background,filter,flood-color,flood-opacity,lighting-color, # stop-color,stop-opacity,pointing-events,color-interpolation,color-interpolation-filters, # color-profile,color-rendering,fill-opacity,fill-rule,image-rendering, # marker,marker-end,marker-start,shape-rendering, # stroke,stroke-width, # stroke-dasharray,stroke-dashoffset,stroke-line-cap,stroke-linejoin,stroke-miterlimit,stroke-opacity, # text-anchor,writing-mode # text-rendering,alignment-baseline,baseline-shift,dominant-baseline, # glyph-orientation-horizontal,glyph-orientation-vertical,kerning # ref: http://www.hcn.zaq.ne.jp/___/REC-SVG11-20030114/styling.html#SVGStylingProperties # our default values: markerUnits="strokeWidth", fill="none", orient="auto" markers = { "triangle": """ """, "arrow": """ """, "circle": """ """, } symbols = { # symbols defined using is spinous "circle": '', "box": '', "cross": '', "plus": '' } # example "MyCircle":'' # example "MyCircle":'' patterns = { # NB: designe patterns is in box of width=100, height=100 so that we prevent jaggy # stroke-width must be in "cross": """ """, "grid": """ """, # diagonal is a bit buggy "diagonal1": """ """, "diagonal2": """ """, "vertical": """ """, "horizontal": """ """, } filters = { "shadow": # ref: http://www.h2.dion.ne.jp/~defghi/svgMemo/svgMemo_11.htm """ """ } dic = {"marker-unit":"markerUnit", "marker-width":"markerWidth", "marker-height":"markerHeight"} exceptions = {"font":"font-family","href":"xlink:href","base":"xml:base","lang":"xml:lang","space":"xml:space"} gid = 0 # group ID for preventing duplicated gid tag_p = None # tag pattern used in tagname() opt_p = None def str2dic(str): # convert style string to a dictionary # str is "XX1:YY1;XX2:YY2;" or "YY3;XX4:YY4;" # then return {"XX1":"YY1","XX2":"YY2"} or {"":"YY3","XX4":"YY4"} d = {} u = str.split(";") for x in u: x = x.strip() if x == "": continue r = x.split(":") if len(r) > 2: raise Exception("str2dic: too many ':' in %s"%x) r[0] = r[0].strip() if len(r) == 1: d[""] = r[0] else: d[r[0]] = r[1].strip() return d def str2ndic(str,attr): # convert attr value to a normalized dictionary # str is "XX1:YY1;XX2:YY2;" or "YY3;XX4:YY4;" # attr is "AAA" (not "-" in attr) # then return {"AAA-XX1":"YY1","AAA-XX2":"YY2"} or {"AAA":"YY3","AAA-XX4":"YY4"} # this is inapplicable to style string d = {} u = str.split(";") for x in u: x = x.strip() if x == "": continue r = x.split(":") if len(r) > 2: raise Exception("str2dic: too many ':' in %s"%x) r[0] = r[0].strip() if len(r) == 1: d[attr] = r[0] else: d[attr + "-" + r[0]] = r[1].strip() return d def keymatch(key,*keylist): for k in keylist: if k == key[0:len(k)]: return True return False def dic2str(dic,*keylist): # dictionary to string value for style s = "" for key in dic: if keymatch(key,*keylist): s = s + "%s:%s;"%(key,dic[key]) return s def dic2opt(dic,*keylist): # dictionary to string value for options (i.e, attr="value" style) s = "" for key in dic: if keymatch(key,*keylist): s = s + '%s="%s" '%(key,dic[key]) return s def dic2ndic(dic): # convert dic to normalized dictionary # {"font":"Helvetica"} should be converted to {"font-family":"Helvetica"} # {"animate":"<....>"} should be untouched dd = {} for key in dic: d = dic[key] if type(d) == type(""): if key == "style": d = str2dic(d) elif key == "animate": d = {key:d} else: d = str2ndic(d,key) dd = merge_dic(dd,d) else: dd[key] = d for key in exceptions: if key in dd: dd[exceptions[key]] = dd.pop(key) return dd def merge_dic(db1,db2): # merge dictionary db2 into db1 # dictionary must be normalized # db2 override db1 # produce normalized dictionary for k in db2: db1[k] = db2[k] return db1 def mdelkey(dic,mkey): # remove matched keys from the dictionary m = len(mkey) d = dic.copy() for k in dic: if k[0:m] == mkey: del d[k] return d def default(db,de): # db override de ndb = dic2ndic(db) nde = dic2ndic(de) return merge_dic(nde,ndb) def tagname(s): global tag_p if tag_p == None: tag_p = re.compile("<([a-zA-Z]+)") m = tag_p.match(s) return m.group(1) def opt2dic(s): # convert option part in tag to dictionary global opt_p dic = {} def func(mo): # mo is match object # print mo.group(0) att = mo.group(1) val = mo.group(2) dic[att] = val if opt_p == None: opt_p = re.compile(' *([a-zA-Z][a-zA-Z-]*)="([^"]*)"') opt_p.sub(func,s) return dic def dexml(s): # decompose xml to (TAG,OPTS,STR) # "STR" ---> (TAG,OPTS,STR) # "" ---> (TAG,OPTS,None) tag = tagname(s) s = s[len(tag)+1:] stop = s.find(">") if s[stop-1] == "/": return (tag,s[:stop-1],None) opt = s[:stop] s = s[stop+1:] stop = s.find(""%tag) return (tag,opt,s[:stop]) def xmlesc(s): # & to & # < to < # > to > s = s.replace("&","&") s = s.replace("<","<") s = s.replace(">",">") return s class Canvas: (oX,oY) = (0,0) (sX,sY) = (1,1) mapBox = viewBox = (0,0,None,None) marker_no = 0 patt_no = 0 symbol_no = 0 filter_no = 0 gid = 0 # group id for the group. Canvas is root group, the id is 0 out = [] # output buffer. the elements are strings child = [] # list of child group parent = None def __init__(self,x1,y1,x2,y2,**kd): kd = default(kd,{"font-family":"HelveticaNeue","fill":"none","stroke":"black","stroke-width":1}) self.mapBox = self.viewBox = (x1,y1,x2,y2) if kd and "map" in kd: m = kd.pop("map") self.map(m[0],m[1],m[2],m[3]) opt = dic2opt(kd,"") self.svgout("""\n""" + """\n"""%version + """\n"""%(x1,y1,x2-x1,y2-y1) + """"""%(self.oX,self.oY,opt) ) (self.oX,self.oY) = (0,0) def marker(self,id,**kd): kd["id"] = new_id = "marker.%d"%(self.marker_no) self.marker_no = self.marker_no + 1 (tag,opts,mdefs) = dexml(markers[id]) # tag should be marker dic = default(opt2dic(opts),{"stroke":"black","fill":"none","markerUnits":"strokeWidth","orient":"auto"}) kd = merge_dic(dic,kd) # default is markerUnits="strokeWidth" markerWidth="3" markerHeight="3" orient="auto" if "scale" in kd: scale = kd["scale"] if isnum(scale): scaleX = scaleY = scale else: (scaleX,scaleY) = scale (kd["markerWidth"],kd["markerHeight"]) = (scaleX * float(kd["markerWidth"]),scaleY * float(kd["markerHeight"])) opt1 = dic2opt(kd,"id","viewBox","ref","marker","orient","rotate") opt2 = dic2opt(kd,"stroke","fill","transform") self.svgout("\n"%opt2 + ""%opt1 + "%s\n"%mdefs ) return "url(#"+new_id+")" def symbol(self,id,**kd): # NB: symbols defined using is spinous, so we don't use kd = default(kd,{"style":"stroke:black;fill:none"}) kd = self.xtransform(**kd) kd["id"] = new_id = "symbol.%d"%(self.symbol_no) self.symbol_no = self.symbol_no + 1 opt = dic2opt(kd,"") data = '\n%s\n'%(opt,symbols[id]) self.svgout("defs","",data) # note that id in is no effect return new_id def pattern(self,id,**kd0): thedefault = {"style":"stroke:black;stroke-width:1;fill:none","scale":(1,1)} kd = default(kd0,thedefault) new_id = "pattern.%d"%(self.patt_no) self.patt_no = self.patt_no + 1 (tag,opts,pdefs) = dexml(patterns[id]) # tag should be pattern strokewidth = float(kd.pop("stroke-width")) kd = default(kd,opt2dic(opts)) width = float(kd.pop("width")) height = float(kd.pop("height")) # our standard width and height is 10 ratioX = width/10.0 ratioY = height/10.0 scale = kd.pop("scale") if isnum(scale): scaleX = scaleY = scale/ratioX else: scaleX = scale[0]/ratioX scaleY = scale[1]/ratioY kd["stroke-width"] = strokewidth*float(kd["stroke-width"]) kd.pop("patternTransform") opt = dic2opt(kd,"") self.svgout('\n'%(width,height,scaleX,scaleY) + '\n'%opt + "%s\n"%pdefs ) return "url(#"+new_id+")" def xywh(self,*t): (x1,y1,x2,y2) = expand(*t) (x1,y1) = self.xyconv(x1,y1) (x2,y2) = self.xyconv(x2,y2) if x1 > x2: (x1,x2) = (x2,x1) if y1 > y2: (y1,y2) = (y2,y1) (x,y) = (x1,y1) (w,h) = (x2 - x1, y2 - y1) return (x,y,w,h) def filter(self,*t,**kd): (x1,y1,x2,y2) = expand(*t) kd = default(kd, {"filterUnits":"userSpaceOnUse"}) filter = kd.pop("filter") if filter.find("<") == -1: filter = filters[filter] (x,y,w,h) = self.xywh(x1,y1,x2,y2) ret = None if not "id" in kd: kd["id"] = "filter.%d"%self.filter_no ret = "url(#%s)"%kd["id"] self.filter_no = self.filter_no + 1 opt = dic2opt(kd,"id","filterUnits") attr = 'x="%s" y="%s" width="%s" height="%s" %s'%(x,y,w,h,opt) self.svgout("filter",attr,filter) return ret def svgout(self,*arg): n = len(arg) if n == 1: self.out.append(arg[0]) elif n == 2 or arg[2] == None: self.out.append("<%s %s />"%(arg[0],arg[1])) elif n == 3: self.out.append("<%s %s>\n%s\n"%(arg[0],arg[1],arg[2],arg[0])) else: raise Exception("svgout: arguments must be 1,2 or 3") def close(self,**kd): if "save" in kd: name = kd["save"] else: file = sys.argv[0] name = file[0:file.rfind(".")]+".svg" if name == None: f = sys.stdout else: f = open(name,"w") for g in self.child: if g: g.flush() for line in self.out: f.write(line + "\n") f.write("\n") f.close() def map(self, *t): (x1,y1,x2,y2) = expand(*t) self.mapBox = (x1,y1,x2,y2) self.sX = 1.0*(self.viewBox[2] - self.viewBox[0])/(x2-x1) self.sY = 1.0*(self.viewBox[3] - self.viewBox[1])/(y2-y1) self.oX = self.viewBox[0] - self.sX*x1 self.oY = self.viewBox[1] - self.sY*y1 def xyconv(self,*t): (x,y) = expand(*t) if isnum(x): x = x*self.sX if isnum(y): y = y*self.sY return x,y def svgxy(self,*t): t = expand(*t) r = () for n in range(0,len(t),2): r = r + (self.oX+t[n]*self.sX,self.oY+t[n+1]*self.sY) return r def pathxyconv(self,*z): # z[-1] must be a string flag = None # move flag if z[-1] in "ltmsqchva": flag = True if z[-1] in "LTMSQCHVA": flag = False z = z[0:-1] if len(z) == 1: z = z[0] m = len(z) u = [None]*m for n in range(0,m,2): x,y = self.xyconv(float(z[n]),float(z[n+1])) u[n],u[n+1] = str(x),str(y) return u def svgpath(self,s): # translate Python SVG path to SVG path t = s.split() m = len(t) v = [] n = 0 while n < m: key = t[n] if key[0] in "-0123456789": key = key0 else: v = v + [key] n = n + 1 if key in "LTMltm": v = v + self.pathxyconv(t[n:n+2],key); n = n + 2 elif key in "SQsq": v = v + self.pathxyconv(t[n:n+4],key); n = n + 4 elif key in "Cc": v = v + self.pathxyconv(t[n:n+6],key); n = n + 6 elif key in "Hh": v = v + [self.pathxyconv(t[n],0,key)[0]]; n = n + 1 elif key in "Vv": v = v + [self.pathxyconv(0,t[n],key)[1]]; n = n + 1 elif key in "Aa": v = v + self.pathxyconv(t[n:n+2],"a") v = v + t[n+2:n+5] v = v + self.pathxyconv(t[n+5:n+7],key) n = n + 7 key0 = key s = " ".join(v) return s def xtransform(self,**kd): # transform the args such as translate=(x,y), rotate=a, scale=(sx,sy) # to transform="translate=(x,y) rotate=a scale=(sx,sy)" t = "" if "x" in kd and "y" in kd: t = t + "translate(%s,%s) "%(kd.pop("x"),kd.pop("y")) if "rotate" in kd: ro = kd.pop("rotate") t = t + "rotate(%s) "%ro if "scale" in kd: sc = kd.pop("scale") if isnum(sc): sc = (sc,sc) t = t + "scale(%s,%s) "%sc if t != "": if "transform" in kd: kd["transform"] = t + kd["transform"] else: kd["transform"] = t return kd def view(self,*t,**kd): (x1,y1,x2,y2) = expand(*t) kd = dic2ndic(kd) opt = dic2opt(kd,"") if x2 < x1: x1,x2 = x2,x1 if y2 < y1: y1,y2 = y2,y1 self.svgout("view",'viewBox="%s %s %s %s" %s'%(x1,y1,x2-x1,y2-y1,opt)) # ===== drawing elements ====== def path(self,s,**kd): kd = dic2ndic(kd) kd = self.xtransform(**kd) s = self.svgpath(s) animate = None if "animate" in kd: animate = kd.pop("animate") opt = dic2opt(kd,"") self.svgout("path",'d="%s" %s'%(s,opt),animate) def cplot2(self,P,DP,**kd): path = "M %s %s"%tuple(P[0]) for n in range(0,len(DP)-1): P0,Q0 = P[n],cal(P[n],DP[n],3,"/","+") P1,Q1 = P[n+1],cal(P[n+1],DP[n+1],-3,"/","+") path = path + " C %s %s %s %s %s %s"%(expand(Q0,Q1,P1)) self.path(path,**kd) def interpolation(self,*q,**kd): # Overhauser (Catmull-Rom) Spline P = points(*q) kd.pop("smooth") g = 1.0/2 # value by Catmull-Rom if "smooth-looseness" in kd: g = float(kd.pop("smooth-looseness"))/2 DP = [] # list of tangential vectors for n in range(1,len(P)-1): DP.append(cal(P[n+1],P[n-1],"-",g,"*")) self.cplot2(P[1:],DP,**kd) def interpolation2(self,*q,**kd): # Arisawa Spline exclude = None if "exclude" in kd: exclude = kd.pop("exclude") P = points(*q) # R(t) is k0(t)*P0 + k1(t)*P1 + k2(t)*P2 + k3(t)*P3 (where t is in range [0,4]) # k0(t) = (1/6)*(1-t)*(2-t)*(3-t) # k1(t) = (1/2)*t*(2-t)*(3-t) # k2(t) = -(1/2)*t*(1-t)*(3-t) # k3(t) = (1/6)*t*(1-t)*(2-t) P0,P1,P2,P3 = tuple(P[0:4]) # D0,D1,D2,D3 are derivatives of R(t) at t=0,1,2,3 # D0 = P3/3-3*P2/2+3*P1-11*P0/6 D0 = cal(1.0/3,P3,"*",-3.0/2,P2,"*","+",3,P1,"*","+",-11.0/6,P0,"*","+") # D1 = -P3/6+P2-P1/2-P0/3 # D1 is derivative of R(t) at t=1 D1 = cal(-1.0/6,P3,"*",P2,"+",1.0/2,P1,"*","-",1.0/3,P0,"*","-") # D2 = P3/3+P2/2-P1+P0/6 # D2 is derivative of R(t) at t=2 D2 = cal(1.0/3,P3,"*",1.0/2,P2,"*","+",P1,"-",1.0/6,P0,"*","+") DP = [D0,D1] n = len(P) P0,P1,P2,P3 = tuple(P[n-4:n]) # D3 = 11*P3/6-3*P2+3*P1/2-P0/3 # DL is derivative of the last point DL = cal(11.0/6,P3,"*",-3,P2,"*","+",3.0/2,P1,"*","+",-1.0/3,P0,"*","+") b = D2 for n in range(1,len(P)-3): P0,P1,P2,P3 = tuple(P[n:n+4]) D1 = cal(-1.0/6,P3,"*",P2,"+",1.0/2,P1,"*","-",1.0/3,P0,"*","-") D2 = cal(1.0/3,P3,"*",1.0/2,P2,"*","+",P1,"-",1.0/6,P0,"*","+") DP.append(cal(b,D1,"+",2,"/")) b = D2 DP.append(b) DP.append(DL) if exclude in ("start","both"): P,DP = P[1:],DP[1:] if exclude in ("end","both"): P,DP = P[:-1],DP[:-1] self.cplot2(P,DP,**kd) def smoothline(self,*t,**kd): # internal use only t = expand(*t) sm = ["L","S","Q"] c = sm[kd.pop("smooth")] s = "M %s %s %s %s %s"%(t[0],t[1],c,t[2],t[3]) for n in range(4,len(t)-2,2): s = s + " %s %s %s %s %s"%((t[n-2]+t[n])/2.0,(t[n-1]+t[n+1])/2.0,c,t[n],t[n+1]) if len(t)%2 == 0: s = s + " %s %s"%(t[n+2],t[n+3]) else: s = s + " %s %s"%(t[n],t[n+1]) self.path(s,**kd) def mark(self,*t,**kd): t = expand(*t) kd = dic2ndic(kd) kd = self.xtransform(**kd) m = len(t) label = None if "label" in kd: label = kd.pop("label") if type(label) == type(""): label=(label,) if "stroke" in kd: kd.pop("stroke") anchor = pad = dx = dy = None if "anchor" in kd: anchor = tolist(kd.pop("anchor")) if "pad" in kd: pad = tolist(kd.pop("pad")) if "dx" in kd: dx = tolist(kd.pop("dx")) if "dy" in kd: dy = tolist(kd.pop("dy")) kd0 = kd.copy() for n in range(0,len(t),2): if anchor: kd0["anchor"] = anchor[n//2 % len(anchor)] if pad: kd0["pad"] = pad[n//2 % len(pad)] if dx: kd0["dx"] = dx[n//2 % len(dx)] if dy: kd0["dy"] = dy[n//2 % len(dy)] self.text(t[n],t[n+1],text=label[n//2],**kd0) kd = mdelkey(kd,"font") for n in range(0,len(t),2): self.use(t[n],t[n+1],**kd) def line(self,*t,**kd): t = expand(*t) kd = dic2ndic(kd) kd = self.xtransform(**kd) m = len(t) if "smooth" in kd: smooth = kd["smooth"] if smooth == "I": self.interpolation(*t,**kd) return if smooth == "I2": self.interpolation2(*t,**kd) return self.smoothline(*t,**kd) return s = "" for n in range(0,len(t),2): z = self.xyconv(t[n],t[n+1]) s = s + "%s,%s "%(z[0],z[1]) s = s[:-1] animate = None if "animate" in kd: animate = kd.pop("animate") opt = dic2opt(kd,"") self.svgout("polyline", 'points="%s" %s'%(s,opt), animate) def polygon(self,*t,**kd): t = expand(*t) kd = dic2ndic(kd) kd = self.xtransform(**kd) t = expand(*t) m = len(t) t = self.svgxy(t) s = "" for n in range(0,len(t),2): s = s + "%s,%s "%(t[n],t[n + 1]) s = s[:-1] animate = None if "animate" in kd: animate = kd.pop("animate") opt = dic2opt(kd,"") self.svgout("polygon", 'points="%s" %s'%(s,opt),animate) def textpath(self,**kd): kd = self.xtransform(**kd) s = ('\n'%kd.pop("ref") + kd.pop("text") + '\n' + '' ) opt = dic2opt(kd,"") self.svgout("text", opt, s) def textarea(self,*t,**kd): # internal use only # currently, textArea support is very limited: only by Opera (2012/12/25) # we assume *t is expanded # borderline should be supported # then pad should be also supported; (left,top,right,bottom) oder? # In SVG Tiny 1.2 Specification, height="auto" is also possible. Look the Spec. for more details text = kd.pop("text") kd = default(kd,{"fill":"black","stroke":"none","baseline":"inherit"}) #kd = dic2ndic(kd) kd = self.xtransform(**kd) if len(t) == 4: (x1,y1,x2,y2) = expand(*t) (x,y,w,h) = self.xywh(x1,y1,x2,y2) else: (x1,y1) = expand(*t) (x,y) = self.xyconv(x1,y1) w = kd.pop("width") h = kd.pop("height") (w,h) = self.xyconv(w,h) (kd['x'],kd["y"],kd["width"],kd["height"]) = (x,y,w,h) opt = dic2opt(kd,"") self.svgout("textArea", 'xml:space="preserve" %s'%opt,text) def text(self,*t,**kd): t = expand(*t) text = kd.pop("text") decoration = False if "decoration" in kd: decoration = kd.pop("decoration") if decoration == False: text = xmlesc(text) kd = default(kd,{"fill":"black","stroke":"none","baseline":"inherit"}) if len(t) == 0: kd = self.xtransform(**kd) kd["text"] = text self.textpath(**kd) return (kd["x"],kd["y"]) = self.xyconv(t[0],t[1]) kd = self.xtransform(**kd) # it seems there are no way to handle " " in kd["text"] # is not effective. # " " is not allowed. # "
....
" is not allowed. # there are many issues on this problem in the net d = {} b = 0 fsize = 1.0 # ratio to default font height that is given by "em" # we should not apply padding to baseline padx = pady = 0 if "pad" in kd: padx = pady = fsize * kd.pop("pad") if "baseline" in kd: d["baseline"] = kd["baseline"] if "anchor" in kd: anchor = kd.pop("anchor") if "E" in anchor: d["text-anchor"] = "end" padx = -padx if "W" in anchor: d["text-anchor"] = "start" if anchor in "CNS": d["text-anchor"] = "middle" padx = 0 if anchor in ("start","middle","end"): d["text-anchor"] = anchor if "N" in anchor: b = fsize if anchor in "CWE": b = fsize/2 pady = 0 if "S" in anchor: pady = -pady if "baseline" in d: d["alignment-baseline"] = d.pop("baseline") s = text if "animate" in kd: s = s + kd.pop("animate") if decoration and decoration != True: s = ''%decoration + s + '' # if we have transform data such as rotate, we must put x,y into transform="translate(x,y)" # and remove x="x",y="y" if "dx" in kd: padx = float(kd.pop("dx")) if "dy" in kd: pady = float(kd.pop("dy")) op = dic2opt(d,"") + " " + dic2opt(kd,"") self.svgout("text", 'xml:space="preserve" dx="%sem" dy="%sem" %s'%(padx,0.7*b+pady,op), s) # we have alignment-baseline and dominant-baseline. I don't know these usage. # alignment-baseline: auto,baseline,before-edge,text-before-edge,middle,central,after-edge, # text-after-edge,ideographic,alphabetic,hanging,mathematical,inherit # dominant-baseline: auto,use-script,no-change,reset-size,ideographic,alphabetic,hanging, # mathematical,central,middle,text-after-edge,text-before-edge,inherit # baseline is not effective to some browsers # # font style is # style="font-family:Helvetica;font-size:20" # note that style is not effective to some browsers # # http://nelsonslog.wordpress.com/2011/09/12/svgtext-baseline-considered-harmful/ # dominant-baseline="alphabetic" # dy="0.0em" # S # dy="0.35em" # C # dy="0.7em" # N def circle(self,*t,**kd): (x,y) = expand(*t) kd = dic2ndic(kd) kd = self.xtransform(**kd) r = kd.pop("r") if isnum(r): rx = ry = r else: (rx,ry) = (r[0],r[1]) (x,y) = self.xyconv(x,y) (rx,ry) = self.xyconv(rx,ry) animate = None if "animate" in kd: animate = kd.pop("animate") opt = dic2opt(kd,"") self.svgout("ellipse",'cx="%s" cy="%s" rx="%s" ry="%s" %s'%( x,y,abs(rx),abs(ry),opt),animate) def create_oval(self,*t,**kd): # for Tk (x1,y1,x2,y2) = expand(*t) kd = default(kd,{"outline":"black","fill":"none","width":1}) self.circle((x1+x2)/2.0,(y1+y2)/2.0,r=(abs((x2-x1)/2.0),abs((y2-y1)/2.0)),**kd) def rect(self,*t,**kd): (x1,y1,x2,y2) = expand(*t) kd = dic2ndic(kd) kd = self.xtransform(**kd) (x,y,w,h) = self.xywh(x1,y1,x2,y2) animate = None if "animate" in kd: animate = kd.pop("animate") opt = dic2opt(kd,"") self.svgout("rect",'x="%s" y="%s" width="%s" height="%s" %s'%(x,y,w,h,opt),animate) def create_rectangle(self,*t,**kd): # for Tk (x1,y1,x2,y2) = expand(*t) kd = default(kd,{"fill":"none", "outline":"black", "width":1}) s = "" if "fill" in kd: s = s + 'fill:%s;'%kd["fill"] if "outline" in kd: s = s + 'stroke:%s;'%kd["outline"] if "width" in kd: s = s + 'stroke-width:%s;'%kd["width"] self.rect(x1,y1,x2,y2,style=s) def arc(self,*t,**kd): (x,y) = expand(*t) kd = default(kd,{"shape":"arc", "fill":"none"}) kd = self.xtransform(**kd) # shape is one of "arc","chord","sector"; Tk employs "pieslice" for "sector". # (x,y) is center shape = kd.pop("shape") r = kd["r"] if isnum(r): rx = ry = r else: (rx,ry) = (r[0],r[1]) c = math.pi/180 start = kd.pop("start") extent = kd.pop("extent") st = c * start ex = c * extent stop = st + ex x1,y1 = (rx*math.cos(st)+x,ry*math.sin(st)+y) if shape in ("sector"): s = "M %s %s L %s %s"%(x,y,x1,y1) else: s = "M %s %s"%(x1,y1) x2,y2 = (rx*math.cos(stop)+x,ry*math.sin(stop)+y) flag1 = flag2 = 0 if abs(extent) > 180: flag1 = 1 # large arc flag if extent < 0: flag2 = 1 # large arc flag rotate = 0 if "rotate" in kd: rotate = kd.pop("rotate") s = s + " A %s %s %d %d %d %s %s"%(rx,ry,rotate,flag1,flag2,x2,y2) if shape in ("chord","sector"): s = s + " Z" self.path(s,**kd) def image(self,*t,**kd): (x1,y1,x2,y2) = expand(*t) kd = dic2ndic(kd) kd = self.xtransform(**kd) (x,y,w,h) = self.xywh(x1,y1,x2,y2) animate = None if "animate" in kd: animate = kd.pop("animate") opt = dic2opt(kd,"") self.svgout("image",'x="%s" y="%s" width="%s" height="%s" %s'%(x,y,w,h,opt),animate) def use(self,*t,**kd): t = expand(*t) kd = dic2ndic(kd) kd["xlink:href"] = "#" + kd.pop("ref") if len(t) > 1: kd["x"],kd["y"] = self.xyconv(t[0],t[1]) kd = self.xtransform(**kd) animate = None if "animate" in kd: animate = kd.pop("animate") opt = dic2opt(kd,"") self.svgout("use",opt,animate) def grid(self,*t,**kd): pitchX,pitchY = expand(*t) """=== usage example c.grid(1,1,range=(-1.3,1.3,1.3,-1.3),stroke="cyan", frame="green;width:5",axis="black;width:2", font="Helvetica;size:16",label=("X","Y")) # note that font is for the scale numbers, not for the axis label # it's unable to make fine tuning to label. if you need, do manually! # making too much is worse than do nothing. ===""" # these attributes must be before default frame = None if "frame" in kd: frame = kd.pop("frame") axis = None if "axis" in kd: axis = kd.pop("axis") xlabel = ylabel = None if "label" in kd: xlabel,ylabel = kd.pop("label") kd = default(kd,{"stroke":"green;width:1"}) color = kd.pop("stroke") width = float(kd["stroke-width"]) if "range" in kd: range = kd.pop("range") (mX1,mY1,mX2,mY2) = range else: (mX1,mY1,mX2,mY2) = self.mapBox if mX1 > mX2: (mX1,mX2) = (mX2,mX1) if mY1 > mY2: (mY1,mY2) = (mY2,mY1) # x or y may be a small number, so ... x0 = math.ceil(float(mX1)/pitchX)*pitchX y0 = math.ceil(float(mY1)/pitchY)*pitchY x = x0 while x <= mX2: self.line(x,mY1,x,mY2,stroke="%s;width:%s"%(color,width)); x = x + pitchX y = y0 while y <= mY2: self.line(mX1,y,mX2,y,stroke="%s;width:%s"%(color,width)); y = y + pitchY if xlabel: x = x0 while x <= mX2: self.text(x,0,text=int(x),anchor="NW",**kd); x = x + pitchX self.text((mX1+mX2)/2,mY1,text="%s"%xlabel,font="size:24",dy="1",anchor="N",**kd) if ylabel: y = y0 while y <= mY2: self.text(0,y,text=int(y),anchor="ES",**kd); y = y + pitchY self.text(mX1,(mY1+mY2)/2,text="%s"%ylabel,font="size:24",dx="-1",anchor="E",**kd) if not axis: axis = "%s;width:%s"%(color,2*width) self.line(mX1,0,mX2,0,stroke="%s"%axis) self.line(0,mY1,0,mY2,stroke="%s"%axis) if frame: self.rect(mX1,mY1,mX2,mY2,stroke="%s"%frame) # ======== group family ============== def link(self,**kd): kd = dic2ndic(kd) if "ref" in kd: kd["xlink:href"] = "#" + kd.pop("ref") kd["gtype"] = "a" return self.group(**kd) def clip(self,**kd): kd = default(kd, {"clippathUnits":"userSpaceOnUse"}) kd["gtype"] = "clipPath" return self.group(**kd) def mask(self,*t,**kd): (x1,y1,x2,y2) = expand(*t) kd = default(kd, {"maskUnits":"userSpaceOnUse"}) (kd["x"],kd["y"],kd["width"],kd["height"]) = self.xywh(x1,y1,x2,y2) kd["gtype"] = "mask" return self.group(**kd) def define(self,**kd): kd["gtype"] = "defs" return self.group(**kd) def group(self,**kd): global gid new = copy.deepcopy(self) new.parent = self gid = gid + 1 kd = self.xtransform(**kd) kd = dic2ndic(kd) t = "g" if "gtype" in kd: t = kd.pop("gtype") opt = dic2opt(kd,"") new.out = ['<%s %s>'%(t,opt)] self.child.append(new) new.child = [] new.gid = gid return new def end(self): # user's interface for flush self.flush() def flush(self): # flush the out buffer (including children's) to it's parent out buffer # and hide the 'self' from the parent' child list # keep order of the 'for' statements p = self.parent out = p.out for g in self.child: if g: g.flush() for line in self.out: out.append(line) gtype = tagname(self.out[0]) # g,defs,symbol out.append(""%gtype) for n in range(0,len(p.child)): #print "##+",n,p.child[n],self if p.child[n] == self: p.child[n] = None # === Plot Library def func(self,f,**kd): low,high = kd.pop("range") low,high = float(low),float(high) m = 100 if "n" in kd: m = int(kd.pop("n")) a = (high - low)/m for n in range(0,m,1): (x1,x2) = (a*n + low,a*(n+1) + low) y1,y2 = f(x1),f(x2) x0 = (x1+x2)/2 y0 = 2*f(x0) - (f(x1)+f(x2))/2 self.path("M %s %s Q %s %s %s %s"%(x1,y1,x0,y0,x2,y2))