add some remarks on attributes
[PyX/mjg.git] / pyx / pdfwriter.py
blobe61564030ae0c9ef0d8fba5318519fba562c9b5b
1 #!/usr/bin/env python
2 # -*- coding: ISO-8859-1 -*-
5 # Copyright (C) 2005 Jörg Lehmann <joergl@users.sourceforge.net>
6 # Copyright (C) 2005 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 import copy, warnings
25 try:
26 import zlib
27 haszlib = 1
28 except:
29 haszlib = 0
31 import unit, style, type1font
34 class PDFregistry:
36 def __init__(self):
37 self.types = {}
38 # we need to keep the original order of the resources (for PDFcontentlength)
39 self.resources = []
41 def add(self, resource):
42 """ register resource, merging it with an already registered resource of the same type and id"""
43 resources = self.types.setdefault(resource.type, {})
44 if resources.has_key(resource.id):
45 resources[resource.id].merge(resource)
46 else:
47 self.resources.append(resource)
48 resources[resource.id] = resource
50 def getrefno(self, resource):
51 return self.types[resource.type][resource.id].refno
53 def mergeregistry(self, registry):
54 for resource in registry.resources:
55 self.add(resource)
57 def write(self, file, writer, catalog):
58 # first we set all refnos
59 refno = 1
61 # we recursively inserted the resources such that the topmost resources in
62 # the dependency tree of the resources come last. Hence, we need to
63 # reverse the resources list before writing the output
64 self.resources.reverse()
65 for resource in self.resources:
66 resource.refno = refno
67 refno += 1
69 # second, all objects are written, keeping the positions in the output file
70 fileposes = []
71 for resource in self.resources:
72 fileposes.append(file.tell())
73 file.write("%i 0 obj\n" % resource.refno)
74 resource.outputPDF(file, writer, self)
75 file.write("endobj\n")
77 # xref
78 xrefpos = file.tell()
79 file.write("xref\n"
80 "0 %d\n"
81 "0000000000 65535 f \n" % refno)
83 for filepos in fileposes:
84 file.write("%010i 00000 n \n" % filepos)
86 # trailer
87 file.write("trailer\n"
88 "<<\n"
89 "/Size %i\n"
90 "/Root %i 0 R\n"
91 ">>\n"
92 "startxref\n"
93 "%i\n"
94 "%%%%EOF\n" % (refno, catalog.refno, xrefpos))
97 class PDFobject:
99 def __init__(self, type, _id=None):
100 self.type = type
101 if _id is None:
102 self.id = id(self)
103 else:
104 self.id = _id
105 self.refno = None
107 def merge(self, other):
108 pass
110 def outputPDF(self, file, writer, registry):
111 raise NotImplementedError("outputPDF method has to be provided by PDFobject subclass")
114 class PDFcatalog(PDFobject):
116 def __init__(self, document, registry):
117 PDFobject.__init__(self, "catalog")
118 self.PDFpages = PDFpages(document, registry)
119 registry.add(self.PDFpages)
121 def outputPDF(self, file, writer, registry):
122 file.write("<<\n"
123 "/Type /Catalog\n"
124 "/Pages %i 0 R\n"
125 ">>\n" % registry.getrefno(self.PDFpages))
128 class PDFpages(PDFobject):
130 def __init__(self, document, registry):
131 PDFobject.__init__(self, "pages")
132 self.PDFpagelist = []
133 for pageno, page in enumerate(document.pages):
134 page = PDFpage(page, pageno, self, registry)
135 registry.add(page)
136 self.PDFpagelist.append(page)
138 def outputPDF(self, file, writer, registry):
139 file.write("<<\n"
140 "/Type /Pages\n"
141 "/Kids [%s]\n"
142 "/Count %i\n"
143 ">>\n" % (" ".join(["%i 0 R" % registry.getrefno(page)
144 for page in self.PDFpagelist]),
145 len(self.PDFpagelist)))
148 class PDFpage(PDFobject):
150 def __init__(self, page, pageno, PDFpages, registry):
151 PDFobject.__init__(self, "page", pageno)
152 self.PDFpages = PDFpages
153 self.page = page
155 # every page uses its own registry in order to find out which
156 # resources are used within the page. However, the
157 # pageregistry is also merged in the global registry
158 self.pageregistry = PDFregistry()
160 self.bbox = page.bbox()
161 self.pagetrafo = page.pagetrafo(self.bbox)
162 if self.pagetrafo:
163 self.bbox.transform(self.pagetrafo)
164 self.PDFcontent = PDFcontent(page.canvas, self.pagetrafo, self.pageregistry)
165 self.pageregistry.add(self.PDFcontent)
166 self.page.canvas.registerPDF(self.pageregistry)
167 registry.mergeregistry(self.pageregistry)
169 def outputPDF(self, file, writer, registry):
170 file.write("<<\n"
171 "/Type /Page\n"
172 "/Parent %i 0 R\n" % registry.getrefno(self.PDFpages))
173 paperformat = self.page.paperformat
174 file.write("/MediaBox [0 0 %f %f]\n" % (unit.topt(paperformat.width), unit.topt(paperformat.height)))
175 if self.bbox:
176 file.write("/CropBox [%f %f %f %f]\n" % self.bbox.highrestuple_pt())
177 procset = []
178 if self.pageregistry.types.has_key("font"):
179 procset.append("/Text")
180 if self.pageregistry.types.has_key("image"):
181 if [image for image in self.pageregistry.types["image"].values()
182 if image.colorspace == "/DeviceGray"]:
183 procset.append("/ImageB")
184 if [image for image in self.pageregistry.types["image"].values()
185 if image.colorspace is not None and image.colorspace != "/DeviceGray"]:
186 procset.append("/ImageC")
187 if [image for image in self.pageregistry.types["image"].values()
188 if image.palettedata is not None]:
189 procset.append("/ImageI")
190 file.write("/Resources <<\n"
191 "/ProcSet [ /PDF %s ]\n" % " ".join(procset))
192 if self.pageregistry.types.has_key("font"):
193 file.write("/Font <<\n%s\n>>\n" % "\n".join(["/%s %i 0 R" % (font.name, registry.getrefno(font))
194 for font in self.pageregistry.types["font"].values()]))
195 if self.pageregistry.types.has_key("image"):
196 file.write("/XObject <<\n%s\n>>\n" % "\n".join(["/%s %i 0 R" % (image.name, registry.getrefno(image))
197 for image in self.pageregistry.types["image"].values()]))
198 if self.pageregistry.types.has_key("pattern"):
199 file.write("/Pattern <<\n%s\n>>\n" % "\n".join(["/%s %i 0 R" % (pattern.name, registry.getrefno(pattern))
200 for pattern in self.pageregistry.types["pattern"].values()]))
201 file.write(">>\n")
202 file.write("/Contents %i 0 R\n"
203 ">>\n" % registry.getrefno(self.PDFcontent))
206 class PDFcontent(PDFobject):
208 def __init__(self, canvas, pagetrafo, registry):
209 PDFobject.__init__(self, "content")
210 self.canvas = canvas
211 self.pagetrafo = pagetrafo
212 self.contentlength = PDFcontentlength((self.type, self.id))
213 registry.add(self.contentlength)
215 def outputPDF(self, file, writer, registry):
216 file.write("<<\n"
217 "/Length %i 0 R\n" % registry.getrefno(self.contentlength))
218 if writer.compress:
219 file.write("/Filter /FlateDecode\n")
220 file.write(">>\n"
221 "stream\n")
222 beginstreampos = file.tell()
224 if writer.compress:
225 stream = compressedstream(file, writer.compresslevel)
226 else:
227 stream = file
229 # XXX this should maybe be handled by the page since removing
230 # this code would allow us to (nearly, since we also need to
231 # set more info in the content dict) reuse PDFcontent for
232 # patterns
233 acontext = context()
234 # apply a possible global transformation
235 if self.pagetrafo:
236 self.pagetrafo.outputPDF(stream, writer, acontext)
237 style.linewidth.normal.outputPDF(stream, writer, acontext)
239 self.canvas.outputPDF(stream, writer, acontext)
240 if writer.compress:
241 stream.flush()
243 self.contentlength.contentlength = file.tell() - beginstreampos
244 file.write("\n"
245 "endstream\n")
248 class PDFcontentlength(PDFobject):
250 def __init__(self, contentid):
251 PDFobject.__init__(self, "_contentlength", contentid)
252 self.contentlength = None
254 def outputPDF(self, file, writer, registry):
255 # initially we do not know about the content length
256 # -> it has to be written into the instance later on
257 file.write("%d\n" % self.contentlength)
260 class PDFfont(PDFobject):
262 def __init__(self, font, chars, registry):
263 PDFobject.__init__(self, "font", font.name)
265 self.fontdescriptor = PDFfontdescriptor(font, chars, registry)
266 registry.add(self.fontdescriptor)
268 if font.encoding:
269 self.encoding = PDFencoding(font.encoding)
270 registry.add(self.encoding)
271 else:
272 self.encoding = None
274 self.name = font.name
275 self.basefontname = font.basefontname
276 self.metric = font.metric
278 def outputPDF(self, file, writer, registry):
279 file.write("<<\n"
280 "/Type /Font\n"
281 "/Subtype /Type1\n")
282 file.write("/Name /%s\n" % self.name)
283 file.write("/BaseFont /%s\n" % self.basefontname)
284 if self.fontdescriptor.fontfile is not None and self.fontdescriptor.fontfile.usedchars is not None:
285 usedchars = self.fontdescriptor.fontfile.usedchars
286 firstchar = min(usedchars.keys())
287 lastchar = max(usedchars.keys())
288 file.write("/FirstChar %d\n" % firstchar)
289 file.write("/LastChar %d\n" % lastchar)
290 file.write("/Widths\n"
291 "[")
292 for i in range(firstchar, lastchar+1):
293 if i and not (i % 8):
294 file.write("\n")
295 else:
296 file.write(" ")
297 if usedchars.has_key(i):
298 file.write("%f" % self.metric.getwidth_ds(i))
299 else:
300 file.write("0")
301 file.write(" ]\n")
302 else:
303 file.write("/FirstChar 0\n"
304 "/LastChar 255\n"
305 "/Widths\n"
306 "[")
307 for i in range(256):
308 if i and not (i % 8):
309 file.write("\n")
310 else:
311 file.write(" ")
312 try:
313 width = self.metric.getwidth_ds(i)
314 except (IndexError, AttributeError):
315 width = 0
316 file.write("%f" % width)
317 file.write(" ]\n")
318 file.write("/FontDescriptor %d 0 R\n" % registry.getrefno(self.fontdescriptor))
319 if self.encoding:
320 file.write("/Encoding %d 0 R\n" % registry.getrefno(self.encoding))
321 else:
322 file.write("/Encoding /StandardEncoding\n")
323 file.write(">>\n")
326 class PDFfontdescriptor(PDFobject):
328 def __init__(self, font, chars, registry):
329 PDFobject.__init__(self, "fontdescriptor", font.basefontname)
331 if font.filename is None:
332 self.fontfile = None
333 else:
334 self.fontfile = PDFfontfile(font.basefontname, font.filename, font.encoding, chars)
335 registry.add(self.fontfile)
337 self.name = font.basefontname
338 self.metric = font.metric
340 def outputPDF(self, file, writer, registry):
341 file.write("<<\n"
342 "/Type /FontDescriptor\n"
343 "/FontName /%s\n" % self.name)
344 if self.fontfile is None:
345 file.write("/Flags 32\n")
346 else:
347 file.write("/Flags %d\n" % self.fontfile.getflags())
348 file.write("/FontBBox [%d %d %d %d]\n" % self.metric.fontbbox)
349 file.write("/ItalicAngle %d\n" % self.metric.italicangle)
350 file.write("/Ascent %d\n" % self.metric.ascent)
351 file.write("/Descent %d\n" % self.metric.descent)
352 file.write("/CapHeight %d\n" % self.metric.capheight)
353 file.write("/StemV %d\n" % self.metric.vstem)
354 if self.fontfile is not None:
355 file.write("/FontFile %d 0 R\n" % registry.getrefno(self.fontfile))
356 file.write(">>\n")
359 class PDFfontfile(PDFobject):
361 def __init__(self, name, filename, encoding, chars):
362 PDFobject.__init__(self, "fontfile", filename)
363 self.name = name
364 self.filename = filename
365 if encoding is None:
366 self.encodingfilename = None
367 else:
368 self.encodingfilename = encoding.filename
369 self.usedchars = {}
370 for char in chars:
371 self.usedchars[char] = 1
373 # for flags-caching
374 self.fontfile = None
375 self.flags = None
377 def merge(self, other):
378 self.fontfile = None # remove fontfile cache when adding further stuff after writing
379 if self.encodingfilename != other.encodingfilename:
380 self.usedchars = None # stripping of font not possible
381 else:
382 self.usedchars.update(other.usedchars)
384 def mkfontfile(self):
385 if self.fontfile is None:
386 self.fontfile = type1font.fontfile(self.name,
387 self.filename,
388 self.usedchars,
389 self.encodingfilename)
391 def getflags(self):
392 if not self.flags:
393 self.mkfontfile()
394 self.flags = self.fontfile.getflags()
395 return self.flags
397 def outputPDF(self, file, writer, registry):
398 self.mkfontfile()
399 self.fontfile.outputPDF(file, writer, registry)
402 class PDFencoding(PDFobject):
404 def __init__(self, encoding):
405 PDFobject.__init__(self, "encoding", encoding.name)
406 self.encoding = encoding
408 def outputPDF(self, file, writer, registry):
409 encodingfile = type1font.encodingfile(self.encoding.name, self.encoding.filename)
410 encodingfile.outputPDF(file, writer, registry)
413 class PDFwriter:
415 def __init__(self, document, filename, compress=0, compresslevel=6):
416 if filename[-4:] != ".pdf":
417 filename = filename + ".pdf"
418 try:
419 file = open(filename, "wb")
420 except IOError:
421 raise IOError("cannot open output file")
423 if compress and not haszlib:
424 compress = 0
425 warnings.warn("compression disabled due to missing zlib module")
426 self.compress = compress
427 self.compresslevel = compresslevel
429 file.write("%%PDF-1.4\n%%%s%s%s%s\n" % (chr(195), chr(182), chr(195), chr(169)))
431 # the PDFcatalog class automatically builds up the pdfobjects from a document
432 registry = PDFregistry()
433 catalog = PDFcatalog(document, registry)
434 registry.add(catalog)
435 registry.write(file, self, catalog)
436 file.close()
439 class compressedstream:
441 def __init__(self, file, compresslevel):
442 self.file = file
443 self.compressobj = zlib.compressobj(compresslevel)
445 def write(self, string):
446 self.file.write(self.compressobj.compress(string))
448 def flush(self):
449 self.file.write(self.compressobj.flush())
452 class context:
454 def __init__(self):
455 self.linewidth_pt = None
456 # XXX there are both stroke and fill color spaces
457 self.colorspace = None
458 self.strokeattr = 1
459 self.fillattr = 1
460 self.font = None
461 self.textregion = 0
463 def __call__(self, **kwargs):
464 newcontext = copy.copy(self)
465 for key, value in kwargs.items():
466 setattr(newcontext, key, value)
467 return newcontext