remove shebang -- see comment 3 on https://bugzilla.redhat.com/bugzilla/show_bug...
[PyX/mjg.git] / pyx / pdfwriter.py
blob25ac3f7fa17a3e8ac8018347b32ddb23a0728c83
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, warnings, time
24 try:
25 import zlib
26 haszlib = 1
27 except:
28 haszlib = 0
30 import bbox, unit, style, type1font, version
32 try:
33 enumerate([])
34 except NameError:
35 # fallback implementation for Python 2.2 and below
36 def enumerate(list):
37 return zip(xrange(len(list)), list)
39 try:
40 dict([])
41 except NameError:
42 # fallback implementation for Python 2.1
43 def dict(list):
44 result = {}
45 for key, value in list:
46 result[key] = value
47 return result
50 class PDFregistry:
52 def __init__(self):
53 self.types = {}
54 # we want to keep the order of the resources
55 self.objects = []
56 self.resources = {}
57 self.procsets = {"PDF": 1}
58 self.merged = None
60 def add(self, object):
61 """ register object, merging it with an already registered object of the same type and id """
62 sameobjects = self.types.setdefault(object.type, {})
63 if sameobjects.has_key(object.id):
64 sameobjects[object.id].merge(object)
65 else:
66 self.objects.append(object)
67 sameobjects[object.id] = object
69 def getrefno(self, object):
70 if self.merged:
71 return self.merged.getrefno(object)
72 else:
73 return self.types[object.type][object.id].refno
75 def mergeregistry(self, registry):
76 for object in registry.objects:
77 self.add(object)
78 registry.merged = self
80 def write(self, file, writer, catalog):
81 # first we set all refnos
82 refno = 1
83 for object in self.objects:
84 object.refno = refno
85 refno += 1
87 # second, all objects are written, keeping the positions in the output file
88 fileposes = []
89 for object in self.objects:
90 fileposes.append(file.tell())
91 file.write("%i 0 obj\n" % object.refno)
92 object.write(file, writer, self)
93 file.write("endobj\n")
95 # xref
96 xrefpos = file.tell()
97 file.write("xref\n"
98 "0 %d\n"
99 "0000000000 65535 f \n" % refno)
101 for filepos in fileposes:
102 file.write("%010i 00000 n \n" % filepos)
104 # trailer
105 file.write("trailer\n"
106 "<<\n"
107 "/Size %i\n" % refno)
108 file.write("/Root %i 0 R\n" % self.getrefno(catalog))
109 file.write("/Info %i 0 R\n" % self.getrefno(catalog.PDFinfo))
110 file.write(">>\n"
111 "startxref\n"
112 "%i\n" % xrefpos)
113 file.write("%%EOF\n")
115 def addresource(self, resourcetype, resourcename, object, procset=None):
116 self.resources.setdefault(resourcetype, {})[resourcename] = object
117 if procset:
118 self.procsets[procset] = 1
120 def writeresources(self, file):
121 file.write("/Resources <<\n")
122 file.write("/ProcSet [ %s ]\n" % " ".join(["/%s" % p for p in self.procsets.keys()]))
123 if self.resources:
124 for resourcetype, resources in self.resources.items():
125 file.write("/%s <<\n%s\n>>\n" % (resourcetype, "\n".join(["/%s %i 0 R" % (name, self.getrefno(object))
126 for name, object in resources.items()])))
127 file.write(">>\n")
130 class PDFobject:
132 def __init__(self, type, _id=None):
133 """create a PDFobject
134 - type has to be a string describing the type of the object
135 - _id is a unique identification used for the object if it is not None.
136 Otherwise id(self) is used
138 self.type = type
139 if _id is None:
140 self.id = id(self)
141 else:
142 self.id = _id
144 def merge(self, other):
145 pass
147 def write(self, file, writer, registry):
148 raise NotImplementedError("write method has to be provided by PDFobject subclass")
151 class PDFcatalog(PDFobject):
153 def __init__(self, document, writer, registry):
154 PDFobject.__init__(self, "catalog")
155 self.PDFpages = PDFpages(document, writer, registry)
156 registry.add(self.PDFpages)
157 self.PDFinfo = PDFinfo()
158 registry.add(self.PDFinfo)
160 def write(self, file, writer, registry):
161 file.write("<<\n"
162 "/Type /Catalog\n"
163 "/Pages %i 0 R\n" % registry.getrefno(self.PDFpages))
164 if writer.fullscreen:
165 file.write("/PageMode /FullScreen\n")
166 file.write(">>\n")
169 class PDFinfo(PDFobject):
171 def __init__(self):
172 PDFobject.__init__(self, "info")
174 def write(self, file, writer, registry):
175 if time.timezone < 0:
176 # divmod on positive numbers, otherwise the minutes have a different sign from the hours
177 timezone = "-%02i'%02i'" % divmod(-time.timezone/60, 60)
178 elif time.timezone > 0:
179 timezone = "+%02i'%02i'" % divmod(time.timezone/60, 60)
180 else:
181 timezone = "Z00'00'"
183 def pdfstring(s):
184 r = ""
185 for c in s:
186 if 32 <= ord(c) <= 127 and c not in "()[]<>\\":
187 r += c
188 else:
189 r += "\\%03o" % ord(c)
190 return r
192 file.write("<<\n")
193 if writer.title:
194 file.write("/Title (%s)\n" % pdfstring(writer.title))
195 if writer.author:
196 file.write("/Author (%s)\n" % pdfstring(writer.author))
197 if writer.subject:
198 file.write("/Subject (%s)\n" % pdfstring(writer.subject))
199 if writer.keywords:
200 file.write("/Keywords (%s)\n" % pdfstring(writer.keywords))
201 file.write("/Creator (PyX %s)\n" % version.version)
202 file.write("/CreationDate (D:%s%s)\n" % (time.strftime("%Y%m%d%H%M"), timezone))
203 file.write(">>\n")
206 class PDFpages(PDFobject):
208 def __init__(self, document, writer, registry):
209 PDFobject.__init__(self, "pages")
210 self.PDFpagelist = []
211 for pageno, page in enumerate(document.pages):
212 page = PDFpage(page, pageno, self, writer, registry)
213 registry.add(page)
214 self.PDFpagelist.append(page)
216 def write(self, file, writer, registry):
217 file.write("<<\n"
218 "/Type /Pages\n"
219 "/Kids [%s]\n"
220 "/Count %i\n"
221 ">>\n" % (" ".join(["%i 0 R" % registry.getrefno(page)
222 for page in self.PDFpagelist]),
223 len(self.PDFpagelist)))
226 class PDFpage(PDFobject):
228 def __init__(self, page, pageno, PDFpages, writer, registry):
229 PDFobject.__init__(self, "page")
230 self.PDFpages = PDFpages
231 self.page = page
233 # every page uses its own registry in order to find out which
234 # resources are used within the page. However, the
235 # pageregistry is also merged in the global registry
236 self.pageregistry = PDFregistry()
238 self.PDFcontent = PDFcontent(page, writer, self.pageregistry)
239 self.pageregistry.add(self.PDFcontent)
240 registry.mergeregistry(self.pageregistry)
242 def write(self, file, writer, registry):
243 file.write("<<\n"
244 "/Type /Page\n"
245 "/Parent %i 0 R\n" % registry.getrefno(self.PDFpages))
246 paperformat = self.page.paperformat
247 if paperformat:
248 file.write("/MediaBox [0 0 %f %f]\n" % (unit.topt(paperformat.width), unit.topt(paperformat.height)))
249 else:
250 file.write("/MediaBox [%f %f %f %f]\n" % self.PDFcontent.bbox.highrestuple_pt())
251 if self.PDFcontent.bbox and writer.writebbox:
252 file.write("/CropBox [%f %f %f %f]\n" % self.PDFcontent.bbox.highrestuple_pt())
253 if self.page.rotated:
254 file.write("/Rotate 90\n")
255 file.write("/Contents %i 0 R\n" % registry.getrefno(self.PDFcontent))
256 self.pageregistry.writeresources(file)
257 file.write(">>\n")
260 class PDFcontent(PDFobject):
262 def __init__(self, page, writer, registry):
263 PDFobject.__init__(self, registry, "content")
264 contentfile = cStringIO.StringIO()
265 self.bbox = bbox.empty()
266 acontext = context()
267 page.processPDF(contentfile, writer, acontext, registry, self.bbox)
268 self.content = contentfile.getvalue()
269 contentfile.close()
271 def write(self, file, writer, registry):
272 if writer.compress:
273 content = zlib.compress(self.content)
274 else:
275 content = self.content
276 file.write("<<\n"
277 "/Length %i\n" % len(content))
278 if writer.compress:
279 file.write("/Filter /FlateDecode\n")
280 file.write(">>\n"
281 "stream\n")
282 file.write(content)
283 file.write("endstream\n")
286 class PDFfont(PDFobject):
288 def __init__(self, font, chars, writer, registry):
289 PDFobject.__init__(self, "font", font.name)
290 registry.addresource("Font", font.name, self, procset="Text")
292 self.fontdescriptor = PDFfontdescriptor(font, chars, writer, registry)
293 registry.add(self.fontdescriptor)
295 if font.encoding:
296 self.encoding = PDFencoding(font.encoding, writer, registry)
297 registry.add(self.encoding)
298 else:
299 self.encoding = None
301 self.name = font.name
302 self.basefontname = font.basefontname
303 self.metric = font.metric
305 def write(self, file, writer, registry):
306 file.write("<<\n"
307 "/Type /Font\n"
308 "/Subtype /Type1\n")
309 file.write("/Name /%s\n" % self.name)
310 file.write("/BaseFont /%s\n" % self.basefontname)
311 if self.fontdescriptor.fontfile is not None and self.fontdescriptor.fontfile.usedchars is not None:
312 usedchars = self.fontdescriptor.fontfile.usedchars
313 firstchar = min(usedchars.keys())
314 lastchar = max(usedchars.keys())
315 file.write("/FirstChar %d\n" % firstchar)
316 file.write("/LastChar %d\n" % lastchar)
317 file.write("/Widths\n"
318 "[")
319 for i in range(firstchar, lastchar+1):
320 if i and not (i % 8):
321 file.write("\n")
322 else:
323 file.write(" ")
324 if usedchars.has_key(i):
325 file.write("%f" % self.metric.getwidth_ds(i))
326 else:
327 file.write("0")
328 file.write(" ]\n")
329 else:
330 file.write("/FirstChar 0\n"
331 "/LastChar 255\n"
332 "/Widths\n"
333 "[")
334 for i in range(256):
335 if i and not (i % 8):
336 file.write("\n")
337 else:
338 file.write(" ")
339 try:
340 width = self.metric.getwidth_ds(i)
341 except (IndexError, AttributeError):
342 width = 0
343 file.write("%f" % width)
344 file.write(" ]\n")
345 file.write("/FontDescriptor %d 0 R\n" % registry.getrefno(self.fontdescriptor))
346 if self.encoding:
347 file.write("/Encoding %d 0 R\n" % registry.getrefno(self.encoding))
348 file.write(">>\n")
351 class PDFfontdescriptor(PDFobject):
353 def __init__(self, font, chars, writer, registry):
354 PDFobject.__init__(self, "fontdescriptor", font.basefontname)
356 if font.filename is None:
357 self.fontfile = None
358 else:
359 self.fontfile = PDFfontfile(font.basefontname, font.filename, font.encoding, chars, writer, registry)
360 registry.add(self.fontfile)
362 self.name = font.basefontname
363 self.fontinfo = font.metric.fontinfo()
365 def write(self, file, writer, registry):
366 file.write("<<\n"
367 "/Type /FontDescriptor\n"
368 "/FontName /%s\n" % self.name)
369 if self.fontfile is None:
370 file.write("/Flags 32\n")
371 else:
372 file.write("/Flags %d\n" % self.fontfile.getflags())
373 file.write("/FontBBox [%d %d %d %d]\n" % self.fontinfo.fontbbox)
374 file.write("/ItalicAngle %d\n" % self.fontinfo.italicangle)
375 file.write("/Ascent %d\n" % self.fontinfo.ascent)
376 file.write("/Descent %d\n" % self.fontinfo.descent)
377 file.write("/CapHeight %d\n" % self.fontinfo.capheight)
378 file.write("/StemV %d\n" % self.fontinfo.vstem)
379 if self.fontfile is not None:
380 file.write("/FontFile %d 0 R\n" % registry.getrefno(self.fontfile))
381 file.write(">>\n")
384 class PDFfontfile(PDFobject):
386 def __init__(self, name, filename, encoding, chars, writer, registry):
387 PDFobject.__init__(self, "fontfile", filename)
388 self.name = name
389 self.filename = filename
390 if encoding is None:
391 self.encodingfilename = None
392 else:
393 self.encodingfilename = encoding.filename
394 self.usedchars = {}
395 for char in chars:
396 self.usedchars[char] = 1
398 self.strip = 1
399 self.font = None
401 def merge(self, other):
402 if self.encodingfilename == other.encodingfilename:
403 self.usedchars.update(other.usedchars)
404 else:
405 # TODO: need to resolve the encoding when several encodings are in the play
406 self.strip = 0
408 def mkfontfile(self):
409 import font.t1font
410 self.font = font.t1font.T1pfbfont(self.filename)
412 def getflags(self):
413 if self.font is None:
414 self.mkfontfile()
415 return self.font.getflags()
417 def write(self, file, writer, registry):
418 if self.font is None:
419 self.mkfontfile()
420 if self.strip:
421 # XXX: access to the encoding file
422 if self.encodingfilename:
423 encodingfile = type1font.encodingfile(self.encodingfilename, self.encodingfilename)
424 usedglyphs = dict([(encodingfile.decode(char)[1:], 1) for char in self.usedchars.keys()])
425 else:
426 self.font._encoding()
427 usedglyphs = dict([(self.font.encoding.decode(char), 1) for char in self.usedchars.keys()])
428 strippedfont = self.font.getstrippedfont(usedglyphs)
429 else:
430 strippedfont = self.font
431 strippedfont.outputPDF(file, writer)
434 class PDFencoding(PDFobject):
436 def __init__(self, encoding, writer, registry):
437 PDFobject.__init__(self, "encoding", encoding.name)
438 self.encoding = encoding
440 def write(self, file, writer, registry):
441 encodingfile = type1font.encodingfile(self.encoding.name, self.encoding.filename)
442 encodingfile.outputPDF(file, writer)
445 class PDFwriter:
447 def __init__(self, document, file,
448 title=None, author=None, subject=None, keywords=None,
449 fullscreen=0, writebbox=0, compress=1, compresslevel=6):
450 try:
451 file.write("")
452 except:
453 filename = file
454 if not filename.endswith(".pdf"):
455 filename += ".pdf"
456 try:
457 file = open(filename, "wb")
458 except IOError:
459 raise IOError("cannot open output file")
461 self.title = title
462 self.author = author
463 self.subject = subject
464 self.keywords = keywords
465 self.fullscreen = fullscreen
466 self.writebbox = writebbox
467 if compress and not haszlib:
468 compress = 0
469 warnings.warn("compression disabled due to missing zlib module")
470 self.compress = compress
471 self.compresslevel = compresslevel
473 # the PDFcatalog class automatically builds up the pdfobjects from a document
474 registry = PDFregistry()
475 catalog = PDFcatalog(document, self, registry)
476 registry.add(catalog)
478 file.write("%%PDF-1.4\n%%%s%s%s%s\n" % (chr(195), chr(182), chr(195), chr(169)))
479 registry.write(file, self, catalog)
480 file.close()
483 class context:
485 def __init__(self):
486 self.linewidth_pt = None
487 # XXX there are both stroke and fill color spaces
488 self.colorspace = None
489 self.strokeattr = 1
490 self.fillattr = 1
491 self.font = None
492 self.textregion = 0
494 def __call__(self, **kwargs):
495 newcontext = copy.copy(self)
496 for key, value in kwargs.items():
497 setattr(newcontext, key, value)
498 return newcontext