in to inch
[PyX/mjg.git] / pyx / canvas.py
blob42ee71de094dc54b0d14fc4a97a4209e98026b4a
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 # TODO:
25 # - should we improve on the arc length -> arg parametrization routine or
26 # should we at least factor it out?
27 # - How should we handle the passing of stroke and fill styles to
28 # arrows? Calls, new instances, ...?
31 """The canvas module provides a PostScript canvas class and related classes
33 A PostScript canvas is the pivotal object for the creation of (E)PS-Files.
34 It collects all the elements that should be displayed (PSCmds) together
35 with attributes if applicable. Furthermore, a canvas can be globally
36 transformed (i.e. translated, rotated, etc.) and clipped.
38 """
40 import math, string, StringIO, time
41 import attrlist, base, bbox, helper, path, unit, prolog, text, trafo, version
43 # known paperformats as tuple(width, height)
45 _paperformats = { "a4" : ("210 t mm", "297 t mm"),
46 "a3" : ("297 t mm", "420 t mm"),
47 "a2" : ("420 t mm", "594 t mm"),
48 "a1" : ("594 t mm", "840 t mm"),
49 "a0" : ("840 t mm", "1188 t mm"),
50 "a0b" : ("910 t mm", "1370 t mm"),
51 "letter" : ("8.5 t inch", "11 t inch"),
52 "legal" : ("8.5 t inch", "14 t inch")}
56 # Exceptions
59 class CanvasException(Exception): pass
62 class linecap(base.PathStyle):
64 """linecap of paths"""
66 def __init__(self, value=0):
67 self.value=value
69 def write(self, file):
70 file.write("%d setlinecap\n" % self.value)
72 linecap.butt = linecap(0)
73 linecap.round = linecap(1)
74 linecap.square = linecap(2)
77 class linejoin(base.PathStyle):
79 """linejoin of paths"""
81 def __init__(self, value=0):
82 self.value=value
84 def write(self, file):
85 file.write("%d setlinejoin\n" % self.value)
87 linejoin.miter = linejoin(0)
88 linejoin.round = linejoin(1)
89 linejoin.bevel = linejoin(2)
92 class miterlimit(base.PathStyle):
94 """miterlimit of paths"""
96 def __init__(self, value=10.0):
97 self.value=value
99 def write(self, file):
100 file.write("%f setmiterlimit\n" % self.value)
103 miterlimit.lessthan180deg = miterlimit(1/math.sin(math.pi*180/360))
104 miterlimit.lessthan90deg = miterlimit(1/math.sin(math.pi*90/360))
105 miterlimit.lessthan60deg = miterlimit(1/math.sin(math.pi*60/360))
106 miterlimit.lessthan45deg = miterlimit(1/math.sin(math.pi*45/360))
107 miterlimit.lessthan11deg = miterlimit(10) # the default, approximately 11.4783 degress
109 class dash(base.PathStyle):
111 """dash of paths"""
113 def __init__(self, pattern=[], offset=0):
114 self.pattern=pattern
115 self.offset=offset
117 def write(self, file):
118 patternstring=""
119 for element in self.pattern:
120 patternstring=patternstring + `element` + " "
122 file.write("[%s] %d setdash\n" % (patternstring, self.offset))
125 class linestyle(base.PathStyle):
127 """linestyle (linecap together with dash) of paths"""
129 def __init__(self, c=linecap.butt, d=dash([])):
130 self.c=c
131 self.d=d
133 def write(self, file):
134 self.c.write(file)
135 self.d.write(file)
137 linestyle.solid = linestyle(linecap.butt, dash([]))
138 linestyle.dashed = linestyle(linecap.butt, dash([2]))
139 linestyle.dotted = linestyle(linecap.round, dash([0, 3]))
140 linestyle.dashdotted = linestyle(linecap.round, dash([0, 3, 3, 3]))
143 class linewidth(base.PathStyle, unit.length):
145 """linewidth of paths"""
147 def __init__(self, l="0 cm"):
148 unit.length.__init__(self, l=l, default_type="w")
150 def write(self, file):
151 file.write("%f setlinewidth\n" % unit.topt(self))
153 _base=0.02
155 linewidth.THIN = linewidth("%f cm" % (_base/math.sqrt(32)))
156 linewidth.THIn = linewidth("%f cm" % (_base/math.sqrt(16)))
157 linewidth.THin = linewidth("%f cm" % (_base/math.sqrt(8)))
158 linewidth.Thin = linewidth("%f cm" % (_base/math.sqrt(4)))
159 linewidth.thin = linewidth("%f cm" % (_base/math.sqrt(2)))
160 linewidth.normal = linewidth("%f cm" % _base)
161 linewidth.thick = linewidth("%f cm" % (_base*math.sqrt(2)))
162 linewidth.Thick = linewidth("%f cm" % (_base*math.sqrt(4)))
163 linewidth.THick = linewidth("%f cm" % (_base*math.sqrt(8)))
164 linewidth.THIck = linewidth("%f cm" % (_base*math.sqrt(16)))
165 linewidth.THICk = linewidth("%f cm" % (_base*math.sqrt(32)))
166 linewidth.THICK = linewidth("%f cm" % (_base*math.sqrt(64)))
170 # Decorated path
173 class decoratedpath(base.PSCmd):
174 """Decorated path
176 The main purpose of this class is during the drawing
177 (stroking/filling) of a path. It collects attributes for the
178 stroke and/or fill operations.
181 def __init__(self,
182 path, strokepath=None, fillpath=None,
183 styles=None, strokestyles=None, fillstyles=None,
184 subdps=None):
186 self.path = path
188 # path to be stroked or filled (or None)
189 self.strokepath = strokepath
190 self.fillpath = fillpath
192 # global style for stroking and filling and subdps
193 self.styles = helper.ensurelist(styles)
195 # styles which apply only for stroking and filling
196 self.strokestyles = helper.ensurelist(strokestyles)
197 self.fillstyles = helper.ensurelist(fillstyles)
199 # additional elements of the path, e.g., arrowheads,
200 # which are by themselves decoratedpaths
201 self.subdps = helper.ensurelist(subdps)
203 def addsubdp(self, subdp):
204 """add a further decorated path to the list of subdps"""
205 self.subdps.append(subdp)
207 def bbox(self):
208 return reduce(lambda x,y: x+y.bbox(),
209 self.subdps,
210 self.path.bbox())
212 def prolog(self):
213 result = []
214 for style in list(self.styles) + list(self.fillstyles) + list(self.strokestyles):
215 result.extend(style.prolog())
216 return result
218 def write(self, file):
219 # draw (stroke and/or fill) the decoratedpath on the canvas
220 # while trying to produce an efficient output, e.g., by
221 # not writing one path two times
223 # small helper
224 def _writestyles(styles, file=file):
225 for style in styles:
226 style.write(file)
228 # apply global styles
229 if self.styles:
230 _gsave().write(file)
231 _writestyles(self.styles)
233 if self.fillpath:
234 _newpath().write(file)
235 self.fillpath.write(file)
237 if self.strokepath==self.fillpath:
238 # do efficient stroking + filling
239 _gsave().write(file)
241 if self.fillstyles:
242 _writestyles(self.fillstyles)
244 _fill().write(file)
245 _grestore().write(file)
247 if self.strokestyles:
248 _gsave().write(file)
249 _writestyles(self.strokestyles)
251 _stroke().write(file)
253 if self.strokestyles:
254 _grestore().write(file)
255 else:
256 # only fill fillpath - for the moment
257 if self.fillstyles:
258 _gsave().write(file)
259 _writestyles(self.fillstyles)
261 _fill().write(file)
263 if self.fillstyles:
264 _grestore().write(file)
266 if self.strokepath and self.strokepath!=self.fillpath:
267 # this is the only relevant case still left
268 # Note that a possible stroking has already been done.
270 if self.strokestyles:
271 _gsave().write(file)
272 _writestyles(self.strokestyles)
274 _newpath().write(file)
275 self.strokepath.write(file)
276 _stroke().write(file)
278 if self.strokestyles:
279 _grestore().write(file)
281 if not self.strokepath and not self.fillpath:
282 raise RuntimeError("Path neither to be stroked nor filled")
284 # now, draw additional subdps
285 for subdp in self.subdps:
286 subdp.write(file)
288 # restore global styles
289 if self.styles:
290 _grestore().write(file)
293 # Path decorators
296 class PathDeco:
298 """Path decorators
300 In contrast to path styles, path decorators depend on the concrete
301 path to which they are applied. In particular, they don't make
302 sense without any path and can thus not be used in canvas.set!
306 def decorate(self, dp):
307 """apply a style to a given decoratedpath object dp
309 decorate accepts a decoratedpath object dp, applies PathStyle
310 by modifying dp in place and returning the new dp.
313 pass
316 # stroked and filled: basic PathDecos which stroked and fill,
317 # respectively the path
320 class stroked(PathDeco):
322 """stroked is a PathDecorator, which draws the outline of the path"""
324 def __init__(self, *styles):
325 self.styles = list(styles)
327 def decorate(self, dp):
328 dp.strokepath = dp.path
329 dp.strokestyles = self.styles
331 return dp
334 class filled(PathDeco):
336 """filled is a PathDecorator, which fills the interior of the path"""
338 def __init__(self, *styles):
339 self.styles = list(styles)
341 def decorate(self, dp):
342 dp.fillpath = dp.path
343 dp.fillstyles = self.styles
345 return dp
347 def _arrowheadtemplatelength(anormpath, size):
348 "calculate length of arrowhead template (in parametrisation of anormpath)"
349 # get tip (tx, ty)
350 tx, ty = anormpath.begin()
352 # obtain arrow template by using path up to first intersection
353 # with circle around tip (as suggested by Michael Schindler)
354 ipar = anormpath.intersect(path.circle(tx, ty, size))
355 if ipar[0]:
356 alen = ipar[0][0]
357 else:
358 # if this doesn't work, use first order conversion from pts to
359 # the bezier curve's parametrization
360 tlen = unit.topt(anormpath.tangent(0).arclength())
361 try:
362 alen = unit.topt(size)/tlen
363 except ArithmeticError:
364 # take maximum, we can get
365 alen = anormpath.range()
366 if alen > anormpath.range(): alen = anormpath.range()
368 return alen
371 def _arrowhead(anormpath, size, angle, constriction):
373 """helper routine, which returns an arrowhead for a normpath
375 returns arrowhead at begin of anormpath with size,
376 opening angle and relative constriction
379 alen = _arrowheadtemplatelength(anormpath, size)
380 tx, ty = anormpath.begin()
382 # now we construct the template for our arrow but cutting
383 # the path a the corresponding length
384 arrowtemplate = anormpath.split(alen)[0]
386 # from this template, we construct the two outer curves
387 # of the arrow
388 arrowl = arrowtemplate.transformed(trafo.rotate(-angle/2.0, tx, ty))
389 arrowr = arrowtemplate.transformed(trafo.rotate( angle/2.0, tx, ty))
391 # now come the joining backward parts
392 if constriction:
393 # arrow with constriction
395 # constriction point (cx, cy) lies on path
396 cx, cy = anormpath.at(constriction*alen)
398 arrowcr= path.line(*(arrowr.end()+(cx,cy)))
400 arrow = arrowl.reversed() << arrowr << arrowcr
401 arrow.append(path.closepath())
402 else:
403 # arrow without constriction
404 arrow = arrowl.reversed() << arrowr
405 arrow.append(path.closepath())
407 return arrow
409 class arrow(PathDeco):
411 """A general arrow"""
413 def __init__(self,
414 position, size, angle=45, constriction=0.8,
415 styles=None, strokestyles=None, fillstyles=None):
416 self.position = position
417 self.size = size
418 self.angle = angle
419 self.constriction = constriction
420 self.styles = helper.ensurelist(styles)
421 self.strokestyles = helper.ensurelist(strokestyles)
422 self.fillstyles = helper.ensurelist(fillstyles)
424 def __call__(self, *styles):
425 fillstyles = [ style for s in styles if isinstance(s, filled)
426 for style in s.styles ]
428 strokestyles = [ style for s in styles if isinstance(s, stroked)
429 for style in s.styles ]
431 styles = [ style for style in styles
432 if not (isinstance(style, filled) or
433 isinstance(style, stroked)) ]
435 return arrow(position=self.position,
436 size=self.size,
437 angle=self.angle,
438 constriction=self.constriction,
439 styles=styles,
440 strokestyles=strokestyles,
441 fillstyles=fillstyles)
443 def decorate(self, dp):
445 # TODO: error, when strokepath is not defined
447 # convert to normpath if necessary
448 if isinstance(dp.strokepath, path.normpath):
449 anormpath=dp.strokepath
450 else:
451 anormpath=path.normpath(dp.path)
453 if self.position:
454 anormpath=anormpath.reversed()
456 ahead = _arrowhead(anormpath, self.size, self.angle, self.constriction)
458 dp.addsubdp(decoratedpath(ahead,
459 strokepath=ahead, fillpath=ahead,
460 styles=self.styles,
461 strokestyles=self.strokestyles,
462 fillstyles=self.fillstyles))
464 alen = _arrowheadtemplatelength(anormpath, self.size)
466 if self.constriction:
467 ilen = alen*self.constriction
468 else:
469 ilen = alen
471 # correct somewhat for rotation of arrow segments
472 ilen = ilen*math.cos(math.pi*self.angle/360.0)
474 # this is the rest of the path, we have to draw
475 anormpath = anormpath.split(ilen)[1]
477 # go back to original orientation, if necessary
478 if self.position:
479 anormpath=anormpath.reversed()
481 # set the new (shortened) strokepath
482 dp.strokepath=anormpath
484 return dp
487 class barrow(arrow):
489 """arrow at begin of path"""
491 def __init__(self, size, angle=45, constriction=0.8,
492 styles=None, strokestyles=None, fillstyles=None):
493 arrow.__init__(self,
494 position=0,
495 size=size,
496 angle=angle,
497 constriction=constriction,
498 styles=styles,
499 strokestyles=strokestyles,
500 fillstyles=fillstyles)
502 _base = unit.v_pt(4)
504 barrow.SMALL = barrow(_base/math.sqrt(64))
505 barrow.SMALl = barrow(_base/math.sqrt(32))
506 barrow.SMAll = barrow(_base/math.sqrt(16))
507 barrow.SMall = barrow(_base/math.sqrt(8))
508 barrow.Small = barrow(_base/math.sqrt(4))
509 barrow.small = barrow(_base/math.sqrt(2))
510 barrow.normal = barrow(_base)
511 barrow.large = barrow(_base*math.sqrt(2))
512 barrow.Large = barrow(_base*math.sqrt(4))
513 barrow.LArge = barrow(_base*math.sqrt(8))
514 barrow.LARge = barrow(_base*math.sqrt(16))
515 barrow.LARGe = barrow(_base*math.sqrt(32))
516 barrow.LARGE = barrow(_base*math.sqrt(64))
519 class earrow(arrow):
521 """arrow at end of path"""
523 def __init__(self, size, angle=45, constriction=0.8,
524 styles=[], strokestyles=[], fillstyles=[]):
525 arrow.__init__(self,
526 position=1,
527 size=size,
528 angle=angle,
529 constriction=constriction,
530 styles=styles,
531 strokestyles=strokestyles,
532 fillstyles=fillstyles)
535 earrow.SMALL = earrow(_base/math.sqrt(64))
536 earrow.SMALl = earrow(_base/math.sqrt(32))
537 earrow.SMAll = earrow(_base/math.sqrt(16))
538 earrow.SMall = earrow(_base/math.sqrt(8))
539 earrow.Small = earrow(_base/math.sqrt(4))
540 earrow.small = earrow(_base/math.sqrt(2))
541 earrow.normal = earrow(_base)
542 earrow.large = earrow(_base*math.sqrt(2))
543 earrow.Large = earrow(_base*math.sqrt(4))
544 earrow.LArge = earrow(_base*math.sqrt(8))
545 earrow.LARge = earrow(_base*math.sqrt(16))
546 earrow.LARGe = earrow(_base*math.sqrt(32))
547 earrow.LARGE = earrow(_base*math.sqrt(64))
550 # clipping class
553 class clip(base.PSCmd):
555 """class for use in canvas constructor which clips to a path"""
557 def __init__(self, path):
558 """construct a clip instance for a given path"""
559 self.path = path
561 def bbox(self):
562 # as a PSCmd a clipping path has NO influence on the bbox...
563 return bbox._bbox()
565 def clipbbox(self):
566 # ... but for clipping, we nevertheless need the bbox
567 return self.path.bbox()
569 def write(self, file):
570 _newpath().write(file)
571 self.path.write(file)
572 _clip().write(file)
575 # some very primitive Postscript operators
578 class _newpath(base.PSOp):
579 def write(self, file):
580 file.write("newpath\n")
583 class _stroke(base.PSOp):
584 def write(self, file):
585 file.write("stroke\n")
588 class _fill(base.PSOp):
589 def write(self, file):
590 file.write("fill\n")
593 class _clip(base.PSOp):
594 def write(self, file):
595 file.write("clip\n")
598 class _gsave(base.PSOp):
599 def write(self, file):
600 file.write("gsave\n")
603 class _grestore(base.PSOp):
604 def write(self, file):
605 file.write("grestore\n")
611 class _canvas(base.PSCmd, attrlist.attrlist):
613 """a canvas is a collection of PSCmds together with PSAttrs"""
615 def __init__(self, *args):
617 """construct a canvas
619 The canvas can be modfied by supplying args, which have
620 to be instances of one of the following classes:
621 - trafo.trafo (leading to a global transformation of the canvas)
622 - canvas.clip (clips the canvas)
623 - base.PathStyle (sets some global attributes of the canvas)
625 Note that, while the first two properties are fixed for the
626 whole canvas, the last one can be changed via canvas.set()
630 self.PSOps = []
631 self.trafo = trafo.trafo()
632 self.clipbbox = bbox._bbox()
633 self.texrunner = text.defaulttexrunner
635 for arg in args:
636 if isinstance(arg, trafo._trafo):
637 self.trafo = self.trafo*arg
638 self.PSOps.append(arg)
639 elif isinstance(arg, clip):
640 self.clipbbox=(self.clipbbox*
641 arg.clipbbox().transformed(self.trafo))
642 self.PSOps.append(arg)
643 else:
644 self.set(arg)
646 def bbox(self):
647 """returns bounding box of canvas"""
648 obbox = reduce(lambda x,y:
649 isinstance(y, base.PSCmd) and x+y.bbox() or x,
650 self.PSOps,
651 bbox._bbox())
653 # transform according to our global transformation and
654 # intersect with clipping bounding box (which have already been
655 # transformed in canvas.__init__())
656 return obbox.transformed(self.trafo)*self.clipbbox
658 def prolog(self):
659 result = []
660 for cmd in self.PSOps:
661 result.extend(cmd.prolog())
662 return result
664 def write(self, file):
665 _gsave().write(file)
666 for cmd in self.PSOps:
667 cmd.write(file)
668 _grestore().write(file)
670 def insert(self, PSOp, *args):
671 """insert PSOp in the canvas.
673 If args are given, then insert a canvas containing PSOp applying args.
675 returns the PSOp
679 if args:
680 sc = _canvas(*args)
681 sc.insert(PSOp)
682 self.PSOps.append(sc)
683 else:
684 self.PSOps.append(PSOp)
686 return PSOp
688 def set(self, *styles):
689 """sets styles args globally for the rest of the canvas
691 returns canvas
695 for style in styles:
696 if not isinstance(style, base.PathStyle):
697 raise NotImplementedError, "can only set PathStyle"
699 self.insert(style)
701 return self
703 def draw(self, path, *args):
704 """draw path on canvas using the style given by args
706 The argument list args consists of PathStyles, which modify
707 the appearance of the path, or PathDecos,
708 which add some new visual elements to the path.
710 returns the canvas
714 self.attrcheck(args, allowmulti=(base.PathStyle, PathDeco))
716 dp = decoratedpath(path)
718 # set global styles
719 dp.styles = self.attrgetall(args, base.PathStyle, ())
721 # add path decorations and modify path accordingly
722 for deco in self.attrgetall(args, PathDeco, ()):
723 dp = deco.decorate(dp)
725 self.insert(dp)
727 return self
729 def stroke(self, path, *args):
730 """stroke path on canvas using the style given by args
732 The argument list args consists of PathStyles, which modify
733 the appearance of the path, or PathDecos,
734 which add some new visual elements to the path.
736 returns the canvas
740 return self.draw(path, stroked(), *args)
742 def fill(self, path, *args):
743 """fill path on canvas using the style given by args
745 The argument list args consists of PathStyles, which modify
746 the appearance of the path, or PathDecos,
747 which add some new visual elements to the path.
749 returns the canvas
753 return self.draw(path, filled(), *args)
755 def settexrunner(self, texrunner):
756 """sets the texrunner to be used to within the text and _text methods"""
758 self.texrunner = texrunner
760 def text(self, x, y, atext, *args):
761 """insert a text into the canvas
763 inserts a textbox created by self.texrunner.text into the canvas
765 returns the inserted textbox"""
767 return self.insert(self.texrunner.text(x, y, atext, *args))
770 def _text(self, x, y, atext, *args):
771 """insert a text into the canvas
773 inserts a textbox created by self.texrunner._text into the canvas
775 returns the inserted textbox"""
777 return self.insert(self.texrunner._text(x, y, atext, *args))
780 # canvas for patterns
783 class pattern(_canvas, base.PathStyle):
785 def __init__(self, painttype=1, tilingtype=1, xstep=None, ystep=None, bbox=None, trafo=None):
786 _canvas.__init__(self)
787 self.id = "pattern%d" % id(self)
788 # XXX: some checks are in order
789 if painttype not in (1,2):
790 raise ValueError("painttype must be 1 or 2")
791 self.painttype = painttype
792 if tilingtype not in (1,2,3):
793 raise ValueError("tilingtype must be 1, 2 or 3")
794 self.tilingtype = tilingtype
795 self.xstep = xstep
796 self.ystep = ystep
797 self.patternbbox = bbox
798 self.patterntrafo = trafo
800 def bbox(self):
801 return bbox._bbox()
803 def write(self, file):
804 file.write("%s setpattern\n" % self.id)
806 def prolog(self):
807 realpatternbbox = _canvas.bbox(self)
808 if self.xstep is None:
809 xstep = unit.topt(realpatternbbox.width())
810 else:
811 xstep = unit.topt(unit.length(self.xstep))
812 if self.ystep is None:
813 ystep = unit.topt(realpatternbbox.height())
814 else:
815 ystep = unit.topt(unit.length(self.ystep))
816 if not xstep:
817 raise ValueError("xstep in pattern cannot be zero")
818 if not ystep:
819 raise ValueError("ystep in pattern cannot be zero")
820 patternbbox = self.patternbbox or realpatternbbox.enlarged("5 pt")
822 patternprefix = string.join(("<<",
823 "/PatternType 1",
824 "/PaintType %d" % self.painttype,
825 "/TilingType %d" % self.tilingtype,
826 "/BBox[%s]" % str(patternbbox),
827 "/XStep %g" % xstep,
828 "/YStep %g" % ystep,
829 "/PaintProc {\nbegin\n"),
830 sep="\n")
831 stringfile = StringIO.StringIO()
832 _canvas.write(self, stringfile)
833 patternproc = stringfile.getvalue()
834 stringfile.close()
835 patterntrafostring = self.patterntrafo is None and "matrix" or str(self.patterntrafo)
836 patternsuffix = "end\n} bind\n>>\n%s\nmakepattern" % patterntrafostring
838 pr = _canvas.prolog(self)
839 pr.append(prolog.definition(self.id, string.join((patternprefix, patternproc, patternsuffix), "")))
840 return pr
843 # The main canvas class
846 class canvas(_canvas):
848 """a canvas is a collection of PSCmds together with PSAttrs"""
850 def writetofile(self, filename, paperformat=None, rotated=0, fittosize=0, margin="1 t cm",
851 bbox=None, bboxenlarge="1 t pt"):
852 """write canvas to EPS file
854 If paperformat is set to a known paperformat, the output will be centered on
855 the page.
857 If rotated is set, the output will first be rotated by 90 degrees.
859 If fittosize is set, then the output is scaled to the size of the
860 page (minus margin). In that case, the paperformat the specification
861 of the paperformat is obligatory.
863 The bbox parameter overrides the automatic bounding box determination.
864 bboxenlarge may be used to enlarge the bbox of the canvas (or the
865 manually specified bbox).
868 if filename[-4:]!=".eps":
869 filename = filename + ".eps"
871 try:
872 file = open(filename, "w")
873 except IOError:
874 raise IOError("cannot open output file")
876 abbox = bbox is not None and bbox or self.bbox()
877 abbox = abbox.enlarged(bboxenlarge)
878 ctrafo = None # global transformation of canvas
880 if rotated:
881 ctrafo = trafo._rotate(90,
882 0.5*(abbox.llx+abbox.urx),
883 0.5*(abbox.lly+abbox.ury))
885 if paperformat:
886 # center (optionally rotated) output on page
887 try:
888 width, height = _paperformats[paperformat]
889 except KeyError:
890 raise KeyError, "unknown paperformat '%s'" % paperformat
891 width = unit.topt(width)
892 height = unit.topt(height)
894 if not ctrafo: ctrafo=trafo.trafo()
896 ctrafo = ctrafo._translated(0.5*(width -(abbox.urx-abbox.llx))-
897 abbox.llx,
898 0.5*(height-(abbox.ury-abbox.lly))-
899 abbox.lly)
901 if fittosize:
902 # scale output to pagesize - margins
903 margin=unit.topt(margin)
905 if rotated:
906 sfactor = min((height-2*margin)/(abbox.urx-abbox.llx),
907 (width-2*margin)/(abbox.ury-abbox.lly))
908 else:
909 sfactor = min((width-2*margin)/(abbox.urx-abbox.llx),
910 (height-2*margin)/(abbox.ury-abbox.lly))
912 ctrafo = ctrafo._scaled(sfactor, sfactor, 0.5*width, 0.5*height)
915 elif fittosize:
916 raise ValueError("must specify paper size for fittosize")
918 # if there has been a global transformation, adjust the bounding box
919 # accordingly
920 if ctrafo: abbox = abbox.transformed(ctrafo)
922 file.write("%!PS-Adobe-3.0 EPSF 3.0\n")
923 abbox.write(file)
924 file.write("%%%%Creator: PyX %s\n" % version.version)
925 file.write("%%%%Title: %s\n" % filename)
926 file.write("%%%%CreationDate: %s\n" %
927 time.asctime(time.localtime(time.time())))
928 file.write("%%EndComments\n")
930 file.write("%%BeginProlog\n")
932 mergedprolog = []
934 for pritem in self.prolog():
935 for mpritem in mergedprolog:
936 if mpritem.merge(pritem) is None: break
937 else:
938 mergedprolog.append(pritem)
940 for pritem in mergedprolog:
941 pritem.write(file)
943 file.write("%%EndProlog\n")
945 # again, if there has occured global transformation, apply it now
946 if ctrafo: ctrafo.write(file)
948 file.write("%f setlinewidth\n" % unit.topt(linewidth.normal))
950 # here comes the actual content
951 self.write(file)
953 file.write("showpage\n")
954 file.write("%%Trailer\n")
955 file.write("%%EOF\n")