- simplify PSregistry as already proposed in comments
[PyX/mjg.git] / pyx / pdfwriter.py
blob8d1c5484a73ce719b16a4d2af1ec37b271d8aec0
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.resources = []
38 self.types = {}
39 self.merged = {} # contains merged resources, where refno's need to be injected
40 # (this is necessary, since some components, which depend on a
41 # given resource might want to lookup the refno, so it needs to
42 # be available even for objects, which have been removed due to
43 # merging)
45 def add(self, resource):
46 resources = self.types.setdefault(resource.type, {})
47 if resources.has_key(resource.id):
48 resources[resource.id].merge(resource)
49 self.merged.setdefault(resource.type, []).append(resource)
50 else:
51 self.resources.append(resource)
52 resources[resource.id] = resource
54 def merge(self, registry):
55 for resource in registry.resources:
56 self.add(resource)
58 def setrefno(self, refno):
59 for resource in self.resources:
60 refno = resource.setrefno(refno)
61 for type, resources in self.merged.items():
62 for resource in resources:
63 resource.refno = self.types[type][resource.id].refno
64 return refno
66 def outputPDFobjects(self, file, writer, context):
67 for resource in self.resources:
68 resource.outputPDFobjects(file, writer, context)
70 def outputPDFxref(self, file):
71 for resource in self.resources:
72 resource.outputPDFxref(file)
75 class PDFobject:
77 def __init__(self, childs=[]):
78 self.childs = childs
80 def merge(self, other):
81 pass
83 def setrefno(self, refno):
84 self.refno = refno
85 refno += 1
86 for child in self.childs:
87 refno = child.setrefno(refno)
88 return refno
90 def outputPDFobjects(self, file, writer, context):
91 self.outputPDFobject(file, writer, context)
92 for child in self.childs:
93 child.outputPDFobjects(file, writer, context)
95 def outputPDFobject(self, file, writer, context):
96 self.filepos = file.tell()
97 file.write("%i 0 obj\n" % self.refno)
98 self.outputPDF(file, writer, context)
99 file.write("endobj\n")
101 def outputPDF(self, file, writer, context):
102 raise NotImplementedError("outputPDF method has to be provided by PDFobject subclass")
104 def outputPDFxref(self, file):
105 file.write("%010i 00000 n \n" % self.filepos)
106 for child in self.childs:
107 child.outputPDFxref(file)
110 class PDFcatalog(PDFobject):
112 def __init__(self, document):
113 self.PDFpages = PDFpages(document)
114 self.registry = PDFregistry()
115 for page in self.PDFpages.PDFpagelist:
116 self.registry.merge(page.registry)
117 PDFobject.__init__(self, [self.PDFpages])
119 def setrefno(self, refno):
120 refno = PDFobject.setrefno(self, refno)
121 return self.registry.setrefno(refno)
123 def outputPDFobjects(self, file, writer, context):
124 PDFobject.outputPDFobjects(self, file, writer, context)
125 self.registry.outputPDFobjects(file, writer, context)
127 def outputPDF(self, file, writer, context):
128 file.write("<<\n"
129 "/Type /Catalog\n"
130 "/Pages %i 0 R\n"
131 ">>\n" % self.PDFpages.refno)
133 def outputPDFxref(self, file):
134 PDFobject.outputPDFxref(self, file)
135 self.registry.outputPDFxref(file)
138 class PDFpages(PDFobject):
140 def __init__(self, document):
141 self.PDFpagelist = []
142 for page in document.pages:
143 self.PDFpagelist.append(PDFpage(page, self))
144 PDFobject.__init__(self, self.PDFpagelist)
146 def outputPDF(self, file, writer, context):
147 file.write("<<\n"
148 "/Type /Pages\n"
149 "/Kids [%s]\n"
150 "/Count %i\n"
151 ">>\n" % (" ".join(["%i 0 R" % page.refno
152 for page in self.PDFpagelist]),
153 len(self.PDFpagelist)))
156 class PDFpage(PDFobject):
158 def __init__(self, page, PDFpages):
159 self.PDFpages = PDFpages
160 self.page = page
161 self.bbox = page.canvas.bbox()
162 self.bbox.enlarge(page.bboxenlarge)
163 self.pagetrafo = page.pagetrafo(self.bbox)
164 if self.pagetrafo:
165 self.bbox.transform(self.pagetrafo)
166 self.PDFcontent = PDFcontent(page.canvas, self.pagetrafo)
167 self.registry = PDFregistry()
168 for resource in page.canvas.resources():
169 resource.PDFregister(self.registry)
170 PDFobject.__init__(self, [self.PDFcontent])
172 def outputPDF(self, file, writer, context):
173 file.write("<<\n"
174 "/Type /Page\n"
175 "/Parent %i 0 R\n" % self.PDFpages.refno)
176 paperformat = self.page.paperformat
177 file.write("/MediaBox [0 0 %d %d]\n" % (unit.topt(paperformat.width), unit.topt(paperformat.height)))
178 file.write("/CropBox " )
179 self.bbox.outputPDF(file, writer, context)
180 if self.registry.types["font"]:
181 file.write("/Resources << /ProcSet [ /PDF /Text ]\n")
182 else:
183 file.write("/Resources << /ProcSet [ /PDF ]\n")
184 if self.registry.types.has_key("font"):
185 file.write("/Font << %s >>" % " ".join(["/%s %i 0 R" % (font.fontname, font.refno)
186 for font in self.registry.types["font"].values()]))
187 file.write(">>\n")
188 file.write("/Contents %i 0 R\n"
189 ">>\n" % (self.PDFcontent.refno))
192 class _compressstream:
194 def __init__(self, file, compresslevel):
195 self.file = file
196 self.compressobj = zlib.compressobj(writer.compresslevel)
198 def write(self, string):
199 self.file.write(self.compressobj.compress(string))
201 def flush(self):
202 self.file.write(self.compressobj.flush())
205 class PDFcontent(PDFobject):
207 def __init__(self, canvas, pagetrafo):
208 self.canvas = canvas
209 self.pagetrafo = pagetrafo
210 self.PDFcontentlength = PDFcontentlength()
211 PDFobject.__init__(self, [self.PDFcontentlength])
213 def outputPDF(self, file, writer, context):
214 file.write("<<\n"
215 "/Length %i 0 R\n" % (self.PDFcontentlength.refno))
216 if writer.compress:
217 file.write("/Filter /FlateDecode\n")
218 file.write(">>\n")
219 file.write("stream\n")
220 beginstreampos = file.tell()
222 if writer.compress:
223 stream = _compressstream(file, writer.compresslevel)
224 else:
225 stream = file
227 # apply a possible global transformation
228 if self.pagetrafo:
229 self.pagetrafo.outputPDF(stream, writer, context)
230 style.linewidth.normal.outputPDF(stream, writer, context)
232 self.canvas.outputPDF(stream, writer, context)
233 if writer.compress:
234 stream.flush()
236 self.PDFcontentlength.contentlength = file.tell() - beginstreampos
237 file.write("endstream\n")
240 class PDFcontentlength(PDFobject):
242 def outputPDF(self, file, writer, context):
243 # initially we do not know about the content length
244 # -> it has to be written into the instance later on
245 file.write("%d\n" % self.contentlength)
248 class PDFfont(PDFobject):
250 def __init__(self, fontname, basepsname, font):
251 self.type = "font"
252 self.id = self.fontname = fontname
253 self.basepsname = basepsname
254 self.fontwidths = PDFfontwidths(font)
255 self.fontdescriptor = PDFfontdescriptor(font)
256 PDFobject.__init__(self, [self.fontwidths, self.fontdescriptor])
258 def register(self, registry):
259 registry.addresource(registry.fonts, self)
261 def outputPDF(self, file, writer, context):
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.fontname, self.basepsname,
273 self.fontwidths.refno, self.fontdescriptor.refno))
275 class PDFfontwidths(PDFobject):
277 def __init__(self, font):
278 self.type = "fontwidth"
279 self.font = font
280 PDFobject.__init__(self)
282 def register(self, registry):
283 registry.addresource(registry.fontwidths, self)
285 def outputPDF(self, file, writer, context):
286 file.write("[\n")
287 for i in range(256):
288 try:
289 width = self.font.getwidth_pt(i)*1000/self.font.getsize_pt()
290 except:
291 width = 0
292 file.write("%f\n" % width)
293 file.write("]\n")
296 class PDFfontdescriptor(PDFobject):
298 def __init__(self, font):
299 self.type = "fontdescriptor"
300 self.font = font
301 path = pykpathsea.find_file(font.getfontfile(), pykpathsea.kpse_type1_format)
302 self.fontfile = PDFfontfile(path)
303 PDFobject.__init__(self, [self.fontfile])
305 def register(self, registry):
306 registry.addresource(registry.fontdescriptors, self)
308 def arrange(self, refno):
309 return PDFobject.arrangeselfandchilds(self, refno, self.fontfile)
311 def outputPDF(self, file, writer, context):
312 file.write("<<\n"
313 "/Type /FontDescriptor\n"
314 "/FontName /%s\n"
315 "/Flags 4\n" # FIXME
316 "/FontBBox [-10 -10 1000 1000]\n" # FIXME
317 "/ItalicAngle 0\n" # FIXME
318 "/Ascent 20\n" # FIXME
319 "/Descent -5\n" # FIXME
320 "/CapHeight 15\n" # FIXME
321 "/StemV 3\n" # FIXME
322 "/FontFile %d 0 R\n" # FIXME
323 # "/CharSet \n" # fill in when stripping
324 ">>\n" % (self.font.getbasepsname(), self.fontfile.refno))
326 class PDFfontfile(PDFobject):
328 def __init__(self, path):
329 self.type = "fontfile"
330 self.path = path
331 PDFobject.__init__(self)
333 def register(self, registry):
334 registry.addresource(registry.fontfiles, self)
336 def outputPDF(self, file, writer, context):
337 fontfile = open(self.path)
338 fontdata = fontfile.read()
339 fontfile.close()
340 if fontdata[0:2] != fullfont._PFB_ASCII:
341 raise RuntimeError("PFB_ASCII mark expected")
342 length1 = fullfont.pfblength(fontdata[2:6])
343 if fontdata[6+length1:8+length1] != fullfont._PFB_BIN:
344 raise RuntimeError("PFB_BIN mark expected")
345 length2 = fullfont.pfblength(fontdata[8+length1:12+length1])
346 if fontdata[12+length1+length2:14+length1+length2] != fullfont._PFB_ASCII:
347 raise RuntimeError("PFB_ASCII mark expected")
348 length3 = fullfont.pfblength(fontdata[14+length1+length2:18+length1+length2])
349 if fontdata[18+length1+length2+length3:20+length1+length2+length3] != fullfont._PFB_DONE:
350 raise RuntimeError("PFB_DONE mark expected")
351 if len(fontdata) != 20 + length1 + length2 + length3:
352 raise RuntimeError("end of pfb file expected")
354 # we might be allowed to skip the third part ...
355 if fontdata[18+length1+length2:18+length1+length2+length3].replace("\n", "").replace("\r", "").replace("\t", "").replace(" ", "") == "0"*512 + "cleartomark":
356 length3 = 0
358 if length3:
359 data = fontdata[6:6+length1] + fontdata[12+length1:12+length1+length2] + fontdata[18+length1+length2:18+length1+length2+length3]
360 else:
361 data = fontdata[6:6+length1] + fontdata[12+length1:12+length1+length2]
362 if writer.compress:
363 data = zlib.compress(data)
365 file.write("<<\n"
366 "/Length %d\n"
367 "/Length1 %d\n"
368 "/Length2 %d\n"
369 "/Length3 %d\n" % (len(data), length1, length2, length3))
370 if writer.compress:
371 file.write("/Filter /FlateDecode\n")
372 file.write(">>\n"
373 "stream\n")
374 file.write(data)
375 file.write("endstream\n")
378 class PDFwriter:
380 def __init__(self, document, filename, compress=1, compresslevel=6):
381 warnings.warn("writePDFfile is experimental and supports only a subset of PyX's features")
383 if filename[-4:] != ".pdf":
384 filename = filename + ".pdf"
385 try:
386 self.file = open(filename, "wb")
387 except IOError:
388 raise IOError("cannot open output file")
390 if compress and not haszlib:
391 compress = 0
392 warnings.warn("compression disabled due to missing zlib module")
393 self.compress = compress
394 self.compresslevel = compresslevel
396 self.file.write("%%PDF-1.4\n%%%s%s%s%s\n" % (chr(195), chr(182), chr(195), chr(169)))
398 # the PDFcatalog class automatically builds up the pdfobjects from a document
399 catalog = PDFcatalog(document)
400 pdfobjects = catalog.setrefno(1)
402 # objects
403 catalog.outputPDFobjects(self.file, self, context())
405 # xref
406 xrefpos = self.file.tell()
407 self.file.write("xref\n"
408 "0 %d\n"
409 "0000000000 65535 f \n" % pdfobjects)
410 catalog.outputPDFxref(self.file)
412 # trailer
413 self.file.write("trailer\n"
414 "<<\n"
415 "/Size %i\n"
416 "/Root %i 0 R\n"
417 ">>\n"
418 "startxref\n"
419 "%i\n"
420 "%%%%EOF\n" % (pdfobjects, catalog.refno, xrefpos))
421 self.file.close()
424 class context:
426 def __init__(self):
427 self.linewidth_pt = None
428 self.colorspace = None
429 self.strokeattr = 1
430 self.fillattr = 1
432 def __call__(self, **kwargs):
433 newcontext = copy.copy(self)
434 for key, value in kwargs.items():
435 setattr(newcontext, key, value)
436 return newcontext