change signature of path.split to not user *parameters any more
[PyX/mjg.git] / pyx / canvas.py
blob6460269e0b3d2d78212740ed0a48c6e870df40ce
1 #!/usr/bin/env python
2 # -*- coding: ISO-8859-1 -*-
5 # Copyright (C) 2002, 2003 Jörg Lehmann <joergl@users.sourceforge.net>
6 # Copyright (C) 2002, 2003 André Wobst <wobsta@users.sourceforge.net>
8 # This file is part of PyX (http://pyx.sourceforge.net/).
10 # PyX is free software; you can redistribute it and/or modify
11 # it under the terms of the GNU General Public License as published by
12 # the Free Software Foundation; either version 2 of the License, or
13 # (at your option) any later version.
15 # PyX is distributed in the hope that it will be useful,
16 # but WITHOUT ANY WARRANTY; without even the implied warranty of
17 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 # GNU General Public License for more details.
20 # You should have received a copy of the GNU General Public License
21 # along with PyX; if not, write to the Free Software
22 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
24 # XXX remove string module
25 # XXX what is a pattern
26 # XXX what is a color
29 """The canvas module provides a PostScript canvas class and related classes
31 A canvas holds a collection of all elements that should be displayed together
32 with their attributes.
33 """
35 import string, cStringIO, time
36 import attr, base, bbox, deco, unit, prolog, style, trafo, version
38 # known paperformats as tuple(width, height)
40 _paperformats = { "A4" : ("210 t mm", "297 t mm"),
41 "A3" : ("297 t mm", "420 t mm"),
42 "A2" : ("420 t mm", "594 t mm"),
43 "A1" : ("594 t mm", "840 t mm"),
44 "A0" : ("840 t mm", "1188 t mm"),
45 "A0B" : ("910 t mm", "1370 t mm"),
46 "LETTER" : ("8.5 t inch", "11 t inch"),
47 "LEGAL" : ("8.5 t inch", "14 t inch")}
51 # clipping class
54 # XXX help me find my identity
56 class clip(base.PSCmd):
58 """class for use in canvas constructor which clips to a path"""
60 def __init__(self, path):
61 """construct a clip instance for a given path"""
62 self.path = path
64 def bbox(self):
65 # as a PSCmd a clipping path has NO influence on the bbox...
66 return bbox._bbox()
68 def clipbbox(self):
69 # ... but for clipping, we nevertheless need the bbox
70 return self.path.bbox()
72 def write(self, file):
73 _newpath().write(file)
74 self.path.write(file)
75 _clip().write(file)
78 # some very primitive Postscript operators
81 class _newpath(base.PSOp):
82 def write(self, file):
83 file.write("newpath\n")
86 class _stroke(base.PSOp):
87 def write(self, file):
88 file.write("stroke\n")
91 class _fill(base.PSOp):
92 def write(self, file):
93 file.write("fill\n")
96 class _clip(base.PSOp):
97 def write(self, file):
98 file.write("clip\n")
101 class _gsave(base.PSOp):
102 def write(self, file):
103 file.write("gsave\n")
106 class _grestore(base.PSOp):
107 def write(self, file):
108 file.write("grestore\n")
114 class _canvas(base.PSCmd):
116 """a canvas is a collection of PSCmds together with PSAttrs"""
118 def __init__(self, *args):
120 """construct a canvas
122 The canvas can be modfied by supplying args, which have
123 to be instances of one of the following classes:
124 - trafo.trafo (leading to a global transformation of the canvas)
125 - canvas.clip (clips the canvas)
126 - base.PathStyle (sets some global attributes of the canvas)
128 Note that, while the first two properties are fixed for the
129 whole canvas, the last one can be changed via canvas.set()
133 self.PSOps = []
134 self.trafo = trafo.trafo()
135 self.clipbbox = bbox._bbox()
137 # prevent cyclic imports
138 import text
139 self.texrunner = text.defaulttexrunner
141 for arg in args:
142 if isinstance(arg, trafo._trafo):
143 self.trafo = self.trafo*arg
144 self.PSOps.append(arg)
145 elif isinstance(arg, clip):
146 self.clipbbox=(self.clipbbox*
147 arg.clipbbox().transformed(self.trafo))
148 self.PSOps.append(arg)
149 else:
150 self.set(arg)
152 def bbox(self):
153 """returns bounding box of canvas"""
154 obbox = reduce(lambda x,y:
155 isinstance(y, base.PSCmd) and x+y.bbox() or x,
156 self.PSOps,
157 bbox._bbox())
159 # transform according to our global transformation and
160 # intersect with clipping bounding box (which have already been
161 # transformed in canvas.__init__())
162 return obbox.transformed(self.trafo)*self.clipbbox
164 def prolog(self):
165 result = []
166 for cmd in self.PSOps:
167 result.extend(cmd.prolog())
168 return result
170 def write(self, file):
171 if self.PSOps:
172 _gsave().write(file)
173 for cmd in self.PSOps:
174 cmd.write(file)
175 _grestore().write(file)
177 def insert(self, PSOp, *args):
178 """insert PSOp in the canvas.
180 If args are given, then insert a canvas containing PSOp applying args.
182 returns the PSOp
186 # XXX check for PSOp
188 if args:
189 sc = _canvas(*args)
190 sc.insert(PSOp)
191 self.PSOps.append(sc)
192 else:
193 self.PSOps.append(PSOp)
195 return PSOp
197 def set(self, *attrs):
198 """sets styles args globally for the rest of the canvas
200 returns canvas
204 attr.checkattrs(attrs, [style.strokestyle, style.fillstyle])
205 for astyle in attrs:
206 self.insert(astyle)
207 return self
209 def draw(self, path, *attrs):
210 """draw path on canvas using the style given by args
212 The argument list args consists of PathStyles, which modify
213 the appearance of the path, PathDecos, which add some new
214 visual elements to the path, or trafos, which are applied
215 before drawing the path.
217 returns the canvas
221 attr.mergeattrs(attrs)
222 attr.checkattrs(attrs, [deco.deco, style.fillstyle, style.strokestyle, trafo._trafo])
224 for t in attr.getattrs(attrs, [trafo._trafo]):
225 path = path.transformed(t)
227 dp = deco.decoratedpath(path)
229 # set global styles
230 dp.styles = attr.getattrs(attrs, [style.fillstyle, style.strokestyle])
232 # add path decorations and modify path accordingly
233 for adeco in attr.getattrs(attrs, [deco.deco]):
234 dp = adeco.decorate(dp)
236 self.insert(dp)
238 return self
240 def stroke(self, path, *attrs):
241 """stroke path on canvas using the style given by args
243 The argument list args consists of PathStyles, which modify
244 the appearance of the path, PathDecos, which add some new
245 visual elements to the path, or trafos, which are applied
246 before drawing the path.
248 returns the canvas
252 return self.draw(path, deco.stroked(), *attrs)
254 def fill(self, path, *attrs):
255 """fill path on canvas using the style given by args
257 The argument list args consists of PathStyles, which modify
258 the appearance of the path, PathDecos, which add some new
259 visual elements to the path, or trafos, which are applied
260 before drawing the path.
262 returns the canvas
266 return self.draw(path, deco.filled(), *attrs)
268 def settexrunner(self, texrunner):
269 """sets the texrunner to be used to within the text and text_pt methods"""
271 self.texrunner = texrunner
273 def text(self, x, y, atext, *args):
274 """insert a text into the canvas
276 inserts a textbox created by self.texrunner.text into the canvas
278 returns the inserted textbox"""
280 return self.insert(self.texrunner.text(x, y, atext, *args))
283 def text_pt(self, x, y, atext, *args):
284 """insert a text into the canvas
286 inserts a textbox created by self.texrunner.text_pt into the canvas
288 returns the inserted textbox"""
290 return self.insert(self.texrunner.text_pt(x, y, atext, *args))
293 # canvas for patterns
296 class pattern(_canvas, style.fillstyle):
298 def __init__(self, painttype=1, tilingtype=1, xstep=None, ystep=None, bbox=None, trafo=None):
299 _canvas.__init__(self)
300 self.id = "pattern%d" % id(self)
301 # XXX: some checks are in order
302 if painttype not in (1,2):
303 raise ValueError("painttype must be 1 or 2")
304 self.painttype = painttype
305 if tilingtype not in (1,2,3):
306 raise ValueError("tilingtype must be 1, 2 or 3")
307 self.tilingtype = tilingtype
308 self.xstep = xstep
309 self.ystep = ystep
310 self.patternbbox = bbox
311 self.patterntrafo = trafo
313 def bbox(self):
314 return bbox._bbox()
316 def write(self, file):
317 file.write("%s setpattern\n" % self.id)
319 def prolog(self):
320 realpatternbbox = _canvas.bbox(self)
321 if self.xstep is None:
322 xstep = unit.topt(realpatternbbox.width())
323 else:
324 xstep = unit.topt(unit.length(self.xstep))
325 if self.ystep is None:
326 ystep = unit.topt(realpatternbbox.height())
327 else:
328 ystep = unit.topt(unit.length(self.ystep))
329 if not xstep:
330 raise ValueError("xstep in pattern cannot be zero")
331 if not ystep:
332 raise ValueError("ystep in pattern cannot be zero")
333 patternbbox = self.patternbbox or realpatternbbox.enlarged("5 pt")
335 patternprefix = string.join(("<<",
336 "/PatternType 1",
337 "/PaintType %d" % self.painttype,
338 "/TilingType %d" % self.tilingtype,
339 "/BBox[%s]" % str(patternbbox),
340 "/XStep %g" % xstep,
341 "/YStep %g" % ystep,
342 "/PaintProc {\nbegin\n"),
343 sep="\n")
344 stringfile = cStringIO.StringIO()
345 _canvas.write(self, stringfile)
346 patternproc = stringfile.getvalue()
347 stringfile.close()
348 patterntrafostring = self.patterntrafo is None and "matrix" or str(self.patterntrafo)
349 patternsuffix = "end\n} bind\n>>\n%s\nmakepattern" % patterntrafostring
351 pr = _canvas.prolog(self)
352 pr.append(prolog.definition(self.id, string.join((patternprefix, patternproc, patternsuffix), "")))
353 return pr
356 # The main canvas class
359 class canvas(_canvas):
361 """a canvas is a collection of PSCmds together with PSAttrs"""
363 def writetofile(self, filename, paperformat=None, rotated=0, fittosize=0, margin="1 t cm",
364 bbox=None, bboxenlarge="1 t pt"):
365 """write canvas to EPS file
367 If paperformat is set to a known paperformat, the output will be centered on
368 the page.
370 If rotated is set, the output will first be rotated by 90 degrees.
372 If fittosize is set, then the output is scaled to the size of the
373 page (minus margin). In that case, the paperformat the specification
374 of the paperformat is obligatory.
376 The bbox parameter overrides the automatic bounding box determination.
377 bboxenlarge may be used to enlarge the bbox of the canvas (or the
378 manually specified bbox).
381 if filename[-4:]!=".eps":
382 filename = filename + ".eps"
384 try:
385 file = open(filename, "w")
386 except IOError:
387 raise IOError("cannot open output file")
389 abbox = bbox is not None and bbox or self.bbox()
390 abbox = abbox.enlarged(bboxenlarge)
391 ctrafo = None # global transformation of canvas
393 if rotated:
394 ctrafo = trafo._rotate(90,
395 0.5*(abbox.llx+abbox.urx),
396 0.5*(abbox.lly+abbox.ury))
398 if paperformat:
399 # center (optionally rotated) output on page
400 try:
401 width, height = _paperformats[paperformat.upper()]
402 except KeyError:
403 raise KeyError, "unknown paperformat '%s'" % paperformat
404 width = unit.topt(width)
405 height = unit.topt(height)
407 if not ctrafo: ctrafo=trafo.trafo()
409 ctrafo = ctrafo._translated(0.5*(width -(abbox.urx-abbox.llx))-
410 abbox.llx,
411 0.5*(height-(abbox.ury-abbox.lly))-
412 abbox.lly)
414 if fittosize:
415 # scale output to pagesize - margins
416 margin = unit.topt(margin)
417 if 2*margin > min(width, height):
418 raise RuntimeError("Margins too broad for selected paperformat. Aborting.")
420 if rotated:
421 sfactor = min((height-2*margin)/(abbox.urx-abbox.llx),
422 (width-2*margin)/(abbox.ury-abbox.lly))
423 else:
424 sfactor = min((width-2*margin)/(abbox.urx-abbox.llx),
425 (height-2*margin)/(abbox.ury-abbox.lly))
427 ctrafo = ctrafo._scaled(sfactor, sfactor, 0.5*width, 0.5*height)
430 elif fittosize:
431 raise ValueError("must specify paper size for fittosize")
433 # if there has been a global transformation, adjust the bounding box
434 # accordingly
435 if ctrafo: abbox = abbox.transformed(ctrafo)
437 file.write("%!PS-Adobe-3.0 EPSF 3.0\n")
438 abbox.write(file)
439 file.write("%%%%Creator: PyX %s\n" % version.version)
440 file.write("%%%%Title: %s\n" % filename)
441 file.write("%%%%CreationDate: %s\n" %
442 time.asctime(time.localtime(time.time())))
443 file.write("%%EndComments\n")
445 file.write("%%BeginProlog\n")
447 mergedprolog = []
449 for pritem in self.prolog():
450 for mpritem in mergedprolog:
451 if mpritem.merge(pritem) is None: break
452 else:
453 mergedprolog.append(pritem)
455 for pritem in mergedprolog:
456 pritem.write(file)
458 file.write("%%EndProlog\n")
460 # again, if there has occured global transformation, apply it now
461 if ctrafo: ctrafo.write(file)
463 file.write("%f setlinewidth\n" % unit.topt(style.linewidth.normal))
465 # here comes the actual content
466 self.write(file)
468 file.write("showpage\n")
469 file.write("%%Trailer\n")
470 file.write("%%EOF\n")