remove duplicated call of base class constructor
[PyX/mjg.git] / pyx / canvas.py
blob3244374e1568100202f8be3a628ebe5cd8ce97fd
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, bbox, deco, deformer, unit, prolog, style, trafo, version
35 # temporarily needed for pdf fonts
36 import zlib
37 from t1strip import fullfont
38 import pykpathsea
40 import pdfwriter
42 try:
43 enumerate([])
44 except NameError:
45 # fallback implementation for Python 2.2. and below
46 def enumerate(list):
47 return zip(xrange(len(list)), list)
49 # known paperformats as tuple (width, height)
51 _paperformats = { "A4" : (210 * unit.t_mm, 297 * unit.t_mm),
52 "A3" : (297 * unit.t_mm, 420 * unit.t_mm),
53 "A2" : (420 * unit.t_mm, 594 * unit.t_mm),
54 "A1" : (594 * unit.t_mm, 840 * unit.t_mm),
55 "A0" : (840 * unit.t_mm, 1188 * unit.t_mm),
56 "A0b" : (910 * unit.t_mm, 1370 * unit.t_mm),
57 "Letter" : (8.5 * unit.t_inch, 11 * unit.t_inch),
58 "Legal" : (8.5 * unit.t_inch, 14 * unit.t_inch)}
61 # clipping class
64 class clip(base.canvasitem):
66 """class for use in canvas constructor which clips to a path"""
68 def __init__(self, path):
69 """construct a clip instance for a given path"""
70 self.path = path
72 def bbox(self):
73 # as a canvasitem a clipping path has NO influence on the bbox...
74 return None
76 def clipbbox(self):
77 # ... but for clipping, we nevertheless need the bbox
78 return self.path.bbox()
80 def outputPS(self, file):
81 file.write("newpath\n")
82 self.path.outputPS(file)
83 file.write("clip\n")
85 def outputPDF(self, file):
86 self.path.outputPDF(file)
87 file.write("W n\n")
90 # general canvas class
93 class _canvas(base.canvasitem):
95 """a canvas holds a collection of canvasitems"""
97 def __init__(self, attrs=[], texrunner=None):
99 """construct a canvas
101 The canvas can be modfied by supplying args, which have
102 to be instances of one of the following classes:
103 - trafo.trafo (leading to a global transformation of the canvas)
104 - canvas.clip (clips the canvas)
105 - style.strokestyle, style.fillstyle (sets some global attributes of the canvas)
107 Note that, while the first two properties are fixed for the
108 whole canvas, the last one can be changed via canvas.set().
110 The texrunner instance used for the text method can be specified
111 using the texrunner argument. It defaults to text.defaulttexrunner
115 self.items = []
116 self.trafo = trafo.trafo()
117 self.clipbbox = None
118 if texrunner is not None:
119 self.texrunner = texrunner
120 else:
121 # prevent cyclic imports
122 import text
123 self.texrunner = text.defaulttexrunner
125 for attr in attrs:
126 if isinstance(attr, trafo.trafo_pt):
127 self.trafo = self.trafo*attr
128 self.items.append(attr)
129 elif isinstance(attr, clip):
130 if self.clipbbox is None:
131 self.clipbbox = attr.clipbbox().transformed(self.trafo)
132 else:
133 self.clippbox *= attr.clipbbox().transformed(self.trafo)
134 self.items.append(attr)
135 else:
136 self.set([attr])
138 def bbox(self):
139 """returns bounding box of canvas"""
140 obbox = None
141 for cmd in self.items:
142 abbox = cmd.bbox()
143 if obbox is None:
144 obbox = abbox
145 elif abbox is not None:
146 obbox += abbox
148 # transform according to our global transformation and
149 # intersect with clipping bounding box (which have already been
150 # transformed in canvas.__init__())
151 if obbox is not None and self.clipbbox is not None:
152 return obbox.transformed(self.trafo)*self.clipbbox
153 elif obbox is not None:
154 return obbox.transformed(self.trafo)
155 else:
156 return self.clipbbox
158 def prolog(self):
159 result = []
160 for cmd in self.items:
161 result.extend(cmd.prolog())
162 return result
164 def outputPS(self, file):
165 if self.items:
166 file.write("gsave\n")
167 for cmd in self.items:
168 cmd.outputPS(file)
169 file.write("grestore\n")
171 def outputPDF(self, file):
172 if self.items:
173 file.write("q\n") # gsave
174 for cmd in self.items:
175 cmd.outputPDF(file)
176 file.write("Q\n") # grestore
178 def insert(self, item, attrs=None):
179 """insert item in the canvas.
181 If attrs are passed, a canvas containing the item is inserted applying attrs.
183 returns the item
187 if not isinstance(item, base.canvasitem):
188 raise RuntimeError("only instances of base.canvasitem can be inserted into a canvas")
190 if attrs:
191 sc = _canvas(attrs)
192 sc.insert(item)
193 self.items.append(sc)
194 else:
195 self.items.append(item)
197 return item
199 def set(self, attrs):
200 """sets styles args globally for the rest of the canvas
203 attr.checkattrs(attrs, [style.strokestyle, style.fillstyle])
204 for astyle in attrs:
205 self.insert(astyle)
207 def draw(self, path, attrs):
208 """draw path on canvas using the style given by args
210 The argument attrs consists of PathStyles, which modify
211 the appearance of the path, PathDecos, which add some new
212 visual elements to the path, or trafos, which are applied
213 before drawing the path.
217 attrs = attr.mergeattrs(attrs)
218 attr.checkattrs(attrs, [deco.deco, deformer.deformer, style.fillstyle, style.strokestyle])
220 for adeformer in attr.getattrs(attrs, [deformer.deformer]):
221 path = adeformer.deform(path)
223 styles = attr.getattrs(attrs, [style.fillstyle, style.strokestyle])
224 dp = deco.decoratedpath(path, styles=styles)
226 # add path decorations and modify path accordingly
227 for adeco in attr.getattrs(attrs, [deco.deco]):
228 dp = adeco.decorate(dp)
230 self.insert(dp)
232 def stroke(self, path, attrs=[]):
233 """stroke 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.stroked]+list(attrs))
244 def fill(self, path, attrs=[]):
245 """fill path on canvas using the style given by args
247 The argument attrs consists of PathStyles, which modify
248 the appearance of the path, PathDecos, which add some new
249 visual elements to the path, or trafos, which are applied
250 before drawing the path.
254 self.draw(path, [deco.filled]+list(attrs))
256 def settexrunner(self, texrunner):
257 """sets the texrunner to be used to within the text and text_pt methods"""
259 self.texrunner = texrunner
261 def text(self, x, y, atext, *args, **kwargs):
262 """insert a text into the canvas
264 inserts a textbox created by self.texrunner.text into the canvas
266 returns the inserted textbox"""
268 return self.insert(self.texrunner.text(x, y, atext, *args, **kwargs))
271 def text_pt(self, x, y, atext, *args):
272 """insert a text into the canvas
274 inserts a textbox created by self.texrunner.text_pt into the canvas
276 returns the inserted textbox"""
278 return self.insert(self.texrunner.text_pt(x, y, atext, *args))
281 # canvas for patterns
284 class pattern(_canvas, attr.exclusiveattr, style.fillstyle):
286 def __init__(self, painttype=1, tilingtype=1, xstep=None, ystep=None, bbox=None, trafo=None):
287 _canvas.__init__(self)
288 attr.exclusiveattr.__init__(self, pattern)
289 self.id = "pattern%d" % id(self)
290 if painttype not in (1,2):
291 raise ValueError("painttype must be 1 or 2")
292 self.painttype = painttype
293 if tilingtype not in (1,2,3):
294 raise ValueError("tilingtype must be 1, 2 or 3")
295 self.tilingtype = tilingtype
296 self.xstep = xstep
297 self.ystep = ystep
298 self.patternbbox = bbox
299 self.patterntrafo = trafo
301 def bbox(self):
302 return None
304 def outputPS(self, file):
305 file.write("%s setpattern\n" % self.id)
307 def prolog(self):
308 realpatternbbox = _canvas.bbox(self)
309 if self.xstep is None:
310 xstep = unit.topt(realpatternbbox.width())
311 else:
312 xstep = unit.topt(self.xstep)
313 if self.ystep is None:
314 ystep = unit.topt(realpatternbbox.height())
315 else:
316 ystep = unit.topt(self.ystep)
317 if not xstep:
318 raise ValueError("xstep in pattern cannot be zero")
319 if not ystep:
320 raise ValueError("ystep in pattern cannot be zero")
321 patternbbox = self.patternbbox or realpatternbbox.enlarged(5*unit.pt)
323 patternprefix = "\n".join(("<<",
324 "/PatternType 1",
325 "/PaintType %d" % self.painttype,
326 "/TilingType %d" % self.tilingtype,
327 "/BBox[%s]" % str(patternbbox),
328 "/XStep %g" % xstep,
329 "/YStep %g" % ystep,
330 "/PaintProc {\nbegin\n"))
331 stringfile = cStringIO.StringIO()
332 _canvas.outputPS(self, stringfile)
333 patternproc = stringfile.getvalue()
334 stringfile.close()
335 patterntrafostring = self.patterntrafo is None and "matrix" or str(self.patterntrafo)
336 patternsuffix = "end\n} bind\n>>\n%s\nmakepattern" % patterntrafostring
338 pr = _canvas.prolog(self)
339 pr.append(prolog.definition(self.id, "".join((patternprefix, patternproc, patternsuffix))))
340 return pr
342 pattern.clear = attr.clearclass(pattern)
344 # helper function
346 def calctrafo(abbox, paperformat, margin, rotated, fittosize):
347 """ calculate a trafo which rotates and fits a canvas with
348 bounding box abbox on the given paperformat with a margin on all
349 sides"""
350 if not isinstance(margin, unit.length):
351 margin = unit.length(margin)
352 atrafo = None # global transformation of canvas
354 if rotated:
355 atrafo = trafo.rotate(90, *abbox.center())
357 if paperformat:
358 # center (optionally rotated) output on page
359 try:
360 paperwidth, paperheight = _paperformats[paperformat.capitalize()]
361 except KeyError:
362 raise KeyError, "unknown paperformat '%s'" % paperformat
364 paperwidth -= 2*margin
365 paperheight -= 2*margin
367 if not atrafo: atrafo = trafo.trafo()
369 atrafo = atrafo.translated(margin + 0.5*(paperwidth - abbox.width()) - abbox.left(),
370 margin + 0.5*(paperheight - abbox.height()) - abbox.bottom())
372 if fittosize:
373 # scale output to pagesize - margins
374 if 2*margin > min(paperwidth, paperheight):
375 raise RuntimeError("Margins too broad for selected paperformat. Aborting.")
377 if rotated:
378 sfactor = min(unit.topt(paperheight)/unit.topt(abbox.width()),
379 unit.topt(paperwidth)/unit.topt(abbox.height()))
380 else:
381 sfactor = min(unit.topt(paperwidth)/unit.topt(abbox.width()),
382 unit.topt(paperheight)/unit.topt(abbox.height()))
384 atrafo = atrafo.scaled(sfactor, sfactor, margin + 0.5*paperwidth, margin + 0.5*paperheight)
385 elif fittosize:
386 raise ValueError("must specify paper size for fittosize")
388 return atrafo
391 # The main canvas class
394 class canvas(_canvas):
396 """a canvas holds a collection of canvasitems"""
398 def writeEPSfile(self, filename, paperformat=None, rotated=0, fittosize=0, margin=1 * unit.t_cm,
399 bbox=None, bboxenlarge=1 * unit.t_pt):
400 """write canvas to EPS file
402 If paperformat is set to a known paperformat, the output will be centered on
403 the page.
405 If rotated is set, the output will first be rotated by 90 degrees.
407 If fittosize is set, then the output is scaled to the size of the
408 page (minus margin). In that case, the paperformat the specification
409 of the paperformat is obligatory.
411 The bbox parameter overrides the automatic bounding box determination.
412 bboxenlarge may be used to enlarge the bbox of the canvas (or the
413 manually specified bbox).
416 if filename[-4:]!=".eps":
417 filename = filename + ".eps"
419 try:
420 file = open(filename, "w")
421 except IOError:
422 raise IOError("cannot open output file")
424 abbox = bbox is not None and bbox or self.bbox()
425 abbox.enlarge(bboxenlarge)
426 ctrafo = calctrafo(abbox, paperformat, margin, rotated, fittosize)
428 # if there has been a global transformation, adjust the bounding box
429 # accordingly
430 if ctrafo: abbox.transform(ctrafo)
432 file.write("%!PS-Adobe-3.0 EPSF-3.0\n")
433 abbox.outputPS(file)
434 file.write("%%%%Creator: PyX %s\n" % version.version)
435 file.write("%%%%Title: %s\n" % filename)
436 file.write("%%%%CreationDate: %s\n" %
437 time.asctime(time.localtime(time.time())))
438 file.write("%%EndComments\n")
440 file.write("%%BeginProlog\n")
442 mergedprolog = []
444 for pritem in self.prolog():
445 for mpritem in mergedprolog:
446 if mpritem.merge(pritem) is None: break
447 else:
448 mergedprolog.append(pritem)
450 for pritem in mergedprolog:
451 pritem.outputPS(file)
453 file.write("%%EndProlog\n")
455 # apply a possible global transformation
456 if ctrafo: ctrafo.outputPS(file)
458 file.write("%f setlinewidth\n" % unit.topt(style.linewidth.normal))
460 # here comes the actual content
461 self.outputPS(file)
463 file.write("showpage\n")
464 file.write("%%Trailer\n")
465 file.write("%%EOF\n")
467 def writePDFfile(self, filename, paperformat=None, rotated=0, fittosize=0, margin=1 * unit.t_cm,
468 bbox=None, bboxenlarge=1 * unit.t_pt):
469 sys.stderr.write("*** PyX Warning: writePDFfile is experimental and supports only a subset of PyX's features\n")
471 if filename[-4:]!=".pdf":
472 filename = filename + ".pdf"
474 try:
475 file = open(filename, "wb")
476 except IOError:
477 raise IOError("cannot open output file")
479 abbox = bbox is not None and bbox or self.bbox()
480 abbox.enlarge(bboxenlarge)
482 ctrafo = calctrafo(abbox, paperformat, margin, rotated, fittosize)
484 # if there has been a global transformation, adjust the bounding box
485 # accordingly
486 if ctrafo: abbox.transform(ctrafo)
488 mergedprolog = []
490 for pritem in self.prolog():
491 for mpritem in mergedprolog:
492 if mpritem.merge(pritem) is None: break
493 else:
494 mergedprolog.append(pritem)
496 file.write("%%PDF-1.4\n%%%s%s%s%s\n" % (chr(195), chr(182), chr(195), chr(169)))
497 reflist = [file.tell()]
498 file.write("1 0 obj\n"
499 "<<\n"
500 "/Type /Catalog\n"
501 "/Pages 2 0 R\n"
502 ">>\n"
503 "endobj\n")
504 reflist.append(file.tell())
505 file.write("2 0 obj\n"
506 "<<\n"
507 "/Type /Pages\n"
508 "/Kids [3 0 R]\n"
509 "/Count 1\n"
510 ">>\n"
511 "endobj\n")
512 reflist.append(file.tell())
513 file.write("3 0 obj\n"
514 "<<\n"
515 "/Type /Page\n"
516 "/Parent 2 0 R\n"
517 "/MediaBox ")
518 abbox.outputPDF(file)
519 file.write("/Contents 4 0 R\n"
520 "/Resources <<\n")
521 fontstartref = 5
523 fontnr = 0
524 if len([pritem for pritem in mergedprolog if isinstance(pritem, prolog.fontdefinition)]):
525 file.write("/Font\n"
526 "<<\n")
527 for pritem in mergedprolog:
528 if isinstance(pritem, prolog.fontdefinition):
529 fontnr += 1
530 file.write("/%s %d 0 R\n" % (pritem.font.getpsname(), fontnr+fontstartref))
531 fontnr += 3 # further objects due to a font
532 file.write(">>\n")
534 file.write(">>\n"
535 ">>\n"
536 "endobj\n")
537 reflist.append(file.tell())
538 file.write("4 0 obj\n"
539 "<< /Length 5 0 R >>\n"
540 "stream\n")
541 streamstartpos = file.tell()
543 # apply a possible global transformation
544 if ctrafo: ctrafo.outputPDF(file)
545 style.linewidth.normal.outputPDF(file)
547 self.outputPDF(file)
548 streamendpos = file.tell()
549 file.write("endstream\n"
550 "endobj\n")
551 reflist.append(file.tell())
552 file.write("5 0 obj\n"
553 "%s\n"
554 "endobj\n" % (streamendpos - streamstartpos))
556 fontnr = 0
557 for pritem in mergedprolog:
558 if isinstance(pritem, prolog.fontdefinition):
559 fontnr += 1
560 reflist.append(file.tell())
561 file.write("%d 0 obj\n"
562 "<<\n"
563 "/Type /Font\n"
564 "/Subtype /Type1\n"
565 "/Name /%s\n"
566 "/BaseFont /%s\n"
567 "/FirstChar 0\n"
568 "/LastChar 255\n"
569 "/Widths %d 0 R\n"
570 "/FontDescriptor %d 0 R\n"
571 "/Encoding /StandardEncoding\n" # FIXME
572 ">>\n"
573 "endobj\n" % (fontnr+fontstartref, pritem.font.getpsname(), pritem.font.getbasepsname(),
574 fontnr+fontstartref+1, fontnr+fontstartref+2))
575 fontnr += 1
576 reflist.append(file.tell())
577 file.write("%d 0 obj\n"
578 "[\n" % (fontnr+fontstartref))
579 for i in range(256):
580 try:
581 width = pritem.font.getwidth_pt(i)*1000/pritem.font.getsize_pt()
582 except:
583 width = 0
584 file.write("%f\n" % width)
585 file.write("]\n"
586 "endobj\n")
587 if pritem.filename:
588 fontnr += 1
589 reflist.append(file.tell())
590 file.write("%d 0 obj\n"
591 "<<\n"
592 "/Type /FontDescriptor\n"
593 "/FontName /%s\n"
594 "/Flags 4\n" # FIXME
595 "/FontBBox [-10 -10 1000 1000]\n" # FIXME
596 "/ItalicAngle 0\n" # FIXME
597 "/Ascent 20\n" # FIXME
598 "/Descent -5\n" # FIXME
599 "/CapHeight 15\n" # FIXME
600 "/StemV 3\n" # FIXME
601 "/FontFile %d 0 R\n" # FIXME
602 # "/CharSet \n" # fill in when stripping
603 ">>\n"
604 "endobj\n" % (fontnr+fontstartref, pritem.font.getbasepsname(),
605 fontnr+fontstartref+1))
607 fontnr += 1
608 reflist.append(file.tell())
610 fontdata = open(pykpathsea.find_file(pritem.filename, pykpathsea.kpse_type1_format)).read()
611 if fontdata[0:2] != fullfont._PFB_ASCII:
612 raise RuntimeError("PFB_ASCII mark expected")
613 length1 = fullfont.pfblength(fontdata[2:6])
614 if fontdata[6+length1:8+length1] != fullfont._PFB_BIN:
615 raise RuntimeError("PFB_BIN mark expected")
616 length2 = fullfont.pfblength(fontdata[8+length1:12+length1])
617 if fontdata[12+length1+length2:14+length1+length2] != fullfont._PFB_ASCII:
618 raise RuntimeError("PFB_ASCII mark expected")
619 length3 = fullfont.pfblength(fontdata[14+length1+length2:18+length1+length2])
620 if fontdata[18+length1+length2+length3:20+length1+length2+length3] != fullfont._PFB_DONE:
621 raise RuntimeError("PFB_DONE mark expected")
622 if len(fontdata) != 20 + length1 + length2 + length3:
623 raise RuntimeError("end of pfb file expected")
625 # we might be allowed to skip the third part ...
626 if fontdata[18+length1+length2:18+length1+length2+length3].replace("\n", "").replace("\r", "").replace("\t", "").replace(" ", "") == "0"*512 + "cleartomark":
627 length3 = 0
629 uncompresseddata = fontdata[6:6+length1] + fontdata[12+length1:12+length1+length2] + fontdata[18+length1+length2:18+length1+length2+length3]
630 compresseddata = zlib.compress(uncompresseddata)
632 file.write("%d 0 obj\n"
633 "<<\n"
634 "/Length %d\n"
635 "/Length1 %d\n"
636 "/Length2 %d\n"
637 "/Length3 %d\n"
638 "/Filter /FlateDecode\n"
639 ">>\n"
640 "stream\n" % (fontnr+fontstartref, len(compresseddata),
641 length1,
642 length2,
643 length3))
644 #file.write(fontdata[6:6+length1])
645 #file.write(fontdata[12+length1:12+length1+length2])
646 #file.write(fontdata[18+length1+length2:18+length1+length2+length3])
647 file.write(compresseddata)
648 file.write("endstream\n"
649 "endobj\n")
650 else:
651 fontnr += 2
653 xrefpos = file.tell()
654 file.write("xref\n"
655 "0 %d\n" % (len(reflist)+1))
656 file.write("0000000000 65535 f \n")
657 for ref in reflist:
658 file.write("%010i 00000 n \n" % ref)
659 file.write("trailer\n"
660 "<<\n"
661 "/Size 8\n"
662 "/Root 1 0 R\n"
663 ">>\n"
664 "startxref\n"
665 "%i\n"
666 "%%%%EOF\n" % xrefpos)
668 def writePDFfile_new(self, filename, paperformat=None, rotated=0, fittosize=0, margin=1 * unit.t_cm,
669 bbox=None, bboxenlarge=1 * unit.t_pt):
670 sys.stderr.write("*** PyX Warning: writePDFfile is experimental and supports only a subset of PyX's features\n")
672 if filename[-4:]!=".pdf":
673 filename = filename + ".pdf"
675 try:
676 writer = pdfwriter.pdfwriter(filename)
677 except IOError:
678 raise IOError("cannot open output file")
680 abbox = bbox is not None and bbox or self.bbox()
681 abbox.enlarge(bboxenlarge)
683 ctrafo = calctrafo(abbox, paperformat, margin, rotated, fittosize)
685 # if there has been a global transformation, adjust the bounding box
686 # accordingly
687 if ctrafo: abbox.transform(ctrafo)
689 mergedprolog = []
691 for pritem in self.prolog():
692 for mpritem in mergedprolog:
693 if mpritem.merge(pritem) is None: break
694 else:
695 mergedprolog.append(pritem)
696 writer.page(abbox, self, mergedprolog, ctrafo)
697 writer.close()
699 def writetofile(self, filename, *args, **kwargs):
700 if filename[-4:] == ".eps":
701 self.writeEPSfile(filename, *args, **kwargs)
702 elif filename[-4:] == ".pdf":
703 self.writePDFfile(filename, *args, **kwargs)
704 else:
705 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")
706 self.writeEPSfile(filename, *args, **kwargs)
708 class page(canvas):
710 def __init__(self, attrs=[], texrunner=None, pagename=None, paperformat="a4", rotated=0, fittosize=0,
711 margin=1 * unit.t_cm, bboxenlarge=1 * unit.t_pt):
712 canvas.__init__(self, attrs, texrunner)
713 self.pagename = pagename
714 self.paperformat = paperformat.capitalize()
715 self.rotated = rotated
716 self.fittosize = fittosize
717 self.margin = margin
718 self.bboxenlarge = bboxenlarge
720 def bbox(self):
721 # the bounding box of a page is fixed by its format and an optional rotation
722 pbbox = bbox.bbox(0, 0, *_paperformats[self.paperformat])
723 pbbox.enlarge(self.bboxenlarge)
724 if self.rotated:
725 pbbox.transform(trafo.rotate(90, *pbbox.center()))
726 return pbbox
728 def outputPS(self, file):
729 file.write("%%%%PageMedia: %s\n" % self.paperformat)
730 file.write("%%%%PageOrientation: %s\n" % (self.rotated and "Landscape" or "Portrait"))
731 # file.write("%%%%PageBoundingBox: %d %d %d %d\n" % (math.floor(pbbox.llx_pt), math.floor(pbbox.lly_pt),
732 # math.ceil(pbbox.urx_pt), math.ceil(pbbox.ury_pt)))
734 # page setup section
735 file.write("%%BeginPageSetup\n")
736 file.write("/pgsave save def\n")
737 # for scaling, we need the real bounding box of the page contents
738 pbbox = canvas.bbox(self)
739 pbbox.enlarge(self.bboxenlarge)
740 ptrafo = calctrafo(pbbox, self.paperformat, self.margin, self.rotated, self.fittosize)
741 if ptrafo:
742 ptrafo.outputPS(file)
743 file.write("%f setlinewidth\n" % unit.topt(style.linewidth.normal))
744 file.write("%%EndPageSetup\n")
746 # here comes the actual content
747 canvas.outputPS(self, file)
748 file.write("pgsave restore\n")
749 file.write("showpage\n")
750 # file.write("%%PageTrailer\n")
753 class document:
755 """holds a collection of page instances which are output as pages of a document"""
757 def __init__(self, pages=[]):
758 self.pages = pages
760 def append(self, page):
761 self.pages.append(page)
763 def writePSfile(self, filename):
764 """write pages to PS file """
766 if filename[-3:]!=".ps":
767 filename = filename + ".ps"
769 try:
770 file = open(filename, "w")
771 except IOError:
772 raise IOError("cannot open output file")
774 docbbox = None
775 for apage in self.pages:
776 pbbox = apage.bbox()
777 if docbbox is None:
778 docbbox = pbbox
779 else:
780 docbbox += pbbox
782 # document header
783 file.write("%!PS-Adobe-3.0\n")
784 docbbox.outputPS(file)
785 file.write("%%%%Creator: PyX %s\n" % version.version)
786 file.write("%%%%Title: %s\n" % filename)
787 file.write("%%%%CreationDate: %s\n" %
788 time.asctime(time.localtime(time.time())))
789 # required paper formats
790 paperformats = {}
791 for apage in self.pages:
792 if isinstance(apage, page):
793 paperformats[apage.paperformat] = _paperformats[apage.paperformat]
794 first = 1
795 for paperformat, size in paperformats.items():
796 if first:
797 file.write("%%DocumentMedia: ")
798 first = 0
799 else:
800 file.write("%%+ ")
801 file.write("%s %d %d 75 white ()\n" % (paperformat, unit.topt(size[0]), unit.topt(size[1])))
803 file.write("%%%%Pages: %d\n" % len(self.pages))
804 file.write("%%PageOrder: Ascend\n")
805 file.write("%%EndComments\n")
807 # document default section
808 #file.write("%%BeginDefaults\n")
809 #if paperformat:
810 # file.write("%%%%PageMedia: %s\n" % paperformat)
811 #file.write("%%%%PageOrientation: %s\n" % (rotated and "Landscape" or "Portrait"))
812 #file.write("%%EndDefaults\n")
814 # document prolog section
815 file.write("%%BeginProlog\n")
816 mergedprolog = []
817 for apage in self.pages:
818 for pritem in apage.prolog():
819 for mpritem in mergedprolog:
820 if mpritem.merge(pritem) is None: break
821 else:
822 mergedprolog.append(pritem)
823 for pritem in mergedprolog:
824 pritem.outputPS(file)
825 file.write("%%EndProlog\n")
827 # document setup section
828 #file.write("%%BeginSetup\n")
829 #file.write("%%EndSetup\n")
831 # pages section
832 for nr, apage in enumerate(self.pages):
833 file.write("%%%%Page: %s %d\n" % (apage.pagename is None and str(nr) or apage.pagename , nr+1))
834 apage.outputPS(file)
836 file.write("%%Trailer\n")
837 file.write("%%EOF\n")