further attribute work
[PyX/mjg.git] / pyx / canvas.py
blob91b1ce75fcf23a5b03f6c171fe2a9bbbd4dbad1a
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, color, deco, path, unit, prolog, style, text, 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()
136 self.texrunner = text.defaulttexrunner
138 for arg in args:
139 if isinstance(arg, trafo._trafo):
140 self.trafo = self.trafo*arg
141 self.PSOps.append(arg)
142 elif isinstance(arg, clip):
143 self.clipbbox=(self.clipbbox*
144 arg.clipbbox().transformed(self.trafo))
145 self.PSOps.append(arg)
146 else:
147 self.set(arg)
149 def bbox(self):
150 """returns bounding box of canvas"""
151 obbox = reduce(lambda x,y:
152 isinstance(y, base.PSCmd) and x+y.bbox() or x,
153 self.PSOps,
154 bbox._bbox())
156 # transform according to our global transformation and
157 # intersect with clipping bounding box (which have already been
158 # transformed in canvas.__init__())
159 return obbox.transformed(self.trafo)*self.clipbbox
161 def prolog(self):
162 result = []
163 for cmd in self.PSOps:
164 result.extend(cmd.prolog())
165 return result
167 def write(self, file):
168 _gsave().write(file)
169 for cmd in self.PSOps:
170 cmd.write(file)
171 _grestore().write(file)
173 def insert(self, PSOp, *args):
174 """insert PSOp in the canvas.
176 If args are given, then insert a canvas containing PSOp applying args.
178 returns the PSOp
182 # XXX check for PSOp
184 if args:
185 sc = _canvas(*args)
186 sc.insert(PSOp)
187 self.PSOps.append(sc)
188 else:
189 self.PSOps.append(PSOp)
191 return PSOp
193 def set(self, *styles):
194 """sets styles args globally for the rest of the canvas
196 returns canvas
200 attr.checkattrs(styles, [style.strokestyle, style.fillstyle])
201 for astyle in styles:
202 self.insert(astyle)
203 return self
205 def draw(self, path, *args):
206 """draw path on canvas using the style given by args
208 The argument list args consists of PathStyles, which modify
209 the appearance of the path, PathDecos, which add some new
210 visual elements to the path, or trafos, which are applied
211 before drawing the path.
213 returns the canvas
217 attr.mergeattrs(args)
218 attr.checkattrs(args, [deco.deco, style.fillstyle, style.strokestyle, trafo._trafo])
220 for t in attr.getattrs(args, [trafo._trafo]):
221 path = path.transformed(t)
223 dp = deco.decoratedpath(path)
225 # set global styles
226 dp.styles = attr.getattrs(args, [style.fillstyle, style.strokestyle])
228 # add path decorations and modify path accordingly
229 for adeco in attr.getattrs(args, [deco.deco]):
230 dp = adeco.decorate(dp)
232 self.insert(dp)
234 return self
236 def stroke(self, path, *args):
237 """stroke path on canvas using the style given by args
239 The argument list args consists of PathStyles, which modify
240 the appearance of the path, PathDecos, which add some new
241 visual elements to the path, or trafos, which are applied
242 before drawing the path.
244 returns the canvas
248 return self.draw(path, deco.stroked(), *args)
250 def fill(self, path, *args):
251 """fill path on canvas using the style given by args
253 The argument list args consists of PathStyles, which modify
254 the appearance of the path, PathDecos, which add some new
255 visual elements to the path, or trafos, which are applied
256 before drawing the path.
258 returns the canvas
262 return self.draw(path, deco.filled(), *args)
264 def settexrunner(self, texrunner):
265 """sets the texrunner to be used to within the text and _text methods"""
267 self.texrunner = texrunner
269 def text(self, x, y, atext, *args):
270 """insert a text into the canvas
272 inserts a textbox created by self.texrunner.text into the canvas
274 returns the inserted textbox"""
276 return self.insert(self.texrunner.text(x, y, atext, *args))
279 def _text(self, x, y, atext, *args):
280 """insert a text into the canvas
282 inserts a textbox created by self.texrunner._text into the canvas
284 returns the inserted textbox"""
286 return self.insert(self.texrunner._text(x, y, atext, *args))
289 # canvas for patterns
292 class pattern(_canvas, style.fillstyle):
294 def __init__(self, painttype=1, tilingtype=1, xstep=None, ystep=None, bbox=None, trafo=None):
295 _canvas.__init__(self)
296 self.id = "pattern%d" % id(self)
297 # XXX: some checks are in order
298 if painttype not in (1,2):
299 raise ValueError("painttype must be 1 or 2")
300 self.painttype = painttype
301 if tilingtype not in (1,2,3):
302 raise ValueError("tilingtype must be 1, 2 or 3")
303 self.tilingtype = tilingtype
304 self.xstep = xstep
305 self.ystep = ystep
306 self.patternbbox = bbox
307 self.patterntrafo = trafo
309 def bbox(self):
310 return bbox._bbox()
312 def write(self, file):
313 file.write("%s setpattern\n" % self.id)
315 def prolog(self):
316 realpatternbbox = _canvas.bbox(self)
317 if self.xstep is None:
318 xstep = unit.topt(realpatternbbox.width())
319 else:
320 xstep = unit.topt(unit.length(self.xstep))
321 if self.ystep is None:
322 ystep = unit.topt(realpatternbbox.height())
323 else:
324 ystep = unit.topt(unit.length(self.ystep))
325 if not xstep:
326 raise ValueError("xstep in pattern cannot be zero")
327 if not ystep:
328 raise ValueError("ystep in pattern cannot be zero")
329 patternbbox = self.patternbbox or realpatternbbox.enlarged("5 pt")
331 patternprefix = string.join(("<<",
332 "/PatternType 1",
333 "/PaintType %d" % self.painttype,
334 "/TilingType %d" % self.tilingtype,
335 "/BBox[%s]" % str(patternbbox),
336 "/XStep %g" % xstep,
337 "/YStep %g" % ystep,
338 "/PaintProc {\nbegin\n"),
339 sep="\n")
340 stringfile = cStringIO.StringIO()
341 _canvas.write(self, stringfile)
342 patternproc = stringfile.getvalue()
343 stringfile.close()
344 patterntrafostring = self.patterntrafo is None and "matrix" or str(self.patterntrafo)
345 patternsuffix = "end\n} bind\n>>\n%s\nmakepattern" % patterntrafostring
347 pr = _canvas.prolog(self)
348 pr.append(prolog.definition(self.id, string.join((patternprefix, patternproc, patternsuffix), "")))
349 return pr
352 # The main canvas class
355 class canvas(_canvas):
357 """a canvas is a collection of PSCmds together with PSAttrs"""
359 def writetofile(self, filename, paperformat=None, rotated=0, fittosize=0, margin="1 t cm",
360 bbox=None, bboxenlarge="1 t pt"):
361 """write canvas to EPS file
363 If paperformat is set to a known paperformat, the output will be centered on
364 the page.
366 If rotated is set, the output will first be rotated by 90 degrees.
368 If fittosize is set, then the output is scaled to the size of the
369 page (minus margin). In that case, the paperformat the specification
370 of the paperformat is obligatory.
372 The bbox parameter overrides the automatic bounding box determination.
373 bboxenlarge may be used to enlarge the bbox of the canvas (or the
374 manually specified bbox).
377 if filename[-4:]!=".eps":
378 filename = filename + ".eps"
380 try:
381 file = open(filename, "w")
382 except IOError:
383 raise IOError("cannot open output file")
385 abbox = bbox is not None and bbox or self.bbox()
386 abbox = abbox.enlarged(bboxenlarge)
387 ctrafo = None # global transformation of canvas
389 if rotated:
390 ctrafo = trafo._rotate(90,
391 0.5*(abbox.llx+abbox.urx),
392 0.5*(abbox.lly+abbox.ury))
394 if paperformat:
395 # center (optionally rotated) output on page
396 try:
397 width, height = _paperformats[paperformat.upper()]
398 except KeyError:
399 raise KeyError, "unknown paperformat '%s'" % paperformat
400 width = unit.topt(width)
401 height = unit.topt(height)
403 if not ctrafo: ctrafo=trafo.trafo()
405 ctrafo = ctrafo._translated(0.5*(width -(abbox.urx-abbox.llx))-
406 abbox.llx,
407 0.5*(height-(abbox.ury-abbox.lly))-
408 abbox.lly)
410 if fittosize:
411 # scale output to pagesize - margins
412 margin = unit.topt(margin)
413 if 2*margin > min(width, height):
414 raise RuntimeError("Margins too broad for selected paperformat. Aborting.")
416 if rotated:
417 sfactor = min((height-2*margin)/(abbox.urx-abbox.llx),
418 (width-2*margin)/(abbox.ury-abbox.lly))
419 else:
420 sfactor = min((width-2*margin)/(abbox.urx-abbox.llx),
421 (height-2*margin)/(abbox.ury-abbox.lly))
423 ctrafo = ctrafo._scaled(sfactor, sfactor, 0.5*width, 0.5*height)
426 elif fittosize:
427 raise ValueError("must specify paper size for fittosize")
429 # if there has been a global transformation, adjust the bounding box
430 # accordingly
431 if ctrafo: abbox = abbox.transformed(ctrafo)
433 file.write("%!PS-Adobe-3.0 EPSF 3.0\n")
434 abbox.write(file)
435 file.write("%%%%Creator: PyX %s\n" % version.version)
436 file.write("%%%%Title: %s\n" % filename)
437 file.write("%%%%CreationDate: %s\n" %
438 time.asctime(time.localtime(time.time())))
439 file.write("%%EndComments\n")
441 file.write("%%BeginProlog\n")
443 mergedprolog = []
445 for pritem in self.prolog():
446 for mpritem in mergedprolog:
447 if mpritem.merge(pritem) is None: break
448 else:
449 mergedprolog.append(pritem)
451 for pritem in mergedprolog:
452 pritem.write(file)
454 file.write("%%EndProlog\n")
456 # again, if there has occured global transformation, apply it now
457 if ctrafo: ctrafo.write(file)
459 file.write("%f setlinewidth\n" % unit.topt(style.linewidth.normal))
461 # here comes the actual content
462 self.write(file)
464 file.write("showpage\n")
465 file.write("%%Trailer\n")
466 file.write("%%EOF\n")