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 what are the correct base classes of clip and pattern
26 """The canvas module provides a PostScript canvas class and related classes
28 A canvas holds a collection of all elements that should be displayed together
29 with their attributes.
32 import sys
, cStringIO
, time
33 import attr
, base
, deco
, unit
, prolog
, style
, trafo
, version
35 # known paperformats as tuple (width, height)
37 _paperformats
= { "A4" : ("210 t mm", "297 t mm"),
38 "A3" : ("297 t mm", "420 t mm"),
39 "A2" : ("420 t mm", "594 t mm"),
40 "A1" : ("594 t mm", "840 t mm"),
41 "A0" : ("840 t mm", "1188 t mm"),
42 "A0B" : ("910 t mm", "1370 t mm"),
43 "LETTER" : ("8.5 t inch", "11 t inch"),
44 "LEGAL" : ("8.5 t inch", "14 t inch")}
50 class clip(base
.PSCmd
):
52 """class for use in canvas constructor which clips to a path"""
54 def __init__(self
, path
):
55 """construct a clip instance for a given path"""
59 # as a PSCmd a clipping path has NO influence on the bbox...
63 # ... but for clipping, we nevertheless need the bbox
64 return self
.path
.bbox()
66 def outputPS(self
, file):
67 file.write("newpath\n")
68 self
.path
.outputPS(file)
71 def outputPDF(self
, file):
72 self
.path
.outputPDF(file)
76 # general canvas class
79 class _canvas(base
.PSCmd
):
81 """a canvas is a collection of PSCmds together with PSAttrs"""
83 def __init__(self
, attrs
=[], texrunner
=None):
87 The canvas can be modfied by supplying args, which have
88 to be instances of one of the following classes:
89 - trafo.trafo (leading to a global transformation of the canvas)
90 - canvas.clip (clips the canvas)
91 - base.PathStyle (sets some global attributes of the canvas)
93 Note that, while the first two properties are fixed for the
94 whole canvas, the last one can be changed via canvas.set().
96 The texrunner instance used for the text method can be specified
97 using the texrunner argument. It defaults to text.defaulttexrunner
102 self
.trafo
= trafo
.trafo()
104 if texrunner
is not None:
105 self
.texrunner
= texrunner
107 # prevent cyclic imports
109 self
.texrunner
= text
.defaulttexrunner
112 if isinstance(attr
, trafo
.trafo_pt
):
113 self
.trafo
= self
.trafo
*attr
114 self
.PSOps
.append(attr
)
115 elif isinstance(attr
, clip
):
116 if self
.clipbbox
is None:
117 self
.clipbbox
= attr
.clipbbox().transformed(self
.trafo
)
119 self
.clippbox
*= attr
.clipbbox().transformed(self
.trafo
)
120 self
.PSOps
.append(attr
)
125 """returns bounding box of canvas"""
127 for cmd
in self
.PSOps
:
128 if isinstance(cmd
, base
.PSCmd
):
132 elif abbox
is not None:
135 # transform according to our global transformation and
136 # intersect with clipping bounding box (which have already been
137 # transformed in canvas.__init__())
138 if obbox
is not None and self
.clipbbox
is not None:
139 return obbox
.transformed(self
.trafo
)*self
.clipbbox
140 elif obbox
is not None:
141 return obbox
.transformed(self
.trafo
)
147 for cmd
in self
.PSOps
:
148 result
.extend(cmd
.prolog())
151 def outputPS(self
, file):
153 file.write("gsave\n")
154 for cmd
in self
.PSOps
:
156 file.write("grestore\n")
158 def outputPDF(self
, file):
160 file.write("q\n") # gsave
161 for cmd
in self
.PSOps
:
163 file.write("Q\n") # grestore
165 def insert(self
, PSOp
, attrs
=[]):
166 """insert PSOp in the canvas.
168 If args are given, then insert a canvas containing PSOp applying args.
179 self
.PSOps
.append(sc
)
181 self
.PSOps
.append(PSOp
)
185 def set(self
, attrs
):
186 """sets styles args globally for the rest of the canvas
189 attr
.checkattrs(attrs
, [style
.strokestyle
, style
.fillstyle
])
193 def draw(self
, path
, attrs
):
194 """draw path on canvas using the style given by args
196 The argument attrs consists of PathStyles, which modify
197 the appearance of the path, PathDecos, which add some new
198 visual elements to the path, or trafos, which are applied
199 before drawing the path.
203 attrs
= attr
.mergeattrs(attrs
)
204 attr
.checkattrs(attrs
, [deco
.deco
, style
.fillstyle
, style
.strokestyle
, trafo
.trafo_pt
])
206 for t
in attr
.getattrs(attrs
, [trafo
.trafo_pt
]):
207 path
= path
.transformed(t
)
209 dp
= deco
.decoratedpath(path
)
212 dp
.styles
= attr
.getattrs(attrs
, [style
.fillstyle
, style
.strokestyle
])
214 # add path decorations and modify path accordingly
215 for adeco
in attr
.getattrs(attrs
, [deco
.deco
]):
216 dp
= adeco
.decorate(dp
)
220 def stroke(self
, path
, attrs
=[]):
221 """stroke 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.
230 self
.draw(path
, [deco
.stroked
]+list(attrs
))
232 def fill(self
, path
, attrs
=[]):
233 """fill path on canvas using the style given by args
235 The argument attrs consists of PathStyles, which modify
236 the appearance of the path, PathDecos, which add some new
237 visual elements to the path, or trafos, which are applied
238 before drawing the path.
242 self
.draw(path
, [deco
.filled
]+list(attrs
))
244 def settexrunner(self
, texrunner
):
245 """sets the texrunner to be used to within the text and text_pt methods"""
247 self
.texrunner
= texrunner
249 def text(self
, x
, y
, atext
, *args
, **kwargs
):
250 """insert a text into the canvas
252 inserts a textbox created by self.texrunner.text into the canvas
254 returns the inserted textbox"""
256 return self
.insert(self
.texrunner
.text(x
, y
, atext
, *args
, **kwargs
))
259 def text_pt(self
, x
, y
, atext
, *args
):
260 """insert a text into the canvas
262 inserts a textbox created by self.texrunner.text_pt into the canvas
264 returns the inserted textbox"""
266 return self
.insert(self
.texrunner
.text_pt(x
, y
, atext
, *args
))
269 # canvas for patterns
272 class pattern(_canvas
, attr
.exclusiveattr
, style
.fillstyle
):
274 def __init__(self
, painttype
=1, tilingtype
=1, xstep
=None, ystep
=None, bbox
=None, trafo
=None):
275 attr
.exclusiveattr
.__init
__(self
, pattern
)
276 _canvas
.__init
__(self
)
277 attr
.exclusiveattr
.__init
__(self
, pattern
)
278 self
.id = "pattern%d" % id(self
)
279 # XXX: some checks are in order
280 if painttype
not in (1,2):
281 raise ValueError("painttype must be 1 or 2")
282 self
.painttype
= painttype
283 if tilingtype
not in (1,2,3):
284 raise ValueError("tilingtype must be 1, 2 or 3")
285 self
.tilingtype
= tilingtype
288 self
.patternbbox
= bbox
289 self
.patterntrafo
= trafo
294 def outputPS(self
, file):
295 file.write("%s setpattern\n" % self
.id)
298 realpatternbbox
= _canvas
.bbox(self
)
299 if self
.xstep
is None:
300 xstep
= unit
.topt(realpatternbbox
.width())
302 xstep
= unit
.topt(unit
.length(self
.xstep
))
303 if self
.ystep
is None:
304 ystep
= unit
.topt(realpatternbbox
.height())
306 ystep
= unit
.topt(unit
.length(self
.ystep
))
308 raise ValueError("xstep in pattern cannot be zero")
310 raise ValueError("ystep in pattern cannot be zero")
311 patternbbox
= self
.patternbbox
or realpatternbbox
.enlarged("5 pt")
313 patternprefix
= "\n".join(("<<",
315 "/PaintType %d" % self
.painttype
,
316 "/TilingType %d" % self
.tilingtype
,
317 "/BBox[%s]" % str(patternbbox
),
320 "/PaintProc {\nbegin\n"))
321 stringfile
= cStringIO
.StringIO()
322 _canvas
.outputPS(self
, stringfile
)
323 patternproc
= stringfile
.getvalue()
325 patterntrafostring
= self
.patterntrafo
is None and "matrix" or str(self
.patterntrafo
)
326 patternsuffix
= "end\n} bind\n>>\n%s\nmakepattern" % patterntrafostring
328 pr
= _canvas
.prolog(self
)
329 pr
.append(prolog
.definition(self
.id, "".join((patternprefix
, patternproc
, patternsuffix
))))
332 pattern
.clear
= attr
.clearclass(pattern
)
335 # The main canvas class
338 class canvas(_canvas
):
340 """a canvas is a collection of PSCmds together with PSAttrs"""
342 def writeEPSfile(self
, filename
, paperformat
=None, rotated
=0, fittosize
=0, margin
="1 t cm",
343 bbox
=None, bboxenlarge
="1 t pt"):
344 """write canvas to EPS file
346 If paperformat is set to a known paperformat, the output will be centered on
349 If rotated is set, the output will first be rotated by 90 degrees.
351 If fittosize is set, then the output is scaled to the size of the
352 page (minus margin). In that case, the paperformat the specification
353 of the paperformat is obligatory.
355 The bbox parameter overrides the automatic bounding box determination.
356 bboxenlarge may be used to enlarge the bbox of the canvas (or the
357 manually specified bbox).
360 if filename
[-4:]!=".eps":
361 filename
= filename
+ ".eps"
364 file = open(filename
, "w")
366 raise IOError("cannot open output file")
368 abbox
= bbox
is not None and bbox
or self
.bbox()
369 abbox
= abbox
.enlarged(bboxenlarge
)
370 ctrafo
= None # global transformation of canvas
373 ctrafo
= trafo
.rotate_pt(90,
374 0.5*(abbox
.llx
+abbox
.urx
),
375 0.5*(abbox
.lly
+abbox
.ury
))
378 # center (optionally rotated) output on page
380 width
, height
= _paperformats
[paperformat
.upper()]
382 raise KeyError, "unknown paperformat '%s'" % paperformat
383 width
= unit
.topt(width
)
384 height
= unit
.topt(height
)
386 if not ctrafo
: ctrafo
=trafo
.trafo()
388 ctrafo
= ctrafo
.translated_pt(0.5*(width
-(abbox
.urx
-abbox
.llx
))-
390 0.5*(height
-(abbox
.ury
-abbox
.lly
))-
394 # scale output to pagesize - margins
395 margin
= unit
.topt(margin
)
396 if 2*margin
> min(width
, height
):
397 raise RuntimeError("Margins too broad for selected paperformat. Aborting.")
400 sfactor
= min((height
-2*margin
)/(abbox
.urx
-abbox
.llx
),
401 (width
-2*margin
)/(abbox
.ury
-abbox
.lly
))
403 sfactor
= min((width
-2*margin
)/(abbox
.urx
-abbox
.llx
),
404 (height
-2*margin
)/(abbox
.ury
-abbox
.lly
))
406 ctrafo
= ctrafo
.scaled_pt(sfactor
, sfactor
, 0.5*width
, 0.5*height
)
410 raise ValueError("must specify paper size for fittosize")
412 # if there has been a global transformation, adjust the bounding box
414 if ctrafo
: abbox
= abbox
.transformed(ctrafo
)
416 file.write("%!PS-Adobe-3.0 EPSF 3.0\n")
418 file.write("%%%%Creator: PyX %s\n" % version
.version
)
419 file.write("%%%%Title: %s\n" % filename
)
420 file.write("%%%%CreationDate: %s\n" %
421 time
.asctime(time
.localtime(time
.time())))
422 file.write("%%EndComments\n")
424 file.write("%%BeginProlog\n")
428 for pritem
in self
.prolog():
429 for mpritem
in mergedprolog
:
430 if mpritem
.merge(pritem
) is None: break
432 mergedprolog
.append(pritem
)
434 for pritem
in mergedprolog
:
435 pritem
.outputPS(file)
437 file.write("%%EndProlog\n")
439 # again, if there has occured global transformation, apply it now
440 if ctrafo
: ctrafo
.outputPS(file)
442 file.write("%f setlinewidth\n" % unit
.topt(style
.linewidth
.normal
))
444 # here comes the actual content
447 file.write("showpage\n")
448 file.write("%%Trailer\n")
449 file.write("%%EOF\n")
451 def writePDFfile(self
, filename
, bbox
=None, bboxenlarge
="1 t pt"):
452 sys
.stderr
.write("*** PyX Warning: writePDFfile is experimental and supports only a subset of PyX's features\n")
454 if filename
[-4:]!=".pdf":
455 filename
= filename
+ ".pdf"
458 file = open(filename
, "wb")
460 raise IOError("cannot open output file")
462 abbox
= bbox
is not None and bbox
or self
.bbox()
463 abbox
= abbox
.enlarged(bboxenlarge
)
465 file.write("%%PDF-1.4\n%%%s%s%s%s\n" % (chr(195), chr(182), chr(195), chr(169)))
466 reflist
= [file.tell()]
467 file.write("1 0 obj\n"
474 reflist
.append(file.tell())
475 file.write("2 0 obj\n"
481 reflist
.append(file.tell())
482 file.write("3 0 obj\n"
489 reflist
.append(file.tell())
490 file.write("4 0 obj\n"
494 abbox
.outputPDF(file)
495 file.write("/Contents 5 0 R\n"
496 "/Resources << /ProcSet 7 0 R >>\n"
499 reflist
.append(file.tell())
500 file.write("5 0 obj\n"
501 "<< /Length 6 0 R >>\n"
503 streamstartpos
= file.tell()
504 style
.linewidth
.normal
.outputPDF(file)
506 streamendpos
= file.tell()
507 file.write("endstream\n"
509 reflist
.append(file.tell())
510 file.write("6 0 obj\n"
512 "endobj\n" % (streamendpos
- streamstartpos
))
513 reflist
.append(file.tell())
514 file.write("7 0 obj\n"
517 xrefpos
= file.tell()
520 file.write("0000000000 65535 f \n")
522 file.write("%010i 00000 n \n" % ref
)
523 file.write("trailer\n"
530 "%%%%EOF\n" % xrefpos
)
532 def writetofile(self
, filename
, *args
, **kwargs
):
533 if filename
[-4:] == ".eps":
534 self
.writeEPSfile(filename
, *args
, **kwargs
)
535 elif filename
[-4:] == ".pdf":
536 self
.writePDFfile(filename
, *args
, **kwargs
)
538 sys
.stderr
.write("*** PyX Warning: deprecated usage of writetofile -- writetofile needs a filename extension or use an explicit call to writeEPSfile or the like\n")
539 self
.writeEPSfile(filename
, *args
, **kwargs
)