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 # - Should we really set linewidth in canvas.writetofile. Why don't we
27 # rely on the PS default (like for all other PathStyles)
28 # - How should we handle the passing of stroke and fill styles to
29 # arrows? Calls, new instances, ...?
32 """The canvas module provides a PostScript canvas class and related classes
34 A PostScript canvas is the pivotal object for the creation of (E)PS-Files.
35 It collects all the elements that should be displayed (PSCmds) together
36 with attributes if applicable. Furthermore, a canvas can be globally
37 transformed (i.e. translated, rotated, etc.) and clipped.
41 import types
, math
, time
44 import bbox
, unit
, trafo
47 # PostScript-procedure definitions
48 # cf. file: 5002.EPSF_Spec_v3.0.pdf
49 # with important correction in EndEPSF:
50 # end operator is missing in the spec!
52 _PSProlog
= """/BeginEPSF {
53 /b4_Inc_state save def
54 /dict_count countdictstack def
55 /op_count count 1 sub def
58 0 setgray 0 setlinecap
59 1 setlinewidth 0 setlinejoin
60 10 setmiterlimit [ ] 0 setdash newpath
64 {false setstrokeadjust false setoverprint
70 count op_count sub {pop} repeat
71 countdictstack dict_count sub {end} repeat
75 # known paperformats as tuple(width, height)
77 _paperformats
= { "a4" : ("210 t mm", "297 t mm"),
78 "a3" : ("297 t mm", "420 t mm"),
79 "a2" : ("420 t mm", "594 t mm"),
80 "a1" : ("594 t mm", "840 t mm"),
81 "a0" : ("840 t mm", "1188 t mm"),
82 "a0b" : ("910 t mm", "1370 t mm"),
83 "letter" : ("8.5 t in", "11 t in"),
84 "legal" : ("8.5 t in", "14 t in")}
91 class CanvasException(Exception): pass
94 class linecap(base
.PathStyle
):
96 """linecap of paths"""
98 def __init__(self
, value
=0):
101 def write(self
, file):
102 file.write("%d setlinecap\n" % self
.value
)
104 linecap
.butt
= linecap(0)
105 linecap
.round = linecap(1)
106 linecap
.square
= linecap(2)
109 class linejoin(base
.PathStyle
):
111 """linejoin of paths"""
113 def __init__(self
, value
=0):
116 def write(self
, file):
117 file.write("%d setlinejoin\n" % self
.value
)
119 linejoin
.miter
= linejoin(0)
120 linejoin
.round = linejoin(1)
121 linejoin
.bevel
= linejoin(2)
124 class miterlimit(base
.PathStyle
):
126 """miterlimit of paths"""
128 def __init__(self
, value
=10.0):
131 def write(self
, file):
132 file.write("%f setmiterlimit\n" % self
.value
)
135 miterlimit
.lessthan180deg
= miterlimit(1/math
.sin(math
.pi
*180/360))
136 miterlimit
.lessthan90deg
= miterlimit(1/math
.sin(math
.pi
*90/360))
137 miterlimit
.lessthan60deg
= miterlimit(1/math
.sin(math
.pi
*60/360))
138 miterlimit
.lessthan45deg
= miterlimit(1/math
.sin(math
.pi
*45/360))
139 miterlimit
.lessthan11deg
= miterlimit(10) # the default, approximately 11.4783 degress
141 class dash(base
.PathStyle
):
145 def __init__(self
, pattern
=[], offset
=0):
149 def write(self
, file):
151 for element
in self
.pattern
:
152 patternstring
=patternstring
+ `element`
+ " "
154 file.write("[%s] %d setdash\n" % (patternstring
, self
.offset
))
157 class linestyle(base
.PathStyle
):
159 """linestyle (linecap together with dash) of paths"""
161 def __init__(self
, c
=linecap
.butt
, d
=dash([])):
165 def write(self
, file):
169 linestyle
.solid
= linestyle(linecap
.butt
, dash([]))
170 linestyle
.dashed
= linestyle(linecap
.butt
, dash([2]))
171 linestyle
.dotted
= linestyle(linecap
.round, dash([0, 3]))
172 linestyle
.dashdotted
= linestyle(linecap
.round, dash([0, 3, 3, 3]))
175 class linewidth(base
.PathStyle
, unit
.length
):
177 """linewidth of paths"""
179 def __init__(self
, l
="0 cm"):
180 unit
.length
.__init
__(self
, l
=l
, default_type
="w")
182 def write(self
, file):
183 file.write("%f setlinewidth\n" % unit
.topt(self
))
187 linewidth
.THIN
= linewidth("%f cm" % (_base
/math
.sqrt(32)))
188 linewidth
.THIn
= linewidth("%f cm" % (_base
/math
.sqrt(16)))
189 linewidth
.THin
= linewidth("%f cm" % (_base
/math
.sqrt(8)))
190 linewidth
.Thin
= linewidth("%f cm" % (_base
/math
.sqrt(4)))
191 linewidth
.thin
= linewidth("%f cm" % (_base
/math
.sqrt(2)))
192 linewidth
.normal
= linewidth("%f cm" % _base
)
193 linewidth
.thick
= linewidth("%f cm" % (_base
*math
.sqrt(2)))
194 linewidth
.Thick
= linewidth("%f cm" % (_base
*math
.sqrt(4)))
195 linewidth
.THick
= linewidth("%f cm" % (_base
*math
.sqrt(8)))
196 linewidth
.THIck
= linewidth("%f cm" % (_base
*math
.sqrt(16)))
197 linewidth
.THICk
= linewidth("%f cm" % (_base
*math
.sqrt(32)))
198 linewidth
.THICK
= linewidth("%f cm" % (_base
*math
.sqrt(64)))
205 class DecoratedPath(base
.PSCmd
):
208 The main purpose of this class is during the drawing
209 (stroking/filling) of a path. It collects attributes for the
210 stroke and/or fill operations.
214 path
, strokepath
=None, fillpath
=None,
215 styles
=None, strokestyles
=None, fillstyles
=None,
220 # path to be stroked or filled (or None)
221 self
.strokepath
= strokepath
222 self
.fillpath
= fillpath
224 # global style for stroking and filling and subdps
225 self
.styles
= styles
or []
227 # styles which apply only for stroking and filling
228 self
.strokestyles
= strokestyles
or []
229 self
.fillstyles
= fillstyles
or []
231 # additional elements of the path, e.g., arrowheads,
232 # which are by themselves DecoratedPaths
233 self
.subdps
= subdps
or []
235 def addsubdp(self
, subdp
):
236 """add a further decorated path to the list of subdps"""
238 self
.subdps
.append(subdp
)
241 return reduce(lambda x
,y
: x
+y
.bbox(),
246 def write(self
, file):
247 # draw (stroke and/or fill) the DecoratedPath on the canvas
248 # while trying to produce an efficient output, e.g., by
249 # not writing one path two times
252 def _writestyles(styles
, file=file):
257 # apply global styles
260 _writestyles(self
.styles
)
263 _newpath().write(file)
264 self
.fillpath
.write(file)
266 if self
.strokepath
==self
.fillpath
:
267 # do efficient stroking + filling
271 _writestyles(self
.fillstyles
)
274 _grestore().write(file)
276 if self
.strokestyles
:
278 _writestyles(self
.strokestyles
)
280 _stroke().write(file)
282 if self
.strokestyles
:
283 _grestore().write(file)
285 # only fill fillpath - for the moment
288 _writestyles(self
.fillstyles
)
293 _grestore().write(file)
295 if self
.strokepath
and self
.strokepath
!=self
.fillpath
:
296 # this is the only relevant case still left
297 # Note that a possible stroking has already been done.
299 if self
.strokestyles
:
301 _writestyles(self
.strokestyles
)
303 _newpath().write(file)
304 self
.strokepath
.write(file)
305 _stroke().write(file)
307 if self
.strokestyles
:
308 _grestore().write(file)
310 # now, draw additional subdps
311 for subdp
in self
.subdps
:
314 # restore global styles
316 _grestore().write(file)
326 In contrast to path styles, path decorators depend on the concrete
327 path to which they are applied. In particular, they don't make
328 sense without any path and can thus not be used in canvas.set!
332 def decorate(self
, dp
):
333 """apply a style to a given DecoratedPath object dp
335 decorate accepts a DecoratedPath object dp, applies PathStyle
336 by modifying dp in place and returning the new dp.
343 # stroked and filled: basic PathDecos which stroked and fill,
344 # respectively the path
347 class stroked(PathDeco
):
349 """stroked is a PathDecorator, which draws the outline of the path"""
351 def __init__(self
, *styles
):
352 self
.styles
=list(styles
)
354 def decorate(self
, dp
):
355 dp
.strokepath
=dp
.path
356 dp
.strokestyles
=self
.styles
361 class filled(PathDeco
):
363 """filled is a PathDecorator, which fills the interior of the path"""
365 def __init__(self
, *styles
):
366 self
.styles
=list(styles
)
368 def decorate(self
, dp
):
370 dp
.fillstyles
=self
.styles
376 # _arrowhead: helper routine
379 def _arrowhead(anormpath
, size
, angle
, constriction
):
381 """helper routine, which returns an arrowhead for a normpath
383 returns arrowhead at pos (0: begin, !=0: end) of anormpath with size,
384 opening angle and relative constriction
387 # first order conversion from pts to the bezier curve's
390 tlen
= unit
.topt(anormpath
.tangent(0).arclength())
392 alen
= unit
.topt(size
)/tlen
394 if alen
>anormpath
.range(): alen
=anormpath().range()
397 tx
, ty
= anormpath
.begin()
399 # now we construct the template for our arrow but cutting
400 # the path a the corresponding length
401 arrowtemplate
= anormpath
.split(alen
)[0]
403 # from this template, we construct the two outer curves
405 arrowl
= arrowtemplate
.transformed(trafo
.rotation(-angle
/2.0, tx
, ty
))
406 arrowr
= arrowtemplate
.transformed(trafo
.rotation( angle
/2.0, tx
, ty
))
408 # now come the joining backward parts
410 # arrow with constriction
412 # constriction point (cx, cy) lies on path
413 cx
, cy
= anormpath
.at(constriction
*alen
)
415 arrowcr
= path
.line(*(arrowr
.end()+(cx
,cy
)))
417 arrow
= arrowl
.reversed() << arrowr
<< arrowcr
418 arrow
.append(path
.closepath())
420 # arrow without constriction
421 arrow
= arrowl
.reversed() << arrowr
422 arrow
.append(path
.closepath())
426 class arrow(PathDeco
):
428 """A general arrow"""
431 position
, size
, angle
=45, constriction
=0.8,
432 styles
=None, strokestyles
=None, fillstyles
=None):
433 self
.position
= position
436 self
.constriction
= constriction
437 self
.styles
= styles
or []
438 self
.strokestyles
= strokestyles
or []
439 self
.fillstyles
= fillstyles
or []
441 def __call__(self
, *styles
):
442 fillstyles
= reduce(lambda x
, y
:list(x
)+(y
.styles
or []),
443 filter(lambda x
: isinstance(x
, filled
),
447 strokestyles
= reduce(lambda x
, y
:list(x
)+(y
.styles
or []),
448 filter(lambda x
: isinstance(x
, stroked
),
452 styles
= filter(lambda x
:
453 not (isinstance(x
,filled
) or
454 isinstance(x
,stroked
)),
458 return arrow(position
=self
.position
,
461 constriction
=self
.constriction
,
463 strokestyles
=strokestyles
,
464 fillstyles
=fillstyles
)
466 def decorate(self
, dp
):
468 # TODO: error, when strokepath is not defined
470 # convert to normpath if necessary
471 if isinstance(dp
.strokepath
, path
.normpath
):
472 anormpath
=dp
.strokepath
474 anormpath
=path
.normpath(dp
.path
)
477 anormpath
=anormpath
.reversed()
479 ahead
= _arrowhead(anormpath
, self
.size
, self
.angle
, self
.constriction
)
481 dp
.addsubdp(DecoratedPath(ahead
,
482 strokepath
=ahead
, fillpath
=ahead
,
484 strokestyles
=self
.strokestyles
,
485 fillstyles
=self
.fillstyles
))
487 # the following lines are copied from arrowhead.init()
488 # TODO: can this be done better?
490 # first order conversion from pts to the bezier curve's
493 tlen
= unit
.topt(anormpath
.tangent(0).arclength())
495 alen
= unit
.topt(self
.size
)/tlen
496 if alen
>anormpath
.range(): alen
=anormpath().range()
498 if self
.constriction
:
499 ilen
= alen
*self
.constriction
503 # correct somewhat for rotation of arrow segments
504 ilen
= ilen
*math
.cos(math
.pi
*self
.angle
/360.0)
506 # this is the rest of the path, we have to draw
507 anormpath
= anormpath
.split(ilen
)[1]
509 # go back to original orientation, if necessary
511 anormpath
=anormpath
.reversed()
513 # set the new (shortened) strokepath
514 dp
.strokepath
=anormpath
521 """arrow at begin of path"""
523 def __init__(self
, size
, angle
=45, constriction
=0.8,
524 styles
=None, strokestyles
=None, fillstyles
=None):
529 constriction
=constriction
,
531 strokestyles
=strokestyles
,
532 fillstyles
=fillstyles
)
536 barrow
.SMALL
= barrow("%f v pt" % (_base
/math
.sqrt(64)))
537 barrow
.SMALl
= barrow("%f v pt" % (_base
/math
.sqrt(32)))
538 barrow
.SMAll
= barrow("%f v pt" % (_base
/math
.sqrt(16)))
539 barrow
.SMall
= barrow("%f v pt" % (_base
/math
.sqrt(8)))
540 barrow
.Small
= barrow("%f v pt" % (_base
/math
.sqrt(4)))
541 barrow
.small
= barrow("%f v pt" % (_base
/math
.sqrt(2)))
542 barrow
.normal
= barrow("%f v pt" % _base
)
543 barrow
.large
= barrow("%f v pt" % (_base
*math
.sqrt(2)))
544 barrow
.Large
= barrow("%f v pt" % (_base
*math
.sqrt(4)))
545 barrow
.LArge
= barrow("%f v pt" % (_base
*math
.sqrt(8)))
546 barrow
.LARge
= barrow("%f v pt" % (_base
*math
.sqrt(16)))
547 barrow
.LARGe
= barrow("%f v pt" % (_base
*math
.sqrt(32)))
548 barrow
.LARGE
= barrow("%f v pt" % (_base
*math
.sqrt(64)))
553 """arrow at end of path"""
555 def __init__(self
, size
, angle
=45, constriction
=0.8,
556 styles
=[], strokestyles
=[], fillstyles
=[]):
561 constriction
=constriction
,
563 strokestyles
=strokestyles
,
564 fillstyles
=fillstyles
)
567 earrow
.SMALL
= earrow("%f v pt" % (_base
/math
.sqrt(64)))
568 earrow
.SMALl
= earrow("%f v pt" % (_base
/math
.sqrt(32)))
569 earrow
.SMAll
= earrow("%f v pt" % (_base
/math
.sqrt(16)))
570 earrow
.SMall
= earrow("%f v pt" % (_base
/math
.sqrt(8)))
571 earrow
.Small
= earrow("%f v pt" % (_base
/math
.sqrt(4)))
572 earrow
.small
= earrow("%f v pt" % (_base
/math
.sqrt(2)))
573 earrow
.normal
= earrow("%f v pt" % _base
)
574 earrow
.large
= earrow("%f v pt" % (_base
*math
.sqrt(2)))
575 earrow
.Large
= earrow("%f v pt" % (_base
*math
.sqrt(4)))
576 earrow
.LArge
= earrow("%f v pt" % (_base
*math
.sqrt(8)))
577 earrow
.LARge
= earrow("%f v pt" % (_base
*math
.sqrt(16)))
578 earrow
.LARGe
= earrow("%f v pt" % (_base
*math
.sqrt(32)))
579 earrow
.LARGE
= earrow("%f v pt" % (_base
*math
.sqrt(64)))
585 class clip(base
.PSCmd
):
587 """class for use in canvas constructor which clips to a path"""
589 def __init__(self
, path
):
590 """construct a clip instance for a given path"""
594 # as a PSCmd a clipping path has NO influence on the bbox...
598 # ... but for clipping, we nevertheless need the bbox
599 return self
.path
.bbox()
601 def write(self
, file):
602 _newpath().write(file)
603 self
.path
.write(file)
607 # some very primitive Postscript operators
610 class _newpath(base
.PSOp
):
611 def write(self
, file):
612 file.write("newpath\n")
615 class _stroke(base
.PSOp
):
616 def write(self
, file):
617 file.write("stroke\n")
620 class _fill(base
.PSOp
):
621 def write(self
, file):
625 class _clip(base
.PSOp
):
626 def write(self
, file):
630 class _gsave(base
.PSOp
):
631 def write(self
, file):
632 file.write("gsave\n")
635 class _grestore(base
.PSOp
):
636 def write(self
, file):
637 file.write("grestore\n")
640 # The main canvas class
643 class canvas(base
.PSCmd
):
645 """a canvas is a collection of PSCmds together with PSAttrs"""
647 def __init__(self
, *args
):
649 """construct a canvas
651 The canvas can be modfied by supplying args, which have
652 to be instances of one of the following classes:
653 - trafo.trafo (leading to a global transformation of the canvas)
654 - canvas.clip (clips the canvas)
655 - base.PathStyle (sets some global attributes of the canvas)
657 Note that, while the first two properties are fixed for the
658 whole canvas, the last one can be changed via canvas.set()
663 self
.trafo
= trafo
.trafo()
664 self
.clipbbox
= bbox
.bbox()
667 if isinstance(arg
, trafo
._trafo
):
668 self
.trafo
= self
.trafo
*arg
669 self
.PSOps
.append(arg
)
670 elif isinstance(arg
, clip
):
671 self
.clipbbox
=(self
.clipbbox
*
672 arg
.clipbbox().transform(self
.trafo
))
673 self
.PSOps
.append(arg
)
678 """returns bounding box of canvas"""
679 obbox
= reduce(lambda x
,y
:
680 isinstance(y
, base
.PSCmd
) and x
+y
.bbox() or x
,
684 # transform according to our global transformation and
685 # intersect with clipping bounding box (which have already been
686 # transformed in canvas.__init__())
687 return obbox
.transform(self
.trafo
)*self
.clipbbox
689 def write(self
, file):
690 for cmd
in self
.PSOps
:
693 def writetofile(self
, filename
, paperformat
=None, rotated
=0, fittosize
=0, margin
="1 t cm", bboxenhance
="1 t pt"):
694 """write canvas to EPS file
696 If paperformat is set to a known paperformat, the output will be centered on
699 If rotated is set, the output will first be rotated by 90 degrees.
701 If fittosize is set, then the output is scaled to the size of the
702 page (minus margin). In that case, the paperformat the specification
703 of the paperformat is obligatory.
709 if filename
[-4:]!=".eps":
710 filename
= filename
+ ".eps"
713 file = open(filename
, "w")
715 assert 0, "cannot open output file" # TODO: Fehlerbehandlung...
717 abbox
=self
.bbox().enhance(bboxenhance
)
718 ctrafo
=None # global transformation of canvas
721 ctrafo
= trafo
._rotation
(90,
722 0.5*(abbox
.llx
+abbox
.urx
),
723 0.5*(abbox
.lly
+abbox
.ury
))
726 # center (optionally rotated) output on page
728 width
, height
= _paperformats
[paperformat
]
729 width
= unit
.topt(width
)
730 height
= unit
.topt(height
)
732 raise KeyError, "unknown paperformat '%s'" % paperformat
734 if not ctrafo
: ctrafo
=trafo
.trafo()
736 ctrafo
= ctrafo
._translate
(0.5*(width
-(abbox
.urx
-abbox
.llx
))-
738 0.5*(height
-(abbox
.ury
-abbox
.lly
))-
742 # scale output to pagesize - margins
743 margin
=unit
.topt(margin
)
746 sfactor
= min((height
-2*margin
)/(abbox
.urx
-abbox
.llx
),
747 (width
-2*margin
)/(abbox
.ury
-abbox
.lly
))
749 sfactor
= min((width
-2*margin
)/(abbox
.urx
-abbox
.llx
),
750 (height
-2*margin
)/(abbox
.ury
-abbox
.lly
))
752 ctrafo
= ctrafo
._scale
(sfactor
, sfactor
, 0.5*width
, 0.5*height
)
756 assert 0, "must specify paper size for fittosize" # TODO: exception...
758 # if there has been a global transformation, adjust the bounding box
760 if ctrafo
: abbox
= abbox
.transform(ctrafo
)
762 file.write("%!PS-Adobe-3.0 EPSF 3.0\n")
764 file.write("%%%%Creator: PyX %s\n" % pyx
.__version
__)
765 file.write("%%%%Title: %s\n" % filename
)
766 file.write("%%%%CreationDate: %s\n" %
767 time
.asctime(time
.localtime(time
.time())))
768 file.write("%%EndComments\n")
770 file.write("%%BeginProlog\n")
771 file.write(_PSProlog
)
772 file.write("\n%%EndProlog\n")
774 # again, if there has occured global transformation, apply it now
775 if ctrafo
: ctrafo
.write(file)
777 file.write("%f setlinewidth\n" % unit
.topt(linewidth
.normal
))
779 # here comes the actual content
782 file.write("showpage\n")
783 file.write("%%Trailer\n")
784 file.write("%%EOF\n")
788 def insert(self
, *PSOps
):
789 """insert one or more PSOps in the canvas
791 All canvases will be encapsulated in a gsave/grestore pair.
793 returns the (last) PSOp
798 if isinstance(PSOp
, canvas
):
799 self
.PSOps
.append(_gsave())
801 self
.PSOps
.append(PSOp
)
803 if isinstance(PSOp
, canvas
):
804 self
.PSOps
.append(_grestore())
806 # save last command for return value
811 def set(self
, *styles
):
812 """sets styles args globally for the rest of the canvas
819 if not isinstance(style
, base
.PathStyle
):
820 raise NotImplementedError, "can only set PathStyle"
826 def draw(self
, path
, *args
):
827 """draw path on canvas using the style given by args
829 The argument list args consists of PathStyles, which modify
830 the appearance of the path, or PathDecos,
831 which add some new visual elements to the path.
837 dp
= DecoratedPath(path
)
840 if [x
for x
in args
if not (isinstance(x
, base
.PathStyle
) or
841 isinstance(x
, PathDeco
))]:
842 raise ValueError("Only instances of base.PathStyle or canvas.PathDeco are allowed")
845 dp
.styles
= filter(lambda x
: isinstance(x
, base
.PathStyle
), args
)
847 # add path decorations and modify path accordingly
848 for deco
in filter(lambda x
: isinstance(x
, PathDeco
), args
):
849 dp
= deco
.decorate(dp
)
855 def stroke(self
, path
, *args
):
856 """stroke path on canvas using the style given by args
858 The argument list args consists of PathStyles, which modify
859 the appearance of the path, or PathDecos,
860 which add some new visual elements to the path.
866 return self
.draw(path
, stroked(), *args
)
868 def fill(self
, path
, *args
):
869 """fill path on canvas using the style given by args
871 The argument list args consists of PathStyles, which modify
872 the appearance of the path, or PathDecos,
873 which add some new visual elements to the path.
879 return self
.draw(path
, filled(), *args
)