- added new helper methods _distributeparams and _findnormpathitem to
[PyX/mjg.git] / pyx / canvas.py
blobfb95937f98bec39fa6fdc8f03a95ccbc7b3dfb5b
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, 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, style.fillstyle, style.strokestyle, trafo.trafo_pt])
220 for t in attr.getattrs(attrs, [trafo.trafo_pt]):
221 path = path.transformed(t)
223 dp = deco.decoratedpath(path)
225 # set global styles
226 dp.styles = attr.getattrs(attrs, [style.fillstyle, style.strokestyle])
228 # add path decorations and modify path accordingly
229 for adeco in attr.getattrs(attrs, [deco.deco]):
230 dp = adeco.decorate(dp)
232 self.insert(dp)
234 def stroke(self, path, attrs=[]):
235 """stroke path on canvas using the style given by args
237 The argument attrs consists of PathStyles, which modify
238 the appearance of the path, PathDecos, which add some new
239 visual elements to the path, or trafos, which are applied
240 before drawing the path.
244 self.draw(path, [deco.stroked]+list(attrs))
246 def fill(self, path, attrs=[]):
247 """fill path on canvas using the style given by args
249 The argument attrs consists of PathStyles, which modify
250 the appearance of the path, PathDecos, which add some new
251 visual elements to the path, or trafos, which are applied
252 before drawing the path.
256 self.draw(path, [deco.filled]+list(attrs))
258 def settexrunner(self, texrunner):
259 """sets the texrunner to be used to within the text and text_pt methods"""
261 self.texrunner = texrunner
263 def text(self, x, y, atext, *args, **kwargs):
264 """insert a text into the canvas
266 inserts a textbox created by self.texrunner.text into the canvas
268 returns the inserted textbox"""
270 return self.insert(self.texrunner.text(x, y, atext, *args, **kwargs))
273 def text_pt(self, x, y, atext, *args):
274 """insert a text into the canvas
276 inserts a textbox created by self.texrunner.text_pt into the canvas
278 returns the inserted textbox"""
280 return self.insert(self.texrunner.text_pt(x, y, atext, *args))
283 # canvas for patterns
286 class pattern(_canvas, attr.exclusiveattr, style.fillstyle):
288 def __init__(self, painttype=1, tilingtype=1, xstep=None, ystep=None, bbox=None, trafo=None):
289 attr.exclusiveattr.__init__(self, pattern)
290 _canvas.__init__(self)
291 attr.exclusiveattr.__init__(self, pattern)
292 self.id = "pattern%d" % id(self)
293 if painttype not in (1,2):
294 raise ValueError("painttype must be 1 or 2")
295 self.painttype = painttype
296 if tilingtype not in (1,2,3):
297 raise ValueError("tilingtype must be 1, 2 or 3")
298 self.tilingtype = tilingtype
299 self.xstep = xstep
300 self.ystep = ystep
301 self.patternbbox = bbox
302 self.patterntrafo = trafo
304 def bbox(self):
305 return None
307 def outputPS(self, file):
308 file.write("%s setpattern\n" % self.id)
310 def prolog(self):
311 realpatternbbox = _canvas.bbox(self)
312 if self.xstep is None:
313 xstep = unit.topt(realpatternbbox.width())
314 else:
315 xstep = unit.topt(self.xstep)
316 if self.ystep is None:
317 ystep = unit.topt(realpatternbbox.height())
318 else:
319 ystep = unit.topt(self.ystep)
320 if not xstep:
321 raise ValueError("xstep in pattern cannot be zero")
322 if not ystep:
323 raise ValueError("ystep in pattern cannot be zero")
324 patternbbox = self.patternbbox or realpatternbbox.enlarged(5*unit.pt)
326 patternprefix = "\n".join(("<<",
327 "/PatternType 1",
328 "/PaintType %d" % self.painttype,
329 "/TilingType %d" % self.tilingtype,
330 "/BBox[%s]" % str(patternbbox),
331 "/XStep %g" % xstep,
332 "/YStep %g" % ystep,
333 "/PaintProc {\nbegin\n"))
334 stringfile = cStringIO.StringIO()
335 _canvas.outputPS(self, stringfile)
336 patternproc = stringfile.getvalue()
337 stringfile.close()
338 patterntrafostring = self.patterntrafo is None and "matrix" or str(self.patterntrafo)
339 patternsuffix = "end\n} bind\n>>\n%s\nmakepattern" % patterntrafostring
341 pr = _canvas.prolog(self)
342 pr.append(prolog.definition(self.id, "".join((patternprefix, patternproc, patternsuffix))))
343 return pr
345 pattern.clear = attr.clearclass(pattern)
347 # helper function
349 def calctrafo(abbox, paperformat, margin, rotated, fittosize):
350 """ calculate a trafo which rotates and fits a canvas with
351 bounding box abbox on the given paperformat with a margin on all
352 sides"""
353 if not isinstance(margin, unit.length):
354 margin = unit.length(margin)
355 atrafo = None # global transformation of canvas
357 if rotated:
358 atrafo = trafo.rotate(90, *abbox.center())
360 if paperformat:
361 # center (optionally rotated) output on page
362 try:
363 paperwidth, paperheight = _paperformats[paperformat.capitalize()]
364 except KeyError:
365 raise KeyError, "unknown paperformat '%s'" % paperformat
367 paperwidth -= 2*margin
368 paperheight -= 2*margin
370 if not atrafo: atrafo = trafo.trafo()
372 atrafo = atrafo.translated(margin + 0.5*(paperwidth - abbox.width()) - abbox.left(),
373 margin + 0.5*(paperheight - abbox.height()) - abbox.bottom())
375 if fittosize:
376 # scale output to pagesize - margins
377 if 2*margin > min(paperwidth, paperheight):
378 raise RuntimeError("Margins too broad for selected paperformat. Aborting.")
380 if rotated:
381 sfactor = min(unit.topt(paperheight)/unit.topt(abbox.width()),
382 unit.topt(paperwidth)/unit.topt(abbox.height()))
383 else:
384 sfactor = min(unit.topt(paperwidth)/unit.topt(abbox.width()),
385 unit.topt(paperheight)/unit.topt(abbox.height()))
387 atrafo = atrafo.scaled(sfactor, sfactor, margin + 0.5*paperwidth, margin + 0.5*paperheight)
388 elif fittosize:
389 raise ValueError("must specify paper size for fittosize")
391 return atrafo
394 # The main canvas class
397 class canvas(_canvas):
399 """a canvas holds a collection of canvasitems"""
401 def writeEPSfile(self, filename, paperformat=None, rotated=0, fittosize=0, margin=1 * unit.t_cm,
402 bbox=None, bboxenlarge=1 * unit.t_pt):
403 """write canvas to EPS file
405 If paperformat is set to a known paperformat, the output will be centered on
406 the page.
408 If rotated is set, the output will first be rotated by 90 degrees.
410 If fittosize is set, then the output is scaled to the size of the
411 page (minus margin). In that case, the paperformat the specification
412 of the paperformat is obligatory.
414 The bbox parameter overrides the automatic bounding box determination.
415 bboxenlarge may be used to enlarge the bbox of the canvas (or the
416 manually specified bbox).
419 if filename[-4:]!=".eps":
420 filename = filename + ".eps"
422 try:
423 file = open(filename, "w")
424 except IOError:
425 raise IOError("cannot open output file")
427 abbox = bbox is not None and bbox or self.bbox()
428 abbox.enlarge(bboxenlarge)
429 ctrafo = calctrafo(abbox, paperformat, margin, rotated, fittosize)
431 # if there has been a global transformation, adjust the bounding box
432 # accordingly
433 if ctrafo: abbox.transform(ctrafo)
435 file.write("%!PS-Adobe-3.0 EPSF 3.0\n")
436 abbox.outputPS(file)
437 file.write("%%%%Creator: PyX %s\n" % version.version)
438 file.write("%%%%Title: %s\n" % filename)
439 file.write("%%%%CreationDate: %s\n" %
440 time.asctime(time.localtime(time.time())))
441 file.write("%%EndComments\n")
443 file.write("%%BeginProlog\n")
445 mergedprolog = []
447 for pritem in self.prolog():
448 for mpritem in mergedprolog:
449 if mpritem.merge(pritem) is None: break
450 else:
451 mergedprolog.append(pritem)
453 for pritem in mergedprolog:
454 pritem.outputPS(file)
456 file.write("%%EndProlog\n")
458 # apply a possible global transformation
459 if ctrafo: ctrafo.outputPS(file)
461 file.write("%f setlinewidth\n" % unit.topt(style.linewidth.normal))
463 # here comes the actual content
464 self.outputPS(file)
466 file.write("showpage\n")
467 file.write("%%Trailer\n")
468 file.write("%%EOF\n")
470 def writePDFfile(self, filename, paperformat=None, rotated=0, fittosize=0, margin=1 * unit.t_cm,
471 bbox=None, bboxenlarge=1 * unit.t_pt):
472 sys.stderr.write("*** PyX Warning: writePDFfile is experimental and supports only a subset of PyX's features\n")
474 if filename[-4:]!=".pdf":
475 filename = filename + ".pdf"
477 try:
478 file = open(filename, "wb")
479 except IOError:
480 raise IOError("cannot open output file")
482 abbox = bbox is not None and bbox or self.bbox()
483 abbox.enlarge(bboxenlarge)
485 ctrafo = calctrafo(abbox, paperformat, margin, rotated, fittosize)
487 # if there has been a global transformation, adjust the bounding box
488 # accordingly
489 if ctrafo: abbox.transform(ctrafo)
491 mergedprolog = []
493 for pritem in self.prolog():
494 for mpritem in mergedprolog:
495 if mpritem.merge(pritem) is None: break
496 else:
497 mergedprolog.append(pritem)
499 file.write("%%PDF-1.4\n%%%s%s%s%s\n" % (chr(195), chr(182), chr(195), chr(169)))
500 reflist = [file.tell()]
501 file.write("1 0 obj\n"
502 "<<\n"
503 "/Type /Catalog\n"
504 "/Pages 2 0 R\n"
505 ">>\n"
506 "endobj\n")
507 reflist.append(file.tell())
508 file.write("2 0 obj\n"
509 "<<\n"
510 "/Type /Pages\n"
511 "/Kids [3 0 R]\n"
512 "/Count 1\n"
513 ">>\n"
514 "endobj\n")
515 reflist.append(file.tell())
516 file.write("3 0 obj\n"
517 "<<\n"
518 "/Type /Page\n"
519 "/Parent 2 0 R\n"
520 "/MediaBox ")
521 abbox.outputPDF(file)
522 file.write("/Contents 4 0 R\n"
523 "/Resources <<\n")
524 fontstartref = 5
526 fontnr = 0
527 if len([pritem for pritem in mergedprolog if isinstance(pritem, prolog.fontdefinition)]):
528 file.write("/Font\n"
529 "<<\n")
530 for pritem in mergedprolog:
531 if isinstance(pritem, prolog.fontdefinition):
532 fontnr += 1
533 file.write("/%s %d 0 R\n" % (pritem.font.getpsname(), fontnr+fontstartref))
534 fontnr += 3 # further objects due to a font
535 file.write(">>\n")
537 file.write(">>\n"
538 ">>\n"
539 "endobj\n")
540 reflist.append(file.tell())
541 file.write("4 0 obj\n"
542 "<< /Length 5 0 R >>\n"
543 "stream\n")
544 streamstartpos = file.tell()
546 # apply a possible global transformation
547 if ctrafo: ctrafo.outputPDF(file)
548 style.linewidth.normal.outputPDF(file)
550 self.outputPDF(file)
551 streamendpos = file.tell()
552 file.write("endstream\n"
553 "endobj\n")
554 reflist.append(file.tell())
555 file.write("5 0 obj\n"
556 "%s\n"
557 "endobj\n" % (streamendpos - streamstartpos))
559 fontnr = 0
560 for pritem in mergedprolog:
561 if isinstance(pritem, prolog.fontdefinition):
562 fontnr += 1
563 reflist.append(file.tell())
564 file.write("%d 0 obj\n"
565 "<<\n"
566 "/Type /Font\n"
567 "/Subtype /Type1\n"
568 "/Name /%s\n"
569 "/BaseFont /%s\n"
570 "/FirstChar 0\n"
571 "/LastChar 255\n"
572 "/Widths %d 0 R\n"
573 "/FontDescriptor %d 0 R\n"
574 "/Encoding /StandardEncoding\n" # FIXME
575 ">>\n"
576 "endobj\n" % (fontnr+fontstartref, pritem.font.getpsname(), pritem.font.getbasepsname(),
577 fontnr+fontstartref+1, fontnr+fontstartref+2))
578 fontnr += 1
579 reflist.append(file.tell())
580 file.write("%d 0 obj\n"
581 "[\n" % (fontnr+fontstartref))
582 for i in range(256):
583 try:
584 width = pritem.font.getwidth_pt(i)*1000/pritem.font.getsize_pt()
585 except:
586 width = 0
587 file.write("%f\n" % width)
588 file.write("]\n"
589 "endobj\n")
590 if pritem.filename:
591 fontnr += 1
592 reflist.append(file.tell())
593 file.write("%d 0 obj\n"
594 "<<\n"
595 "/Type /FontDescriptor\n"
596 "/FontName /%s\n"
597 "/Flags 4\n" # FIXME
598 "/FontBBox [-10 -10 1000 1000]\n" # FIXME
599 "/ItalicAngle 0\n" # FIXME
600 "/Ascent 20\n" # FIXME
601 "/Descent -5\n" # FIXME
602 "/CapHeight 15\n" # FIXME
603 "/StemV 3\n" # FIXME
604 "/FontFile %d 0 R\n" # FIXME
605 # "/CharSet \n" # fill in when stripping
606 ">>\n"
607 "endobj\n" % (fontnr+fontstartref, pritem.font.getbasepsname(),
608 fontnr+fontstartref+1))
610 fontnr += 1
611 reflist.append(file.tell())
613 fontdata = open(pykpathsea.find_file(pritem.filename, pykpathsea.kpse_type1_format)).read()
614 if fontdata[0:2] != fullfont._PFB_ASCII:
615 raise RuntimeError("PFB_ASCII mark expected")
616 length1 = fullfont.pfblength(fontdata[2:6])
617 if fontdata[6+length1:8+length1] != fullfont._PFB_BIN:
618 raise RuntimeError("PFB_BIN mark expected")
619 length2 = fullfont.pfblength(fontdata[8+length1:12+length1])
620 if fontdata[12+length1+length2:14+length1+length2] != fullfont._PFB_ASCII:
621 raise RuntimeError("PFB_ASCII mark expected")
622 length3 = fullfont.pfblength(fontdata[14+length1+length2:18+length1+length2])
623 if fontdata[18+length1+length2+length3:20+length1+length2+length3] != fullfont._PFB_DONE:
624 raise RuntimeError("PFB_DONE mark expected")
625 if len(fontdata) != 20 + length1 + length2 + length3:
626 raise RuntimeError("end of pfb file expected")
628 # we might be allowed to skip the third part ...
629 if fontdata[18+length1+length2:18+length1+length2+length3].replace("\n", "").replace("\r", "").replace("\t", "").replace(" ", "") == "0"*512 + "cleartomark":
630 length3 = 0
632 uncompresseddata = fontdata[6:6+length1] + fontdata[12+length1:12+length1+length2] + fontdata[18+length1+length2:18+length1+length2+length3]
633 compresseddata = zlib.compress(uncompresseddata)
635 file.write("%d 0 obj\n"
636 "<<\n"
637 "/Length %d\n"
638 "/Length1 %d\n"
639 "/Length2 %d\n"
640 "/Length3 %d\n"
641 "/Filter /FlateDecode\n"
642 ">>\n"
643 "stream\n" % (fontnr+fontstartref, len(compresseddata),
644 length1,
645 length2,
646 length3))
647 #file.write(fontdata[6:6+length1])
648 #file.write(fontdata[12+length1:12+length1+length2])
649 #file.write(fontdata[18+length1+length2:18+length1+length2+length3])
650 file.write(compresseddata)
651 file.write("endstream\n"
652 "endobj\n")
653 else:
654 fontnr += 2
656 xrefpos = file.tell()
657 file.write("xref\n"
658 "0 %d\n" % (len(reflist)+1))
659 file.write("0000000000 65535 f \n")
660 for ref in reflist:
661 file.write("%010i 00000 n \n" % ref)
662 file.write("trailer\n"
663 "<<\n"
664 "/Size 8\n"
665 "/Root 1 0 R\n"
666 ">>\n"
667 "startxref\n"
668 "%i\n"
669 "%%%%EOF\n" % xrefpos)
671 def writePDFfile_new(self, filename, paperformat=None, rotated=0, fittosize=0, margin=1 * unit.t_cm,
672 bbox=None, bboxenlarge=1 * unit.t_pt):
673 sys.stderr.write("*** PyX Warning: writePDFfile is experimental and supports only a subset of PyX's features\n")
675 if filename[-4:]!=".pdf":
676 filename = filename + ".pdf"
678 try:
679 writer = pdfwriter.pdfwriter(filename)
680 except IOError:
681 raise IOError("cannot open output file")
683 abbox = bbox is not None and bbox or self.bbox()
684 abbox.enlarge(bboxenlarge)
686 ctrafo = calctrafo(abbox, paperformat, margin, rotated, fittosize)
688 # if there has been a global transformation, adjust the bounding box
689 # accordingly
690 if ctrafo: abbox.transform(ctrafo)
692 mergedprolog = []
694 for pritem in self.prolog():
695 for mpritem in mergedprolog:
696 if mpritem.merge(pritem) is None: break
697 else:
698 mergedprolog.append(pritem)
699 writer.page(abbox, self, mergedprolog, ctrafo)
700 writer.close()
702 def writetofile(self, filename, *args, **kwargs):
703 if filename[-4:] == ".eps":
704 self.writeEPSfile(filename, *args, **kwargs)
705 elif filename[-4:] == ".pdf":
706 self.writePDFfile(filename, *args, **kwargs)
707 else:
708 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")
709 self.writeEPSfile(filename, *args, **kwargs)
711 class page(canvas):
713 def __init__(self, attrs=[], texrunner=None, pagename=None, paperformat="a4", rotated=0, fittosize=0,
714 margin=1 * unit.t_cm, bboxenlarge=1 * unit.t_pt):
715 canvas.__init__(self, attrs, texrunner)
716 self.pagename = pagename
717 self.paperformat = paperformat.capitalize()
718 self.rotated = rotated
719 self.fittosize = fittosize
720 self.margin = margin
721 self.bboxenlarge = bboxenlarge
723 def bbox(self):
724 # the bounding box of a page is fixed by its format and an optional rotation
725 pbbox = bbox.bbox(0, 0, *_paperformats[self.paperformat])
726 pbbox.enlarge(self.bboxenlarge)
727 if self.rotated:
728 pbbox.transform(trafo.rotate(90, *pbbox.center()))
729 return pbbox
731 def outputPS(self, file):
732 file.write("%%%%PageMedia: %s\n" % self.paperformat)
733 file.write("%%%%PageOrientation: %s\n" % (self.rotated and "Landscape" or "Portrait"))
734 # file.write("%%%%PageBoundingBox: %d %d %d %d\n" % (math.floor(pbbox.llx_pt), math.floor(pbbox.lly_pt),
735 # math.ceil(pbbox.urx_pt), math.ceil(pbbox.ury_pt)))
737 # page setup section
738 file.write("%%BeginPageSetup\n")
739 file.write("/pgsave save def\n")
740 # for scaling, we need the real bounding box of the page contents
741 pbbox = canvas.bbox(self)
742 pbbox.enlarge(self.bboxenlarge)
743 ptrafo = calctrafo(pbbox, self.paperformat, self.margin, self.rotated, self.fittosize)
744 if ptrafo:
745 ptrafo.outputPS(file)
746 file.write("%f setlinewidth\n" % unit.topt(style.linewidth.normal))
747 file.write("%%EndPageSetup\n")
749 # here comes the actual content
750 canvas.outputPS(self, file)
751 file.write("pgsave restore\n")
752 file.write("showpage\n")
753 # file.write("%%PageTrailer\n")
756 class document:
758 """holds a collection of page instances which are output as pages of a document"""
760 def __init__(self, pages=[]):
761 self.pages = pages
763 def append(self, page):
764 self.pages.append(page)
766 def writePSfile(self, filename):
767 """write pages to PS file """
769 if filename[-3:]!=".ps":
770 filename = filename + ".ps"
772 try:
773 file = open(filename, "w")
774 except IOError:
775 raise IOError("cannot open output file")
777 docbbox = None
778 for apage in self.pages:
779 pbbox = apage.bbox()
780 if docbbox is None:
781 docbbox = pbbox
782 else:
783 docbbox += pbbox
785 # document header
786 file.write("%!PS-Adobe-3.0\n")
787 docbbox.outputPS(file)
788 file.write("%%%%Creator: PyX %s\n" % version.version)
789 file.write("%%%%Title: %s\n" % filename)
790 file.write("%%%%CreationDate: %s\n" %
791 time.asctime(time.localtime(time.time())))
792 # required paper formats
793 paperformats = {}
794 for apage in self.pages:
795 if isinstance(apage, page):
796 paperformats[apage.paperformat] = _paperformats[apage.paperformat]
797 first = 1
798 for paperformat, size in paperformats.items():
799 if first:
800 file.write("%%DocumentMedia: ")
801 first = 0
802 else:
803 file.write("%%+ ")
804 file.write("%s %d %d 75 white ()\n" % (paperformat, unit.topt(size[0]), unit.topt(size[1])))
806 file.write("%%%%Pages: %d\n" % len(self.pages))
807 file.write("%%PageOrder: Ascend\n")
808 file.write("%%EndComments\n")
810 # document default section
811 #file.write("%%BeginDefaults\n")
812 #if paperformat:
813 # file.write("%%%%PageMedia: %s\n" % paperformat)
814 #file.write("%%%%PageOrientation: %s\n" % (rotated and "Landscape" or "Portrait"))
815 #file.write("%%EndDefaults\n")
817 # document prolog section
818 file.write("%%BeginProlog\n")
819 mergedprolog = []
820 for apage in self.pages:
821 for pritem in apage.prolog():
822 for mpritem in mergedprolog:
823 if mpritem.merge(pritem) is None: break
824 else:
825 mergedprolog.append(pritem)
826 for pritem in mergedprolog:
827 pritem.outputPS(file)
828 file.write("%%EndProlog\n")
830 # document setup section
831 #file.write("%%BeginSetup\n")
832 #file.write("%%EndSetup\n")
834 # pages section
835 for nr, apage in enumerate(self.pages):
836 file.write("%%%%Page: %s %d\n" % (apage.pagename is None and str(nr) or apage.pagename , nr+1))
837 apage.outputPS(file)
839 file.write("%%Trailer\n")
840 file.write("%%EOF\n")