4 # Copyright (C) 2002 Jörg Lehmann <joergl@users.sourceforge.net>
5 # Copyright (C) 2002 André Wobst <wobsta@users.sourceforge.net>
7 # This file is part of PyX (http://pyx.sourceforge.net/).
9 # PyX is free software; you can redistribute it and/or modify
10 # it under the terms of the GNU General Public License as published by
11 # the Free Software Foundation; either version 2 of the License, or
12 # (at your option) any later version.
14 # PyX is distributed in the hope that it will be useful,
15 # but WITHOUT ANY WARRANTY; without even the implied warranty of
16 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 # GNU General Public License for more details.
19 # You should have received a copy of the GNU General Public License
20 # along with PyX; if not, write to the Free Software
21 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
24 # - should we improve on the arc length -> arg parametrization routine or
25 # should we at least factor it out?
26 # - How should we handle the passing of stroke and fill styles to
27 # arrows? Calls, new instances, ...?
30 """The canvas module provides a PostScript canvas class and related classes
32 A PostScript canvas is the pivotal object for the creation of (E)PS-Files.
33 It collects all the elements that should be displayed (PSCmds) together
34 with attributes if applicable. Furthermore, a canvas can be globally
35 transformed (i.e. translated, rotated, etc.) and clipped.
39 import types
, math
, string
, StringIO
, time
40 import attrlist
, base
, bbox
, helper
, path
, unit
, text
, t1strip
, pykpathsea
, trafo
, version
44 """Part of the PostScript Prolog"""
46 def merge(self
, other
):
47 """ try to merge self with other prologitem
49 If the merge succeeds, return None. Otherwise return other.
50 Raise ValueError, if conflicts arise!"""
54 def write(self
, file):
55 """ write self in file """
59 class definition(prologitem
):
61 def __init__(self
, id, body
):
65 def merge(self
, other
):
66 if not isinstance(other
, definition
):
69 if self
.body
==other
.body
:
71 raise ValueError("Conflicting function definitions!")
75 def write(self
, file):
76 file.write("%%%%BeginRessource: %s\n" % self
.id)
77 file.write("%(body)s /%(id)s exch def\n" % self
.__dict
__)
78 file.write("%%EndRessource\n")
81 class fontdefinition(prologitem
):
83 def __init__(self
, font
):
85 self
.usedchars
= font
.usedchars
87 def merge(self
, other
):
88 if not isinstance(other
, fontdefinition
):
90 if self
.name
==other
.name
:
91 for i
in range(len(self
.usedchars
)):
92 self
.usedchars
[i
] = self
.usedchars
[i
] or other
.usedchars
[i
]
97 def write(self
, file):
98 file.write("%%%%BeginFont: %s\n" % self
.name
.upper())
99 file.write("%Included char codes:")
100 for i
in range(len(self
.usedchars
)):
101 if self
.usedchars
[i
]:
102 file.write(" %d" % i
)
104 pfbname
= pykpathsea
.find_file("%s.pfb" % self
.name
, pykpathsea
.kpse_type1_format
)
107 raise "cannot find type 1 font %s" % self
.name
108 t1strip
.t1strip(file, pfbname
, self
.usedchars
)
109 file.write("%%EndFont\n")
111 # known paperformats as tuple(width, height)
113 _paperformats
= { "a4" : ("210 t mm", "297 t mm"),
114 "a3" : ("297 t mm", "420 t mm"),
115 "a2" : ("420 t mm", "594 t mm"),
116 "a1" : ("594 t mm", "840 t mm"),
117 "a0" : ("840 t mm", "1188 t mm"),
118 "a0b" : ("910 t mm", "1370 t mm"),
119 "letter" : ("8.5 t in", "11 t in"),
120 "legal" : ("8.5 t in", "14 t in")}
127 class CanvasException(Exception): pass
130 class linecap(base
.PathStyle
):
132 """linecap of paths"""
134 def __init__(self
, value
=0):
137 def write(self
, file):
138 file.write("%d setlinecap\n" % self
.value
)
140 linecap
.butt
= linecap(0)
141 linecap
.round = linecap(1)
142 linecap
.square
= linecap(2)
145 class linejoin(base
.PathStyle
):
147 """linejoin of paths"""
149 def __init__(self
, value
=0):
152 def write(self
, file):
153 file.write("%d setlinejoin\n" % self
.value
)
155 linejoin
.miter
= linejoin(0)
156 linejoin
.round = linejoin(1)
157 linejoin
.bevel
= linejoin(2)
160 class miterlimit(base
.PathStyle
):
162 """miterlimit of paths"""
164 def __init__(self
, value
=10.0):
167 def write(self
, file):
168 file.write("%f setmiterlimit\n" % self
.value
)
171 miterlimit
.lessthan180deg
= miterlimit(1/math
.sin(math
.pi
*180/360))
172 miterlimit
.lessthan90deg
= miterlimit(1/math
.sin(math
.pi
*90/360))
173 miterlimit
.lessthan60deg
= miterlimit(1/math
.sin(math
.pi
*60/360))
174 miterlimit
.lessthan45deg
= miterlimit(1/math
.sin(math
.pi
*45/360))
175 miterlimit
.lessthan11deg
= miterlimit(10) # the default, approximately 11.4783 degress
177 class dash(base
.PathStyle
):
181 def __init__(self
, pattern
=[], offset
=0):
185 def write(self
, file):
187 for element
in self
.pattern
:
188 patternstring
=patternstring
+ `element`
+ " "
190 file.write("[%s] %d setdash\n" % (patternstring
, self
.offset
))
193 class linestyle(base
.PathStyle
):
195 """linestyle (linecap together with dash) of paths"""
197 def __init__(self
, c
=linecap
.butt
, d
=dash([])):
201 def write(self
, file):
205 linestyle
.solid
= linestyle(linecap
.butt
, dash([]))
206 linestyle
.dashed
= linestyle(linecap
.butt
, dash([2]))
207 linestyle
.dotted
= linestyle(linecap
.round, dash([0, 3]))
208 linestyle
.dashdotted
= linestyle(linecap
.round, dash([0, 3, 3, 3]))
211 class linewidth(base
.PathStyle
, unit
.length
):
213 """linewidth of paths"""
215 def __init__(self
, l
="0 cm"):
216 unit
.length
.__init
__(self
, l
=l
, default_type
="w")
218 def write(self
, file):
219 file.write("%f setlinewidth\n" % unit
.topt(self
))
223 linewidth
.THIN
= linewidth("%f cm" % (_base
/math
.sqrt(32)))
224 linewidth
.THIn
= linewidth("%f cm" % (_base
/math
.sqrt(16)))
225 linewidth
.THin
= linewidth("%f cm" % (_base
/math
.sqrt(8)))
226 linewidth
.Thin
= linewidth("%f cm" % (_base
/math
.sqrt(4)))
227 linewidth
.thin
= linewidth("%f cm" % (_base
/math
.sqrt(2)))
228 linewidth
.normal
= linewidth("%f cm" % _base
)
229 linewidth
.thick
= linewidth("%f cm" % (_base
*math
.sqrt(2)))
230 linewidth
.Thick
= linewidth("%f cm" % (_base
*math
.sqrt(4)))
231 linewidth
.THick
= linewidth("%f cm" % (_base
*math
.sqrt(8)))
232 linewidth
.THIck
= linewidth("%f cm" % (_base
*math
.sqrt(16)))
233 linewidth
.THICk
= linewidth("%f cm" % (_base
*math
.sqrt(32)))
234 linewidth
.THICK
= linewidth("%f cm" % (_base
*math
.sqrt(64)))
241 class decoratedpath(base
.PSCmd
):
244 The main purpose of this class is during the drawing
245 (stroking/filling) of a path. It collects attributes for the
246 stroke and/or fill operations.
250 path
, strokepath
=None, fillpath
=None,
251 styles
=None, strokestyles
=None, fillstyles
=None,
256 # path to be stroked or filled (or None)
257 self
.strokepath
= strokepath
258 self
.fillpath
= fillpath
260 # global style for stroking and filling and subdps
261 self
.styles
= helper
.ensurelist(styles
)
263 # styles which apply only for stroking and filling
264 self
.strokestyles
= helper
.ensurelist(strokestyles
)
265 self
.fillstyles
= helper
.ensurelist(fillstyles
)
267 # additional elements of the path, e.g., arrowheads,
268 # which are by themselves decoratedpaths
269 self
.subdps
= helper
.ensurelist(subdps
)
271 def addsubdp(self
, subdp
):
272 """add a further decorated path to the list of subdps"""
273 self
.subdps
.append(subdp
)
276 return reduce(lambda x
,y
: x
+y
.bbox(),
282 for style
in list(self
.styles
) + list(self
.fillstyles
) + list(self
.strokestyles
):
283 result
.extend(style
.prolog())
286 def write(self
, file):
287 # draw (stroke and/or fill) the decoratedpath on the canvas
288 # while trying to produce an efficient output, e.g., by
289 # not writing one path two times
292 def _writestyles(styles
, file=file):
296 # apply global styles
299 _writestyles(self
.styles
)
302 _newpath().write(file)
303 self
.fillpath
.write(file)
305 if self
.strokepath
==self
.fillpath
:
306 # do efficient stroking + filling
310 _writestyles(self
.fillstyles
)
313 _grestore().write(file)
315 if self
.strokestyles
:
317 _writestyles(self
.strokestyles
)
319 _stroke().write(file)
321 if self
.strokestyles
:
322 _grestore().write(file)
324 # only fill fillpath - for the moment
327 _writestyles(self
.fillstyles
)
332 _grestore().write(file)
334 if self
.strokepath
and self
.strokepath
!=self
.fillpath
:
335 # this is the only relevant case still left
336 # Note that a possible stroking has already been done.
338 if self
.strokestyles
:
340 _writestyles(self
.strokestyles
)
342 _newpath().write(file)
343 self
.strokepath
.write(file)
344 _stroke().write(file)
346 if self
.strokestyles
:
347 _grestore().write(file)
349 if not self
.strokepath
and not self
.fillpath
:
350 raise RuntimeError("Path neither to be stroked nor filled")
352 # now, draw additional subdps
353 for subdp
in self
.subdps
:
356 # restore global styles
358 _grestore().write(file)
368 In contrast to path styles, path decorators depend on the concrete
369 path to which they are applied. In particular, they don't make
370 sense without any path and can thus not be used in canvas.set!
374 def decorate(self
, dp
):
375 """apply a style to a given decoratedpath object dp
377 decorate accepts a decoratedpath object dp, applies PathStyle
378 by modifying dp in place and returning the new dp.
384 # stroked and filled: basic PathDecos which stroked and fill,
385 # respectively the path
388 class stroked(PathDeco
):
390 """stroked is a PathDecorator, which draws the outline of the path"""
392 def __init__(self
, *styles
):
393 self
.styles
= list(styles
)
395 def decorate(self
, dp
):
396 dp
.strokepath
= dp
.path
397 dp
.strokestyles
= self
.styles
402 class filled(PathDeco
):
404 """filled is a PathDecorator, which fills the interior of the path"""
406 def __init__(self
, *styles
):
407 self
.styles
= list(styles
)
409 def decorate(self
, dp
):
410 dp
.fillpath
= dp
.path
411 dp
.fillstyles
= self
.styles
415 def _arrowheadtemplatelength(anormpath
, size
):
416 "calculate length of arrowhead template (in parametrisation of anormpath)"
418 tx
, ty
= anormpath
.begin()
420 # obtain arrow template by using path up to first intersection
421 # with circle around tip (as suggested by Michael Schindler)
422 ipar
= anormpath
.intersect(path
.circle(tx
, ty
, size
))
426 # if this doesn't work, use first order conversion from pts to
427 # the bezier curve's parametrization
428 tlen
= unit
.topt(anormpath
.tangent(0).arclength())
430 alen
= unit
.topt(size
)/tlen
431 except ArithmeticError:
432 # take maximum, we can get
433 alen
= anormpath
.range()
434 if alen
>anormpath
.range(): alen
=anormpath().range()
439 def _arrowhead(anormpath
, size
, angle
, constriction
):
441 """helper routine, which returns an arrowhead for a normpath
443 returns arrowhead at begin of anormpath with size,
444 opening angle and relative constriction
447 alen
= _arrowheadtemplatelength(anormpath
, size
)
448 tx
, ty
= anormpath
.begin()
450 # now we construct the template for our arrow but cutting
451 # the path a the corresponding length
452 arrowtemplate
= anormpath
.split(alen
)[0]
454 # from this template, we construct the two outer curves
456 arrowl
= arrowtemplate
.transformed(trafo
.rotate(-angle
/2.0, tx
, ty
))
457 arrowr
= arrowtemplate
.transformed(trafo
.rotate( angle
/2.0, tx
, ty
))
459 # now come the joining backward parts
461 # arrow with constriction
463 # constriction point (cx, cy) lies on path
464 cx
, cy
= anormpath
.at(constriction
*alen
)
466 arrowcr
= path
.line(*(arrowr
.end()+(cx
,cy
)))
468 arrow
= arrowl
.reversed() << arrowr
<< arrowcr
469 arrow
.append(path
.closepath())
471 # arrow without constriction
472 arrow
= arrowl
.reversed() << arrowr
473 arrow
.append(path
.closepath())
477 class arrow(PathDeco
):
479 """A general arrow"""
482 position
, size
, angle
=45, constriction
=0.8,
483 styles
=None, strokestyles
=None, fillstyles
=None):
484 self
.position
= position
487 self
.constriction
= constriction
488 self
.styles
= helper
.ensurelist(styles
)
489 self
.strokestyles
= helper
.ensurelist(strokestyles
)
490 self
.fillstyles
= helper
.ensurelist(fillstyles
)
492 def __call__(self
, *styles
):
493 fillstyles
= [ style
for s
in styles
if isinstance(s
, filled
)
494 for style
in s
.styles
]
496 strokestyles
= [ style
for s
in styles
if isinstance(s
, stroked
)
497 for style
in s
.styles
]
499 styles
= [ style
for style
in styles
500 if not (isinstance(style
, filled
) or
501 isinstance(style
, stroked
)) ]
503 return arrow(position
=self
.position
,
506 constriction
=self
.constriction
,
508 strokestyles
=strokestyles
,
509 fillstyles
=fillstyles
)
511 def decorate(self
, dp
):
513 # TODO: error, when strokepath is not defined
515 # convert to normpath if necessary
516 if isinstance(dp
.strokepath
, path
.normpath
):
517 anormpath
=dp
.strokepath
519 anormpath
=path
.normpath(dp
.path
)
522 anormpath
=anormpath
.reversed()
524 ahead
= _arrowhead(anormpath
, self
.size
, self
.angle
, self
.constriction
)
526 dp
.addsubdp(decoratedpath(ahead
,
527 strokepath
=ahead
, fillpath
=ahead
,
529 strokestyles
=self
.strokestyles
,
530 fillstyles
=self
.fillstyles
))
532 alen
= _arrowheadtemplatelength(anormpath
, self
.size
)
534 if self
.constriction
:
535 ilen
= alen
*self
.constriction
539 # correct somewhat for rotation of arrow segments
540 ilen
= ilen
*math
.cos(math
.pi
*self
.angle
/360.0)
542 # this is the rest of the path, we have to draw
543 anormpath
= anormpath
.split(ilen
)[1]
545 # go back to original orientation, if necessary
547 anormpath
=anormpath
.reversed()
549 # set the new (shortened) strokepath
550 dp
.strokepath
=anormpath
557 """arrow at begin of path"""
559 def __init__(self
, size
, angle
=45, constriction
=0.8,
560 styles
=None, strokestyles
=None, fillstyles
=None):
565 constriction
=constriction
,
567 strokestyles
=strokestyles
,
568 fillstyles
=fillstyles
)
572 barrow
.SMALL
= barrow(_base
/math
.sqrt(64))
573 barrow
.SMALl
= barrow(_base
/math
.sqrt(32))
574 barrow
.SMAll
= barrow(_base
/math
.sqrt(16))
575 barrow
.SMall
= barrow(_base
/math
.sqrt(8))
576 barrow
.Small
= barrow(_base
/math
.sqrt(4))
577 barrow
.small
= barrow(_base
/math
.sqrt(2))
578 barrow
.normal
= barrow(_base
)
579 barrow
.large
= barrow(_base
*math
.sqrt(2))
580 barrow
.Large
= barrow(_base
*math
.sqrt(4))
581 barrow
.LArge
= barrow(_base
*math
.sqrt(8))
582 barrow
.LARge
= barrow(_base
*math
.sqrt(16))
583 barrow
.LARGe
= barrow(_base
*math
.sqrt(32))
584 barrow
.LARGE
= barrow(_base
*math
.sqrt(64))
589 """arrow at end of path"""
591 def __init__(self
, size
, angle
=45, constriction
=0.8,
592 styles
=[], strokestyles
=[], fillstyles
=[]):
597 constriction
=constriction
,
599 strokestyles
=strokestyles
,
600 fillstyles
=fillstyles
)
603 earrow
.SMALL
= earrow(_base
/math
.sqrt(64))
604 earrow
.SMALl
= earrow(_base
/math
.sqrt(32))
605 earrow
.SMAll
= earrow(_base
/math
.sqrt(16))
606 earrow
.SMall
= earrow(_base
/math
.sqrt(8))
607 earrow
.Small
= earrow(_base
/math
.sqrt(4))
608 earrow
.small
= earrow(_base
/math
.sqrt(2))
609 earrow
.normal
= earrow(_base
)
610 earrow
.large
= earrow(_base
*math
.sqrt(2))
611 earrow
.Large
= earrow(_base
*math
.sqrt(4))
612 earrow
.LArge
= earrow(_base
*math
.sqrt(8))
613 earrow
.LARge
= earrow(_base
*math
.sqrt(16))
614 earrow
.LARGe
= earrow(_base
*math
.sqrt(32))
615 earrow
.LARGE
= earrow(_base
*math
.sqrt(64))
621 class clip(base
.PSCmd
):
623 """class for use in canvas constructor which clips to a path"""
625 def __init__(self
, path
):
626 """construct a clip instance for a given path"""
630 # as a PSCmd a clipping path has NO influence on the bbox...
634 # ... but for clipping, we nevertheless need the bbox
635 return self
.path
.bbox()
637 def write(self
, file):
638 _newpath().write(file)
639 self
.path
.write(file)
643 # some very primitive Postscript operators
646 class _newpath(base
.PSOp
):
647 def write(self
, file):
648 file.write("newpath\n")
651 class _stroke(base
.PSOp
):
652 def write(self
, file):
653 file.write("stroke\n")
656 class _fill(base
.PSOp
):
657 def write(self
, file):
661 class _clip(base
.PSOp
):
662 def write(self
, file):
666 class _gsave(base
.PSOp
):
667 def write(self
, file):
668 file.write("gsave\n")
671 class _grestore(base
.PSOp
):
672 def write(self
, file):
673 file.write("grestore\n")
679 class _canvas(base
.PSCmd
, attrlist
.attrlist
):
681 """a canvas is a collection of PSCmds together with PSAttrs"""
683 def __init__(self
, *args
):
685 """construct a canvas
687 The canvas can be modfied by supplying args, which have
688 to be instances of one of the following classes:
689 - trafo.trafo (leading to a global transformation of the canvas)
690 - canvas.clip (clips the canvas)
691 - base.PathStyle (sets some global attributes of the canvas)
693 Note that, while the first two properties are fixed for the
694 whole canvas, the last one can be changed via canvas.set()
699 self
.trafo
= trafo
.trafo()
700 self
.clipbbox
= bbox
._bbox
()
701 self
.texrunner
= text
.defaulttexrunner
704 if isinstance(arg
, trafo
._trafo
):
705 self
.trafo
= self
.trafo
*arg
706 self
.PSOps
.append(arg
)
707 elif isinstance(arg
, clip
):
708 self
.clipbbox
=(self
.clipbbox
*
709 arg
.clipbbox().transformed(self
.trafo
))
710 self
.PSOps
.append(arg
)
715 """returns bounding box of canvas"""
716 obbox
= reduce(lambda x
,y
:
717 isinstance(y
, base
.PSCmd
) and x
+y
.bbox() or x
,
721 # transform according to our global transformation and
722 # intersect with clipping bounding box (which have already been
723 # transformed in canvas.__init__())
724 return obbox
.transformed(self
.trafo
)*self
.clipbbox
728 for cmd
in self
.PSOps
:
729 result
.extend(cmd
.prolog())
732 def write(self
, file):
734 for cmd
in self
.PSOps
:
736 _grestore().write(file)
738 def insert(self
, PSOp
, *args
):
739 """insert PSOp in the canvas.
741 If args are given, then insert a canvas containing PSOp applying args.
750 self
.PSOps
.append(sc
)
752 self
.PSOps
.append(PSOp
)
756 def set(self
, *styles
):
757 """sets styles args globally for the rest of the canvas
764 if not isinstance(style
, base
.PathStyle
):
765 raise NotImplementedError, "can only set PathStyle"
771 def draw(self
, path
, *args
):
772 """draw path on canvas using the style given by args
774 The argument list args consists of PathStyles, which modify
775 the appearance of the path, or PathDecos,
776 which add some new visual elements to the path.
782 self
.attrcheck(args
, allowmulti
=(base
.PathStyle
, PathDeco
))
784 dp
= decoratedpath(path
)
787 dp
.styles
= self
.attrgetall(args
, base
.PathStyle
, ())
789 # add path decorations and modify path accordingly
790 for deco
in self
.attrgetall(args
, PathDeco
, ()):
791 dp
= deco
.decorate(dp
)
797 def stroke(self
, path
, *args
):
798 """stroke path on canvas using the style given by args
800 The argument list args consists of PathStyles, which modify
801 the appearance of the path, or PathDecos,
802 which add some new visual elements to the path.
808 return self
.draw(path
, stroked(), *args
)
810 def fill(self
, path
, *args
):
811 """fill path on canvas using the style given by args
813 The argument list args consists of PathStyles, which modify
814 the appearance of the path, or PathDecos,
815 which add some new visual elements to the path.
821 return self
.draw(path
, filled(), *args
)
823 def settexrunner(self
, texrunner
):
824 """sets the texrunner to be used to within the text and _text methods"""
826 self
.texrunner
= texrunner
828 def text(self
, x
, y
, atext
, *args
):
829 """insert a text into the canvas
831 inserts a textbox created by self.texrunner.text into the canvas
833 returns the inserted textbox"""
835 return self
.insert(self
.texrunner
.text(x
, y
, atext
, *args
))
838 def _text(self
, x
, y
, atext
, *args
):
839 """insert a text into the canvas
841 inserts a textbox created by self.texrunner._text into the canvas
843 returns the inserted textbox"""
845 return self
.insert(self
.texrunner
._text
(x
, y
, atext
, *args
))
848 # canvas for patterns
851 class pattern(_canvas
, base
.PathStyle
):
853 def __init__(self
, painttype
=1, tilingtype
=1, xstep
=None, ystep
=None, bbox
=None, trafo
=None):
854 _canvas
.__init
__(self
)
855 self
.id = "pattern%d" % id(self
)
856 # XXX: some checks are in order
857 if painttype
not in (1,2):
858 raise ValueError("painttype must be 1 or 2")
859 self
.painttype
= painttype
860 if tilingtype
not in (1,2,3):
861 raise ValueError("tilingtype must be 1, 2 or 3")
862 self
.tilingtype
= tilingtype
865 self
.patternbbox
= bbox
866 self
.patterntrafo
= trafo
871 def write(self
, file):
872 file.write("%s setpattern\n" % self
.id)
875 realpatternbbox
= _canvas
.bbox(self
)
876 if self
.xstep
is None:
877 xstep
= unit
.topt(realpatternbbox
.width())
879 xstep
= unit
.topt(unit
.length(self
.xstep
))
880 if self
.ystep
is None:
881 ystep
= unit
.topt(realpatternbbox
.height())
883 ystep
= unit
.topt(unit
.length(self
.ystep
))
885 raise ValueError("xstep in pattern cannot be zero")
887 raise ValueError("ystep in pattern cannot be zero")
888 patternbbox
= self
.patternbbox
or realpatternbbox
.enlarged("5 pt")
890 patternprefix
= string
.join(("<<",
892 "/PaintType %d" % self
.painttype
,
893 "/TilingType %d" % self
.tilingtype
,
894 "/BBox[%s]" % str(patternbbox
),
897 "/PaintProc {\nbegin\n"),
899 stringfile
= StringIO
.StringIO()
900 _canvas
.write(self
, stringfile
)
901 patternproc
= stringfile
.getvalue()
903 patterntrafostring
= self
.patterntrafo
is None and "matrix" or str(self
.patterntrafo
)
904 patternsuffix
= "end\n} bind\n>>\n%s\nmakepattern" % patterntrafostring
906 pr
= _canvas
.prolog(self
)
907 pr
.append(definition(self
.id, string
.join((patternprefix
, patternproc
, patternsuffix
), "")))
911 # The main canvas class
914 class canvas(_canvas
):
916 """a canvas is a collection of PSCmds together with PSAttrs"""
918 def __init__(self
, *args
):
920 """construct a canvas
922 The canvas can be modfied by supplying args, which have
923 to be instances of one of the following classes:
924 - trafo.trafo (leading to a global transformation of the canvas)
925 - canvas.clip (clips the canvas)
926 - base.PathStyle (sets some global attributes of the canvas)
928 Note that, while the first two properties are fixed for the
929 whole canvas, the last one can be changed via canvas.set()
934 self
.trafo
= trafo
.trafo()
935 self
.clipbbox
= bbox
._bbox
()
936 self
.texrunner
= text
.defaulttexrunner
939 if isinstance(arg
, trafo
._trafo
):
940 self
.trafo
= self
.trafo
*arg
941 self
.PSOps
.append(arg
)
942 elif isinstance(arg
, clip
):
943 self
.clipbbox
=(self
.clipbbox
*
944 arg
.clipbbox().transformed(self
.trafo
))
945 self
.PSOps
.append(arg
)
949 def writetofile(self
, filename
, paperformat
=None, rotated
=0, fittosize
=0, margin
="1 t cm",
950 bbox
=None, bboxenlarge
="1 t pt"):
951 """write canvas to EPS file
953 If paperformat is set to a known paperformat, the output will be centered on
956 If rotated is set, the output will first be rotated by 90 degrees.
958 If fittosize is set, then the output is scaled to the size of the
959 page (minus margin). In that case, the paperformat the specification
960 of the paperformat is obligatory.
962 The bbox parameter overrides the automatic bounding box determination.
963 bboxenlarge may be used to enlarge the bbox of the canvas (or the
964 manually specified bbox).
967 if filename
[-4:]!=".eps":
968 filename
= filename
+ ".eps"
971 file = open(filename
, "w")
973 assert 0, "cannot open output file" # TODO: Fehlerbehandlung...
975 abbox
= bbox
is not None and bbox
or self
.bbox()
976 abbox
= abbox
.enlarged(bboxenlarge
)
977 ctrafo
= None # global transformation of canvas
980 ctrafo
= trafo
._rotate
(90,
981 0.5*(abbox
.llx
+abbox
.urx
),
982 0.5*(abbox
.lly
+abbox
.ury
))
985 # center (optionally rotated) output on page
987 width
, height
= _paperformats
[paperformat
]
988 width
= unit
.topt(width
)
989 height
= unit
.topt(height
)
991 raise KeyError, "unknown paperformat '%s'" % paperformat
993 if not ctrafo
: ctrafo
=trafo
.trafo()
995 ctrafo
= ctrafo
._translated
(0.5*(width
-(abbox
.urx
-abbox
.llx
))-
997 0.5*(height
-(abbox
.ury
-abbox
.lly
))-
1001 # scale output to pagesize - margins
1002 margin
=unit
.topt(margin
)
1005 sfactor
= min((height
-2*margin
)/(abbox
.urx
-abbox
.llx
),
1006 (width
-2*margin
)/(abbox
.ury
-abbox
.lly
))
1008 sfactor
= min((width
-2*margin
)/(abbox
.urx
-abbox
.llx
),
1009 (height
-2*margin
)/(abbox
.ury
-abbox
.lly
))
1011 ctrafo
= ctrafo
._scaled
(sfactor
, sfactor
, 0.5*width
, 0.5*height
)
1015 assert 0, "must specify paper size for fittosize" # TODO: exception...
1017 # if there has been a global transformation, adjust the bounding box
1019 if ctrafo
: abbox
= abbox
.transformed(ctrafo
)
1021 file.write("%!PS-Adobe-3.0 EPSF 3.0\n")
1023 file.write("%%%%Creator: PyX %s\n" % version
.version
)
1024 file.write("%%%%Title: %s\n" % filename
)
1025 file.write("%%%%CreationDate: %s\n" %
1026 time
.asctime(time
.localtime(time
.time())))
1027 file.write("%%EndComments\n")
1029 file.write("%%BeginProlog\n")
1033 for pritem
in self
.prolog():
1034 for mpritem
in mergedprolog
:
1035 if mpritem
.merge(pritem
) is None: break
1037 mergedprolog
.append(pritem
)
1039 for pritem
in mergedprolog
:
1042 file.write("%%EndProlog\n")
1044 # again, if there has occured global transformation, apply it now
1045 if ctrafo
: ctrafo
.write(file)
1047 file.write("%f setlinewidth\n" % unit
.topt(linewidth
.normal
))
1049 # here comes the actual content
1052 file.write("showpage\n")
1053 file.write("%%Trailer\n")
1054 file.write("%%EOF\n")