implement central and parallel projection in 3d graphs; correct axis (tick direction...
[PyX/mjg.git] / pyx / pswriter.py
blobe9f838ad7e64aeb3140340c59a961ba2617c2928
1 # -*- coding: ISO-8859-1 -*-
4 # Copyright (C) 2005-2006 Jörg Lehmann <joergl@users.sourceforge.net>
5 # Copyright (C) 2005-2006 André Wobst <wobsta@users.sourceforge.net>
7 # This file is part of PyX (http://pyx.sourceforge.net/).
9 # PyX is free software; you can redistribute it and/or modify
10 # it under the terms of the GNU General Public License as published by
11 # the Free Software Foundation; either version 2 of the License, or
12 # (at your option) any later version.
14 # PyX is distributed in the hope that it will be useful,
15 # but WITHOUT ANY WARRANTY; without even the implied warranty of
16 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 # GNU General Public License for more details.
19 # You should have received a copy of the GNU General Public License
20 # along with PyX; if not, write to the Free Software
21 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
23 import cStringIO, copy, time, math
24 import bbox, style, version, type1font, unit, trafo
26 try:
27 enumerate([])
28 except NameError:
29 # fallback implementation for Python 2.2 and below
30 def enumerate(list):
31 return zip(xrange(len(list)), list)
33 try:
34 dict([])
35 except NameError:
36 # fallback implementation for Python 2.1
37 def dict(list):
38 result = {}
39 for key, value in list:
40 result[key] = value
41 return result
44 class PSregistry:
46 def __init__(self):
47 # in order to keep a consistent order of the registered resources we
48 # not only store them in a hash but also keep an ordered list (up to a
49 # possible merging of resources, in which case the first instance is
50 # kept)
51 self.resourceshash = {}
52 self.resourceslist = []
54 def add(self, resource):
55 rkey = (resource.type, resource.id)
56 if self.resourceshash.has_key(rkey):
57 self.resourceshash[rkey].merge(resource)
58 else:
59 self.resourceshash[rkey] = resource
60 self.resourceslist.append(resource)
62 def mergeregistry(self, registry):
63 for resource in registry.resources:
64 self.add(resource)
66 def output(self, file, writer):
67 """ write all PostScript code of the prolog resources """
68 for resource in self.resourceslist:
69 resource.output(file, writer, self)
72 # Abstract base class
75 class PSresource:
77 """ a PostScript resource """
79 def __init__(self, type, id):
80 # Every PSresource has to have a type and a unique id.
81 # Resources with the same type and id will be merged
82 # when they are registered in the PSregistry
83 self.type = type
84 self.id = id
86 def merge(self, other):
87 """ merge self with other, which has to be a resource of the same type and with
88 the same id"""
89 pass
91 def output(self, file, writer, registry):
92 raise NotImplementedError("output not implemented for %s" % repr(self))
95 # Different variants of prolog items
98 class PSdefinition(PSresource):
100 """ PostScript function definition included in the prolog """
102 def __init__(self, id, body):
103 self.type = "definition"
104 self.id = id
105 self.body = body
107 def output(self, file, writer, registry):
108 file.write("%%%%BeginRessource: %s\n" % self.id)
109 file.write("%(body)s /%(id)s exch def\n" % self.__dict__)
110 file.write("%%EndRessource\n")
113 class PSfont:
115 def __init__(self, font, chars, registry):
116 if font.filename:
117 registry.add(PSfontfile(font.basefontname,
118 font.filename,
119 font.encoding,
120 chars))
121 if font.encoding and font.slant:
122 assert font.encname
123 # do first the reencoding and then the slanting:
124 enc_basename, enc_finalname = font.basefontname, font.encname
125 slt_basename, slt_finalname = tfont.encname, font.name
126 elif font.encoding:
127 enc_basename, enc_finalname = font.basefontname, font.name
128 elif font.slant:
129 slt_basename, slt_finalname = font.basefontname, font.name
131 if font.encoding:
132 registry.add(_ReEncodeFont)
133 registry.add(PSfontencoding(font.encoding))
134 registry.add(PSfontreencoding(enc_finalname, enc_basename, font.encoding.name))
136 if font.slant:
137 # we need the current fontmatrix in order to manipulate it:
138 # for this we need to re-read the fontfile as below in
139 # PSfontfile.ouput:
140 # XXX Is there a better way to do this?
141 t = trafo.trafo_pt(matrix=((1, font.slant), (0, 1)))
142 if font.filename:
143 # for the builtin fonts, we assume a trivial fontmatrix
144 import font.t1font as t1fontmodule
145 t1font = t1fontmodule.T1pfbfont(font.filename)
146 m = t1font.fontmatrixpattern.search(t1font.data1)
147 m11, m12, m21, m22, v1, v2 = map(float, m.groups()[:6])
148 t *= trafo.trafo_pt(matrix=((m11, m12), (m21, m22)), vector=(v1, v2))
149 else:
150 raise NotImplementedError(
151 "cannot slant unembedded fonts -- try to include \"download35.map\" in fontmaps")
152 registry.add(PSfontslanting(slt_finalname, slt_basename, t.__str__()))
155 class PSfontfile(PSresource):
157 """ PostScript font definition included in the prolog """
159 def __init__(self, name, filename, encoding, chars):
160 """ include type 1 font defined by the following parameters
162 - name: name of the PostScript font
163 - filename: name (without path) of file containing the font definition
164 - encfilename: name (without path) of file containing used encoding of font
165 or None (if no encoding file used)
166 - chars: character list to fill usedchars
170 # Note that here we only need the encoding for selecting the used glyphs!
172 self.type = "fontfile"
173 self.id = self.name = name
174 self.filename = filename
175 if encoding is None:
176 self.encodingfilename = None
177 else:
178 self.encodingfilename = encoding.filename
179 self.usedchars = {}
180 for char in chars:
181 self.usedchars[char] = 1
183 self.strip = 1
185 def merge(self, other):
186 if self.encodingfilename == other.encodingfilename:
187 self.usedchars.update(other.usedchars)
188 else:
189 # TODO: need to resolve the encoding when several encodings are in the play
190 self.strip = 0
192 def output(self, file, writer, registry):
193 import font.t1font
194 font = font.t1font.T1pfbfont(self.filename)
196 file.write("%%%%BeginFont: %s\n" % self.name)
197 # file.write("%%Included glyphs: %s\n" % " ".join(usedglyphs))
198 if self.strip:
199 # XXX: access to the encoding file
200 if self.encodingfilename:
201 encodingfile = type1font.encodingfile(self.encodingfilename, self.encodingfilename)
202 usedglyphs = dict([(encodingfile.decode(char)[1:], 1) for char in self.usedchars.keys()])
203 else:
204 font._encoding()
205 usedglyphs = dict([(font.encoding.decode(char), 1) for char in self.usedchars.keys()])
206 strippedfont = font.getstrippedfont(usedglyphs)
207 else:
208 strippedfont = font
209 strippedfont.outputPS(file, writer)
210 file.write("\n%%EndFont\n")
213 class PSfontencoding(PSresource):
215 """ PostScript font encoding vector included in the prolog """
217 def __init__(self, encoding):
218 """ include font encoding vector specified by encoding """
220 self.type = "fontencoding"
221 self.id = encoding.name
222 self.encoding = encoding
224 def output(self, file, writer, registry):
225 encodingfile = type1font.encodingfile(self.encoding.name, self.encoding.filename)
226 encodingfile.outputPS(file, writer)
229 class PSfontslanting(PSresource):
231 """ PostScript font slanting directive included in the prolog """
233 def __init__(self, fontname, basefontname, matrixstring):
234 """ include transformed font directive specified by
236 - fontname: PostScript FontName of the new slanted font
237 - basefontname: PostScript FontName of the original font
238 - slant: the value of slanting
241 self.type = "fontslanting"
242 self.id = self.fontname = fontname
243 self.basefontname = basefontname
244 self.matrixstring = matrixstring
246 def output(self, file, writer, registry):
247 file.write("%%%%BeginProcSet: %s\n" % self.fontname)
248 file.write("/%s findfont\n" % self.basefontname)
249 file.write("dup length dict begin\n")
250 file.write("{ 1 index /FID ne {def} {pop pop} ifelse } forall\n")
251 file.write("/FontMatrix %s readonly def\n" % self.matrixstring)
252 file.write("currentdict\n")
253 file.write("end\n")
254 file.write("/%s exch definefont pop\n" % self.fontname)
255 file.write("%%EndProcSet\n")
257 class PSfontreencoding(PSresource):
259 """ PostScript font re-encoding directive included in the prolog """
261 def __init__(self, fontname, basefontname, encodingname):
262 """ include font re-encoding directive specified by
264 - fontname: PostScript FontName of the new reencoded font
265 - basefontname: PostScript FontName of the original font
266 - encname: name of the encoding
268 Before being able to reencode a font, you have to include the
269 encoding via a fontencoding prolog item with name=encname
273 self.type = "fontreencoding"
274 self.id = self.fontname = fontname
275 self.basefontname = basefontname
276 self.encodingname = encodingname
278 def output(self, file, writer, registry):
279 file.write("%%%%BeginProcSet: %s\n" % self.fontname)
280 file.write("/%s /%s %s ReEncodeFont\n" % (self.basefontname, self.fontname, self.encodingname))
281 file.write("%%EndProcSet\n")
284 _ReEncodeFont = PSdefinition("ReEncodeFont", """{
285 5 dict
286 begin
287 /newencoding exch def
288 /newfontname exch def
289 /basefontname exch def
290 /basefontdict basefontname findfont def
291 /newfontdict basefontdict maxlength dict def
292 basefontdict {
293 exch dup dup /FID ne exch /Encoding ne and
294 { exch newfontdict 3 1 roll put }
295 { pop pop }
296 ifelse
297 } forall
298 newfontdict /FontName newfontname put
299 newfontdict /Encoding newencoding put
300 newfontname newfontdict definefont pop
302 }""")
305 class epswriter:
307 def __init__(self, document, file):
308 if len(document.pages) != 1:
309 raise ValueError("EPS file can be constructed out of a single page document only")
310 page = document.pages[0]
311 canvas = page.canvas
313 try:
314 file.write("")
315 except:
316 filename = file
317 if not filename.endswith(".eps"):
318 filename += ".eps"
319 try:
320 file = open(filename, "w")
321 except IOError:
322 raise IOError("cannot open output file")
323 else:
324 filename = "stream"
326 pagefile = cStringIO.StringIO()
327 registry = PSregistry()
328 acontext = context()
329 pagebbox = bbox.empty()
331 page.processPS(pagefile, self, acontext, registry, pagebbox)
333 file.write("%!PS-Adobe-3.0 EPSF-3.0\n")
334 if pagebbox:
335 file.write("%%%%BoundingBox: %d %d %d %d\n" % pagebbox.lowrestuple_pt())
336 file.write("%%%%HiResBoundingBox: %g %g %g %g\n" % pagebbox.highrestuple_pt())
337 file.write("%%%%Creator: PyX %s\n" % version.version)
338 file.write("%%%%Title: %s\n" % filename)
339 file.write("%%%%CreationDate: %s\n" %
340 time.asctime(time.localtime(time.time())))
341 file.write("%%EndComments\n")
343 file.write("%%BeginProlog\n")
344 registry.output(file, self)
345 file.write("%%EndProlog\n")
347 file.write(pagefile.getvalue())
348 pagefile.close()
350 file.write("showpage\n")
351 file.write("%%Trailer\n")
352 file.write("%%EOF\n")
355 class pswriter:
357 def __init__(self, document, file, writebbox=0):
358 try:
359 file.write("")
360 except:
361 filename = file
362 if not filename.endswith(".ps"):
363 filename += ".ps"
364 try:
365 file = open(filename, "w")
366 except IOError:
367 raise IOError("cannot open output file")
368 else:
369 filename = "stream"
371 # We first have to process the content of the pages, writing them into the stream pagesfile
372 # Doing so, we fill the registry and also calculate the page bounding boxes, which are
373 # stored in page._bbox for every page
374 pagesfile = cStringIO.StringIO()
375 registry = PSregistry()
377 # calculated bounding boxes of the whole document
378 documentbbox = bbox.empty()
380 for nr, page in enumerate(document.pages):
381 # process contents of page
382 pagefile = cStringIO.StringIO()
383 acontext = context()
384 pagebbox = bbox.empty()
385 page.processPS(pagefile, self, acontext, registry, pagebbox)
387 documentbbox += pagebbox
389 pagesfile.write("%%%%Page: %s %d\n" % (page.pagename is None and str(nr+1) or page.pagename, nr+1))
390 if page.paperformat:
391 pagesfile.write("%%%%PageMedia: %s\n" % page.paperformat.name)
392 pagesfile.write("%%%%PageOrientation: %s\n" % (page.rotated and "Landscape" or "Portrait"))
393 if pagebbox and writebbox:
394 pagesfile.write("%%%%PageBoundingBox: %d %d %d %d\n" % pagebbox.lowrestuple_pt())
396 # page setup section
397 pagesfile.write("%%BeginPageSetup\n")
398 pagesfile.write("/pgsave save def\n")
400 pagesfile.write("%%EndPageSetup\n")
401 pagesfile.write(pagefile.getvalue())
402 pagefile.close()
403 pagesfile.write("pgsave restore\n")
404 pagesfile.write("showpage\n")
405 pagesfile.write("%%PageTrailer\n")
407 file.write("%!PS-Adobe-3.0\n")
408 if documentbbox and writebbox:
409 file.write("%%%%BoundingBox: %d %d %d %d\n" % documentbbox.lowrestuple_pt())
410 file.write("%%%%HiResBoundingBox: %g %g %g %g\n" % documentbbox.highrestuple_pt())
411 file.write("%%%%Creator: PyX %s\n" % version.version)
412 file.write("%%%%Title: %s\n" % filename)
413 file.write("%%%%CreationDate: %s\n" %
414 time.asctime(time.localtime(time.time())))
416 # required paper formats
417 paperformats = {}
418 for page in document.pages:
419 if page.paperformat:
420 paperformats[page.paperformat] = page.paperformat
422 first = 1
423 for paperformat in paperformats.values():
424 if first:
425 file.write("%%DocumentMedia: ")
426 first = 0
427 else:
428 file.write("%%+ ")
429 file.write("%s %d %d 75 white ()\n" % (paperformat.name,
430 unit.topt(paperformat.width),
431 unit.topt(paperformat.height)))
433 # file.write(%%DocumentNeededResources: ") # register not downloaded fonts here
435 file.write("%%%%Pages: %d\n" % len(document.pages))
436 file.write("%%PageOrder: Ascend\n")
437 file.write("%%EndComments\n")
439 # document defaults section
440 #file.write("%%BeginDefaults\n")
441 #file.write("%%EndDefaults\n")
443 # document prolog section
444 file.write("%%BeginProlog\n")
445 registry.output(file, self)
446 file.write("%%EndProlog\n")
448 # document setup section
449 #file.write("%%BeginSetup\n")
450 #file.write("%%EndSetup\n")
452 file.write(pagesfile.getvalue())
453 pagesfile.close()
455 file.write("%%Trailer\n")
456 file.write("%%EOF\n")
458 class context:
460 def __init__(self):
461 self.linewidth_pt = None
462 self.colorspace = None
463 self.font = None
465 def __call__(self, **kwargs):
466 newcontext = copy.copy(self)
467 for key, value in kwargs.items():
468 setattr(newcontext, key, value)
469 return newcontext