ws change
[PyX/mjg.git] / pyx / canvas.py
blob1e8b342b762fd922d35a10e0985753cb6f02cdc7
1 #!/usr/bin/env python
2 # -*- coding: ISO-8859-1 -*-
5 # Copyright (C) 2002-2004 Jörg Lehmann <joergl@users.sourceforge.net>
6 # Copyright (C) 2002-2004 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 None
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 = None
137 # prevent cyclic imports
138 import text
139 self.texrunner = text.defaulttexrunner
141 for arg in args:
142 if isinstance(arg, trafo.trafo_pt):
143 self.trafo = self.trafo*arg
144 self.PSOps.append(arg)
145 elif isinstance(arg, clip):
146 if self.clipbbox is None:
147 self.clipbbox = arg.clipbbox().transformed(self.trafo)
148 else:
149 self.clippbox *= arg.clipbbox().transformed(self.trafo)
150 self.PSOps.append(arg)
151 else:
152 self.set(arg)
154 def bbox(self):
155 """returns bounding box of canvas"""
156 obbox = None
157 for cmd in self.PSOps:
158 if isinstance(cmd, base.PSCmd):
159 abbox = cmd.bbox()
160 if obbox is None:
161 obbox = abbox
162 elif abbox is not None:
163 obbox += abbox
165 # transform according to our global transformation and
166 # intersect with clipping bounding box (which have already been
167 # transformed in canvas.__init__())
168 if obbox is not None and self.clipbbox is not None:
169 return obbox.transformed(self.trafo)*self.clipbbox
170 elif obbox is not None:
171 return obbox.transformed(self.trafo)
172 else:
173 return self.clipbbox
175 def prolog(self):
176 result = []
177 for cmd in self.PSOps:
178 result.extend(cmd.prolog())
179 return result
181 def write(self, file):
182 if self.PSOps:
183 _gsave().write(file)
184 for cmd in self.PSOps:
185 cmd.write(file)
186 _grestore().write(file)
188 def insert(self, PSOp, args=[]):
189 """insert PSOp in the canvas.
191 If args are given, then insert a canvas containing PSOp applying args.
193 returns the PSOp
197 # XXX check for PSOp
199 if args:
200 sc = _canvas(*args)
201 sc.insert(PSOp)
202 self.PSOps.append(sc)
203 else:
204 self.PSOps.append(PSOp)
206 return PSOp
208 def set(self, attrs):
209 """sets styles args globally for the rest of the canvas
211 returns canvas
215 attr.checkattrs(attrs, [style.strokestyle, style.fillstyle])
216 for astyle in attrs:
217 self.insert(astyle)
218 return self
220 def draw(self, path, attrs):
221 """draw path on canvas using the style given by args
223 The argument attrs consists of PathStyles, which modify
224 the appearance of the path, PathDecos, which add some new
225 visual elements to the path, or trafos, which are applied
226 before drawing the path.
228 returns the canvas
232 attrs = attr.mergeattrs(attrs)
233 attr.checkattrs(attrs, [deco.deco, style.fillstyle, style.strokestyle, trafo.trafo_pt])
235 for t in attr.getattrs(attrs, [trafo.trafo_pt]):
236 path = path.transformed(t)
238 dp = deco.decoratedpath(path)
240 # set global styles
241 dp.styles = attr.getattrs(attrs, [style.fillstyle, style.strokestyle])
243 # add path decorations and modify path accordingly
244 for adeco in attr.getattrs(attrs, [deco.deco]):
245 dp = adeco.decorate(dp)
247 self.insert(dp)
249 return self
251 def stroke(self, path, attrs=[]):
252 """stroke path on canvas using the style given by args
254 The argument attrs consists of PathStyles, which modify
255 the appearance of the path, PathDecos, which add some new
256 visual elements to the path, or trafos, which are applied
257 before drawing the path.
259 returns the canvas
263 return self.draw(path, [deco.stroked]+list(attrs))
265 def fill(self, path, attrs=[]):
266 """fill path on canvas using the style given by args
268 The argument attrs consists of PathStyles, which modify
269 the appearance of the path, PathDecos, which add some new
270 visual elements to the path, or trafos, which are applied
271 before drawing the path.
273 returns the canvas
277 return self.draw(path, [deco.filled]+list(attrs))
279 def settexrunner(self, texrunner):
280 """sets the texrunner to be used to within the text and text_pt methods"""
282 self.texrunner = texrunner
284 def text(self, x, y, atext, *args, **kwargs):
285 """insert a text into the canvas
287 inserts a textbox created by self.texrunner.text into the canvas
289 returns the inserted textbox"""
291 return self.insert(self.texrunner.text(x, y, atext, *args, **kwargs))
294 def text_pt(self, x, y, atext, *args):
295 """insert a text into the canvas
297 inserts a textbox created by self.texrunner.text_pt into the canvas
299 returns the inserted textbox"""
301 return self.insert(self.texrunner.text_pt(x, y, atext, *args))
304 # canvas for patterns
307 class pattern(_canvas, attr.exclusiveattr, style.fillstyle):
309 def __init__(self, painttype=1, tilingtype=1, xstep=None, ystep=None, bbox=None, trafo=None):
310 attr.exclusiveattr.__init__(self, pattern)
311 _canvas.__init__(self)
312 attr.exclusiveattr.__init__(self, pattern)
313 self.id = "pattern%d" % id(self)
314 # XXX: some checks are in order
315 if painttype not in (1,2):
316 raise ValueError("painttype must be 1 or 2")
317 self.painttype = painttype
318 if tilingtype not in (1,2,3):
319 raise ValueError("tilingtype must be 1, 2 or 3")
320 self.tilingtype = tilingtype
321 self.xstep = xstep
322 self.ystep = ystep
323 self.patternbbox = bbox
324 self.patterntrafo = trafo
326 def bbox(self):
327 return None
329 def write(self, file):
330 file.write("%s setpattern\n" % self.id)
332 def prolog(self):
333 realpatternbbox = _canvas.bbox(self)
334 if self.xstep is None:
335 xstep = unit.topt(realpatternbbox.width())
336 else:
337 xstep = unit.topt(unit.length(self.xstep))
338 if self.ystep is None:
339 ystep = unit.topt(realpatternbbox.height())
340 else:
341 ystep = unit.topt(unit.length(self.ystep))
342 if not xstep:
343 raise ValueError("xstep in pattern cannot be zero")
344 if not ystep:
345 raise ValueError("ystep in pattern cannot be zero")
346 patternbbox = self.patternbbox or realpatternbbox.enlarged("5 pt")
348 patternprefix = string.join(("<<",
349 "/PatternType 1",
350 "/PaintType %d" % self.painttype,
351 "/TilingType %d" % self.tilingtype,
352 "/BBox[%s]" % str(patternbbox),
353 "/XStep %g" % xstep,
354 "/YStep %g" % ystep,
355 "/PaintProc {\nbegin\n"),
356 sep="\n")
357 stringfile = cStringIO.StringIO()
358 _canvas.write(self, stringfile)
359 patternproc = stringfile.getvalue()
360 stringfile.close()
361 patterntrafostring = self.patterntrafo is None and "matrix" or str(self.patterntrafo)
362 patternsuffix = "end\n} bind\n>>\n%s\nmakepattern" % patterntrafostring
364 pr = _canvas.prolog(self)
365 pr.append(prolog.definition(self.id, string.join((patternprefix, patternproc, patternsuffix), "")))
366 return pr
368 pattern.clear = attr.clearclass(pattern)
371 # The main canvas class
374 class canvas(_canvas):
376 """a canvas is a collection of PSCmds together with PSAttrs"""
378 def writetofile(self, filename, paperformat=None, rotated=0, fittosize=0, margin="1 t cm",
379 bbox=None, bboxenlarge="1 t pt"):
380 """write canvas to EPS file
382 If paperformat is set to a known paperformat, the output will be centered on
383 the page.
385 If rotated is set, the output will first be rotated by 90 degrees.
387 If fittosize is set, then the output is scaled to the size of the
388 page (minus margin). In that case, the paperformat the specification
389 of the paperformat is obligatory.
391 The bbox parameter overrides the automatic bounding box determination.
392 bboxenlarge may be used to enlarge the bbox of the canvas (or the
393 manually specified bbox).
396 if filename[-4:]!=".eps":
397 filename = filename + ".eps"
399 try:
400 file = open(filename, "w")
401 except IOError:
402 raise IOError("cannot open output file")
404 abbox = bbox is not None and bbox or self.bbox()
405 abbox = abbox.enlarged(bboxenlarge)
406 ctrafo = None # global transformation of canvas
408 if rotated:
409 ctrafo = trafo.rotate_pt(90,
410 0.5*(abbox.llx+abbox.urx),
411 0.5*(abbox.lly+abbox.ury))
413 if paperformat:
414 # center (optionally rotated) output on page
415 try:
416 width, height = _paperformats[paperformat.upper()]
417 except KeyError:
418 raise KeyError, "unknown paperformat '%s'" % paperformat
419 width = unit.topt(width)
420 height = unit.topt(height)
422 if not ctrafo: ctrafo=trafo.trafo()
424 ctrafo = ctrafo.translated_pt(0.5*(width -(abbox.urx-abbox.llx))-
425 abbox.llx,
426 0.5*(height-(abbox.ury-abbox.lly))-
427 abbox.lly)
429 if fittosize:
430 # scale output to pagesize - margins
431 margin = unit.topt(margin)
432 if 2*margin > min(width, height):
433 raise RuntimeError("Margins too broad for selected paperformat. Aborting.")
435 if rotated:
436 sfactor = min((height-2*margin)/(abbox.urx-abbox.llx),
437 (width-2*margin)/(abbox.ury-abbox.lly))
438 else:
439 sfactor = min((width-2*margin)/(abbox.urx-abbox.llx),
440 (height-2*margin)/(abbox.ury-abbox.lly))
442 ctrafo = ctrafo.scaled_pt(sfactor, sfactor, 0.5*width, 0.5*height)
445 elif fittosize:
446 raise ValueError("must specify paper size for fittosize")
448 # if there has been a global transformation, adjust the bounding box
449 # accordingly
450 if ctrafo: abbox = abbox.transformed(ctrafo)
452 file.write("%!PS-Adobe-3.0 EPSF 3.0\n")
453 abbox.write(file)
454 file.write("%%%%Creator: PyX %s\n" % version.version)
455 file.write("%%%%Title: %s\n" % filename)
456 file.write("%%%%CreationDate: %s\n" %
457 time.asctime(time.localtime(time.time())))
458 file.write("%%EndComments\n")
460 file.write("%%BeginProlog\n")
462 mergedprolog = []
464 for pritem in self.prolog():
465 for mpritem in mergedprolog:
466 if mpritem.merge(pritem) is None: break
467 else:
468 mergedprolog.append(pritem)
470 for pritem in mergedprolog:
471 pritem.write(file)
473 file.write("%%EndProlog\n")
475 # again, if there has occured global transformation, apply it now
476 if ctrafo: ctrafo.write(file)
478 file.write("%f setlinewidth\n" % unit.topt(style.linewidth.normal))
480 # here comes the actual content
481 self.write(file)
483 file.write("showpage\n")
484 file.write("%%Trailer\n")
485 file.write("%%EOF\n")