- start of rewrite of pdfwriter
[PyX/mjg.git] / pyx / canvas.py
blobbefd36986a73ca899d7730eeea5af822eb33544c
1 #!/usr/bin/env python
2 # -*- coding: ISO-8859-1 -*-
5 # Copyright (C) 2002-2005 Jörg Lehmann <joergl@users.sourceforge.net>
6 # Copyright (C) 2002-2005 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 and corresponding attributes to be
29 displayed. """
31 import sys, cStringIO
32 import attr, base, deco, deformer, unit, prolog, style, trafo
35 # clipping class
38 class clip(base.canvasitem):
40 """class for use in canvas constructor which clips to a path"""
42 def __init__(self, path):
43 """construct a clip instance for a given path"""
44 self.path = path
46 def bbox(self):
47 # as a canvasitem a clipping path has NO influence on the bbox...
48 return None
50 def clipbbox(self):
51 # ... but for clipping, we nevertheless need the bbox
52 return self.path.bbox()
54 def outputPS(self, file):
55 file.write("newpath\n")
56 self.path.outputPS(file)
57 file.write("clip\n")
59 def outputPDF(self, file):
60 self.path.outputPDF(file)
61 file.write("W n\n")
64 # general canvas class
67 class _canvas(base.canvasitem):
69 """a canvas holds a collection of canvasitems"""
71 def __init__(self, attrs=[], texrunner=None):
73 """construct a canvas
75 The canvas can be modfied by supplying args, which have
76 to be instances of one of the following classes:
77 - trafo.trafo (leading to a global transformation of the canvas)
78 - canvas.clip (clips the canvas)
79 - style.strokestyle, style.fillstyle (sets some global attributes of the canvas)
81 Note that, while the first two properties are fixed for the
82 whole canvas, the last one can be changed via canvas.set().
84 The texrunner instance used for the text method can be specified
85 using the texrunner argument. It defaults to text.defaulttexrunner
87 """
89 self.items = []
90 self.trafo = trafo.trafo()
91 self.clipbbox = None
92 if texrunner is not None:
93 self.texrunner = texrunner
94 else:
95 # prevent cyclic imports
96 import text
97 self.texrunner = text.defaulttexrunner
99 for attr in attrs:
100 if isinstance(attr, trafo.trafo_pt):
101 self.trafo = self.trafo*attr
102 self.items.append(attr)
103 elif isinstance(attr, clip):
104 if self.clipbbox is None:
105 self.clipbbox = attr.clipbbox().transformed(self.trafo)
106 else:
107 self.clippbox *= attr.clipbbox().transformed(self.trafo)
108 self.items.append(attr)
109 else:
110 self.set([attr])
112 def bbox(self):
113 """returns bounding box of canvas"""
114 obbox = None
115 for cmd in self.items:
116 abbox = cmd.bbox()
117 if obbox is None:
118 obbox = abbox
119 elif abbox is not None:
120 obbox += abbox
122 # transform according to our global transformation and
123 # intersect with clipping bounding box (which has already been
124 # transformed in canvas.__init__())
125 if obbox is not None and self.clipbbox is not None:
126 return obbox.transformed(self.trafo)*self.clipbbox
127 elif obbox is not None:
128 return obbox.transformed(self.trafo)
129 else:
130 return self.clipbbox
132 def prolog(self):
133 result = []
134 for cmd in self.items:
135 result.extend(cmd.prolog())
136 return result
138 def outputPS(self, file):
139 if self.items:
140 file.write("gsave\n")
141 for cmd in self.items:
142 cmd.outputPS(file)
143 file.write("grestore\n")
145 def outputPDF(self, file):
146 if self.items:
147 file.write("q\n") # gsave
148 for cmd in self.items:
149 cmd.outputPDF(file)
150 file.write("Q\n") # grestore
152 def insert(self, item, attrs=None):
153 """insert item in the canvas.
155 If attrs are passed, a canvas containing the item is inserted applying attrs.
157 returns the item
161 if not isinstance(item, base.canvasitem):
162 raise RuntimeError("only instances of base.canvasitem can be inserted into a canvas")
164 if attrs:
165 sc = _canvas(attrs)
166 sc.insert(item)
167 self.items.append(sc)
168 else:
169 self.items.append(item)
171 return item
173 def set(self, attrs):
174 """sets styles args globally for the rest of the canvas
177 attr.checkattrs(attrs, [style.strokestyle, style.fillstyle])
178 for astyle in attrs:
179 self.insert(astyle)
181 def draw(self, path, attrs):
182 """draw path on canvas using the style given by args
184 The argument attrs consists of PathStyles, which modify
185 the appearance of the path, PathDecos, which add some new
186 visual elements to the path, or trafos, which are applied
187 before drawing the path.
191 attrs = attr.mergeattrs(attrs)
192 attr.checkattrs(attrs, [deco.deco, deformer.deformer, style.fillstyle, style.strokestyle])
194 for adeformer in attr.getattrs(attrs, [deformer.deformer]):
195 path = adeformer.deform(path)
197 styles = attr.getattrs(attrs, [style.fillstyle, style.strokestyle])
198 dp = deco.decoratedpath(path, styles=styles)
200 # add path decorations and modify path accordingly
201 for adeco in attr.getattrs(attrs, [deco.deco]):
202 dp = adeco.decorate(dp)
204 self.insert(dp)
206 def stroke(self, path, attrs=[]):
207 """stroke path on canvas using the style given by args
209 The argument attrs consists of PathStyles, which modify
210 the appearance of the path, PathDecos, which add some new
211 visual elements to the path, or trafos, which are applied
212 before drawing the path.
216 self.draw(path, [deco.stroked]+list(attrs))
218 def fill(self, path, attrs=[]):
219 """fill path on canvas using the style given by args
221 The argument attrs consists of PathStyles, which modify
222 the appearance of the path, PathDecos, which add some new
223 visual elements to the path, or trafos, which are applied
224 before drawing the path.
228 self.draw(path, [deco.filled]+list(attrs))
230 def settexrunner(self, texrunner):
231 """sets the texrunner to be used to within the text and text_pt methods"""
233 self.texrunner = texrunner
235 def text(self, x, y, atext, *args, **kwargs):
236 """insert a text into the canvas
238 inserts a textbox created by self.texrunner.text into the canvas
240 returns the inserted textbox"""
242 return self.insert(self.texrunner.text(x, y, atext, *args, **kwargs))
245 def text_pt(self, x, y, atext, *args):
246 """insert a text into the canvas
248 inserts a textbox created by self.texrunner.text_pt into the canvas
250 returns the inserted textbox"""
252 return self.insert(self.texrunner.text_pt(x, y, atext, *args))
255 # canvas for patterns
258 class pattern(_canvas, attr.exclusiveattr, style.fillstyle):
260 def __init__(self, painttype=1, tilingtype=1, xstep=None, ystep=None, bbox=None, trafo=None):
261 _canvas.__init__(self)
262 attr.exclusiveattr.__init__(self, pattern)
263 self.id = "pattern%d" % id(self)
264 if painttype not in (1,2):
265 raise ValueError("painttype must be 1 or 2")
266 self.painttype = painttype
267 if tilingtype not in (1,2,3):
268 raise ValueError("tilingtype must be 1, 2 or 3")
269 self.tilingtype = tilingtype
270 self.xstep = xstep
271 self.ystep = ystep
272 self.patternbbox = bbox
273 self.patterntrafo = trafo
275 def bbox(self):
276 return None
278 def outputPS(self, file):
279 file.write("%s setpattern\n" % self.id)
281 def prolog(self):
282 realpatternbbox = _canvas.bbox(self)
283 if self.xstep is None:
284 xstep = unit.topt(realpatternbbox.width())
285 else:
286 xstep = unit.topt(self.xstep)
287 if self.ystep is None:
288 ystep = unit.topt(realpatternbbox.height())
289 else:
290 ystep = unit.topt(self.ystep)
291 if not xstep:
292 raise ValueError("xstep in pattern cannot be zero")
293 if not ystep:
294 raise ValueError("ystep in pattern cannot be zero")
295 patternbbox = self.patternbbox or realpatternbbox.enlarged(5*unit.pt)
297 patternprefix = "\n".join(("<<",
298 "/PatternType 1",
299 "/PaintType %d" % self.painttype,
300 "/TilingType %d" % self.tilingtype,
301 "/BBox[%s]" % str(patternbbox),
302 "/XStep %g" % xstep,
303 "/YStep %g" % ystep,
304 "/PaintProc {\nbegin\n"))
305 stringfile = cStringIO.StringIO()
306 _canvas.outputPS(self, stringfile)
307 patternproc = stringfile.getvalue()
308 stringfile.close()
309 patterntrafostring = self.patterntrafo is None and "matrix" or str(self.patterntrafo)
310 patternsuffix = "end\n} bind\n>>\n%s\nmakepattern" % patterntrafostring
312 pr = _canvas.prolog(self)
313 pr.append(prolog.definition(self.id, "".join((patternprefix, patternproc, patternsuffix))))
314 return pr
316 pattern.clear = attr.clearclass(pattern)
319 # user canvas class which adds a few convenience methods for single page output
322 class canvas(_canvas):
324 """a canvas holds a collection of canvasitems"""
326 def writeEPSfile(self, filename, **kwargs):
327 import document
328 document.document([document.page(self, **kwargs)]).writeEPSfile(filename)
330 def writePDFfile(self, filename, **kwargs):
331 import document
332 document.document([document.page(self, **kwargs)]).writePDFfile(filename)
334 def writetofile(self, filename, *args, **kwargs):
335 if filename[-4:] == ".eps":
336 self.writeEPSfile(filename, *args, **kwargs)
337 elif filename[-4:] == ".pdf":
338 self.writePDFfile(filename, *args, **kwargs)
339 else:
340 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")
341 self.writeEPSfile(filename, *args, **kwargs)