GPL -> Latex license
[PyX/mjg.git] / pyx / canvas.py
blob0abd02579907a78ef43ea3c4be82f2a722943cf4
1 #!/usr/bin/env python
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
23 # TODO:
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.
37 """
39 import types, math, string, StringIO, time
40 import attrlist, base, bbox, helper, path, unit, text, t1strip, pykpathsea, trafo, version
42 class prologitem:
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!"""
52 pass
54 def write(self, file):
55 """ write self in file """
56 pass
59 class definition(prologitem):
61 def __init__(self, id, body):
62 self.id = id
63 self.body = body
65 def merge(self, other):
66 if not isinstance(other, definition):
67 return other
68 if self.id==other.id:
69 if self.body==other.body:
70 return None
71 raise ValueError("Conflicting function definitions!")
72 else:
73 return other
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):
84 self.name = font.name
85 self.usedchars = font.usedchars
87 def merge(self, other):
88 if not isinstance(other, fontdefinition):
89 return other
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]
93 return None
94 else:
95 return other
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)
103 file.write("\n")
104 pfbname = pykpathsea.find_file("%s.pfb" % self.name, pykpathsea.kpse_type1_format)
105 if pfbname is None:
106 # XXX Exception
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")}
124 # Exceptions
127 class CanvasException(Exception): pass
130 class linecap(base.PathStyle):
132 """linecap of paths"""
134 def __init__(self, value=0):
135 self.value=value
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):
150 self.value=value
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):
165 self.value=value
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):
179 """dash of paths"""
181 def __init__(self, pattern=[], offset=0):
182 self.pattern=pattern
183 self.offset=offset
185 def write(self, file):
186 patternstring=""
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([])):
198 self.c=c
199 self.d=d
201 def write(self, file):
202 self.c.write(file)
203 self.d.write(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))
221 _base=0.02
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)))
238 # Decorated path
241 class decoratedpath(base.PSCmd):
242 """Decorated path
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.
249 def __init__(self,
250 path, strokepath=None, fillpath=None,
251 styles=None, strokestyles=None, fillstyles=None,
252 subdps=None):
254 self.path = path
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)
275 def bbox(self):
276 return reduce(lambda x,y: x+y.bbox(),
277 self.subdps,
278 self.path.bbox())
280 def prolog(self):
281 result = []
282 for style in list(self.styles) + list(self.fillstyles) + list(self.strokestyles):
283 result.extend(style.prolog())
284 return result
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
291 # small helper
292 def _writestyles(styles, file=file):
293 for style in styles:
294 style.write(file)
296 # apply global styles
297 if self.styles:
298 _gsave().write(file)
299 _writestyles(self.styles)
301 if self.fillpath:
302 _newpath().write(file)
303 self.fillpath.write(file)
305 if self.strokepath==self.fillpath:
306 # do efficient stroking + filling
307 _gsave().write(file)
309 if self.fillstyles:
310 _writestyles(self.fillstyles)
312 _fill().write(file)
313 _grestore().write(file)
315 if self.strokestyles:
316 _gsave().write(file)
317 _writestyles(self.strokestyles)
319 _stroke().write(file)
321 if self.strokestyles:
322 _grestore().write(file)
323 else:
324 # only fill fillpath - for the moment
325 if self.fillstyles:
326 _gsave().write(file)
327 _writestyles(self.fillstyles)
329 _fill().write(file)
331 if 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:
339 _gsave().write(file)
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:
354 subdp.write(file)
356 # restore global styles
357 if self.styles:
358 _grestore().write(file)
361 # Path decorators
364 class PathDeco:
366 """Path decorators
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.
381 pass
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
399 return dp
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
413 return dp
415 def _arrowheadtemplatelength(anormpath, size):
416 "calculate length of arrowhead template (in parametrisation of anormpath)"
417 # get tip (tx, ty)
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))
423 if ipar[0]:
424 alen = ipar[0][0]
425 else:
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())
429 try:
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()
436 return alen
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
455 # of the arrow
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
460 if constriction:
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())
470 else:
471 # arrow without constriction
472 arrow = arrowl.reversed() << arrowr
473 arrow.append(path.closepath())
475 return arrow
477 class arrow(PathDeco):
479 """A general arrow"""
481 def __init__(self,
482 position, size, angle=45, constriction=0.8,
483 styles=None, strokestyles=None, fillstyles=None):
484 self.position = position
485 self.size = size
486 self.angle = angle
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,
504 size=self.size,
505 angle=self.angle,
506 constriction=self.constriction,
507 styles=styles,
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
518 else:
519 anormpath=path.normpath(dp.path)
521 if self.position:
522 anormpath=anormpath.reversed()
524 ahead = _arrowhead(anormpath, self.size, self.angle, self.constriction)
526 dp.addsubdp(decoratedpath(ahead,
527 strokepath=ahead, fillpath=ahead,
528 styles=self.styles,
529 strokestyles=self.strokestyles,
530 fillstyles=self.fillstyles))
532 alen = _arrowheadtemplatelength(anormpath, self.size)
534 if self.constriction:
535 ilen = alen*self.constriction
536 else:
537 ilen = alen
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
546 if self.position:
547 anormpath=anormpath.reversed()
549 # set the new (shortened) strokepath
550 dp.strokepath=anormpath
552 return dp
555 class barrow(arrow):
557 """arrow at begin of path"""
559 def __init__(self, size, angle=45, constriction=0.8,
560 styles=None, strokestyles=None, fillstyles=None):
561 arrow.__init__(self,
562 position=0,
563 size=size,
564 angle=angle,
565 constriction=constriction,
566 styles=styles,
567 strokestyles=strokestyles,
568 fillstyles=fillstyles)
570 _base = unit.v_pt(4)
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))
587 class earrow(arrow):
589 """arrow at end of path"""
591 def __init__(self, size, angle=45, constriction=0.8,
592 styles=[], strokestyles=[], fillstyles=[]):
593 arrow.__init__(self,
594 position=1,
595 size=size,
596 angle=angle,
597 constriction=constriction,
598 styles=styles,
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))
618 # clipping class
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"""
627 self.path = path
629 def bbox(self):
630 # as a PSCmd a clipping path has NO influence on the bbox...
631 return bbox._bbox()
633 def clipbbox(self):
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)
640 _clip().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):
658 file.write("fill\n")
661 class _clip(base.PSOp):
662 def write(self, file):
663 file.write("clip\n")
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()
698 self.PSOps = []
699 self.trafo = trafo.trafo()
700 self.clipbbox = bbox._bbox()
701 self.texrunner = text.defaulttexrunner
703 for arg in args:
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)
711 else:
712 self.set(arg)
714 def bbox(self):
715 """returns bounding box of canvas"""
716 obbox = reduce(lambda x,y:
717 isinstance(y, base.PSCmd) and x+y.bbox() or x,
718 self.PSOps,
719 bbox._bbox())
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
726 def prolog(self):
727 result = []
728 for cmd in self.PSOps:
729 result.extend(cmd.prolog())
730 return result
732 def write(self, file):
733 _gsave().write(file)
734 for cmd in self.PSOps:
735 cmd.write(file)
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.
743 returns the PSOp
747 if args:
748 sc = _canvas(*args)
749 sc.insert(PSOp)
750 self.PSOps.append(sc)
751 else:
752 self.PSOps.append(PSOp)
754 return PSOp
756 def set(self, *styles):
757 """sets styles args globally for the rest of the canvas
759 returns canvas
763 for style in styles:
764 if not isinstance(style, base.PathStyle):
765 raise NotImplementedError, "can only set PathStyle"
767 self.insert(style)
769 return self
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.
778 returns the canvas
782 self.attrcheck(args, allowmulti=(base.PathStyle, PathDeco))
784 dp = decoratedpath(path)
786 # set global styles
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)
793 self.insert(dp)
795 return self
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.
804 returns the canvas
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.
817 returns the canvas
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
863 self.xstep = xstep
864 self.ystep = ystep
865 self.patternbbox = bbox
866 self.patterntrafo = trafo
868 def bbox(self):
869 return bbox._bbox()
871 def write(self, file):
872 file.write("%s setpattern\n" % self.id)
874 def prolog(self):
875 realpatternbbox = _canvas.bbox(self)
876 if self.xstep is None:
877 xstep = unit.topt(realpatternbbox.width())
878 else:
879 xstep = unit.topt(unit.length(self.xstep))
880 if self.ystep is None:
881 ystep = unit.topt(realpatternbbox.height())
882 else:
883 ystep = unit.topt(unit.length(self.ystep))
884 if not xstep:
885 raise ValueError("xstep in pattern cannot be zero")
886 if not ystep:
887 raise ValueError("ystep in pattern cannot be zero")
888 patternbbox = self.patternbbox or realpatternbbox.enlarged("5 pt")
890 patternprefix = string.join(("<<",
891 "/PatternType 1",
892 "/PaintType %d" % self.painttype,
893 "/TilingType %d" % self.tilingtype,
894 "/BBox[%s]" % str(patternbbox),
895 "/XStep %g" % xstep,
896 "/YStep %g" % ystep,
897 "/PaintProc {\nbegin\n"),
898 sep="\n")
899 stringfile = StringIO.StringIO()
900 _canvas.write(self, stringfile)
901 patternproc = stringfile.getvalue()
902 stringfile.close()
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), "")))
908 return pr
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()
933 self.PSOps = []
934 self.trafo = trafo.trafo()
935 self.clipbbox = bbox._bbox()
936 self.texrunner = text.defaulttexrunner
938 for arg in args:
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)
946 else:
947 self.set(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
954 the page.
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"
970 try:
971 file = open(filename, "w")
972 except IOError:
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
979 if rotated:
980 ctrafo = trafo._rotate(90,
981 0.5*(abbox.llx+abbox.urx),
982 0.5*(abbox.lly+abbox.ury))
984 if paperformat:
985 # center (optionally rotated) output on page
986 try:
987 width, height = _paperformats[paperformat]
988 width = unit.topt(width)
989 height = unit.topt(height)
990 except KeyError:
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))-
996 abbox.llx,
997 0.5*(height-(abbox.ury-abbox.lly))-
998 abbox.lly)
1000 if fittosize:
1001 # scale output to pagesize - margins
1002 margin=unit.topt(margin)
1004 if rotated:
1005 sfactor = min((height-2*margin)/(abbox.urx-abbox.llx),
1006 (width-2*margin)/(abbox.ury-abbox.lly))
1007 else:
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)
1014 elif fittosize:
1015 assert 0, "must specify paper size for fittosize" # TODO: exception...
1017 # if there has been a global transformation, adjust the bounding box
1018 # accordingly
1019 if ctrafo: abbox = abbox.transformed(ctrafo)
1021 file.write("%!PS-Adobe-3.0 EPSF 3.0\n")
1022 abbox.write(file)
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")
1031 mergedprolog = []
1033 for pritem in self.prolog():
1034 for mpritem in mergedprolog:
1035 if mpritem.merge(pritem) is None: break
1036 else:
1037 mergedprolog.append(pritem)
1039 for pritem in mergedprolog:
1040 pritem.write(file)
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
1050 self.write(file)
1052 file.write("showpage\n")
1053 file.write("%%Trailer\n")
1054 file.write("%%EOF\n")