make bitmap functional after pdf object reorganization
[PyX/mjg.git] / pyx / pdfwriter.py
blob3b7e4a13ca5eebf4663b8af0b29d435d8e0f3693
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}
50 def add(self, object):
51 """ register object, merging it with an already registered object of the same type and id """
52 sameobjects = self.types.setdefault(object.type, {})
53 if sameobjects.has_key(object.id):
54 sameobjects[object.id].merge(object)
55 else:
56 self.objects.append(object)
57 sameobjects[object.id] = object
59 def getrefno(self, object):
60 return self.types[object.type][object.id].refno
62 def mergeregistry(self, registry):
63 for object in registry.objects:
64 self.add(object)
66 def write(self, file, writer, catalog):
67 # first we set all refnos
68 refno = 1
69 for object in self.objects:
70 object.refno = refno
71 refno += 1
73 # second, all objects are written, keeping the positions in the output file
74 fileposes = []
75 for object in self.objects:
76 fileposes.append(file.tell())
77 file.write("%i 0 obj\n" % object.refno)
78 object.write(file, writer, self)
79 file.write("endobj\n")
81 # xref
82 xrefpos = file.tell()
83 file.write("xref\n"
84 "0 %d\n"
85 "0000000000 65535 f \n" % refno)
87 for filepos in fileposes:
88 file.write("%010i 00000 n \n" % filepos)
90 # trailer
91 file.write("trailer\n"
92 "<<\n"
93 "/Size %i\n" % refno)
94 file.write("/Root %i 0 R\n" % self.getrefno(catalog))
95 file.write("/Info %i 0 R\n" % self.getrefno(catalog.PDFinfo))
96 file.write(">>\n"
97 "startxref\n"
98 "%i\n" % xrefpos)
99 file.write("%%EOF\n")
101 def addresource(self, resourcetype, resourcename, object, procset=None):
102 self.resources.setdefault(resourcetype, {})[resourcename] = object
103 if procset:
104 self.procsets[procset] = 1
106 def writeresources(self, file):
107 file.write("/Resources <<\n")
108 file.write("/ProcSet [ %s ]\n" % " ".join(["/%s" % p for p in self.procsets.keys()]))
109 if self.resources:
110 for resourcetype, resources in self.resources.items():
111 file.write("/%s <<\n%s\n>>\n" % (resourcetype, "\n".join(["/%s %i 0 R" % (name, self.getrefno(object))
112 for name, object in resources.items()])))
113 file.write(">>\n")
116 class PDFobject:
118 def __init__(self, type, _id=None):
119 """create a PDFobject
120 - type has to be a string describing the type of the object
121 - _id is a unique identification used for the object if it is not None.
122 Otherwise id(self) is used
124 self.type = type
125 if _id is None:
126 self.id = id(self)
127 else:
128 self.id = _id
130 def merge(self, other):
131 pass
133 def write(self, file, writer, registry):
134 raise NotImplementedError("write method has to be provided by PDFobject subclass")
137 class PDFcatalog(PDFobject):
139 def __init__(self, document, writer, registry):
140 PDFobject.__init__(self, "catalog")
141 self.PDFpages = PDFpages(document, writer, registry)
142 registry.add(self.PDFpages)
143 self.PDFinfo = PDFinfo()
144 registry.add(self.PDFinfo)
146 def write(self, file, writer, registry):
147 file.write("<<\n"
148 "/Type /Catalog\n"
149 "/Pages %i 0 R\n" % registry.getrefno(self.PDFpages))
150 if writer.fullscreen:
151 file.write("/PageMode /FullScreen\n")
152 file.write(">>\n")
155 class PDFinfo(PDFobject):
157 def __init__(self):
158 PDFobject.__init__(self, "info")
160 def write(self, file, writer, registry):
161 if time.timezone < 0:
162 # divmod on positive numbers, otherwise the minutes have a different sign from the hours
163 timezone = "-%02i'%02i'" % divmod(-time.timezone/60, 60)
164 elif time.timezone > 0:
165 timezone = "+%02i'%02i'" % divmod(time.timezone/60, 60)
166 else:
167 timezone = "Z00'00'"
169 def pdfstring(s):
170 r = ""
171 for c in s:
172 if 32 <= ord(c) <= 127 and c not in "()[]<>\\":
173 r += c
174 else:
175 r += "\\%03o" % ord(c)
176 return r
178 file.write("<<\n")
179 if writer.title:
180 file.write("/Title (%s)\n" % pdfstring(writer.title))
181 if writer.author:
182 file.write("/Author (%s)\n" % pdfstring(writer.author))
183 if writer.subject:
184 file.write("/Subject (%s)\n" % pdfstring(writer.subject))
185 if writer.keywords:
186 file.write("/Keywords (%s)\n" % pdfstring(writer.keywords))
187 file.write("/Creator (PyX %s)\n" % version.version)
188 file.write("/CreationDate (D:%s%s)\n" % (time.strftime("%Y%m%d%H%M"), timezone))
189 file.write(">>\n")
192 class PDFpages(PDFobject):
194 def __init__(self, document, writer, registry):
195 PDFobject.__init__(self, "pages")
196 self.PDFpagelist = []
197 for pageno, page in enumerate(document.pages):
198 page = PDFpage(page, pageno, self, writer, registry)
199 registry.add(page)
200 self.PDFpagelist.append(page)
202 def write(self, file, writer, registry):
203 file.write("<<\n"
204 "/Type /Pages\n"
205 "/Kids [%s]\n"
206 "/Count %i\n"
207 ">>\n" % (" ".join(["%i 0 R" % registry.getrefno(page)
208 for page in self.PDFpagelist]),
209 len(self.PDFpagelist)))
212 class PDFpage(PDFobject):
214 def __init__(self, page, pageno, PDFpages, writer, registry):
215 PDFobject.__init__(self, "page")
216 self.PDFpages = PDFpages
217 self.page = page
219 # every page uses its own registry in order to find out which
220 # resources are used within the page. However, the
221 # pageregistry is also merged in the global registry
222 self.pageregistry = PDFregistry()
224 self.PDFcontent = PDFcontent(page, writer, self.pageregistry)
225 self.pageregistry.add(self.PDFcontent)
226 registry.mergeregistry(self.pageregistry)
228 def write(self, file, writer, registry):
229 file.write("<<\n"
230 "/Type /Page\n"
231 "/Parent %i 0 R\n" % registry.getrefno(self.PDFpages))
232 paperformat = self.page.paperformat
233 if paperformat:
234 file.write("/MediaBox [0 0 %f %f]\n" % (unit.topt(paperformat.width), unit.topt(paperformat.height)))
235 else:
236 file.write("/MediaBox [%f %f %f %f]\n" % self.PDFcontent.bbox.highrestuple_pt())
237 if self.PDFcontent.bbox and writer.writebbox:
238 file.write("/CropBox [%f %f %f %f]\n" % self.PDFcontent.bbox.highrestuple_pt())
239 if self.page.rotated:
240 file.write("/Rotate 90\n")
241 file.write("/Contents %i 0 R\n" % registry.getrefno(self.PDFcontent))
242 self.pageregistry.writeresources(file)
243 file.write(">>\n")
246 class PDFcontent(PDFobject):
248 def __init__(self, page, writer, registry):
249 PDFobject.__init__(self, registry, "content")
250 contentfile = cStringIO.StringIO()
251 self.bbox = bbox.empty()
252 acontext = context()
253 page.process("processPDF", contentfile, writer, acontext, registry, self.bbox)
254 self.content = contentfile.getvalue()
255 contentfile.close()
257 def write(self, file, writer, registry):
258 if writer.compress:
259 content = zlib.compress(self.content)
260 else:
261 content = self.content
262 file.write("<<\n"
263 "/Length %i\n" % len(content))
264 if writer.compress:
265 file.write("/Filter /FlateDecode\n")
266 file.write(">>\n"
267 "stream\n")
268 file.write(content)
269 file.write("endstream\n")
272 class PDFfont(PDFobject):
274 def __init__(self, font, chars, writer, registry):
275 PDFobject.__init__(self, "font", font.name)
276 registry.addresource("Font", font.name, self, procset="Text")
278 self.fontdescriptor = PDFfontdescriptor(font, chars, writer, registry)
279 registry.add(self.fontdescriptor)
281 if font.encoding:
282 self.encoding = PDFencoding(font.encoding, writer, registry)
283 registry.add(self.encoding)
284 else:
285 self.encoding = None
287 self.name = font.name
288 self.basefontname = font.basefontname
289 self.metric = font.metric
291 def write(self, file, writer, registry):
292 file.write("<<\n"
293 "/Type /Font\n"
294 "/Subtype /Type1\n")
295 file.write("/Name /%s\n" % self.name)
296 file.write("/BaseFont /%s\n" % self.basefontname)
297 if self.fontdescriptor.fontfile is not None and self.fontdescriptor.fontfile.usedchars is not None:
298 usedchars = self.fontdescriptor.fontfile.usedchars
299 firstchar = min(usedchars.keys())
300 lastchar = max(usedchars.keys())
301 file.write("/FirstChar %d\n" % firstchar)
302 file.write("/LastChar %d\n" % lastchar)
303 file.write("/Widths\n"
304 "[")
305 for i in range(firstchar, lastchar+1):
306 if i and not (i % 8):
307 file.write("\n")
308 else:
309 file.write(" ")
310 if usedchars.has_key(i):
311 file.write("%f" % self.metric.getwidth_ds(i))
312 else:
313 file.write("0")
314 file.write(" ]\n")
315 else:
316 file.write("/FirstChar 0\n"
317 "/LastChar 255\n"
318 "/Widths\n"
319 "[")
320 for i in range(256):
321 if i and not (i % 8):
322 file.write("\n")
323 else:
324 file.write(" ")
325 try:
326 width = self.metric.getwidth_ds(i)
327 except (IndexError, AttributeError):
328 width = 0
329 file.write("%f" % width)
330 file.write(" ]\n")
331 file.write("/FontDescriptor %d 0 R\n" % registry.getrefno(self.fontdescriptor))
332 if self.encoding:
333 file.write("/Encoding %d 0 R\n" % registry.getrefno(self.encoding))
334 file.write(">>\n")
337 class PDFfontdescriptor(PDFobject):
339 def __init__(self, font, chars, writer, registry):
340 PDFobject.__init__(self, "fontdescriptor", font.basefontname)
342 if font.filename is None:
343 self.fontfile = None
344 else:
345 self.fontfile = PDFfontfile(font.basefontname, font.filename, font.encoding, chars, writer, registry)
346 registry.add(self.fontfile)
348 self.name = font.basefontname
349 self.fontinfo = font.metric.fontinfo()
351 def write(self, file, writer, registry):
352 file.write("<<\n"
353 "/Type /FontDescriptor\n"
354 "/FontName /%s\n" % self.name)
355 if self.fontfile is None:
356 file.write("/Flags 32\n")
357 else:
358 file.write("/Flags %d\n" % self.fontfile.getflags())
359 file.write("/FontBBox [%d %d %d %d]\n" % self.fontinfo.fontbbox)
360 file.write("/ItalicAngle %d\n" % self.fontinfo.italicangle)
361 file.write("/Ascent %d\n" % self.fontinfo.ascent)
362 file.write("/Descent %d\n" % self.fontinfo.descent)
363 file.write("/CapHeight %d\n" % self.fontinfo.capheight)
364 file.write("/StemV %d\n" % self.fontinfo.vstem)
365 if self.fontfile is not None:
366 file.write("/FontFile %d 0 R\n" % registry.getrefno(self.fontfile))
367 file.write(">>\n")
370 class PDFfontfile(PDFobject):
372 def __init__(self, name, filename, encoding, chars, writer, registry):
373 PDFobject.__init__(self, "fontfile", filename)
374 self.name = name
375 self.filename = filename
376 if encoding is None:
377 self.encodingfilename = None
378 else:
379 self.encodingfilename = encoding.filename
380 self.usedchars = {}
381 for char in chars:
382 self.usedchars[char] = 1
384 self.strip = 1
385 self.font = None
387 def merge(self, other):
388 if self.encodingfilename == other.encodingfilename:
389 self.usedchars.update(other.usedchars)
390 else:
391 # TODO: need to resolve the encoding when several encodings are in the play
392 self.strip = 0
394 def mkfontfile(self):
395 import font.t1font
396 self.font = font.t1font.T1pfbfont(self.filename)
398 def getflags(self):
399 if self.font is None:
400 self.mkfontfile()
401 return self.font.getflags()
403 def write(self, file, writer, registry):
404 if self.font is None:
405 self.mkfontfile()
406 if self.strip:
407 # XXX: access to the encoding file
408 if self.encodingfilename:
409 encodingfile = type1font.encodingfile(self.encodingfilename, self.encodingfilename)
410 usedglyphs = [encodingfile.decode(char)[1:] for char in self.usedchars.keys()]
411 else:
412 self.font._encoding()
413 usedglyphs = [self.font.encoding.decode(char) for char in self.usedchars.keys()]
414 strippedfont = self.font.getstrippedfont(usedglyphs)
415 else:
416 strippedfont = self.font
417 strippedfont.outputPDF(file, writer)
420 class PDFencoding(PDFobject):
422 def __init__(self, encoding, writer, registry):
423 PDFobject.__init__(self, "encoding", encoding.name)
424 self.encoding = encoding
426 def write(self, file, writer, registry):
427 encodingfile = type1font.encodingfile(self.encoding.name, self.encoding.filename)
428 encodingfile.outputPDF(file, writer)
431 class PDFwriter:
433 def __init__(self, document, filename,
434 title=None, author=None, subject=None, keywords=None,
435 fullscreen=0, writebbox=0, compress=1, compresslevel=6):
436 if not filename.endswith(".pdf"):
437 filename = filename + ".pdf"
438 try:
439 file = open(filename, "wb")
440 except IOError:
441 raise IOError("cannot open output file")
443 self.title = title
444 self.author = author
445 self.subject = subject
446 self.keywords = keywords
447 self.fullscreen = fullscreen
448 self.writebbox = writebbox
449 if compress and not haszlib:
450 compress = 0
451 warnings.warn("compression disabled due to missing zlib module")
452 self.compress = compress
453 self.compresslevel = compresslevel
455 # the PDFcatalog class automatically builds up the pdfobjects from a document
456 registry = PDFregistry()
457 catalog = PDFcatalog(document, self, registry)
458 registry.add(catalog)
460 file.write("%%PDF-1.4\n%%%s%s%s%s\n" % (chr(195), chr(182), chr(195), chr(169)))
461 registry.write(file, self, catalog)
462 file.close()
465 class context:
467 def __init__(self):
468 self.linewidth_pt = None
469 # XXX there are both stroke and fill color spaces
470 self.colorspace = None
471 self.strokeattr = 1
472 self.fillattr = 1
473 self.font = None
474 self.textregion = 0
476 def __call__(self, **kwargs):
477 newcontext = copy.copy(self)
478 for key, value in kwargs.items():
479 setattr(newcontext, key, value)
480 return newcontext