def __init__(self, p1=P(0, 0), p2=0, **options): if isinstance(p1, P): c = p1 else: c = P(p1, p2) Circle.__init__(self, **options) self.c = c
def fit_curve(self, p0, p1): ''' fit a natural looking spline to end slopes ''' # first get the angles ... if type(self.c0) in [type(10), type(10.0)]: # turn this into a unit vector in that direction w0 = U(self.c0) elif isinstance(self.c0, R): # already have unit vectior w0 = self.c0 elif isinstance(self.c0, P): # non-unit vector giving direction w0 = (self.c0 - p0) else: raise ValueError, "Unknown control type c0" if type(self.c1) in [type(10), type(10.0)]: # turn this into a unit vector in that direction w1 = U(self.c1) elif isinstance(self.c1, R): # already have unit vectior w1 = self.c1 elif isinstance(self.c1, P): # non-unit vector giving direction w1 = (self.c1 - p1) else: raise ValueError, "Unknown control type c1" t = ((p1 - p0).arg - w0.arg) * pi / 180. p = -((-p1 + p0).arg - w1.arg) * pi / 180. a = self._a b = self._b c = self._c alpha = a * (sin(t) - b * sin(p)) * (sin(p) - b * sin(t)) * (cos(t) - cos(p)) rho = (2 + alpha) / (1 + (1 - c) * cos(t) + c * cos(p)) sigma = (2 - alpha) / (1 + (1 - c) * cos(p) + c * cos(t)) c0 = P( p0.x + rho * ((p1.x - p0.x) * cos(t) - (p1.y - p0.y) * sin(t)) / (3 * self.t0), p0.y + rho * ((p1.y - p0.y) * cos(t) + (p1.x - p0.x) * sin(t)) / (3 * self.t0)) c1 = P( p0.x + (p1.x - p0.x) * (1 - sigma * cos(p) / (3 * self.t1)) - (p1.y - p0.y) * sigma * sin(p) / (3 * self.t1), p0.y + (p1.y - p0.y) * (1 - sigma * cos(p) / (3 * self.t1)) + (p1.x - p0.x) * sigma * sin(p) / (3 * self.t1)) # only change if we were given an angle if type(self.c0) in [type(10), type(10.0)]: self.c0 = c0 if type(self.c1) in [type(10), type(10.0)]: self.c1 = c1
def locus(self, angle, target=None): ''' Set or get a point on the locus @param angle: locus point in degrees (Degrees clockwise from north) @type angle: float @param target: target point @return: - target is None: point on circumference at that angle - else: set point to the target, and return reference to object @rtype: self or P ''' r = self.r x = r * sin(angle / 180.0 * pi) y = r * cos(angle / 180.0 * pi) l = P(x, y) if target is None: return self.itoe(l) else: self.move(target - self.locus(angle)) return self
def render(*objects, **options): ''' render the file @param objects: list of objects to render @type objects: list @param options: dictionary of options to use when rendering @type options: dict ''' if not options.has_key('file'): raise LookupError, "No filename given" out = open(options['file'], "w") if len(objects) == 0: raise ValueError, "No objects to render!" elif len(objects) == 1: if isinstance(objects[0], Eps): obj = objects[0] elif isinstance(objects[0], Pages): obj = objects[0] elif isinstance(objects[0], Page): # wrap in Pages environment obj = apply(Pages, objects) else: # assume it's an eps and wrap obj = apply(Eps, objects) else: if isinstance(objects[0], Page): # assume we have pages obj = apply(Pages, objects) else: # we have an eps obj = apply(Eps, objects) if isinstance(obj, Eps): # Make the sw corner (0,0) since some brain-dead previewers # don't understand bounding-boxes x1, y1, x2, y2 = obj.bbox_pp() obj.move((P(0, 0) - P(x1, y1)) / float(defaults.units)) obj.write(out) out.close() print "Wrote", options['file']
def area(self): ''' return an area object same size as page in default units @rtype: Area ''' d1, d2, w, h = self.bbox_pp() w, h = w/float(defaults.units), h/float(defaults.units) return Area(sw = P(0, 0), width = w, height = h)
def __init__(self, text="", **options): self.text = text print "Obtaining TeX object's boundingbox ..." # this should be a tempfile ? tempName = "temp1" fp = open("%s.tex" % tempName, "w") fp.write(defaults.tex_head) fp.write(text) fp.write(defaults.tex_tail) fp.close() foe = os.popen(defaults.tex_command % tempName) sys.stderr.write(foe.read(-1)) sys.stderr.write('\n') # Help the user out by throwing the latex log to stderr if os.path.exists("%s.log" % tempName): fp = open("%s.log" % tempName, 'r') sys.stderr.write(fp.read(-1)) fp.close() if foe.close() is not None: raise RuntimeError, "Latex Error" fi, foe = os.popen4("dvips -q -E -o %s.eps %s.dvi" % \ (tempName, tempName)) err = foe.read(-1) sys.stderr.write(err) sys.stderr.write('\n') fi.close() if len(err) > 0: raise RuntimeError, "dvips Error" fp = open("%s.eps" % tempName, "r") eps = fp.read(-1) fp.close() # grab boundingbox ... only thing we want at this stage bbox_so = re.search("\%\%boundingbox:\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)", eps, re.I) bbox = [] for ii in bbox_so.groups(): bbox.append(int(ii)) self.width = (bbox[2] - bbox[0]) / float(defaults.units) self.height = (bbox[3] - bbox[1]) / float(defaults.units) # now we have a width and height we can initialise Area Area.__init__(self, **options) self.offset = -P(bbox[0], bbox[1]) / float(defaults.units) self.scale(self.iscale)
def __init__(self, file, **options): ''' @param file: path to epsf file @type file: string @return: The eps figure as an area object ''' self.file = file print "Loading %s" % file fp = open(file, 'r') self.all = fp.read(-1) fp.close() bbox_so = re.compile( "\%\%boundingbox:\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)", re.I | re.S) so = bbox_so.search(self.all) x1s, y1s, x2s, y2s = so.groups() d = float(defaults.units) x1 = float(x1s) / d y1 = float(y1s) / d x2 = float(x2s) / d y2 = float(y2s) / d self.offset = -P(x1, y1) self.width = x2 - x1 self.height = y2 - y1 # width and height have special meaning here if options.has_key('width') and options.has_key('height'): sx = options['width'] / float(self.width) sy = options['height'] / float(self.height) del options['width'] del options['height'] elif options.has_key('width'): sx = sy = options['width'] / float(self.width) del options['width'] elif options.has_key('height'): sx = sy = options['height'] / float(self.height) del options['height'] else: sx = sy = 1 self.scale(sx, sy) Area.__init__(self, **options)
def bbox(self, itoe=Identity): """ Return the bounding box """ p0 = itoe(self.s) p1 = itoe(self.e) x0 = min(p0[0], p1[0]) x1 = max(p0[0], p1[0]) y0 = min(p0[1], p1[1]) y1 = max(p0[1], p1[1]) return Bbox(sw=P(x0, y0), width=x1 - x0, height=y1 - y0)
def bbox(self): """ Return the bounding box of the Path """ # the (0,0) point: p0 = self.itoe(P(0, 0)) xmax = xmin = p0.x ymax = ymin = p0.y for bez in self.shape: c1x, c1y, c2x, c2y, p2x, p2y = bez p1 = self.itoe(P(c1x, c1y) / float(defaults.units)) p2 = self.itoe(P(c2x, c2y) / float(defaults.units)) p3 = self.itoe(P(p2x, p2y) / float(defaults.units)) xmax = max(xmax, p1.x, p2.x, p3.x) xmin = min(xmin, p1.x, p2.x, p3.x) ymax = max(ymax, p1.y, p2.y, p3.y) ymin = min(ymin, p1.y, p2.y, p3.y) return Bbox(sw=P(xmin, ymin), width=xmax - xmin, height=ymax - ymin)
def bbox(self): """ Return the bounding box of the object """ x1, y1 = self.sw x2, y2 = self.ne for p in [self.sw, self.nw, self.ne, self.se]: x1 = min(x1, p[0]) y1 = min(y1, p[1]) x2 = max(x2, p[0]) y2 = max(y2, p[1]) return Bbox(sw=P(x1, y1), width=x2 - x1, height=y2 - y1)
def move(self, *args): ''' translate object by a certain amount @param args: amount to move by, can be given as - dx,dy - P @return: reference to self @rtype: self ''' if len(args) == 1: # assume we have a point self.o += args[0] else: # assume we have dx,dy self.o += P(args[0], args[1]) return self
def bbox(self, itoe=Identity): """ Return the bounding box of the object """ # run through the list of points to get the bounding box #if self.length is None: # self._cache() p0 = itoe(self.s) x0, y0 = p0 x1, y1 = p0 for p in self._points: p1 = itoe(p) x0 = min(x0, p1[0]) x1 = max(x1, p1[0]) y0 = min(y0, p1[1]) y1 = max(y1, p1[1]) return Bbox(sw=P(x0, y0), width=x1 - x0, height=y1 - y0)
def _get_c(self): """ Get the "centre" point """ return self.itoe(P(self.width / 2., self.height / 2.) + self.isw)
class ArrowHead(AffineObj): ''' Arrow head object @cvar tip: where to position the tip of the arrow head @cvar angle: the direction to point Convenience variables modifying the head size: @cvar scalew: scale the width by this amount @cvar scaleh: scale height by this amount The actual shape of the arrowhead is defined by the following, distances are given in points @cvar start: tuple giving starting point for path @cvar shape: list of tuples giving arguments to postscripts curveto operator @cvar closed: whether to close the path or not @cvar fg: line color or None for no line @cvar bg: fill color or None for no fill @cvar linewidth: linewidth @cvar linejoin: 0=miter, 1=round, 2=bevel @cvar mitrelimit: length of mitre of corners ''' fg = Color(0) bg = Color(0) # used by Path object to set position and direction reverse = 0 pos = 1 tip = P(0, 0) angle = 0 start = (0, 0) # triangular share in the Golden ratio # positions in pixels shape = [(0, 0, 1.5, -4.854, 1.5, -4.854), (1.5, -4.854, -1.5, -4.854, -1.5, -4.854), (-1.5, -4.854, 0, 0, 0, 0)] closed = 1 scalew = 1 scaleh = 1 linewidth = 0.2 linejoin = 2 #0=miter, 1=round, 2=bevel # miterlimit: # 1.414 cuts off miters at angles less than 90 degrees. # 2.0 cuts off miters at angles less than 60 degrees. # 10.0 cuts off miters at angles less than 11 degrees. # 1.0 cuts off miters at all angles, so that bevels are always produced miterlimit = 2 def __init__(self, *param, **options): # remember this angle for when instance is copied... # this assumes all rotations that have been applied # are represented by angle and reverse angle0 = self.angle + self.reverse * 180 AffineObj.__init__(self, **options) if len(param) == 1: self.pos = param[0] sx = self.scalew sy = self.scaleh self.start = (self.start[0] * sx, self.start[1] * sy) shape = [] for b in self.shape: shape.append((b[0] * sx, b[1] * sy, b[2] * sx, b[3] * sy, b[4] * sx, b[5] * sy)) self.shape = shape self.rotate(self.angle + self.reverse * 180 - angle0) self.move(self.tip) def body(self): """ Return the postscript body of the Path """ out = cStringIO.StringIO() if self.linewidth is not None: out.write("%g setlinewidth " % self.linewidth) if self.linejoin is not None: out.write("%d setlinejoin " % self.linejoin) if self.miterlimit is not None: out.write("%f setmiterlimit " % self.miterlimit) out.write('newpath %g %g moveto\n' % self.start) for bez in self.shape: out.write('%g %g %g %g %g %g curveto\n' % bez) if self.closed: out.write('closepath\n') if self.bg is not None: out.write("gsave %s fill grestore\n" % self.bg) if self.fg is not None: out.write("%s stroke\n" % self.fg) return out.getvalue() def bbox(self): """ Return the bounding box of the Path """ # the (0,0) point: p0 = self.itoe(P(0, 0)) xmax = xmin = p0.x ymax = ymin = p0.y for bez in self.shape: c1x, c1y, c2x, c2y, p2x, p2y = bez p1 = self.itoe(P(c1x, c1y) / float(defaults.units)) p2 = self.itoe(P(c2x, c2y) / float(defaults.units)) p3 = self.itoe(P(p2x, p2y) / float(defaults.units)) xmax = max(xmax, p1.x, p2.x, p3.x) xmin = min(xmin, p1.x, p2.x, p3.x) ymax = max(ymax, p1.y, p2.y, p3.y) ymin = min(ymin, p1.y, p2.y, p3.y) return Bbox(sw=P(xmin, ymin), width=xmax - xmin, height=ymax - ymin)
def _get_sw(self): """ Get the "south-west" point """ return self.itoe(P(-self.r, -self.r))
def _get_ne(self): """ Get the "north-east" point """ return self.itoe(P(self.width, self.height) + self.isw)
def _get_nw(self): """ Get the "nort-west" point """ return self.itoe(P(-self.r, self.r))
def _get_se(self): """ Get the "south-east" point """ return self.itoe(P(self.r, -self.r))
class Area(AffineObj): """ A Rectangular area defined by sw corner and width and height. defines the following compass points that can be set and retrived:: nw--n--ne | | w c e | | sw--s--se The origin is the sw corner and the others are calculated from the width and height attributes. If a subclass should have the origin somewhere other than sw then overide the sw attribute to make it a function @cvar width: the width @cvar height: the height @cvar c: centre point (simillarly for n,ne etc) """ #XXX allow the changing of sw corner away from origin eg Text isw = P(0, 0) width = 0 height = 0 # Dynamic locations def _get_n(self): """ Get the "north" point """ return self.itoe(P(self.width / 2., self.height) + self.isw) def _set_n(self, pe): """ Set the "north" point """ self.move(pe - self.n) n = property(_get_n, _set_n) def _get_ne(self): """ Get the "north-east" point """ return self.itoe(P(self.width, self.height) + self.isw) def _set_ne(self, pe): """ Set the "north-east" point """ self.move(pe - self.ne) ne = property(_get_ne, _set_ne) def _get_e(self): """ Get the "east" point """ return self.itoe(P(self.width, self.height / 2.) + self.isw) def _set_e(self, pe): """ Set the "east" point """ self.move(pe - self.e) e = property(_get_e, _set_e) def _get_se(self): """ Get the "south-east" point """ return self.itoe(P(self.width, 0) + self.isw) def _set_se(self, pe): """ Set the "south-east" point """ self.move(pe - self.se) se = property(_get_se, _set_se) def _get_s(self): """ Get the "south" point """ return self.itoe(P(self.width / 2., 0) + self.isw) def _set_s(self, pe): """ Set the "south" point """ self.move(pe - self.s) s = property(_get_s, _set_s) def _get_sw(self): """ Get the "south-west" point """ return self.itoe(self.isw) def _set_sw(self, pe): """ Set the "south-west" point """ self.move(pe - self.sw) sw = property(_get_sw, _set_sw) def _get_w(self): """ Get the "west" point """ return self.itoe(P(0, self.height / 2.) + self.isw) def _set_w(self, pe): """ Set the "west" point """ self.move(pe - self.w) w = property(_get_w, _set_w) def _get_nw(self): """ Get the "north-west" point """ return self.itoe(P(0, self.height) + self.isw) def _set_nw(self, pe): """ Set the "north-west" point """ self.move(pe - self.nw) nw = property(_get_nw, _set_nw) def _get_c(self): """ Get the "centre" point """ return self.itoe(P(self.width / 2., self.height / 2.) + self.isw) def _set_c(self, pe): """ Set the "centre" point """ self.move(pe - self.c) c = property(_get_c, _set_c) def bbox(self): """ Return the bounding box of the object """ x1, y1 = self.sw x2, y2 = self.ne for p in [self.sw, self.nw, self.ne, self.se]: x1 = min(x1, p[0]) y1 = min(y1, p[1]) x2 = max(x2, p[0]) y2 = max(y2, p[1]) return Bbox(sw=P(x1, y1), width=x2 - x1, height=y2 - y1)
def _get_nw(self): """ Get the "north-west" point """ return self.itoe(P(0, self.height) + self.isw)
def _get_n(self): """ Get the "north" point """ return self.itoe(P(self.width / 2., self.height) + self.isw)
def body(self): """ Returns the object's postscript body """ out = cStringIO.StringIO() if self.linewidth: out.write("%g setlinewidth " % self.linewidth) if self.dash is not None: out.write(str(self.dash)) # make sure we have a sensible radius r = min(self.width / 2., self.height / 2., self.r) w = self.width h = self.height ATTR = { 'bg': self.bg, 'fg': self.fg, 'width': w, 'height': h, 'r': r, 'ne': P(w, h), 'n': P(w / 2., h), 'nw': P(0, h), 'w': P(0, h / 2.), 'sw': P(0, 0), 's': P(w / 2., 0), 'se': P(w, 0), 'e': P(w, h / 2.), } if self.bg is not None: if self.r == 0: out.write("%(bg)s 0 0 %(width)g uu %(height)g uu rectfill\n"\ % ATTR) else: out.write("%(bg)s newpath %(w)s moveto\n" % ATTR) out.write("%(nw)s %(n)s %(r)s uu arcto 4 {pop} repeat\n" % ATTR) out.write("%(ne)s %(e)s %(r)s uu arcto 4 {pop} repeat\n" % ATTR) out.write("%(se)s %(s)s %(r)s uu arcto 4 {pop} repeat\n" % ATTR) out.write("%(sw)s %(w)s %(r)s uu arcto 4 {pop} repeat\n" % ATTR) out.write("closepath fill\n") if self.fg is not None: if self.r == 0: out.write("%(fg)s 0 0 %(width)g uu %(height)g uu rectstroke\n"\ % ATTR) else: out.write("%(fg)s newpath %(w)s moveto\n" % ATTR) out.write("%(nw)s %(n)s %(r)s uu arcto 4 {pop} repeat\n" % ATTR) out.write("%(ne)s %(e)s %(r)s uu arcto 4 {pop} repeat\n" % ATTR) out.write("%(se)s %(s)s %(r)s uu arcto 4 {pop} repeat\n" % ATTR) out.write("%(sw)s %(w)s %(r)s uu arcto 4 {pop} repeat\n" % ATTR) out.write("closepath stroke\n") return out.getvalue()
def _get_se(self): """ Get the "south-east" point """ return self.itoe(P(self.width, 0) + self.isw)
def _get_s(self): """ Get the "south" point """ return self.itoe(P(self.width / 2., 0) + self.isw)
class AffineObj(PsObj): ''' A base class for object that should implement affine transformations, this should apply to any object that draws on the page. ''' o = P(0, 0) T = Matrix(1, 0, 0, 1) def concat(self, t, p=None): ''' concat matrix t to tranformation matrix @param t: a 2x2 Matrix dectribing Affine transformation @param p: the origin for the transformation @return: reference to self @rtype: self ''' # update transformation matrix self.T = t * self.T #if p is not None: # o=self.o # o is in external co-ords # self.move(p-o) # self.move(t*(o-p)) o = self.o # o is in external co-ords # set origin at (0,0) self.move(-o) # move to transformed p id defined if p is not None: self.move(p) self.move(t * (-p)) # move to transformed origin self.move(t * o) return self def move(self, *args): ''' translate object by a certain amount @param args: amount to move by, can be given as - dx,dy - P @return: reference to self @rtype: self ''' if len(args) == 1: # assume we have a point self.o += args[0] else: # assume we have dx,dy self.o += P(args[0], args[1]) return self def rotate(self, angle, p=None): """ rotate object, the rotation is around p when supplied otherwise it's the objects origin @param angle: angle in degrees, clockwise @param p: point to rotate around (external co-ords) @return: reference to self @rtype: self """ angle = angle / 180.0 * pi # convert angle to radians t = Matrix(cos(angle), sin(angle), -sin(angle), cos(angle)) self.concat(t, p) return self def scale(self, sx, sy=None, p=None): ''' scale object size (towards objects origin or p) @param sx: x scale factor, or total scale factor if sy=None @param sy: y scale factor @param p: point around which to scale @return: reference to self @rtype: self ''' if sy is None: sy = sx t = Matrix(sx, 0, 0, sy) self.concat(t, p) return self def reflect(self, angle, p=None): ''' reflect object in mirror @param angle: angle of mirror (deg clockwise from top) @param p: origin of reflection @return: reference to self @rtype: self ''' # convert angle to radians, clockwise from top angle = angle / 180.0 * pi - pi / 2 t = Matrix( cos(angle)**2 - sin(angle)**2, -2 * sin(angle) * cos(angle), -2 * sin(angle) * cos(angle), sin(angle)**2 - cos(angle)**2) self.concat(t, p) return self def shear(self, s, angle, p=None): ''' shear object @param s: amount of shear @param angle: direction of shear (deg clockwise from top) @param p: origin of shear @return: reference to self @rtype: self ''' self.rotate(angle, p) t = Matrix(1, 0, -s, 1) self.concat(t, p) self.rotate(-angle, p) return self def itoe(self, p_i): ''' convert internal to external co-ords @param p_i: internal co-ordinate @return: external co-ordinate @rtype: P ''' assert isinstance(p_i, P), "object not a P()" return self.T * p_i + self.o def etoi(self, p_e): ''' convert external to internal co-ords @param p_e: external co-ordinate @return: internal co-ordinate @rtype: P ''' assert isinstance(p_e, P), "object not a P()" return self.T.inverse() * (p_e - self.o) def prebody(self): ''' set up transformation of coordinate system @rtype: string ''' T = self.T o = self.o S = "gsave " if T == Matrix(1, 0, 0, 1): S = S + "%s translate\n" % o else: # NB postscript matrix is the transpose of what you'd expect! S = S + "[%g %g %g %g %s] concat\n" % (T[0], T[2], T[1], T[3], o()) return S def postbody(self): ''' undo coordinate system transformation @rtype: string ''' return "grestore\n"
def _get_e(self): """ Get the "east" point """ return self.itoe(P(self.width, self.height / 2.) + self.isw)
def _get_ne(self): """ Get the "nort-east" point """ return self.itoe(P(self.r, self.r))
def _typeset(self): """ Typeset the Text object (including kerning info) """ string = self.text afm = AFM(self._font) # set the correct postscript font name self._font = afm.FontName size = self.size sc = size / 1000. chars = map(ord, list(string)) # order: width l b r t # use 'reduce' and 'map' as they're written in C # add up all the widths width = reduce(lambda x, y: x + afm[y][0], chars, 0) # subtract the kerning if self.kerning == 1: if len(chars) > 1: kerns = map(lambda x, y: afm[(x, y)], chars[:-1], chars[1:]) charlist = list(string) out = "(" for ii in kerns: if ii != 0: out += charlist.pop(0) + ") %s (" % str(ii * sc) else: out += charlist.pop(0) out += charlist.pop(0) + ")" settext = out kern = reduce(lambda x, y: x + y, kerns) width += kern else: # this is to catch the case when there are no characters # in the string, but self.kerning==1 settext = "(" + string + ")" else: settext = "(" + string + ")" # get rid of the end bits start = afm[chars[0]][1] f = afm[chars[-1]] width = width - start - (f[0] - f[3]) # accumulate maximum height top = reduce(lambda x, y: max(x, afm[y][4]), chars, 0) # accumulate lowest point bottom = reduce(lambda x, y: min(x, afm[y][2]), chars, afm[chars[0]][2]) x1 = start * sc y1 = bottom * sc x2 = x1 + width * sc y2 = top * sc self.settext = settext self.offset = -P(x1, y1) / float(defaults.units) self.width = (x2 - x1) / float(defaults.units) self.height = (y2 - y1) / float(defaults.units)
def _get_w(self): """ Get the "west" point """ return self.itoe(P(0, self.height / 2.) + self.isw)