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()
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 if self
.clipbbox
is None:
147 self
.clipbbox
= arg
.clipbbox().transformed(self
.trafo
)
149 self
.clippbox
*= arg
.clipbbox().transformed(self
.trafo
)
150 self
.PSOps
.append(arg
)
155 """returns bounding box of canvas"""
157 for cmd
in self
.PSOps
:
158 if isinstance(cmd
, base
.PSCmd
):
162 elif abbox
is not None:
165 # transform according to our global transformation and
166 # intersect with clipping bounding box (which have already been
167 # transformed in canvas.__init__())
168 if obbox
is not None and self
.clipbbox
is not None:
169 return obbox
.transformed(self
.trafo
)*self
.clipbbox
170 elif obbox
is not None:
171 return obbox
.transformed(self
.trafo
)
177 for cmd
in self
.PSOps
:
178 result
.extend(cmd
.prolog())
181 def write(self
, file):
184 for cmd
in self
.PSOps
:
186 _grestore().write(file)
188 def insert(self
, PSOp
, args
=[]):
189 """insert PSOp in the canvas.
191 If args are given, then insert a canvas containing PSOp applying args.
202 self
.PSOps
.append(sc
)
204 self
.PSOps
.append(PSOp
)
208 def set(self
, attrs
):
209 """sets styles args globally for the rest of the canvas
215 attr
.checkattrs(attrs
, [style
.strokestyle
, style
.fillstyle
])
220 def draw(self
, path
, attrs
):
221 """draw path on canvas using the style given by args
223 The argument attrs consists of PathStyles, which modify
224 the appearance of the path, PathDecos, which add some new
225 visual elements to the path, or trafos, which are applied
226 before drawing the path.
232 attrs
= attr
.mergeattrs(attrs
)
233 attr
.checkattrs(attrs
, [deco
.deco
, style
.fillstyle
, style
.strokestyle
, trafo
.trafo_pt
])
235 for t
in attr
.getattrs(attrs
, [trafo
.trafo_pt
]):
236 path
= path
.transformed(t
)
238 dp
= deco
.decoratedpath(path
)
241 dp
.styles
= attr
.getattrs(attrs
, [style
.fillstyle
, style
.strokestyle
])
243 # add path decorations and modify path accordingly
244 for adeco
in attr
.getattrs(attrs
, [deco
.deco
]):
245 dp
= adeco
.decorate(dp
)
251 def stroke(self
, path
, attrs
=[]):
252 """stroke path on canvas using the style given by args
254 The argument attrs consists of PathStyles, which modify
255 the appearance of the path, PathDecos, which add some new
256 visual elements to the path, or trafos, which are applied
257 before drawing the path.
263 return self
.draw(path
, [deco
.stroked
]+list(attrs
))
265 def fill(self
, path
, attrs
=[]):
266 """fill path on canvas using the style given by args
268 The argument attrs consists of PathStyles, which modify
269 the appearance of the path, PathDecos, which add some new
270 visual elements to the path, or trafos, which are applied
271 before drawing the path.
277 return self
.draw(path
, [deco
.filled
]+list(attrs
))
279 def settexrunner(self
, texrunner
):
280 """sets the texrunner to be used to within the text and text_pt methods"""
282 self
.texrunner
= texrunner
284 def text(self
, x
, y
, atext
, *args
, **kwargs
):
285 """insert a text into the canvas
287 inserts a textbox created by self.texrunner.text into the canvas
289 returns the inserted textbox"""
291 return self
.insert(self
.texrunner
.text(x
, y
, atext
, *args
, **kwargs
))
294 def text_pt(self
, x
, y
, atext
, *args
):
295 """insert a text into the canvas
297 inserts a textbox created by self.texrunner.text_pt into the canvas
299 returns the inserted textbox"""
301 return self
.insert(self
.texrunner
.text_pt(x
, y
, atext
, *args
))
304 # canvas for patterns
307 class pattern(_canvas
, attr
.exclusiveattr
, style
.fillstyle
):
309 def __init__(self
, painttype
=1, tilingtype
=1, xstep
=None, ystep
=None, bbox
=None, trafo
=None):
310 attr
.exclusiveattr
.__init
__(self
, pattern
)
311 _canvas
.__init
__(self
)
312 attr
.exclusiveattr
.__init
__(self
, pattern
)
313 self
.id = "pattern%d" % id(self
)
314 # XXX: some checks are in order
315 if painttype
not in (1,2):
316 raise ValueError("painttype must be 1 or 2")
317 self
.painttype
= painttype
318 if tilingtype
not in (1,2,3):
319 raise ValueError("tilingtype must be 1, 2 or 3")
320 self
.tilingtype
= tilingtype
323 self
.patternbbox
= bbox
324 self
.patterntrafo
= trafo
329 def write(self
, file):
330 file.write("%s setpattern\n" % self
.id)
333 realpatternbbox
= _canvas
.bbox(self
)
334 if self
.xstep
is None:
335 xstep
= unit
.topt(realpatternbbox
.width())
337 xstep
= unit
.topt(unit
.length(self
.xstep
))
338 if self
.ystep
is None:
339 ystep
= unit
.topt(realpatternbbox
.height())
341 ystep
= unit
.topt(unit
.length(self
.ystep
))
343 raise ValueError("xstep in pattern cannot be zero")
345 raise ValueError("ystep in pattern cannot be zero")
346 patternbbox
= self
.patternbbox
or realpatternbbox
.enlarged("5 pt")
348 patternprefix
= string
.join(("<<",
350 "/PaintType %d" % self
.painttype
,
351 "/TilingType %d" % self
.tilingtype
,
352 "/BBox[%s]" % str(patternbbox
),
355 "/PaintProc {\nbegin\n"),
357 stringfile
= cStringIO
.StringIO()
358 _canvas
.write(self
, stringfile
)
359 patternproc
= stringfile
.getvalue()
361 patterntrafostring
= self
.patterntrafo
is None and "matrix" or str(self
.patterntrafo
)
362 patternsuffix
= "end\n} bind\n>>\n%s\nmakepattern" % patterntrafostring
364 pr
= _canvas
.prolog(self
)
365 pr
.append(prolog
.definition(self
.id, string
.join((patternprefix
, patternproc
, patternsuffix
), "")))
368 pattern
.clear
= attr
.clearclass(pattern
)
371 # The main canvas class
374 class canvas(_canvas
):
376 """a canvas is a collection of PSCmds together with PSAttrs"""
378 def writetofile(self
, filename
, paperformat
=None, rotated
=0, fittosize
=0, margin
="1 t cm",
379 bbox
=None, bboxenlarge
="1 t pt"):
380 """write canvas to EPS file
382 If paperformat is set to a known paperformat, the output will be centered on
385 If rotated is set, the output will first be rotated by 90 degrees.
387 If fittosize is set, then the output is scaled to the size of the
388 page (minus margin). In that case, the paperformat the specification
389 of the paperformat is obligatory.
391 The bbox parameter overrides the automatic bounding box determination.
392 bboxenlarge may be used to enlarge the bbox of the canvas (or the
393 manually specified bbox).
396 if filename
[-4:]!=".eps":
397 filename
= filename
+ ".eps"
400 file = open(filename
, "w")
402 raise IOError("cannot open output file")
404 abbox
= bbox
is not None and bbox
or self
.bbox()
405 abbox
= abbox
.enlarged(bboxenlarge
)
406 ctrafo
= None # global transformation of canvas
409 ctrafo
= trafo
.rotate_pt(90,
410 0.5*(abbox
.llx
+abbox
.urx
),
411 0.5*(abbox
.lly
+abbox
.ury
))
414 # center (optionally rotated) output on page
416 width
, height
= _paperformats
[paperformat
.upper()]
418 raise KeyError, "unknown paperformat '%s'" % paperformat
419 width
= unit
.topt(width
)
420 height
= unit
.topt(height
)
422 if not ctrafo
: ctrafo
=trafo
.trafo()
424 ctrafo
= ctrafo
.translated_pt(0.5*(width
-(abbox
.urx
-abbox
.llx
))-
426 0.5*(height
-(abbox
.ury
-abbox
.lly
))-
430 # scale output to pagesize - margins
431 margin
= unit
.topt(margin
)
432 if 2*margin
> min(width
, height
):
433 raise RuntimeError("Margins too broad for selected paperformat. Aborting.")
436 sfactor
= min((height
-2*margin
)/(abbox
.urx
-abbox
.llx
),
437 (width
-2*margin
)/(abbox
.ury
-abbox
.lly
))
439 sfactor
= min((width
-2*margin
)/(abbox
.urx
-abbox
.llx
),
440 (height
-2*margin
)/(abbox
.ury
-abbox
.lly
))
442 ctrafo
= ctrafo
.scaled_pt(sfactor
, sfactor
, 0.5*width
, 0.5*height
)
446 raise ValueError("must specify paper size for fittosize")
448 # if there has been a global transformation, adjust the bounding box
450 if ctrafo
: abbox
= abbox
.transformed(ctrafo
)
452 file.write("%!PS-Adobe-3.0 EPSF 3.0\n")
454 file.write("%%%%Creator: PyX %s\n" % version
.version
)
455 file.write("%%%%Title: %s\n" % filename
)
456 file.write("%%%%CreationDate: %s\n" %
457 time
.asctime(time
.localtime(time
.time())))
458 file.write("%%EndComments\n")
460 file.write("%%BeginProlog\n")
464 for pritem
in self
.prolog():
465 for mpritem
in mergedprolog
:
466 if mpritem
.merge(pritem
) is None: break
468 mergedprolog
.append(pritem
)
470 for pritem
in mergedprolog
:
473 file.write("%%EndProlog\n")
475 # again, if there has occured global transformation, apply it now
476 if ctrafo
: ctrafo
.write(file)
478 file.write("%f setlinewidth\n" % unit
.topt(style
.linewidth
.normal
))
480 # here comes the actual content
483 file.write("showpage\n")
484 file.write("%%Trailer\n")
485 file.write("%%EOF\n")