remove string module and clean up some comments
[PyX/mjg.git] / pyx / canvas.py
blobe0eb11b5e2e39df7a803585a6441995b28cfa962
1 #!/usr/bin/env python
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.
30 """
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")}
47 # clipping class
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"""
56 self.path = path
58 def bbox(self):
59 # as a PSCmd a clipping path has NO influence on the bbox...
60 return None
62 def clipbbox(self):
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)
69 file.write("clip\n")
71 def outputPDF(self, file):
72 self.path.outputPDF(file)
73 file.write("W n\n")
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):
85 """construct a canvas
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
99 """
101 self.PSOps = []
102 self.trafo = trafo.trafo()
103 self.clipbbox = None
104 if texrunner is not None:
105 self.texrunner = texrunner
106 else:
107 # prevent cyclic imports
108 import text
109 self.texrunner = text.defaulttexrunner
111 for attr in attrs:
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)
118 else:
119 self.clippbox *= attr.clipbbox().transformed(self.trafo)
120 self.PSOps.append(attr)
121 else:
122 self.set([attr])
124 def bbox(self):
125 """returns bounding box of canvas"""
126 obbox = None
127 for cmd in self.PSOps:
128 if isinstance(cmd, base.PSCmd):
129 abbox = cmd.bbox()
130 if obbox is None:
131 obbox = abbox
132 elif abbox is not None:
133 obbox += abbox
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)
142 else:
143 return self.clipbbox
145 def prolog(self):
146 result = []
147 for cmd in self.PSOps:
148 result.extend(cmd.prolog())
149 return result
151 def outputPS(self, file):
152 if self.PSOps:
153 file.write("gsave\n")
154 for cmd in self.PSOps:
155 cmd.outputPS(file)
156 file.write("grestore\n")
158 def outputPDF(self, file):
159 if self.PSOps:
160 file.write("q\n") # gsave
161 for cmd in self.PSOps:
162 cmd.outputPDF(file)
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.
170 returns the PSOp
174 # XXX check for PSOp
176 if attrs:
177 sc = _canvas(attrs)
178 sc.insert(PSOp)
179 self.PSOps.append(sc)
180 else:
181 self.PSOps.append(PSOp)
183 return 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])
190 for astyle in attrs:
191 self.insert(astyle)
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)
211 # set global styles
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)
218 self.insert(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
286 self.xstep = xstep
287 self.ystep = ystep
288 self.patternbbox = bbox
289 self.patterntrafo = trafo
291 def bbox(self):
292 return None
294 def outputPS(self, file):
295 file.write("%s setpattern\n" % self.id)
297 def prolog(self):
298 realpatternbbox = _canvas.bbox(self)
299 if self.xstep is None:
300 xstep = unit.topt(realpatternbbox.width())
301 else:
302 xstep = unit.topt(unit.length(self.xstep))
303 if self.ystep is None:
304 ystep = unit.topt(realpatternbbox.height())
305 else:
306 ystep = unit.topt(unit.length(self.ystep))
307 if not xstep:
308 raise ValueError("xstep in pattern cannot be zero")
309 if not ystep:
310 raise ValueError("ystep in pattern cannot be zero")
311 patternbbox = self.patternbbox or realpatternbbox.enlarged("5 pt")
313 patternprefix = "\n".join(("<<",
314 "/PatternType 1",
315 "/PaintType %d" % self.painttype,
316 "/TilingType %d" % self.tilingtype,
317 "/BBox[%s]" % str(patternbbox),
318 "/XStep %g" % xstep,
319 "/YStep %g" % ystep,
320 "/PaintProc {\nbegin\n"))
321 stringfile = cStringIO.StringIO()
322 _canvas.outputPS(self, stringfile)
323 patternproc = stringfile.getvalue()
324 stringfile.close()
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))))
330 return pr
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
347 the page.
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"
363 try:
364 file = open(filename, "w")
365 except IOError:
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
372 if rotated:
373 ctrafo = trafo.rotate_pt(90,
374 0.5*(abbox.llx+abbox.urx),
375 0.5*(abbox.lly+abbox.ury))
377 if paperformat:
378 # center (optionally rotated) output on page
379 try:
380 width, height = _paperformats[paperformat.upper()]
381 except KeyError:
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))-
389 abbox.llx,
390 0.5*(height-(abbox.ury-abbox.lly))-
391 abbox.lly)
393 if fittosize:
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.")
399 if rotated:
400 sfactor = min((height-2*margin)/(abbox.urx-abbox.llx),
401 (width-2*margin)/(abbox.ury-abbox.lly))
402 else:
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)
409 elif fittosize:
410 raise ValueError("must specify paper size for fittosize")
412 # if there has been a global transformation, adjust the bounding box
413 # accordingly
414 if ctrafo: abbox = abbox.transformed(ctrafo)
416 file.write("%!PS-Adobe-3.0 EPSF 3.0\n")
417 abbox.outputPS(file)
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")
426 mergedprolog = []
428 for pritem in self.prolog():
429 for mpritem in mergedprolog:
430 if mpritem.merge(pritem) is None: break
431 else:
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
445 self.outputPS(file)
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"
457 try:
458 file = open(filename, "wb")
459 except IOError:
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"
468 "<<\n"
469 "/Type /Catalog\n"
470 "/Outlines 2 0 R\n"
471 "/Pages 3 0 R\n"
472 ">>\n"
473 "endobj\n")
474 reflist.append(file.tell())
475 file.write("2 0 obj\n"
476 "<<\n"
477 "/Type Outlines\n"
478 "/Count 0\n"
479 ">>\n"
480 "endobj\n")
481 reflist.append(file.tell())
482 file.write("3 0 obj\n"
483 "<<\n"
484 "/Type /Pages\n"
485 "/Kids [4 0 R]\n"
486 "/Count 1\n"
487 ">>\n"
488 "endobj\n")
489 reflist.append(file.tell())
490 file.write("4 0 obj\n"
491 "<<\n"
492 "/Type /Page\n"
493 "/Parent 3 0 R\n")
494 abbox.outputPDF(file)
495 file.write("/Contents 5 0 R\n"
496 "/Resources << /ProcSet 7 0 R >>\n"
497 ">>\n"
498 "endobj\n")
499 reflist.append(file.tell())
500 file.write("5 0 obj\n"
501 "<< /Length 6 0 R >>\n"
502 "stream\n")
503 streamstartpos = file.tell()
504 style.linewidth.normal.outputPDF(file)
505 self.outputPDF(file)
506 streamendpos = file.tell()
507 file.write("endstream\n"
508 "endobj\n")
509 reflist.append(file.tell())
510 file.write("6 0 obj\n"
511 "%s\n"
512 "endobj\n" % (streamendpos - streamstartpos))
513 reflist.append(file.tell())
514 file.write("7 0 obj\n"
515 "[/PDF]\n"
516 "endobj\n")
517 xrefpos = file.tell()
518 file.write("xref\n"
519 "0 8\n")
520 file.write("0000000000 65535 f \n")
521 for ref in reflist:
522 file.write("%010i 00000 n \n" % ref)
523 file.write("trailer\n"
524 "<<\n"
525 "/Size 8\n"
526 "/Root 1 0 R\n"
527 ">>\n"
528 "startxref\n"
529 "%i\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)
537 else:
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)