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
, math
, time
33 import attr
, base
, bbox
, deco
, unit
, prolog
, style
, trafo
, version
38 # fallback implementation for Python 2.2. and below
40 return zip(xrange(len(list)), list)
42 # known paperformats as tuple (width, height)
44 _paperformats
= { "A4" : (210 * unit
.t_mm
, 297 * unit
.t_mm
),
45 "A3" : (297 * unit
.t_mm
, 420 * unit
.t_mm
),
46 "A2" : (420 * unit
.t_mm
, 594 * unit
.t_mm
),
47 "A1" : (594 * unit
.t_mm
, 840 * unit
.t_mm
),
48 "A0" : (840 * unit
.t_mm
, 1188 * unit
.t_mm
),
49 "A0b" : (910 * unit
.t_mm
, 1370 * unit
.t_mm
),
50 "Letter" : (8.5 * unit
.t_inch
, 11 * unit
.t_inch
),
51 "Legal" : (8.5 * unit
.t_inch
, 14 * unit
.t_inch
)}
57 class clip(base
.PSCmd
):
59 """class for use in canvas constructor which clips to a path"""
61 def __init__(self
, path
):
62 """construct a clip instance for a given path"""
66 # as a PSCmd a clipping path has NO influence on the bbox...
70 # ... but for clipping, we nevertheless need the bbox
71 return self
.path
.bbox()
73 def outputPS(self
, file):
74 file.write("newpath\n")
75 self
.path
.outputPS(file)
78 def outputPDF(self
, file):
79 self
.path
.outputPDF(file)
83 # general canvas class
86 class _canvas(base
.PSCmd
):
88 """a canvas is a collection of PSCmds together with PSAttrs"""
90 def __init__(self
, attrs
=[], texrunner
=None):
94 The canvas can be modfied by supplying args, which have
95 to be instances of one of the following classes:
96 - trafo.trafo (leading to a global transformation of the canvas)
97 - canvas.clip (clips the canvas)
98 - base.PathStyle (sets some global attributes of the canvas)
100 Note that, while the first two properties are fixed for the
101 whole canvas, the last one can be changed via canvas.set().
103 The texrunner instance used for the text method can be specified
104 using the texrunner argument. It defaults to text.defaulttexrunner
109 self
.trafo
= trafo
.trafo()
111 if texrunner
is not None:
112 self
.texrunner
= texrunner
114 # prevent cyclic imports
116 self
.texrunner
= text
.defaulttexrunner
119 if isinstance(attr
, trafo
.trafo_pt
):
120 self
.trafo
= self
.trafo
*attr
121 self
.PSOps
.append(attr
)
122 elif isinstance(attr
, clip
):
123 if self
.clipbbox
is None:
124 self
.clipbbox
= attr
.clipbbox().transformed(self
.trafo
)
126 self
.clippbox
*= attr
.clipbbox().transformed(self
.trafo
)
127 self
.PSOps
.append(attr
)
132 """returns bounding box of canvas"""
134 for cmd
in self
.PSOps
:
135 if isinstance(cmd
, base
.PSCmd
):
139 elif abbox
is not None:
142 # transform according to our global transformation and
143 # intersect with clipping bounding box (which have already been
144 # transformed in canvas.__init__())
145 if obbox
is not None and self
.clipbbox
is not None:
146 return obbox
.transformed(self
.trafo
)*self
.clipbbox
147 elif obbox
is not None:
148 return obbox
.transformed(self
.trafo
)
154 for cmd
in self
.PSOps
:
155 result
.extend(cmd
.prolog())
158 def outputPS(self
, file):
160 file.write("gsave\n")
161 for cmd
in self
.PSOps
:
163 file.write("grestore\n")
165 def outputPDF(self
, file):
167 file.write("q\n") # gsave
168 for cmd
in self
.PSOps
:
170 file.write("Q\n") # grestore
172 def insert(self
, PSOp
, attrs
=[]):
173 """insert PSOp in the canvas.
175 If attrss are given, a canvas containing the PSOp is inserted applying attrs.
186 self
.PSOps
.append(sc
)
188 self
.PSOps
.append(PSOp
)
192 def set(self
, attrs
):
193 """sets styles args globally for the rest of the canvas
196 attr
.checkattrs(attrs
, [style
.strokestyle
, style
.fillstyle
])
200 def draw(self
, path
, attrs
):
201 """draw path on canvas using the style given by args
203 The argument attrs consists of PathStyles, which modify
204 the appearance of the path, PathDecos, which add some new
205 visual elements to the path, or trafos, which are applied
206 before drawing the path.
210 attrs
= attr
.mergeattrs(attrs
)
211 attr
.checkattrs(attrs
, [deco
.deco
, style
.fillstyle
, style
.strokestyle
, trafo
.trafo_pt
])
213 for t
in attr
.getattrs(attrs
, [trafo
.trafo_pt
]):
214 path
= path
.transformed(t
)
216 dp
= deco
.decoratedpath(path
)
219 dp
.styles
= attr
.getattrs(attrs
, [style
.fillstyle
, style
.strokestyle
])
221 # add path decorations and modify path accordingly
222 for adeco
in attr
.getattrs(attrs
, [deco
.deco
]):
223 dp
= adeco
.decorate(dp
)
227 def stroke(self
, path
, attrs
=[]):
228 """stroke path on canvas using the style given by args
230 The argument attrs consists of PathStyles, which modify
231 the appearance of the path, PathDecos, which add some new
232 visual elements to the path, or trafos, which are applied
233 before drawing the path.
237 self
.draw(path
, [deco
.stroked
]+list(attrs
))
239 def fill(self
, path
, attrs
=[]):
240 """fill path on canvas using the style given by args
242 The argument attrs consists of PathStyles, which modify
243 the appearance of the path, PathDecos, which add some new
244 visual elements to the path, or trafos, which are applied
245 before drawing the path.
249 self
.draw(path
, [deco
.filled
]+list(attrs
))
251 def settexrunner(self
, texrunner
):
252 """sets the texrunner to be used to within the text and text_pt methods"""
254 self
.texrunner
= texrunner
256 def text(self
, x
, y
, atext
, *args
, **kwargs
):
257 """insert a text into the canvas
259 inserts a textbox created by self.texrunner.text into the canvas
261 returns the inserted textbox"""
263 return self
.insert(self
.texrunner
.text(x
, y
, atext
, *args
, **kwargs
))
266 def text_pt(self
, x
, y
, atext
, *args
):
267 """insert a text into the canvas
269 inserts a textbox created by self.texrunner.text_pt into the canvas
271 returns the inserted textbox"""
273 return self
.insert(self
.texrunner
.text_pt(x
, y
, atext
, *args
))
276 # canvas for patterns
279 class pattern(_canvas
, attr
.exclusiveattr
, style
.fillstyle
):
281 def __init__(self
, painttype
=1, tilingtype
=1, xstep
=None, ystep
=None, bbox
=None, trafo
=None):
282 attr
.exclusiveattr
.__init
__(self
, pattern
)
283 _canvas
.__init
__(self
)
284 attr
.exclusiveattr
.__init
__(self
, pattern
)
285 self
.id = "pattern%d" % id(self
)
286 if painttype
not in (1,2):
287 raise ValueError("painttype must be 1 or 2")
288 self
.painttype
= painttype
289 if tilingtype
not in (1,2,3):
290 raise ValueError("tilingtype must be 1, 2 or 3")
291 self
.tilingtype
= tilingtype
294 self
.patternbbox
= bbox
295 self
.patterntrafo
= trafo
300 def outputPS(self
, file):
301 file.write("%s setpattern\n" % self
.id)
304 realpatternbbox
= _canvas
.bbox(self
)
305 if self
.xstep
is None:
306 xstep
= unit
.topt(realpatternbbox
.width())
308 xstep
= unit
.topt(unit
.length(self
.xstep
))
309 if self
.ystep
is None:
310 ystep
= unit
.topt(realpatternbbox
.height())
312 ystep
= unit
.topt(unit
.length(self
.ystep
))
314 raise ValueError("xstep in pattern cannot be zero")
316 raise ValueError("ystep in pattern cannot be zero")
317 patternbbox
= self
.patternbbox
or realpatternbbox
.enlarged("5 pt")
319 patternprefix
= "\n".join(("<<",
321 "/PaintType %d" % self
.painttype
,
322 "/TilingType %d" % self
.tilingtype
,
323 "/BBox[%s]" % str(patternbbox
),
326 "/PaintProc {\nbegin\n"))
327 stringfile
= cStringIO
.StringIO()
328 _canvas
.outputPS(self
, stringfile
)
329 patternproc
= stringfile
.getvalue()
331 patterntrafostring
= self
.patterntrafo
is None and "matrix" or str(self
.patterntrafo
)
332 patternsuffix
= "end\n} bind\n>>\n%s\nmakepattern" % patterntrafostring
334 pr
= _canvas
.prolog(self
)
335 pr
.append(prolog
.definition(self
.id, "".join((patternprefix
, patternproc
, patternsuffix
))))
338 pattern
.clear
= attr
.clearclass(pattern
)
342 def calctrafo(abbox
, paperformat
, margin
, rotated
, fittosize
):
343 """ calculate a trafo which rotates and fits a canvas with
344 bounding box abbox on the given paperformat with a margin on all
346 margin
= unit
.length(margin
)
347 atrafo
= None # global transformation of canvas
350 atrafo
= trafo
.rotate(90, *abbox
.center())
353 # center (optionally rotated) output on page
355 paperwidth
, paperheight
= _paperformats
[paperformat
.capitalize()]
357 raise KeyError, "unknown paperformat '%s'" % paperformat
359 paperwidth
-= 2*margin
360 paperheight
-= 2*margin
362 if not atrafo
: atrafo
= trafo
.trafo()
364 atrafo
= atrafo
.translated(margin
+ 0.5*(paperwidth
- abbox
.width()) - abbox
.left(),
365 margin
+ 0.5*(paperheight
- abbox
.height()) - abbox
.bottom())
368 # scale output to pagesize - margins
369 if 2*margin
> min(paperwidth
, paperheight
):
370 raise RuntimeError("Margins too broad for selected paperformat. Aborting.")
373 sfactor
= min(unit
.topt(paperheight
)/unit
.topt(abbox
.width()),
374 unit
.topt(paperwidth
)/unit
.topt(abbox
.height()))
376 sfactor
= min(unit
.topt(paperwidth
)/unit
.topt(abbox
.width()),
377 unit
.topt(paperheight
)/unit
.topt(abbox
.height()))
379 atrafo
= atrafo
.scaled(sfactor
, sfactor
, margin
+ 0.5*paperwidth
, margin
+ 0.5*paperheight
)
381 raise ValueError("must specify paper size for fittosize")
386 # The main canvas class
389 class canvas(_canvas
):
391 """a canvas is a collection of PSCmds together with PSAttrs"""
393 def writeEPSfile(self
, filename
, paperformat
=None, rotated
=0, fittosize
=0, margin
="1 t cm",
394 bbox
=None, bboxenlarge
="1 t pt"):
395 """write canvas to EPS file
397 If paperformat is set to a known paperformat, the output will be centered on
400 If rotated is set, the output will first be rotated by 90 degrees.
402 If fittosize is set, then the output is scaled to the size of the
403 page (minus margin). In that case, the paperformat the specification
404 of the paperformat is obligatory.
406 The bbox parameter overrides the automatic bounding box determination.
407 bboxenlarge may be used to enlarge the bbox of the canvas (or the
408 manually specified bbox).
411 if filename
[-4:]!=".eps":
412 filename
= filename
+ ".eps"
415 file = open(filename
, "w")
417 raise IOError("cannot open output file")
419 abbox
= bbox
is not None and bbox
or self
.bbox()
420 abbox
.enlarge(bboxenlarge
)
421 ctrafo
= calctrafo(abbox
, paperformat
, margin
, rotated
, fittosize
)
423 # if there has been a global transformation, adjust the bounding box
425 if ctrafo
: abbox
.transform(ctrafo
)
427 file.write("%!PS-Adobe-3.0 EPSF 3.0\n")
429 file.write("%%%%Creator: PyX %s\n" % version
.version
)
430 file.write("%%%%Title: %s\n" % filename
)
431 file.write("%%%%CreationDate: %s\n" %
432 time
.asctime(time
.localtime(time
.time())))
433 file.write("%%EndComments\n")
435 file.write("%%BeginProlog\n")
439 for pritem
in self
.prolog():
440 for mpritem
in mergedprolog
:
441 if mpritem
.merge(pritem
) is None: break
443 mergedprolog
.append(pritem
)
445 for pritem
in mergedprolog
:
446 pritem
.outputPS(file)
448 file.write("%%EndProlog\n")
450 # apply a possible global transformation
451 if ctrafo
: ctrafo
.outputPS(file)
453 file.write("%f setlinewidth\n" % unit
.topt(style
.linewidth
.normal
))
455 # here comes the actual content
458 file.write("showpage\n")
459 file.write("%%Trailer\n")
460 file.write("%%EOF\n")
462 def writePDFfile(self
, filename
, paperformat
=None, rotated
=0, fittosize
=0, margin
="1 t cm",
463 bbox
=None, bboxenlarge
="1 t pt"):
464 sys
.stderr
.write("*** PyX Warning: writePDFfile is experimental and supports only a subset of PyX's features\n")
466 if filename
[-4:]!=".pdf":
467 filename
= filename
+ ".pdf"
470 file = open(filename
, "wb")
472 raise IOError("cannot open output file")
474 abbox
= bbox
is not None and bbox
or self
.bbox()
475 abbox
.enlarge(bboxenlarge
)
477 ctrafo
= calctrafo(abbox
, paperformat
, margin
, rotated
, fittosize
)
479 # if there has been a global transformation, adjust the bounding box
481 if ctrafo
: abbox
.transform(ctrafo
)
485 for pritem
in self
.prolog():
486 for mpritem
in mergedprolog
:
487 if mpritem
.merge(pritem
) is None: break
489 mergedprolog
.append(pritem
)
491 file.write("%%PDF-1.4\n%%%s%s%s%s\n" % (chr(195), chr(182), chr(195), chr(169)))
492 reflist
= [file.tell()]
493 file.write("1 0 obj\n"
500 reflist
.append(file.tell())
501 file.write("2 0 obj\n"
507 reflist
.append(file.tell())
508 file.write("3 0 obj\n"
515 reflist
.append(file.tell())
516 file.write("4 0 obj\n"
520 abbox
.outputPDF(file)
521 file.write("/Contents 5 0 R\n"
526 for pritem
in mergedprolog
:
527 if isinstance(pritem
, prolog
.fontreencoding
):
529 file.write("/Font << /%s %d 0 R>>\n" % (pritem
.fontname
, fontnr
+7))
534 reflist
.append(file.tell())
535 file.write("5 0 obj\n"
536 "<< /Length 6 0 R >>\n"
538 streamstartpos
= file.tell()
540 # apply a possible global transformation
541 if ctrafo
: ctrafo
.outputPDF(file)
542 style
.linewidth
.normal
.outputPDF(file)
545 streamendpos
= file.tell()
546 file.write("endstream\n"
548 reflist
.append(file.tell())
549 file.write("6 0 obj\n"
551 "endobj\n" % (streamendpos
- streamstartpos
))
552 reflist
.append(file.tell())
553 file.write("7 0 obj\n"
558 for pritem
in mergedprolog
:
559 if isinstance(pritem
, prolog
.fontreencoding
):
561 reflist
.append(file.tell())
562 file.write("%d 0 obj\n"
567 "/BaseFont /Helvetica\n"
568 "/Encoding /MacRomanEncoding\n"
570 "endobj\n" % (fontnr
+7, pritem
.fontname
))
572 xrefpos
= file.tell()
574 "0 %d\n" % (len(reflist
)+1))
575 file.write("0000000000 65535 f \n")
577 file.write("%010i 00000 n \n" % ref
)
578 file.write("trailer\n"
585 "%%%%EOF\n" % xrefpos
)
587 def writetofile(self
, filename
, *args
, **kwargs
):
588 if filename
[-4:] == ".eps":
589 self
.writeEPSfile(filename
, *args
, **kwargs
)
590 elif filename
[-4:] == ".pdf":
591 self
.writePDFfile(filename
, *args
, **kwargs
)
593 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")
594 self
.writeEPSfile(filename
, *args
, **kwargs
)
598 def __init__(self
, attrs
=[], texrunner
=None, pagename
=None, paperformat
="a4", rotated
=0, fittosize
=0,
599 margin
="1 t cm", bboxenlarge
="1 t pt"):
600 canvas
.__init
__(self
, attrs
, texrunner
)
601 self
.pagename
= pagename
602 self
.paperformat
= paperformat
.capitalize()
603 self
.rotated
= rotated
604 self
.fittosize
= fittosize
606 self
.bboxenlarge
= bboxenlarge
609 # the bounding box of a page is fixed by its format and an optional rotation
610 pbbox
= bbox
.bbox(0, 0, *_paperformats
[self
.paperformat
])
611 pbbox
.enlarge(self
.bboxenlarge
)
613 pbbox
.transform(trafo
.rotate(90, *pbbox
.center()))
616 def outputPS(self
, file):
617 file.write("%%%%PageMedia: %s\n" % self
.paperformat
)
618 file.write("%%%%PageOrientation: %s\n" % (self
.rotated
and "Landscape" or "Portrait"))
619 # file.write("%%%%PageBoundingBox: %d %d %d %d\n" % (math.floor(pbbox.llx_pt), math.floor(pbbox.lly_pt),
620 # math.ceil(pbbox.urx_pt), math.ceil(pbbox.ury_pt)))
623 file.write("%%BeginPageSetup\n")
624 file.write("/pgsave save def\n")
625 # for scaling, we need the real bounding box of the page contents
626 pbbox
= canvas
.bbox(self
)
627 pbbox
.enlarge(self
.bboxenlarge
)
628 ptrafo
= calctrafo(pbbox
, self
.paperformat
, self
.margin
, self
.rotated
, self
.fittosize
)
630 ptrafo
.outputPS(file)
631 file.write("%f setlinewidth\n" % unit
.topt(style
.linewidth
.normal
))
632 file.write("%%EndPageSetup\n")
634 # here comes the actual content
635 canvas
.outputPS(self
, file)
636 file.write("pgsave restore\n")
637 file.write("showpage\n")
638 # file.write("%%PageTrailer\n")
643 """holds a collection of page instances which are output as pages of a document"""
645 def __init__(self
, pages
=[]):
648 def append(self
, page
):
649 self
.pages
.append(page
)
651 def writePSfile(self
, filename
):
652 """write pages to PS file """
654 if filename
[-3:]!=".ps":
655 filename
= filename
+ ".ps"
658 file = open(filename
, "w")
660 raise IOError("cannot open output file")
663 for apage
in self
.pages
:
671 file.write("%!PS-Adobe-3.0\n")
672 docbbox
.outputPS(file)
673 file.write("%%%%Creator: PyX %s\n" % version
.version
)
674 file.write("%%%%Title: %s\n" % filename
)
675 file.write("%%%%CreationDate: %s\n" %
676 time
.asctime(time
.localtime(time
.time())))
677 # required paper formats
679 for apage
in self
.pages
:
680 if isinstance(apage
, page
):
681 paperformats
[apage
.paperformat
] = _paperformats
[apage
.paperformat
]
683 for paperformat
, size
in paperformats
.items():
685 file.write("%%DocumentMedia: ")
689 file.write("%s %d %d 75 white ()\n" % (paperformat
, unit
.topt(size
[0]), unit
.topt(size
[1])))
691 file.write("%%%%Pages: %d\n" % len(self
.pages
))
692 file.write("%%PageOrder: Ascend\n")
693 file.write("%%EndComments\n")
695 # document default section
696 #file.write("%%BeginDefaults\n")
698 # file.write("%%%%PageMedia: %s\n" % paperformat)
699 #file.write("%%%%PageOrientation: %s\n" % (rotated and "Landscape" or "Portrait"))
700 #file.write("%%EndDefaults\n")
702 # document prolog section
703 file.write("%%BeginProlog\n")
705 for apage
in self
.pages
:
706 for pritem
in apage
.prolog():
707 for mpritem
in mergedprolog
:
708 if mpritem
.merge(pritem
) is None: break
710 mergedprolog
.append(pritem
)
711 for pritem
in mergedprolog
:
712 pritem
.outputPS(file)
713 file.write("%%EndProlog\n")
715 # document setup section
716 #file.write("%%BeginSetup\n")
717 #file.write("%%EndSetup\n")
720 for nr
, apage
in enumerate(self
.pages
):
721 file.write("%%%%Page: %s %d\n" % (apage
.pagename
is None and str(nr
) or apage
.pagename
, nr
+1))
724 file.write("%%Trailer\n")
725 file.write("%%EOF\n")