make the process with processmethod local to the page (still getattr, but at least...
[PyX.git] / pyx / pdfwriter.py
blobc11e8bad1642a0e71e5dce0a331a49afc5d43217
1 #!/usr/bin/env python
2 # -*- coding: ISO-8859-1 -*-
5 # Copyright (C) 2005-2006 Jörg Lehmann <joergl@users.sourceforge.net>
6 # Copyright (C) 2005-2006 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
24 import cStringIO, copy, warnings, time
25 try:
26 import zlib
27 haszlib = 1
28 except:
29 haszlib = 0
31 import bbox, unit, style, type1font, version
33 try:
34 enumerate([])
35 except NameError:
36 # fallback implementation for Python 2.2 and below
37 def enumerate(list):
38 return zip(xrange(len(list)), list)
41 class PDFregistry:
43 def __init__(self):
44 self.types = {}
45 # we want to keep the order of the resources
46 self.objects = []
47 self.resources = {}
48 self.procsets = {"PDF": 1}
49 self.merged = None
51 def add(self, object):
52 """ register object, merging it with an already registered object of the same type and id """
53 sameobjects = self.types.setdefault(object.type, {})
54 if sameobjects.has_key(object.id):
55 sameobjects[object.id].merge(object)
56 else:
57 self.objects.append(object)
58 sameobjects[object.id] = object
60 def getrefno(self, object):
61 if self.merged:
62 return self.merged.getrefno(object)
63 else:
64 return self.types[object.type][object.id].refno
66 def mergeregistry(self, registry):
67 for object in registry.objects:
68 self.add(object)
69 registry.merged = self
71 def write(self, file, writer, catalog):
72 # first we set all refnos
73 refno = 1
74 for object in self.objects:
75 object.refno = refno
76 refno += 1
78 # second, all objects are written, keeping the positions in the output file
79 fileposes = []
80 for object in self.objects:
81 fileposes.append(file.tell())
82 file.write("%i 0 obj\n" % object.refno)
83 object.write(file, writer, self)
84 file.write("endobj\n")
86 # xref
87 xrefpos = file.tell()
88 file.write("xref\n"
89 "0 %d\n"
90 "0000000000 65535 f \n" % refno)
92 for filepos in fileposes:
93 file.write("%010i 00000 n \n" % filepos)
95 # trailer
96 file.write("trailer\n"
97 "<<\n"
98 "/Size %i\n" % refno)
99 file.write("/Root %i 0 R\n" % self.getrefno(catalog))
100 file.write("/Info %i 0 R\n" % self.getrefno(catalog.PDFinfo))
101 file.write(">>\n"
102 "startxref\n"
103 "%i\n" % xrefpos)
104 file.write("%%EOF\n")
106 def addresource(self, resourcetype, resourcename, object, procset=None):
107 self.resources.setdefault(resourcetype, {})[resourcename] = object
108 if procset:
109 self.procsets[procset] = 1
111 def writeresources(self, file):
112 file.write("/Resources <<\n")
113 file.write("/ProcSet [ %s ]\n" % " ".join(["/%s" % p for p in self.procsets.keys()]))
114 if self.resources:
115 for resourcetype, resources in self.resources.items():
116 file.write("/%s <<\n%s\n>>\n" % (resourcetype, "\n".join(["/%s %i 0 R" % (name, self.getrefno(object))
117 for name, object in resources.items()])))
118 file.write(">>\n")
121 class PDFobject:
123 def __init__(self, type, _id=None):
124 """create a PDFobject
125 - type has to be a string describing the type of the object
126 - _id is a unique identification used for the object if it is not None.
127 Otherwise id(self) is used
129 self.type = type
130 if _id is None:
131 self.id = id(self)
132 else:
133 self.id = _id
135 def merge(self, other):
136 pass
138 def write(self, file, writer, registry):
139 raise NotImplementedError("write method has to be provided by PDFobject subclass")
142 class PDFcatalog(PDFobject):
144 def __init__(self, document, writer, registry):
145 PDFobject.__init__(self, "catalog")
146 self.PDFpages = PDFpages(document, writer, registry)
147 registry.add(self.PDFpages)
148 self.PDFinfo = PDFinfo()
149 registry.add(self.PDFinfo)
151 def write(self, file, writer, registry):
152 file.write("<<\n"
153 "/Type /Catalog\n"
154 "/Pages %i 0 R\n" % registry.getrefno(self.PDFpages))
155 if writer.fullscreen:
156 file.write("/PageMode /FullScreen\n")
157 file.write(">>\n")
160 class PDFinfo(PDFobject):
162 def __init__(self):
163 PDFobject.__init__(self, "info")
165 def write(self, file, writer, registry):
166 if time.timezone < 0:
167 # divmod on positive numbers, otherwise the minutes have a different sign from the hours
168 timezone = "-%02i'%02i'" % divmod(-time.timezone/60, 60)
169 elif time.timezone > 0:
170 timezone = "+%02i'%02i'" % divmod(time.timezone/60, 60)
171 else:
172 timezone = "Z00'00'"
174 def pdfstring(s):
175 r = ""
176 for c in s:
177 if 32 <= ord(c) <= 127 and c not in "()[]<>\\":
178 r += c
179 else:
180 r += "\\%03o" % ord(c)
181 return r
183 file.write("<<\n")
184 if writer.title:
185 file.write("/Title (%s)\n" % pdfstring(writer.title))
186 if writer.author:
187 file.write("/Author (%s)\n" % pdfstring(writer.author))
188 if writer.subject:
189 file.write("/Subject (%s)\n" % pdfstring(writer.subject))
190 if writer.keywords:
191 file.write("/Keywords (%s)\n" % pdfstring(writer.keywords))
192 file.write("/Creator (PyX %s)\n" % version.version)
193 file.write("/CreationDate (D:%s%s)\n" % (time.strftime("%Y%m%d%H%M"), timezone))
194 file.write(">>\n")
197 class PDFpages(PDFobject):
199 def __init__(self, document, writer, registry):
200 PDFobject.__init__(self, "pages")
201 self.PDFpagelist = []
202 for pageno, page in enumerate(document.pages):
203 page = PDFpage(page, pageno, self, writer, registry)
204 registry.add(page)
205 self.PDFpagelist.append(page)
207 def write(self, file, writer, registry):
208 file.write("<<\n"
209 "/Type /Pages\n"
210 "/Kids [%s]\n"
211 "/Count %i\n"
212 ">>\n" % (" ".join(["%i 0 R" % registry.getrefno(page)
213 for page in self.PDFpagelist]),
214 len(self.PDFpagelist)))
217 class PDFpage(PDFobject):
219 def __init__(self, page, pageno, PDFpages, writer, registry):
220 PDFobject.__init__(self, "page")
221 self.PDFpages = PDFpages
222 self.page = page
224 # every page uses its own registry in order to find out which
225 # resources are used within the page. However, the
226 # pageregistry is also merged in the global registry
227 self.pageregistry = PDFregistry()
229 self.PDFcontent = PDFcontent(page, writer, self.pageregistry)
230 self.pageregistry.add(self.PDFcontent)
231 registry.mergeregistry(self.pageregistry)
233 def write(self, file, writer, registry):
234 file.write("<<\n"
235 "/Type /Page\n"
236 "/Parent %i 0 R\n" % registry.getrefno(self.PDFpages))
237 paperformat = self.page.paperformat
238 if paperformat:
239 file.write("/MediaBox [0 0 %f %f]\n" % (unit.topt(paperformat.width), unit.topt(paperformat.height)))
240 else:
241 file.write("/MediaBox [%f %f %f %f]\n" % self.PDFcontent.bbox.highrestuple_pt())
242 if self.PDFcontent.bbox and writer.writebbox:
243 file.write("/CropBox [%f %f %f %f]\n" % self.PDFcontent.bbox.highrestuple_pt())
244 if self.page.rotated:
245 file.write("/Rotate 90\n")
246 file.write("/Contents %i 0 R\n" % registry.getrefno(self.PDFcontent))
247 self.pageregistry.writeresources(file)
248 file.write(">>\n")
251 class PDFcontent(PDFobject):
253 def __init__(self, page, writer, registry):
254 PDFobject.__init__(self, registry, "content")
255 contentfile = cStringIO.StringIO()
256 self.bbox = bbox.empty()
257 acontext = context()
258 page.processPDF(contentfile, writer, acontext, registry, self.bbox)
259 self.content = contentfile.getvalue()
260 contentfile.close()
262 def write(self, file, writer, registry):
263 if writer.compress:
264 content = zlib.compress(self.content)
265 else:
266 content = self.content
267 file.write("<<\n"
268 "/Length %i\n" % len(content))
269 if writer.compress:
270 file.write("/Filter /FlateDecode\n")
271 file.write(">>\n"
272 "stream\n")
273 file.write(content)
274 file.write("endstream\n")
277 class PDFfont(PDFobject):
279 def __init__(self, font, chars, writer, registry):
280 PDFobject.__init__(self, "font", font.name)
281 registry.addresource("Font", font.name, self, procset="Text")
283 self.fontdescriptor = PDFfontdescriptor(font, chars, writer, registry)
284 registry.add(self.fontdescriptor)
286 if font.encoding:
287 self.encoding = PDFencoding(font.encoding, writer, registry)
288 registry.add(self.encoding)
289 else:
290 self.encoding = None
292 self.name = font.name
293 self.basefontname = font.basefontname
294 self.metric = font.metric
296 def write(self, file, writer, registry):
297 file.write("<<\n"
298 "/Type /Font\n"
299 "/Subtype /Type1\n")
300 file.write("/Name /%s\n" % self.name)
301 file.write("/BaseFont /%s\n" % self.basefontname)
302 if self.fontdescriptor.fontfile is not None and self.fontdescriptor.fontfile.usedchars is not None:
303 usedchars = self.fontdescriptor.fontfile.usedchars
304 firstchar = min(usedchars.keys())
305 lastchar = max(usedchars.keys())
306 file.write("/FirstChar %d\n" % firstchar)
307 file.write("/LastChar %d\n" % lastchar)
308 file.write("/Widths\n"
309 "[")
310 for i in range(firstchar, lastchar+1):
311 if i and not (i % 8):
312 file.write("\n")
313 else:
314 file.write(" ")
315 if usedchars.has_key(i):
316 file.write("%f" % self.metric.getwidth_ds(i))
317 else:
318 file.write("0")
319 file.write(" ]\n")
320 else:
321 file.write("/FirstChar 0\n"
322 "/LastChar 255\n"
323 "/Widths\n"
324 "[")
325 for i in range(256):
326 if i and not (i % 8):
327 file.write("\n")
328 else:
329 file.write(" ")
330 try:
331 width = self.metric.getwidth_ds(i)
332 except (IndexError, AttributeError):
333 width = 0
334 file.write("%f" % width)
335 file.write(" ]\n")
336 file.write("/FontDescriptor %d 0 R\n" % registry.getrefno(self.fontdescriptor))
337 if self.encoding:
338 file.write("/Encoding %d 0 R\n" % registry.getrefno(self.encoding))
339 file.write(">>\n")
342 class PDFfontdescriptor(PDFobject):
344 def __init__(self, font, chars, writer, registry):
345 PDFobject.__init__(self, "fontdescriptor", font.basefontname)
347 if font.filename is None:
348 self.fontfile = None
349 else:
350 self.fontfile = PDFfontfile(font.basefontname, font.filename, font.encoding, chars, writer, registry)
351 registry.add(self.fontfile)
353 self.name = font.basefontname
354 self.fontinfo = font.metric.fontinfo()
356 def write(self, file, writer, registry):
357 file.write("<<\n"
358 "/Type /FontDescriptor\n"
359 "/FontName /%s\n" % self.name)
360 if self.fontfile is None:
361 file.write("/Flags 32\n")
362 else:
363 file.write("/Flags %d\n" % self.fontfile.getflags())
364 file.write("/FontBBox [%d %d %d %d]\n" % self.fontinfo.fontbbox)
365 file.write("/ItalicAngle %d\n" % self.fontinfo.italicangle)
366 file.write("/Ascent %d\n" % self.fontinfo.ascent)
367 file.write("/Descent %d\n" % self.fontinfo.descent)
368 file.write("/CapHeight %d\n" % self.fontinfo.capheight)
369 file.write("/StemV %d\n" % self.fontinfo.vstem)
370 if self.fontfile is not None:
371 file.write("/FontFile %d 0 R\n" % registry.getrefno(self.fontfile))
372 file.write(">>\n")
375 class PDFfontfile(PDFobject):
377 def __init__(self, name, filename, encoding, chars, writer, registry):
378 PDFobject.__init__(self, "fontfile", filename)
379 self.name = name
380 self.filename = filename
381 if encoding is None:
382 self.encodingfilename = None
383 else:
384 self.encodingfilename = encoding.filename
385 self.usedchars = {}
386 for char in chars:
387 self.usedchars[char] = 1
389 self.strip = 1
390 self.font = None
392 def merge(self, other):
393 if self.encodingfilename == other.encodingfilename:
394 self.usedchars.update(other.usedchars)
395 else:
396 # TODO: need to resolve the encoding when several encodings are in the play
397 self.strip = 0
399 def mkfontfile(self):
400 import font.t1font
401 self.font = font.t1font.T1pfbfont(self.filename)
403 def getflags(self):
404 if self.font is None:
405 self.mkfontfile()
406 return self.font.getflags()
408 def write(self, file, writer, registry):
409 if self.font is None:
410 self.mkfontfile()
411 if self.strip:
412 # XXX: access to the encoding file
413 if self.encodingfilename:
414 encodingfile = type1font.encodingfile(self.encodingfilename, self.encodingfilename)
415 usedglyphs = dict([(encodingfile.decode(char)[1:], 1) for char in self.usedchars.keys()])
416 else:
417 self.font._encoding()
418 usedglyphs = dict([(self.font.encoding.decode(char), 1) for char in self.usedchars.keys()])
419 strippedfont = self.font.getstrippedfont(usedglyphs)
420 else:
421 strippedfont = self.font
422 strippedfont.outputPDF(file, writer)
425 class PDFencoding(PDFobject):
427 def __init__(self, encoding, writer, registry):
428 PDFobject.__init__(self, "encoding", encoding.name)
429 self.encoding = encoding
431 def write(self, file, writer, registry):
432 encodingfile = type1font.encodingfile(self.encoding.name, self.encoding.filename)
433 encodingfile.outputPDF(file, writer)
436 class PDFwriter:
438 def __init__(self, document, filename,
439 title=None, author=None, subject=None, keywords=None,
440 fullscreen=0, writebbox=0, compress=1, compresslevel=6):
441 if not filename.endswith(".pdf"):
442 filename = filename + ".pdf"
443 try:
444 file = open(filename, "wb")
445 except IOError:
446 raise IOError("cannot open output file")
448 self.title = title
449 self.author = author
450 self.subject = subject
451 self.keywords = keywords
452 self.fullscreen = fullscreen
453 self.writebbox = writebbox
454 if compress and not haszlib:
455 compress = 0
456 warnings.warn("compression disabled due to missing zlib module")
457 self.compress = compress
458 self.compresslevel = compresslevel
460 # the PDFcatalog class automatically builds up the pdfobjects from a document
461 registry = PDFregistry()
462 catalog = PDFcatalog(document, self, registry)
463 registry.add(catalog)
465 file.write("%%PDF-1.4\n%%%s%s%s%s\n" % (chr(195), chr(182), chr(195), chr(169)))
466 registry.write(file, self, catalog)
467 file.close()
470 class context:
472 def __init__(self):
473 self.linewidth_pt = None
474 # XXX there are both stroke and fill color spaces
475 self.colorspace = None
476 self.strokeattr = 1
477 self.fillattr = 1
478 self.font = None
479 self.textregion = 0
481 def __call__(self, **kwargs):
482 newcontext = copy.copy(self)
483 for key, value in kwargs.items():
484 setattr(newcontext, key, value)
485 return newcontext