2 # -*- coding: ISO-8859-1 -*-
5 # Copyright (C) 2002-2004 Jörg Lehmann <joergl@users.sourceforge.net>
6 # Copyright (C) 2002-2004 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 # XXX remove string module
25 # XXX what is a pattern
29 """The canvas module provides a PostScript canvas class and related classes
31 A canvas holds a collection of all elements that should be displayed together
32 with their attributes.
35 import string
, cStringIO
, time
36 import attr
, base
, bbox
, deco
, unit
, prolog
, style
, trafo
, version
38 # known paperformats as tuple(width, height)
40 _paperformats
= { "A4" : ("210 t mm", "297 t mm"),
41 "A3" : ("297 t mm", "420 t mm"),
42 "A2" : ("420 t mm", "594 t mm"),
43 "A1" : ("594 t mm", "840 t mm"),
44 "A0" : ("840 t mm", "1188 t mm"),
45 "A0B" : ("910 t mm", "1370 t mm"),
46 "LETTER" : ("8.5 t inch", "11 t inch"),
47 "LEGAL" : ("8.5 t inch", "14 t inch")}
54 # XXX help me find my identity
56 class clip(base
.PSCmd
):
58 """class for use in canvas constructor which clips to a path"""
60 def __init__(self
, path
):
61 """construct a clip instance for a given path"""
65 # as a PSCmd a clipping path has NO influence on the bbox...
69 # ... but for clipping, we nevertheless need the bbox
70 return self
.path
.bbox()
72 def write(self
, file):
73 _newpath().write(file)
78 # some very primitive Postscript operators
81 class _newpath(base
.PSOp
):
82 def write(self
, file):
83 file.write("newpath\n")
86 class _stroke(base
.PSOp
):
87 def write(self
, file):
88 file.write("stroke\n")
91 class _fill(base
.PSOp
):
92 def write(self
, file):
96 class _clip(base
.PSOp
):
97 def write(self
, file):
101 class _gsave(base
.PSOp
):
102 def write(self
, file):
103 file.write("gsave\n")
106 class _grestore(base
.PSOp
):
107 def write(self
, file):
108 file.write("grestore\n")
114 class _canvas(base
.PSCmd
):
116 """a canvas is a collection of PSCmds together with PSAttrs"""
118 def __init__(self
, *args
):
120 """construct a canvas
122 The canvas can be modfied by supplying args, which have
123 to be instances of one of the following classes:
124 - trafo.trafo (leading to a global transformation of the canvas)
125 - canvas.clip (clips the canvas)
126 - base.PathStyle (sets some global attributes of the canvas)
128 Note that, while the first two properties are fixed for the
129 whole canvas, the last one can be changed via canvas.set()
134 self
.trafo
= trafo
.trafo()
135 self
.clipbbox
= bbox
._bbox
()
137 # prevent cyclic imports
139 self
.texrunner
= text
.defaulttexrunner
142 if isinstance(arg
, trafo
.trafo_pt
):
143 self
.trafo
= self
.trafo
*arg
144 self
.PSOps
.append(arg
)
145 elif isinstance(arg
, clip
):
146 self
.clipbbox
=(self
.clipbbox
*
147 arg
.clipbbox().transformed(self
.trafo
))
148 self
.PSOps
.append(arg
)
153 """returns bounding box of canvas"""
154 obbox
= reduce(lambda x
,y
:
155 isinstance(y
, base
.PSCmd
) and x
+y
.bbox() or x
,
159 # transform according to our global transformation and
160 # intersect with clipping bounding box (which have already been
161 # transformed in canvas.__init__())
162 return obbox
.transformed(self
.trafo
)*self
.clipbbox
166 for cmd
in self
.PSOps
:
167 result
.extend(cmd
.prolog())
170 def write(self
, file):
173 for cmd
in self
.PSOps
:
175 _grestore().write(file)
177 def insert(self
, PSOp
, args
=[]):
178 """insert PSOp in the canvas.
180 If args are given, then insert a canvas containing PSOp applying args.
191 self
.PSOps
.append(sc
)
193 self
.PSOps
.append(PSOp
)
197 def set(self
, attrs
):
198 """sets styles args globally for the rest of the canvas
204 attr
.checkattrs(attrs
, [style
.strokestyle
, style
.fillstyle
])
209 def draw(self
, path
, attrs
):
210 """draw path on canvas using the style given by args
212 The argument attrs consists of PathStyles, which modify
213 the appearance of the path, PathDecos, which add some new
214 visual elements to the path, or trafos, which are applied
215 before drawing the path.
221 attrs
= attr
.mergeattrs(attrs
)
222 attr
.checkattrs(attrs
, [deco
.deco
, style
.fillstyle
, style
.strokestyle
, trafo
.trafo_pt
])
224 for t
in attr
.getattrs(attrs
, [trafo
.trafo_pt
]):
225 path
= path
.transformed(t
)
227 dp
= deco
.decoratedpath(path
)
230 dp
.styles
= attr
.getattrs(attrs
, [style
.fillstyle
, style
.strokestyle
])
232 # add path decorations and modify path accordingly
233 for adeco
in attr
.getattrs(attrs
, [deco
.deco
]):
234 dp
= adeco
.decorate(dp
)
240 def stroke(self
, path
, attrs
=[]):
241 """stroke path on canvas using the style given by args
243 The argument attrs consists of PathStyles, which modify
244 the appearance of the path, PathDecos, which add some new
245 visual elements to the path, or trafos, which are applied
246 before drawing the path.
252 return self
.draw(path
, [deco
.stroked
]+list(attrs
))
254 def fill(self
, path
, attrs
=[]):
255 """fill path on canvas using the style given by args
257 The argument attrs consists of PathStyles, which modify
258 the appearance of the path, PathDecos, which add some new
259 visual elements to the path, or trafos, which are applied
260 before drawing the path.
266 return self
.draw(path
, [deco
.filled
]+list(attrs
))
268 def settexrunner(self
, texrunner
):
269 """sets the texrunner to be used to within the text and text_pt methods"""
271 self
.texrunner
= texrunner
273 def text(self
, x
, y
, atext
, *args
, **kwargs
):
274 """insert a text into the canvas
276 inserts a textbox created by self.texrunner.text into the canvas
278 returns the inserted textbox"""
280 return self
.insert(self
.texrunner
.text(x
, y
, atext
, *args
, **kwargs
))
283 def text_pt(self
, x
, y
, atext
, *args
):
284 """insert a text into the canvas
286 inserts a textbox created by self.texrunner.text_pt into the canvas
288 returns the inserted textbox"""
290 return self
.insert(self
.texrunner
.text_pt(x
, y
, atext
, *args
))
293 # canvas for patterns
296 class pattern(_canvas
, attr
.exclusiveattr
, style
.fillstyle
):
298 def __init__(self
, painttype
=1, tilingtype
=1, xstep
=None, ystep
=None, bbox
=None, trafo
=None):
299 attr
.exclusiveattr
.__init
__(self
, pattern
)
300 _canvas
.__init
__(self
)
301 attr
.exclusiveattr
.__init
__(self
, pattern
)
302 self
.id = "pattern%d" % id(self
)
303 # XXX: some checks are in order
304 if painttype
not in (1,2):
305 raise ValueError("painttype must be 1 or 2")
306 self
.painttype
= painttype
307 if tilingtype
not in (1,2,3):
308 raise ValueError("tilingtype must be 1, 2 or 3")
309 self
.tilingtype
= tilingtype
312 self
.patternbbox
= bbox
313 self
.patterntrafo
= trafo
318 def write(self
, file):
319 file.write("%s setpattern\n" % self
.id)
322 realpatternbbox
= _canvas
.bbox(self
)
323 if self
.xstep
is None:
324 xstep
= unit
.topt(realpatternbbox
.width())
326 xstep
= unit
.topt(unit
.length(self
.xstep
))
327 if self
.ystep
is None:
328 ystep
= unit
.topt(realpatternbbox
.height())
330 ystep
= unit
.topt(unit
.length(self
.ystep
))
332 raise ValueError("xstep in pattern cannot be zero")
334 raise ValueError("ystep in pattern cannot be zero")
335 patternbbox
= self
.patternbbox
or realpatternbbox
.enlarged("5 pt")
337 patternprefix
= string
.join(("<<",
339 "/PaintType %d" % self
.painttype
,
340 "/TilingType %d" % self
.tilingtype
,
341 "/BBox[%s]" % str(patternbbox
),
344 "/PaintProc {\nbegin\n"),
346 stringfile
= cStringIO
.StringIO()
347 _canvas
.write(self
, stringfile
)
348 patternproc
= stringfile
.getvalue()
350 patterntrafostring
= self
.patterntrafo
is None and "matrix" or str(self
.patterntrafo
)
351 patternsuffix
= "end\n} bind\n>>\n%s\nmakepattern" % patterntrafostring
353 pr
= _canvas
.prolog(self
)
354 pr
.append(prolog
.definition(self
.id, string
.join((patternprefix
, patternproc
, patternsuffix
), "")))
357 pattern
.clear
= attr
.clearclass(pattern
)
360 # The main canvas class
363 class canvas(_canvas
):
365 """a canvas is a collection of PSCmds together with PSAttrs"""
367 def writetofile(self
, filename
, paperformat
=None, rotated
=0, fittosize
=0, margin
="1 t cm",
368 bbox
=None, bboxenlarge
="1 t pt"):
369 """write canvas to EPS file
371 If paperformat is set to a known paperformat, the output will be centered on
374 If rotated is set, the output will first be rotated by 90 degrees.
376 If fittosize is set, then the output is scaled to the size of the
377 page (minus margin). In that case, the paperformat the specification
378 of the paperformat is obligatory.
380 The bbox parameter overrides the automatic bounding box determination.
381 bboxenlarge may be used to enlarge the bbox of the canvas (or the
382 manually specified bbox).
385 if filename
[-4:]!=".eps":
386 filename
= filename
+ ".eps"
389 file = open(filename
, "w")
391 raise IOError("cannot open output file")
393 abbox
= bbox
is not None and bbox
or self
.bbox()
394 abbox
= abbox
.enlarged(bboxenlarge
)
395 ctrafo
= None # global transformation of canvas
398 ctrafo
= trafo
.rotate_pt(90,
399 0.5*(abbox
.llx
+abbox
.urx
),
400 0.5*(abbox
.lly
+abbox
.ury
))
403 # center (optionally rotated) output on page
405 width
, height
= _paperformats
[paperformat
.upper()]
407 raise KeyError, "unknown paperformat '%s'" % paperformat
408 width
= unit
.topt(width
)
409 height
= unit
.topt(height
)
411 if not ctrafo
: ctrafo
=trafo
.trafo()
413 ctrafo
= ctrafo
.translated_pt(0.5*(width
-(abbox
.urx
-abbox
.llx
))-
415 0.5*(height
-(abbox
.ury
-abbox
.lly
))-
419 # scale output to pagesize - margins
420 margin
= unit
.topt(margin
)
421 if 2*margin
> min(width
, height
):
422 raise RuntimeError("Margins too broad for selected paperformat. Aborting.")
425 sfactor
= min((height
-2*margin
)/(abbox
.urx
-abbox
.llx
),
426 (width
-2*margin
)/(abbox
.ury
-abbox
.lly
))
428 sfactor
= min((width
-2*margin
)/(abbox
.urx
-abbox
.llx
),
429 (height
-2*margin
)/(abbox
.ury
-abbox
.lly
))
431 ctrafo
= ctrafo
.scaled_pt(sfactor
, sfactor
, 0.5*width
, 0.5*height
)
435 raise ValueError("must specify paper size for fittosize")
437 # if there has been a global transformation, adjust the bounding box
439 if ctrafo
: abbox
= abbox
.transformed(ctrafo
)
441 file.write("%!PS-Adobe-3.0 EPSF 3.0\n")
443 file.write("%%%%Creator: PyX %s\n" % version
.version
)
444 file.write("%%%%Title: %s\n" % filename
)
445 file.write("%%%%CreationDate: %s\n" %
446 time
.asctime(time
.localtime(time
.time())))
447 file.write("%%EndComments\n")
449 file.write("%%BeginProlog\n")
453 for pritem
in self
.prolog():
454 for mpritem
in mergedprolog
:
455 if mpritem
.merge(pritem
) is None: break
457 mergedprolog
.append(pritem
)
459 for pritem
in mergedprolog
:
462 file.write("%%EndProlog\n")
464 # again, if there has occured global transformation, apply it now
465 if ctrafo
: ctrafo
.write(file)
467 file.write("%f setlinewidth\n" % unit
.topt(style
.linewidth
.normal
))
469 # here comes the actual content
472 file.write("showpage\n")
473 file.write("%%Trailer\n")
474 file.write("%%EOF\n")