- adjustments to the new graph data+style handling
[PyX/mjg.git] / pyx / canvas.py
blob61975355a5f8ae850e6e006972872c86b41f5d08
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 - base.PathStyle (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 attr.exclusiveattr.__init__(self, pattern)
288 _canvas.__init__(self)
289 attr.exclusiveattr.__init__(self, pattern)
290 self.id = "pattern%d" % id(self)
291 if painttype not in (1,2):
292 raise ValueError("painttype must be 1 or 2")
293 self.painttype = painttype
294 if tilingtype not in (1,2,3):
295 raise ValueError("tilingtype must be 1, 2 or 3")
296 self.tilingtype = tilingtype
297 self.xstep = xstep
298 self.ystep = ystep
299 self.patternbbox = bbox
300 self.patterntrafo = trafo
302 def bbox(self):
303 return None
305 def outputPS(self, file):
306 file.write("%s setpattern\n" % self.id)
308 def prolog(self):
309 realpatternbbox = _canvas.bbox(self)
310 if self.xstep is None:
311 xstep = unit.topt(realpatternbbox.width())
312 else:
313 xstep = unit.topt(self.xstep)
314 if self.ystep is None:
315 ystep = unit.topt(realpatternbbox.height())
316 else:
317 ystep = unit.topt(self.ystep)
318 if not xstep:
319 raise ValueError("xstep in pattern cannot be zero")
320 if not ystep:
321 raise ValueError("ystep in pattern cannot be zero")
322 patternbbox = self.patternbbox or realpatternbbox.enlarged(5*unit.pt)
324 patternprefix = "\n".join(("<<",
325 "/PatternType 1",
326 "/PaintType %d" % self.painttype,
327 "/TilingType %d" % self.tilingtype,
328 "/BBox[%s]" % str(patternbbox),
329 "/XStep %g" % xstep,
330 "/YStep %g" % ystep,
331 "/PaintProc {\nbegin\n"))
332 stringfile = cStringIO.StringIO()
333 _canvas.outputPS(self, stringfile)
334 patternproc = stringfile.getvalue()
335 stringfile.close()
336 patterntrafostring = self.patterntrafo is None and "matrix" or str(self.patterntrafo)
337 patternsuffix = "end\n} bind\n>>\n%s\nmakepattern" % patterntrafostring
339 pr = _canvas.prolog(self)
340 pr.append(prolog.definition(self.id, "".join((patternprefix, patternproc, patternsuffix))))
341 return pr
343 pattern.clear = attr.clearclass(pattern)
345 # helper function
347 def calctrafo(abbox, paperformat, margin, rotated, fittosize):
348 """ calculate a trafo which rotates and fits a canvas with
349 bounding box abbox on the given paperformat with a margin on all
350 sides"""
351 if not isinstance(margin, unit.length):
352 margin = unit.length(margin)
353 atrafo = None # global transformation of canvas
355 if rotated:
356 atrafo = trafo.rotate(90, *abbox.center())
358 if paperformat:
359 # center (optionally rotated) output on page
360 try:
361 paperwidth, paperheight = _paperformats[paperformat.capitalize()]
362 except KeyError:
363 raise KeyError, "unknown paperformat '%s'" % paperformat
365 paperwidth -= 2*margin
366 paperheight -= 2*margin
368 if not atrafo: atrafo = trafo.trafo()
370 atrafo = atrafo.translated(margin + 0.5*(paperwidth - abbox.width()) - abbox.left(),
371 margin + 0.5*(paperheight - abbox.height()) - abbox.bottom())
373 if fittosize:
374 # scale output to pagesize - margins
375 if 2*margin > min(paperwidth, paperheight):
376 raise RuntimeError("Margins too broad for selected paperformat. Aborting.")
378 if rotated:
379 sfactor = min(unit.topt(paperheight)/unit.topt(abbox.width()),
380 unit.topt(paperwidth)/unit.topt(abbox.height()))
381 else:
382 sfactor = min(unit.topt(paperwidth)/unit.topt(abbox.width()),
383 unit.topt(paperheight)/unit.topt(abbox.height()))
385 atrafo = atrafo.scaled(sfactor, sfactor, margin + 0.5*paperwidth, margin + 0.5*paperheight)
386 elif fittosize:
387 raise ValueError("must specify paper size for fittosize")
389 return atrafo
392 # The main canvas class
395 class canvas(_canvas):
397 """a canvas holds a collection of canvasitems"""
399 def writeEPSfile(self, filename, paperformat=None, rotated=0, fittosize=0, margin=1 * unit.t_cm,
400 bbox=None, bboxenlarge=1 * unit.t_pt):
401 """write canvas to EPS file
403 If paperformat is set to a known paperformat, the output will be centered on
404 the page.
406 If rotated is set, the output will first be rotated by 90 degrees.
408 If fittosize is set, then the output is scaled to the size of the
409 page (minus margin). In that case, the paperformat the specification
410 of the paperformat is obligatory.
412 The bbox parameter overrides the automatic bounding box determination.
413 bboxenlarge may be used to enlarge the bbox of the canvas (or the
414 manually specified bbox).
417 if filename[-4:]!=".eps":
418 filename = filename + ".eps"
420 try:
421 file = open(filename, "w")
422 except IOError:
423 raise IOError("cannot open output file")
425 abbox = bbox is not None and bbox or self.bbox()
426 abbox.enlarge(bboxenlarge)
427 ctrafo = calctrafo(abbox, paperformat, margin, rotated, fittosize)
429 # if there has been a global transformation, adjust the bounding box
430 # accordingly
431 if ctrafo: abbox.transform(ctrafo)
433 file.write("%!PS-Adobe-3.0 EPSF 3.0\n")
434 abbox.outputPS(file)
435 file.write("%%%%Creator: PyX %s\n" % version.version)
436 file.write("%%%%Title: %s\n" % filename)
437 file.write("%%%%CreationDate: %s\n" %
438 time.asctime(time.localtime(time.time())))
439 file.write("%%EndComments\n")
441 file.write("%%BeginProlog\n")
443 mergedprolog = []
445 for pritem in self.prolog():
446 for mpritem in mergedprolog:
447 if mpritem.merge(pritem) is None: break
448 else:
449 mergedprolog.append(pritem)
451 for pritem in mergedprolog:
452 pritem.outputPS(file)
454 file.write("%%EndProlog\n")
456 # apply a possible global transformation
457 if ctrafo: ctrafo.outputPS(file)
459 file.write("%f setlinewidth\n" % unit.topt(style.linewidth.normal))
461 # here comes the actual content
462 self.outputPS(file)
464 file.write("showpage\n")
465 file.write("%%Trailer\n")
466 file.write("%%EOF\n")
468 def writePDFfile(self, filename, paperformat=None, rotated=0, fittosize=0, margin=1 * unit.t_cm,
469 bbox=None, bboxenlarge=1 * unit.t_pt):
470 sys.stderr.write("*** PyX Warning: writePDFfile is experimental and supports only a subset of PyX's features\n")
472 if filename[-4:]!=".pdf":
473 filename = filename + ".pdf"
475 try:
476 file = open(filename, "wb")
477 except IOError:
478 raise IOError("cannot open output file")
480 abbox = bbox is not None and bbox or self.bbox()
481 abbox.enlarge(bboxenlarge)
483 ctrafo = calctrafo(abbox, paperformat, margin, rotated, fittosize)
485 # if there has been a global transformation, adjust the bounding box
486 # accordingly
487 if ctrafo: abbox.transform(ctrafo)
489 mergedprolog = []
491 for pritem in self.prolog():
492 for mpritem in mergedprolog:
493 if mpritem.merge(pritem) is None: break
494 else:
495 mergedprolog.append(pritem)
497 file.write("%%PDF-1.4\n%%%s%s%s%s\n" % (chr(195), chr(182), chr(195), chr(169)))
498 reflist = [file.tell()]
499 file.write("1 0 obj\n"
500 "<<\n"
501 "/Type /Catalog\n"
502 "/Pages 2 0 R\n"
503 ">>\n"
504 "endobj\n")
505 reflist.append(file.tell())
506 file.write("2 0 obj\n"
507 "<<\n"
508 "/Type /Pages\n"
509 "/Kids [3 0 R]\n"
510 "/Count 1\n"
511 ">>\n"
512 "endobj\n")
513 reflist.append(file.tell())
514 file.write("3 0 obj\n"
515 "<<\n"
516 "/Type /Page\n"
517 "/Parent 2 0 R\n"
518 "/MediaBox ")
519 abbox.outputPDF(file)
520 file.write("/Contents 4 0 R\n"
521 "/Resources <<\n")
522 fontstartref = 5
524 fontnr = 0
525 if len([pritem for pritem in mergedprolog if isinstance(pritem, prolog.fontdefinition)]):
526 file.write("/Font\n"
527 "<<\n")
528 for pritem in mergedprolog:
529 if isinstance(pritem, prolog.fontdefinition):
530 fontnr += 1
531 file.write("/%s %d 0 R\n" % (pritem.font.getpsname(), fontnr+fontstartref))
532 fontnr += 3 # further objects due to a font
533 file.write(">>\n")
535 file.write(">>\n"
536 ">>\n"
537 "endobj\n")
538 reflist.append(file.tell())
539 file.write("4 0 obj\n"
540 "<< /Length 5 0 R >>\n"
541 "stream\n")
542 streamstartpos = file.tell()
544 # apply a possible global transformation
545 if ctrafo: ctrafo.outputPDF(file)
546 style.linewidth.normal.outputPDF(file)
548 self.outputPDF(file)
549 streamendpos = file.tell()
550 file.write("endstream\n"
551 "endobj\n")
552 reflist.append(file.tell())
553 file.write("5 0 obj\n"
554 "%s\n"
555 "endobj\n" % (streamendpos - streamstartpos))
557 fontnr = 0
558 for pritem in mergedprolog:
559 if isinstance(pritem, prolog.fontdefinition):
560 fontnr += 1
561 reflist.append(file.tell())
562 file.write("%d 0 obj\n"
563 "<<\n"
564 "/Type /Font\n"
565 "/Subtype /Type1\n"
566 "/Name /%s\n"
567 "/BaseFont /%s\n"
568 "/FirstChar 0\n"
569 "/LastChar 255\n"
570 "/Widths %d 0 R\n"
571 "/FontDescriptor %d 0 R\n"
572 "/Encoding /StandardEncoding\n" # FIXME
573 ">>\n"
574 "endobj\n" % (fontnr+fontstartref, pritem.font.getpsname(), pritem.font.getbasepsname(),
575 fontnr+fontstartref+1, fontnr+fontstartref+2))
576 fontnr += 1
577 reflist.append(file.tell())
578 file.write("%d 0 obj\n"
579 "[\n" % (fontnr+fontstartref))
580 for i in range(256):
581 try:
582 width = pritem.font.getwidth_pt(i)*1000/pritem.font.getsize_pt()
583 except:
584 width = 0
585 file.write("%f\n" % width)
586 file.write("]\n"
587 "endobj\n")
588 if pritem.filename:
589 fontnr += 1
590 reflist.append(file.tell())
591 file.write("%d 0 obj\n"
592 "<<\n"
593 "/Type /FontDescriptor\n"
594 "/FontName /%s\n"
595 "/Flags 4\n" # FIXME
596 "/FontBBox [-10 -10 1000 1000]\n" # FIXME
597 "/ItalicAngle 0\n" # FIXME
598 "/Ascent 20\n" # FIXME
599 "/Descent -5\n" # FIXME
600 "/CapHeight 15\n" # FIXME
601 "/StemV 3\n" # FIXME
602 "/FontFile %d 0 R\n" # FIXME
603 # "/CharSet \n" # fill in when stripping
604 ">>\n"
605 "endobj\n" % (fontnr+fontstartref, pritem.font.getbasepsname(),
606 fontnr+fontstartref+1))
608 fontnr += 1
609 reflist.append(file.tell())
611 fontdata = open(pykpathsea.find_file(pritem.filename, pykpathsea.kpse_type1_format)).read()
612 if fontdata[0:2] != fullfont._PFB_ASCII:
613 raise RuntimeError("PFB_ASCII mark expected")
614 length1 = fullfont.pfblength(fontdata[2:6])
615 if fontdata[6+length1:8+length1] != fullfont._PFB_BIN:
616 raise RuntimeError("PFB_BIN mark expected")
617 length2 = fullfont.pfblength(fontdata[8+length1:12+length1])
618 if fontdata[12+length1+length2:14+length1+length2] != fullfont._PFB_ASCII:
619 raise RuntimeError("PFB_ASCII mark expected")
620 length3 = fullfont.pfblength(fontdata[14+length1+length2:18+length1+length2])
621 if fontdata[18+length1+length2+length3:20+length1+length2+length3] != fullfont._PFB_DONE:
622 raise RuntimeError("PFB_DONE mark expected")
623 if len(fontdata) != 20 + length1 + length2 + length3:
624 raise RuntimeError("end of pfb file expected")
626 # we might be allowed to skip the third part ...
627 if fontdata[18+length1+length2:18+length1+length2+length3].replace("\n", "").replace("\r", "").replace("\t", "").replace(" ", "") == "0"*512 + "cleartomark":
628 length3 = 0
630 uncompresseddata = fontdata[6:6+length1] + fontdata[12+length1:12+length1+length2] + fontdata[18+length1+length2:18+length1+length2+length3]
631 compresseddata = zlib.compress(uncompresseddata)
633 file.write("%d 0 obj\n"
634 "<<\n"
635 "/Length %d\n"
636 "/Length1 %d\n"
637 "/Length2 %d\n"
638 "/Length3 %d\n"
639 "/Filter /FlateDecode\n"
640 ">>\n"
641 "stream\n" % (fontnr+fontstartref, len(compresseddata),
642 length1,
643 length2,
644 length3))
645 #file.write(fontdata[6:6+length1])
646 #file.write(fontdata[12+length1:12+length1+length2])
647 #file.write(fontdata[18+length1+length2:18+length1+length2+length3])
648 file.write(compresseddata)
649 file.write("endstream\n"
650 "endobj\n")
651 else:
652 fontnr += 2
654 xrefpos = file.tell()
655 file.write("xref\n"
656 "0 %d\n" % (len(reflist)+1))
657 file.write("0000000000 65535 f \n")
658 for ref in reflist:
659 file.write("%010i 00000 n \n" % ref)
660 file.write("trailer\n"
661 "<<\n"
662 "/Size 8\n"
663 "/Root 1 0 R\n"
664 ">>\n"
665 "startxref\n"
666 "%i\n"
667 "%%%%EOF\n" % xrefpos)
669 def writePDFfile_new(self, filename, paperformat=None, rotated=0, fittosize=0, margin=1 * unit.t_cm,
670 bbox=None, bboxenlarge=1 * unit.t_pt):
671 sys.stderr.write("*** PyX Warning: writePDFfile is experimental and supports only a subset of PyX's features\n")
673 if filename[-4:]!=".pdf":
674 filename = filename + ".pdf"
676 try:
677 writer = pdfwriter.pdfwriter(filename)
678 except IOError:
679 raise IOError("cannot open output file")
681 abbox = bbox is not None and bbox or self.bbox()
682 abbox.enlarge(bboxenlarge)
684 ctrafo = calctrafo(abbox, paperformat, margin, rotated, fittosize)
686 # if there has been a global transformation, adjust the bounding box
687 # accordingly
688 if ctrafo: abbox.transform(ctrafo)
690 mergedprolog = []
692 for pritem in self.prolog():
693 for mpritem in mergedprolog:
694 if mpritem.merge(pritem) is None: break
695 else:
696 mergedprolog.append(pritem)
697 writer.page(abbox, self, mergedprolog, ctrafo)
698 writer.close()
700 def writetofile(self, filename, *args, **kwargs):
701 if filename[-4:] == ".eps":
702 self.writeEPSfile(filename, *args, **kwargs)
703 elif filename[-4:] == ".pdf":
704 self.writePDFfile(filename, *args, **kwargs)
705 else:
706 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")
707 self.writeEPSfile(filename, *args, **kwargs)
709 class page(canvas):
711 def __init__(self, attrs=[], texrunner=None, pagename=None, paperformat="a4", rotated=0, fittosize=0,
712 margin=1 * unit.t_cm, bboxenlarge=1 * unit.t_pt):
713 canvas.__init__(self, attrs, texrunner)
714 self.pagename = pagename
715 self.paperformat = paperformat.capitalize()
716 self.rotated = rotated
717 self.fittosize = fittosize
718 self.margin = margin
719 self.bboxenlarge = bboxenlarge
721 def bbox(self):
722 # the bounding box of a page is fixed by its format and an optional rotation
723 pbbox = bbox.bbox(0, 0, *_paperformats[self.paperformat])
724 pbbox.enlarge(self.bboxenlarge)
725 if self.rotated:
726 pbbox.transform(trafo.rotate(90, *pbbox.center()))
727 return pbbox
729 def outputPS(self, file):
730 file.write("%%%%PageMedia: %s\n" % self.paperformat)
731 file.write("%%%%PageOrientation: %s\n" % (self.rotated and "Landscape" or "Portrait"))
732 # file.write("%%%%PageBoundingBox: %d %d %d %d\n" % (math.floor(pbbox.llx_pt), math.floor(pbbox.lly_pt),
733 # math.ceil(pbbox.urx_pt), math.ceil(pbbox.ury_pt)))
735 # page setup section
736 file.write("%%BeginPageSetup\n")
737 file.write("/pgsave save def\n")
738 # for scaling, we need the real bounding box of the page contents
739 pbbox = canvas.bbox(self)
740 pbbox.enlarge(self.bboxenlarge)
741 ptrafo = calctrafo(pbbox, self.paperformat, self.margin, self.rotated, self.fittosize)
742 if ptrafo:
743 ptrafo.outputPS(file)
744 file.write("%f setlinewidth\n" % unit.topt(style.linewidth.normal))
745 file.write("%%EndPageSetup\n")
747 # here comes the actual content
748 canvas.outputPS(self, file)
749 file.write("pgsave restore\n")
750 file.write("showpage\n")
751 # file.write("%%PageTrailer\n")
754 class document:
756 """holds a collection of page instances which are output as pages of a document"""
758 def __init__(self, pages=[]):
759 self.pages = pages
761 def append(self, page):
762 self.pages.append(page)
764 def writePSfile(self, filename):
765 """write pages to PS file """
767 if filename[-3:]!=".ps":
768 filename = filename + ".ps"
770 try:
771 file = open(filename, "w")
772 except IOError:
773 raise IOError("cannot open output file")
775 docbbox = None
776 for apage in self.pages:
777 pbbox = apage.bbox()
778 if docbbox is None:
779 docbbox = pbbox
780 else:
781 docbbox += pbbox
783 # document header
784 file.write("%!PS-Adobe-3.0\n")
785 docbbox.outputPS(file)
786 file.write("%%%%Creator: PyX %s\n" % version.version)
787 file.write("%%%%Title: %s\n" % filename)
788 file.write("%%%%CreationDate: %s\n" %
789 time.asctime(time.localtime(time.time())))
790 # required paper formats
791 paperformats = {}
792 for apage in self.pages:
793 if isinstance(apage, page):
794 paperformats[apage.paperformat] = _paperformats[apage.paperformat]
795 first = 1
796 for paperformat, size in paperformats.items():
797 if first:
798 file.write("%%DocumentMedia: ")
799 first = 0
800 else:
801 file.write("%%+ ")
802 file.write("%s %d %d 75 white ()\n" % (paperformat, unit.topt(size[0]), unit.topt(size[1])))
804 file.write("%%%%Pages: %d\n" % len(self.pages))
805 file.write("%%PageOrder: Ascend\n")
806 file.write("%%EndComments\n")
808 # document default section
809 #file.write("%%BeginDefaults\n")
810 #if paperformat:
811 # file.write("%%%%PageMedia: %s\n" % paperformat)
812 #file.write("%%%%PageOrientation: %s\n" % (rotated and "Landscape" or "Portrait"))
813 #file.write("%%EndDefaults\n")
815 # document prolog section
816 file.write("%%BeginProlog\n")
817 mergedprolog = []
818 for apage in self.pages:
819 for pritem in apage.prolog():
820 for mpritem in mergedprolog:
821 if mpritem.merge(pritem) is None: break
822 else:
823 mergedprolog.append(pritem)
824 for pritem in mergedprolog:
825 pritem.outputPS(file)
826 file.write("%%EndProlog\n")
828 # document setup section
829 #file.write("%%BeginSetup\n")
830 #file.write("%%EndSetup\n")
832 # pages section
833 for nr, apage in enumerate(self.pages):
834 file.write("%%%%Page: %s %d\n" % (apage.pagename is None and str(nr) or apage.pagename , nr+1))
835 apage.outputPS(file)
837 file.write("%%Trailer\n")
838 file.write("%%EOF\n")