disable outlines (we do not yet have those)
[PyX/mjg.git] / pyx / canvas.py
blob5dae53626248ac72270ceec1411d398754fa30e2
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, math, time
33 import attr, base, bbox, deco, unit, prolog, style, trafo, version
35 # temporary for pdf fonts:
36 import zlib
37 from t1strip import fullfont
38 import pykpathsea
40 try:
41 enumerate([])
42 except NameError:
43 # fallback implementation for Python 2.2. and below
44 def enumerate(list):
45 return zip(xrange(len(list)), list)
47 # known paperformats as tuple (width, height)
49 _paperformats = { "A4" : (210 * unit.t_mm, 297 * unit.t_mm),
50 "A3" : (297 * unit.t_mm, 420 * unit.t_mm),
51 "A2" : (420 * unit.t_mm, 594 * unit.t_mm),
52 "A1" : (594 * unit.t_mm, 840 * unit.t_mm),
53 "A0" : (840 * unit.t_mm, 1188 * unit.t_mm),
54 "A0b" : (910 * unit.t_mm, 1370 * unit.t_mm),
55 "Letter" : (8.5 * unit.t_inch, 11 * unit.t_inch),
56 "Legal" : (8.5 * unit.t_inch, 14 * unit.t_inch)}
59 # clipping class
62 class clip(base.PSCmd):
64 """class for use in canvas constructor which clips to a path"""
66 def __init__(self, path):
67 """construct a clip instance for a given path"""
68 self.path = path
70 def bbox(self):
71 # as a PSCmd a clipping path has NO influence on the bbox...
72 return None
74 def clipbbox(self):
75 # ... but for clipping, we nevertheless need the bbox
76 return self.path.bbox()
78 def outputPS(self, file):
79 file.write("newpath\n")
80 self.path.outputPS(file)
81 file.write("clip\n")
83 def outputPDF(self, file):
84 self.path.outputPDF(file)
85 file.write("W n\n")
88 # general canvas class
91 class _canvas(base.PSCmd):
93 """a canvas is a collection of PSCmds together with PSAttrs"""
95 def __init__(self, attrs=[], texrunner=None):
97 """construct a canvas
99 The canvas can be modfied by supplying args, which have
100 to be instances of one of the following classes:
101 - trafo.trafo (leading to a global transformation of the canvas)
102 - canvas.clip (clips the canvas)
103 - base.PathStyle (sets some global attributes of the canvas)
105 Note that, while the first two properties are fixed for the
106 whole canvas, the last one can be changed via canvas.set().
108 The texrunner instance used for the text method can be specified
109 using the texrunner argument. It defaults to text.defaulttexrunner
113 self.PSOps = []
114 self.trafo = trafo.trafo()
115 self.clipbbox = None
116 if texrunner is not None:
117 self.texrunner = texrunner
118 else:
119 # prevent cyclic imports
120 import text
121 self.texrunner = text.defaulttexrunner
123 for attr in attrs:
124 if isinstance(attr, trafo.trafo_pt):
125 self.trafo = self.trafo*attr
126 self.PSOps.append(attr)
127 elif isinstance(attr, clip):
128 if self.clipbbox is None:
129 self.clipbbox = attr.clipbbox().transformed(self.trafo)
130 else:
131 self.clippbox *= attr.clipbbox().transformed(self.trafo)
132 self.PSOps.append(attr)
133 else:
134 self.set([attr])
136 def bbox(self):
137 """returns bounding box of canvas"""
138 obbox = None
139 for cmd in self.PSOps:
140 if isinstance(cmd, base.PSCmd):
141 abbox = cmd.bbox()
142 if obbox is None:
143 obbox = abbox
144 elif abbox is not None:
145 obbox += abbox
147 # transform according to our global transformation and
148 # intersect with clipping bounding box (which have already been
149 # transformed in canvas.__init__())
150 if obbox is not None and self.clipbbox is not None:
151 return obbox.transformed(self.trafo)*self.clipbbox
152 elif obbox is not None:
153 return obbox.transformed(self.trafo)
154 else:
155 return self.clipbbox
157 def prolog(self):
158 result = []
159 for cmd in self.PSOps:
160 result.extend(cmd.prolog())
161 return result
163 def outputPS(self, file):
164 if self.PSOps:
165 file.write("gsave\n")
166 for cmd in self.PSOps:
167 cmd.outputPS(file)
168 file.write("grestore\n")
170 def outputPDF(self, file):
171 if self.PSOps:
172 file.write("q\n") # gsave
173 for cmd in self.PSOps:
174 cmd.outputPDF(file)
175 file.write("Q\n") # grestore
177 def insert(self, PSOp, attrs=[]):
178 """insert PSOp in the canvas.
180 If attrss are given, a canvas containing the PSOp is inserted applying attrs.
182 returns the PSOp
186 # XXX check for PSOp
188 if attrs:
189 sc = _canvas(attrs)
190 sc.insert(PSOp)
191 self.PSOps.append(sc)
192 else:
193 self.PSOps.append(PSOp)
195 return PSOp
197 def set(self, attrs):
198 """sets styles args globally for the rest of the canvas
201 attr.checkattrs(attrs, [style.strokestyle, style.fillstyle])
202 for astyle in attrs:
203 self.insert(astyle)
205 def draw(self, path, attrs):
206 """draw path on canvas using the style given by args
208 The argument attrs consists of PathStyles, which modify
209 the appearance of the path, PathDecos, which add some new
210 visual elements to the path, or trafos, which are applied
211 before drawing the path.
215 attrs = attr.mergeattrs(attrs)
216 attr.checkattrs(attrs, [deco.deco, style.fillstyle, style.strokestyle, trafo.trafo_pt])
218 for t in attr.getattrs(attrs, [trafo.trafo_pt]):
219 path = path.transformed(t)
221 dp = deco.decoratedpath(path)
223 # set global styles
224 dp.styles = attr.getattrs(attrs, [style.fillstyle, style.strokestyle])
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(unit.length(self.xstep))
314 if self.ystep is None:
315 ystep = unit.topt(realpatternbbox.height())
316 else:
317 ystep = unit.topt(unit.length(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 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 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 is a collection of PSCmds together with PSAttrs"""
398 def writeEPSfile(self, filename, paperformat=None, rotated=0, fittosize=0, margin="1 t cm",
399 bbox=None, bboxenlarge="1 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 t cm",
468 bbox=None, bboxenlarge="1 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 abbox.outputPDF(file)
518 file.write("/Contents 4 0 R\n"
519 "/Resources <<\n")
520 fontstartref = 5
522 fontnr = 0
523 if len([pritem for pritem in mergedprolog if isinstance(pritem, prolog.fontdefinition)]):
524 file.write("/Font\n"
525 "<<\n")
526 for pritem in mergedprolog:
527 if isinstance(pritem, prolog.fontdefinition):
528 fontnr += 1
529 file.write("/%s %d 0 R\n" % (pritem.font.getpsname(), fontnr+fontstartref))
530 fontnr += 3 # further objects due to a font
531 file.write(">>\n")
533 file.write(">>\n"
534 ">>\n"
535 "endobj\n")
536 reflist.append(file.tell())
537 file.write("4 0 obj\n"
538 "<< /Length 5 0 R >>\n"
539 "stream\n")
540 streamstartpos = file.tell()
542 # apply a possible global transformation
543 if ctrafo: ctrafo.outputPDF(file)
544 style.linewidth.normal.outputPDF(file)
546 self.outputPDF(file)
547 streamendpos = file.tell()
548 file.write("endstream\n"
549 "endobj\n")
550 reflist.append(file.tell())
551 file.write("5 0 obj\n"
552 "%s\n"
553 "endobj\n" % (streamendpos - streamstartpos))
555 fontnr = 0
556 for pritem in mergedprolog:
557 if isinstance(pritem, prolog.fontdefinition):
558 fontnr += 1
559 reflist.append(file.tell())
560 file.write("%d 0 obj\n"
561 "<<\n"
562 "/Type /Font\n"
563 "/Subtype /Type1\n"
564 "/Name /%s\n"
565 "/BaseFont /%s\n"
566 "/FirstChar 0\n"
567 "/LastChar 255\n"
568 "/Widths %d 0 R\n"
569 "/FontDescriptor %d 0 R\n"
570 "/Encoding /StandardEncoding\n" # FIXME
571 ">>\n"
572 "endobj\n" % (fontnr+fontstartref, pritem.font.getpsname(), pritem.font.getbasepsname(),
573 fontnr+fontstartref+1, fontnr+fontstartref+2))
574 fontnr += 1
575 reflist.append(file.tell())
576 file.write("%d 0 obj\n"
577 "[\n" % (fontnr+fontstartref))
578 tfmconv_times_conv = 9.50111398661e-07 # FIXME this is the product of tfmconv and conv
579 # we need to get that from the dvifile!?
580 for i in range(256):
581 try:
582 width = pritem.font.getwidth(i)*tfmconv_times_conv*1000/pritem.font.getsize()
583 except:
584 width = 0
585 file.write("%f\n" % width)
586 file.write("]\n"
587 "endobj\n")
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 34\n" # FIXME
595 "/FontBBox [-10 -10 40 40]\n" # FIXME
596 "/ItalicAngle 0\n" # FIXME
597 "/Ascent 20\n" # FIXME
598 "/Descent -5\n" # FIXME
599 "/CapHeight 15\n" # FIXME
600 "/StemV 1\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")
651 xrefpos = file.tell()
652 file.write("xref\n"
653 "0 %d\n" % (len(reflist)+1))
654 file.write("0000000000 65535 f \n")
655 for ref in reflist:
656 file.write("%010i 00000 n \n" % ref)
657 file.write("trailer\n"
658 "<<\n"
659 "/Size 8\n"
660 "/Root 1 0 R\n"
661 ">>\n"
662 "startxref\n"
663 "%i\n"
664 "%%%%EOF\n" % xrefpos)
666 def writetofile(self, filename, *args, **kwargs):
667 if filename[-4:] == ".eps":
668 self.writeEPSfile(filename, *args, **kwargs)
669 elif filename[-4:] == ".pdf":
670 self.writePDFfile(filename, *args, **kwargs)
671 else:
672 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")
673 self.writeEPSfile(filename, *args, **kwargs)
675 class page(canvas):
677 def __init__(self, attrs=[], texrunner=None, pagename=None, paperformat="a4", rotated=0, fittosize=0,
678 margin="1 t cm", bboxenlarge="1 t pt"):
679 canvas.__init__(self, attrs, texrunner)
680 self.pagename = pagename
681 self.paperformat = paperformat.capitalize()
682 self.rotated = rotated
683 self.fittosize = fittosize
684 self.margin = margin
685 self.bboxenlarge = bboxenlarge
687 def bbox(self):
688 # the bounding box of a page is fixed by its format and an optional rotation
689 pbbox = bbox.bbox(0, 0, *_paperformats[self.paperformat])
690 pbbox.enlarge(self.bboxenlarge)
691 if self.rotated:
692 pbbox.transform(trafo.rotate(90, *pbbox.center()))
693 return pbbox
695 def outputPS(self, file):
696 file.write("%%%%PageMedia: %s\n" % self.paperformat)
697 file.write("%%%%PageOrientation: %s\n" % (self.rotated and "Landscape" or "Portrait"))
698 # file.write("%%%%PageBoundingBox: %d %d %d %d\n" % (math.floor(pbbox.llx_pt), math.floor(pbbox.lly_pt),
699 # math.ceil(pbbox.urx_pt), math.ceil(pbbox.ury_pt)))
701 # page setup section
702 file.write("%%BeginPageSetup\n")
703 file.write("/pgsave save def\n")
704 # for scaling, we need the real bounding box of the page contents
705 pbbox = canvas.bbox(self)
706 pbbox.enlarge(self.bboxenlarge)
707 ptrafo = calctrafo(pbbox, self.paperformat, self.margin, self.rotated, self.fittosize)
708 if ptrafo:
709 ptrafo.outputPS(file)
710 file.write("%f setlinewidth\n" % unit.topt(style.linewidth.normal))
711 file.write("%%EndPageSetup\n")
713 # here comes the actual content
714 canvas.outputPS(self, file)
715 file.write("pgsave restore\n")
716 file.write("showpage\n")
717 # file.write("%%PageTrailer\n")
720 class document:
722 """holds a collection of page instances which are output as pages of a document"""
724 def __init__(self, pages=[]):
725 self.pages = pages
727 def append(self, page):
728 self.pages.append(page)
730 def writePSfile(self, filename):
731 """write pages to PS file """
733 if filename[-3:]!=".ps":
734 filename = filename + ".ps"
736 try:
737 file = open(filename, "w")
738 except IOError:
739 raise IOError("cannot open output file")
741 docbbox = None
742 for apage in self.pages:
743 pbbox = apage.bbox()
744 if docbbox is None:
745 docbbox = pbbox
746 else:
747 docbbox += pbbox
749 # document header
750 file.write("%!PS-Adobe-3.0\n")
751 docbbox.outputPS(file)
752 file.write("%%%%Creator: PyX %s\n" % version.version)
753 file.write("%%%%Title: %s\n" % filename)
754 file.write("%%%%CreationDate: %s\n" %
755 time.asctime(time.localtime(time.time())))
756 # required paper formats
757 paperformats = {}
758 for apage in self.pages:
759 if isinstance(apage, page):
760 paperformats[apage.paperformat] = _paperformats[apage.paperformat]
761 first = 1
762 for paperformat, size in paperformats.items():
763 if first:
764 file.write("%%DocumentMedia: ")
765 first = 0
766 else:
767 file.write("%%+ ")
768 file.write("%s %d %d 75 white ()\n" % (paperformat, unit.topt(size[0]), unit.topt(size[1])))
770 file.write("%%%%Pages: %d\n" % len(self.pages))
771 file.write("%%PageOrder: Ascend\n")
772 file.write("%%EndComments\n")
774 # document default section
775 #file.write("%%BeginDefaults\n")
776 #if paperformat:
777 # file.write("%%%%PageMedia: %s\n" % paperformat)
778 #file.write("%%%%PageOrientation: %s\n" % (rotated and "Landscape" or "Portrait"))
779 #file.write("%%EndDefaults\n")
781 # document prolog section
782 file.write("%%BeginProlog\n")
783 mergedprolog = []
784 for apage in self.pages:
785 for pritem in apage.prolog():
786 for mpritem in mergedprolog:
787 if mpritem.merge(pritem) is None: break
788 else:
789 mergedprolog.append(pritem)
790 for pritem in mergedprolog:
791 pritem.outputPS(file)
792 file.write("%%EndProlog\n")
794 # document setup section
795 #file.write("%%BeginSetup\n")
796 #file.write("%%EndSetup\n")
798 # pages section
799 for nr, apage in enumerate(self.pages):
800 file.write("%%%%Page: %s %d\n" % (apage.pagename is None and str(nr) or apage.pagename , nr+1))
801 apage.outputPS(file)
803 file.write("%%Trailer\n")
804 file.write("%%EOF\n")