remove limitation on number of fonts in dvi file
[PyX/mjg.git] / pyx / canvas.py
blob3bd5cf380ea421b6b5c29b55a80f4563d32095f5
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 is not None:
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 is not None 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 is not None 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, PathDecos, which add some new
708 visual elements to the path, or trafos, which are applied
709 before drawing the path.
711 returns the canvas
715 self.attrcheck(args, allowmulti=(base.PathStyle, PathDeco, trafo._trafo))
717 for t in self.attrgetall(args, trafo._trafo, ()):
718 path = path.transformed(t)
720 dp = decoratedpath(path)
722 # set global styles
723 dp.styles = self.attrgetall(args, base.PathStyle, ())
725 # add path decorations and modify path accordingly
726 for deco in self.attrgetall(args, PathDeco, ()):
727 dp = deco.decorate(dp)
729 self.insert(dp)
731 return self
733 def stroke(self, path, *args):
734 """stroke path on canvas using the style given by args
736 The argument list args consists of PathStyles, which modify
737 the appearance of the path, PathDecos, which add some new
738 visual elements to the path, or trafos, which are applied
739 before drawing the path.
741 returns the canvas
745 return self.draw(path, stroked(), *args)
747 def fill(self, path, *args):
748 """fill path on canvas using the style given by args
750 The argument list args consists of PathStyles, which modify
751 the appearance of the path, PathDecos, which add some new
752 visual elements to the path, or trafos, which are applied
753 before drawing the path.
755 returns the canvas
759 return self.draw(path, filled(), *args)
761 def settexrunner(self, texrunner):
762 """sets the texrunner to be used to within the text and _text methods"""
764 self.texrunner = texrunner
766 def text(self, x, y, atext, *args):
767 """insert a text into the canvas
769 inserts a textbox created by self.texrunner.text into the canvas
771 returns the inserted textbox"""
773 return self.insert(self.texrunner.text(x, y, atext, *args))
776 def _text(self, x, y, atext, *args):
777 """insert a text into the canvas
779 inserts a textbox created by self.texrunner._text into the canvas
781 returns the inserted textbox"""
783 return self.insert(self.texrunner._text(x, y, atext, *args))
786 # canvas for patterns
789 class pattern(_canvas, base.PathStyle):
791 def __init__(self, painttype=1, tilingtype=1, xstep=None, ystep=None, bbox=None, trafo=None):
792 _canvas.__init__(self)
793 self.id = "pattern%d" % id(self)
794 # XXX: some checks are in order
795 if painttype not in (1,2):
796 raise ValueError("painttype must be 1 or 2")
797 self.painttype = painttype
798 if tilingtype not in (1,2,3):
799 raise ValueError("tilingtype must be 1, 2 or 3")
800 self.tilingtype = tilingtype
801 self.xstep = xstep
802 self.ystep = ystep
803 self.patternbbox = bbox
804 self.patterntrafo = trafo
806 def bbox(self):
807 return bbox._bbox()
809 def write(self, file):
810 file.write("%s setpattern\n" % self.id)
812 def prolog(self):
813 realpatternbbox = _canvas.bbox(self)
814 if self.xstep is None:
815 xstep = unit.topt(realpatternbbox.width())
816 else:
817 xstep = unit.topt(unit.length(self.xstep))
818 if self.ystep is None:
819 ystep = unit.topt(realpatternbbox.height())
820 else:
821 ystep = unit.topt(unit.length(self.ystep))
822 if not xstep:
823 raise ValueError("xstep in pattern cannot be zero")
824 if not ystep:
825 raise ValueError("ystep in pattern cannot be zero")
826 patternbbox = self.patternbbox or realpatternbbox.enlarged("5 pt")
828 patternprefix = string.join(("<<",
829 "/PatternType 1",
830 "/PaintType %d" % self.painttype,
831 "/TilingType %d" % self.tilingtype,
832 "/BBox[%s]" % str(patternbbox),
833 "/XStep %g" % xstep,
834 "/YStep %g" % ystep,
835 "/PaintProc {\nbegin\n"),
836 sep="\n")
837 stringfile = StringIO.StringIO()
838 _canvas.write(self, stringfile)
839 patternproc = stringfile.getvalue()
840 stringfile.close()
841 patterntrafostring = self.patterntrafo is None and "matrix" or str(self.patterntrafo)
842 patternsuffix = "end\n} bind\n>>\n%s\nmakepattern" % patterntrafostring
844 pr = _canvas.prolog(self)
845 pr.append(prolog.definition(self.id, string.join((patternprefix, patternproc, patternsuffix), "")))
846 return pr
849 # The main canvas class
852 class canvas(_canvas):
854 """a canvas is a collection of PSCmds together with PSAttrs"""
856 def writetofile(self, filename, paperformat=None, rotated=0, fittosize=0, margin="1 t cm",
857 bbox=None, bboxenlarge="1 t pt"):
858 """write canvas to EPS file
860 If paperformat is set to a known paperformat, the output will be centered on
861 the page.
863 If rotated is set, the output will first be rotated by 90 degrees.
865 If fittosize is set, then the output is scaled to the size of the
866 page (minus margin). In that case, the paperformat the specification
867 of the paperformat is obligatory.
869 The bbox parameter overrides the automatic bounding box determination.
870 bboxenlarge may be used to enlarge the bbox of the canvas (or the
871 manually specified bbox).
874 if filename[-4:]!=".eps":
875 filename = filename + ".eps"
877 try:
878 file = open(filename, "w")
879 except IOError:
880 raise IOError("cannot open output file")
882 abbox = bbox is not None and bbox or self.bbox()
883 abbox = abbox.enlarged(bboxenlarge)
884 ctrafo = None # global transformation of canvas
886 if rotated:
887 ctrafo = trafo._rotate(90,
888 0.5*(abbox.llx+abbox.urx),
889 0.5*(abbox.lly+abbox.ury))
891 if paperformat:
892 # center (optionally rotated) output on page
893 try:
894 width, height = _paperformats[paperformat]
895 except KeyError:
896 raise KeyError, "unknown paperformat '%s'" % paperformat
897 width = unit.topt(width)
898 height = unit.topt(height)
900 if not ctrafo: ctrafo=trafo.trafo()
902 ctrafo = ctrafo._translated(0.5*(width -(abbox.urx-abbox.llx))-
903 abbox.llx,
904 0.5*(height-(abbox.ury-abbox.lly))-
905 abbox.lly)
907 if fittosize:
908 # scale output to pagesize - margins
909 margin = unit.topt(margin)
910 if 2*margin > min(width, height):
911 raise RuntimeError("Margins too broad for selected paperformat. Aborting.")
913 if rotated:
914 sfactor = min((height-2*margin)/(abbox.urx-abbox.llx),
915 (width-2*margin)/(abbox.ury-abbox.lly))
916 else:
917 sfactor = min((width-2*margin)/(abbox.urx-abbox.llx),
918 (height-2*margin)/(abbox.ury-abbox.lly))
920 ctrafo = ctrafo._scaled(sfactor, sfactor, 0.5*width, 0.5*height)
923 elif fittosize:
924 raise ValueError("must specify paper size for fittosize")
926 # if there has been a global transformation, adjust the bounding box
927 # accordingly
928 if ctrafo: abbox = abbox.transformed(ctrafo)
930 file.write("%!PS-Adobe-3.0 EPSF 3.0\n")
931 abbox.write(file)
932 file.write("%%%%Creator: PyX %s\n" % version.version)
933 file.write("%%%%Title: %s\n" % filename)
934 file.write("%%%%CreationDate: %s\n" %
935 time.asctime(time.localtime(time.time())))
936 file.write("%%EndComments\n")
938 file.write("%%BeginProlog\n")
940 mergedprolog = []
942 for pritem in self.prolog():
943 for mpritem in mergedprolog:
944 if mpritem.merge(pritem) is None: break
945 else:
946 mergedprolog.append(pritem)
948 for pritem in mergedprolog:
949 pritem.write(file)
951 file.write("%%EndProlog\n")
953 # again, if there has occured global transformation, apply it now
954 if ctrafo: ctrafo.write(file)
956 file.write("%f setlinewidth\n" % unit.topt(linewidth.normal))
958 # here comes the actual content
959 self.write(file)
961 file.write("showpage\n")
962 file.write("%%Trailer\n")
963 file.write("%%EOF\n")