resources rework completed
[PyX.git] / pyx / pdfwriter.py
blobcf16ba912c002b7421230cb872aab3da789f0b13
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 import pykpathsea, unit, resource, style
26 from t1strip import fullfont
27 try:
28 import zlib
29 haszlib = 1
30 except:
31 haszlib = 0
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.canvas.bbox()
161 self.bbox.enlarge(page.bboxenlarge)
162 self.pagetrafo = page.pagetrafo(self.bbox)
163 if self.pagetrafo:
164 self.bbox.transform(self.pagetrafo)
165 self.PDFcontent = PDFcontent(page.canvas, self.pagetrafo, self.pageregistry)
166 self.pageregistry.add(self.PDFcontent)
167 self.page.canvas.registerPDF(self.pageregistry)
168 registry.mergeregistry(self.pageregistry)
170 def outputPDF(self, file, writer, registry):
171 file.write("<<\n"
172 "/Type /Page\n"
173 "/Parent %i 0 R\n" % registry.getrefno(self.PDFpages))
174 paperformat = self.page.paperformat
175 file.write("/MediaBox [0 0 %d %d]\n" % (unit.topt(paperformat.width), unit.topt(paperformat.height)))
176 file.write("/CropBox " )
177 self.bbox.outputPDF(file, writer)
178 if self.pageregistry.types.has_key("font"):
179 file.write("/Resources << /ProcSet [ /PDF /Text ]\n")
180 file.write("/Font << %s >>" % " ".join(["/%s %i 0 R" % (font.font.getpsname(), registry.getrefno(font))
181 for font in self.pageregistry.types["font"].values()]))
182 else:
183 file.write("/Resources << /ProcSet [ /PDF ]\n")
185 file.write(">>\n")
186 file.write("/Contents %i 0 R\n"
187 ">>\n" % registry.getrefno(self.PDFcontent))
190 class _compressstream:
192 def __init__(self, file, compresslevel):
193 self.file = file
194 self.compressobj = zlib.compressobj(compresslevel)
196 def write(self, string):
197 self.file.write(self.compressobj.compress(string))
199 def flush(self):
200 self.file.write(self.compressobj.flush())
203 class PDFcontent(PDFobject):
205 def __init__(self, canvas, pagetrafo, registry):
206 PDFobject.__init__(self, "content")
207 self.canvas = canvas
208 self.pagetrafo = pagetrafo
209 self.PDFcontentlength = PDFcontentlength()
210 registry.add(self.PDFcontentlength)
212 def outputPDF(self, file, writer, registry):
213 file.write("<<\n"
214 "/Length %i 0 R\n" % registry.getrefno(self.PDFcontentlength))
215 if writer.compress:
216 file.write("/Filter /FlateDecode\n")
217 file.write(">>\n")
218 file.write("stream\n")
219 beginstreampos = file.tell()
221 if writer.compress:
222 stream = _compressstream(file, writer.compresslevel)
223 else:
224 stream = file
226 acontext = context()
227 # apply a possible global transformation
228 if self.pagetrafo:
229 self.pagetrafo.outputPDF(stream, writer, acontext)
230 style.linewidth.normal.outputPDF(stream, writer, acontext)
232 self.canvas.outputPDF(stream, writer, acontext)
233 if writer.compress:
234 stream.flush()
236 self.PDFcontentlength.contentlength = file.tell() - beginstreampos
237 file.write("\nendstream\n")
240 class PDFcontentlength(PDFobject):
242 def __init__(self):
243 PDFobject.__init__(self, "_contentlength")
245 def outputPDF(self, file, writer, registry):
246 # initially we do not know about the content length
247 # -> it has to be written into the instance later on
248 file.write("%d\n" % self.contentlength)
251 class PDFfont(PDFobject):
253 def __init__(self, font, registry):
254 PDFobject.__init__(self, "font", font.getpsname())
255 self.font = font
256 self.fontwidths = PDFfontwidths(self.font)
257 registry.add(self.fontwidths)
258 self.fontdescriptor = PDFfontdescriptor(self.font, registry)
259 registry.add(self.fontdescriptor)
261 def outputPDF(self, file, writer, registry):
262 file.write("<<\n"
263 "/Type /Font\n"
264 "/Subtype /Type1\n"
265 "/Name /%s\n"
266 "/BaseFont /%s\n"
267 "/FirstChar 0\n"
268 "/LastChar 255\n"
269 "/Widths %d 0 R\n"
270 "/FontDescriptor %d 0 R\n"
271 "/Encoding /StandardEncoding\n" # FIXME
272 ">>\n" % (self.font.getpsname(), self.font.getbasepsname(),
273 registry.getrefno(self.fontwidths),
274 registry.getrefno(self.fontdescriptor)))
276 class PDFfontwidths(PDFobject):
278 def __init__(self, font):
279 PDFobject.__init__(self, "fontwidths")
280 self.font = font
282 def outputPDF(self, file, writer, registry):
283 file.write("[\n")
284 for i in range(256):
285 try:
286 width = self.font.getwidth_pt(i)*1000/self.font.getsize_pt()
287 except:
288 width = 0
289 file.write("%f\n" % width)
290 file.write("]\n")
293 class PDFfontdescriptor(PDFobject):
295 def __init__(self, font, registry):
296 PDFobject.__init__(self, "fontdescriptor")
297 self.font = font
298 self.fontfile = PDFfontfile(self.font)
299 registry.add(self.fontfile)
301 def outputPDF(self, file, writer, registry):
302 file.write("<<\n"
303 "/Type /FontDescriptor\n"
304 "/FontName /%s\n"
305 "/Flags 4\n" # FIXME
306 "/FontBBox [-10 -10 1000 1000]\n" # FIXME
307 "/ItalicAngle 0\n" # FIXME
308 "/Ascent 20\n" # FIXME
309 "/Descent -5\n" # FIXME
310 "/CapHeight 15\n" # FIXME
311 "/StemV 3\n" # FIXME
312 "/FontFile %d 0 R\n" # FIXME
313 # "/CharSet \n" # fill in when stripping
314 ">>\n" % (self.font.getbasepsname(), registry.getrefno(self.fontfile)))
316 class PDFfontfile(PDFobject):
318 def __init__(self, font):
319 PDFobject.__init__(self, "fontfile", font.getfontfile())
320 self.font = font
322 def outputPDF(self, file, writer, registry):
323 fontfile = open(self.font.getfontfile())
324 fontdata = fontfile.read()
325 fontfile.close()
326 if fontdata[0:2] != fullfont._PFB_ASCII:
327 raise RuntimeError("PFB_ASCII mark expected")
328 length1 = fullfont.pfblength(fontdata[2:6])
329 if fontdata[6+length1:8+length1] != fullfont._PFB_BIN:
330 raise RuntimeError("PFB_BIN mark expected")
331 length2 = fullfont.pfblength(fontdata[8+length1:12+length1])
332 if fontdata[12+length1+length2:14+length1+length2] != fullfont._PFB_ASCII:
333 raise RuntimeError("PFB_ASCII mark expected")
334 length3 = fullfont.pfblength(fontdata[14+length1+length2:18+length1+length2])
335 if fontdata[18+length1+length2+length3:20+length1+length2+length3] != fullfont._PFB_DONE:
336 raise RuntimeError("PFB_DONE mark expected")
337 if len(fontdata) != 20 + length1 + length2 + length3:
338 raise RuntimeError("end of pfb file expected")
340 # we might be allowed to skip the third part ...
341 if fontdata[18+length1+length2:18+length1+length2+length3].replace("\n", "").replace("\r", "").replace("\t", "").replace(" ", "") == "0"*512 + "cleartomark":
342 length3 = 0
344 if length3:
345 data = fontdata[6:6+length1] + fontdata[12+length1:12+length1+length2] + fontdata[18+length1+length2:18+length1+length2+length3]
346 else:
347 data = fontdata[6:6+length1] + fontdata[12+length1:12+length1+length2]
348 if writer.compress:
349 data = zlib.compress(data)
351 file.write("<<\n"
352 "/Length %d\n"
353 "/Length1 %d\n"
354 "/Length2 %d\n"
355 "/Length3 %d\n" % (len(data), length1, length2, length3))
356 if writer.compress:
357 file.write("/Filter /FlateDecode\n")
358 file.write(">>\n"
359 "stream\n")
360 file.write(data)
361 file.write("\nendstream\n")
364 class PDFwriter:
366 def __init__(self, document, filename, compress=0, compresslevel=6):
367 warnings.warn("writePDFfile is experimental and supports only a subset of PyX's features")
369 if filename[-4:] != ".pdf":
370 filename = filename + ".pdf"
371 try:
372 file = open(filename, "wb")
373 except IOError:
374 raise IOError("cannot open output file")
376 if compress and not haszlib:
377 compress = 0
378 warnings.warn("compression disabled due to missing zlib module")
379 self.compress = compress
380 self.compresslevel = compresslevel
382 file.write("%%PDF-1.4\n%%%s%s%s%s\n" % (chr(195), chr(182), chr(195), chr(169)))
384 # the PDFcatalog class automatically builds up the pdfobjects from a document
385 registry = PDFregistry()
386 catalog = PDFcatalog(document, registry)
387 registry.add(catalog)
388 registry.write(file, self, catalog)
389 file.close()
392 class context:
394 def __init__(self):
395 self.linewidth_pt = None
396 self.colorspace = None
397 self.strokeattr = 1
398 self.fillattr = 1
400 def __call__(self, **kwargs):
401 newcontext = copy.copy(self)
402 for key, value in kwargs.items():
403 setattr(newcontext, key, value)
404 return newcontext