various cleanups and corrections of smaller errors
[PyX/mjg.git] / pyx / pdfwriter.py
blobd0d12c59aac0b3fe95759afeb245e60852cd157c
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, sys
25 import prolog, pykpathsea, unit, style
26 from t1strip import fullfont
27 try:
28 import zlib
29 haszlib = 1
30 except:
31 haszlib = 0
34 class PDFobject:
36 def __init__(self, writer, refno):
37 self.refno = refno
39 def outputPDFobject(self, file):
40 self.filepos = file.tell()
41 file.write("%i 0 obj\n" % self.refno)
42 self.outputPDF(file)
43 file.write("endobj\n")
45 def outputPDF(self, file):
46 raise NotImplementedError("outputPDF method has to be provided by PDFobject subclass")
49 class PDFcatalog(PDFobject):
51 def __init__(self, writer, refno, document):
52 PDFobject.__init__(self, writer, refno)
53 self.pages = writer.addobject(PDFpages, document)
55 def outputPDF(self, file):
56 file.write("<<\n"
57 "/Type /Catalog\n"
58 "/Pages %i 0 R\n"
59 ">>\n" % self.pages.refno)
62 class PDFpages(PDFobject):
64 def __init__(self, writer, refno, document):
65 PDFobject.__init__(self, writer, refno)
66 self.pages = []
67 for page in document.pages:
68 self.pages.append(writer.addobject(PDFpage, self.refno, page))
70 def outputPDF(self, file):
71 file.write("<<\n"
72 "/Type /Pages\n"
73 "/Kids [%s]\n"
74 "/Count %i\n"
75 ">>\n" % (" ".join(["%i 0 R" % page.refno
76 for page in self.pages]),
77 len(self.pages)))
80 class PDFpage(PDFobject):
82 def __init__(self, writer, refno, pagesrefno, page):
83 PDFobject.__init__(self, writer, refno)
84 self.pagesrefno = pagesrefno
85 self.page = page
86 self.bbox = page.canvas.bbox()
87 self.pagetrafo = page.pagetrafo(self.bbox)
88 if self.pagetrafo:
89 self.bbox.transform(self.pagetrafo)
90 self.content = writer.addobject(PDFcontent, page.canvas, self.pagetrafo)
92 def outputPDF(self, file):
93 file.write("<<\n"
94 "/Type /Page\n"
95 "/Parent %i 0 R\n" % self.pagesrefno)
96 paperformat = self.page.paperformat
97 file.write("/MediaBox [0 0 %d %d]\n" % (unit.topt(paperformat.width), unit.topt(paperformat.height)))
98 file.write("/CropBox " )
99 self.bbox.outputPDF(file)
100 file.write("/Resources << /ProcSet [ /PDF ] >>\n")
101 file.write("/Contents %i 0 R\n"
102 ">>\n" % (self.content.refno))
105 class PDFcontent(PDFobject):
107 def __init__(self, writer, refno, canvas, pagetrafo):
108 PDFobject.__init__(self, writer, refno)
109 self.refno = refno
110 self.canvas = canvas
111 self.pagetrafo = pagetrafo
112 self.contentlength = writer.addobject(PDFcontentlength)
114 def outputPDF(self, file):
115 file.write("<<\n"
116 "/Length %i 0 R\n" % (self.refno + 1))
117 # if self.compress:
118 # self.write("/Filter /FlateDecode\n")
119 file.write(">>\n")
120 file.write("stream\n")
121 beginstreampos = file.tell()
123 # if self.compress:
124 # if self.compressstream is not None:
125 # raise RuntimeError("compression within compression")
126 # self.compressstream = zlib.compressobj(self.compresslevel)
128 # apply a possible global transformation
130 if self.pagetrafo:
131 self.pagetrafo.outputPDF(file)
132 style.linewidth.normal.outputPDF(file)
133 self.canvas.outputPDF(file)
135 # if self.compressstream is not None:
136 # self.file.write(self.compressstream.flush())
137 # self.compressstream = None
139 self.contentlength.contentlength = file.tell() - beginstreampos
140 file.write("endstream\n")
143 class PDFcontentlength(PDFobject):
145 def __init__(self, writer, refno):
146 PDFobject.__init__(self, writer, refno)
147 # initially we do not know about the content length, we
148 # has to be written into the instance later on
149 self.contentlength = None
151 def outputPDF(self, file):
152 file.write("%d\n" % self.contentlength)
155 class PDFfont(PDFobject):
156 def __init__(self, writer, refno, font):
157 PDFobject.__init__(self, writer, refno)
158 self.font = font
159 self.fontwidths = writer.addobject(PDFfontwidths, font)
160 self.fontdescriptor = writer.addobject(PDFfontdescriptor, font)
162 def outputPDF(self, file):
163 self.write("<<\n"
164 "/Type /Font\n"
165 "/Subtype /Type1\n"
166 "/Name /%s\n"
167 "/BaseFont /%s\n"
168 "/FirstChar 0\n"
169 "/LastChar 255\n"
170 "/Widths %d 0 R\n"
171 "/FontDescriptor %d 0 R\n"
172 "/Encoding /StandardEncoding\n" # FIXME
173 ">>\n" % (self.font.getpsname(), self.font.getbasepsname(),
174 self.fontwidths.refno, self.fontdescriptor.refno))
176 class PDFfontwidths(PDFobject):
177 def __init__(self, writer, refno, font):
178 PDFobject.__init__(self, writer, refno)
179 self.font = font
181 def outputPDF(self, file):
182 self.write("[\n")
183 for i in range(256):
184 try:
185 width = self.font.getwidth_pt(i)*1000/self.font.getsize_pt()
186 except:
187 width = 0
188 self.write("%f\n" % width)
189 self.write("]\n")
192 class PDFfontdescriptor(PDFobject):
194 def __init__(self, writer, refno, font):
195 PDFobject.__init__(self, writer, refno)
196 self.font = font
197 path = pykpathsea.find_file(font.filename, pykpathsea.kpse_type1_format)
198 self.fontfile = writer.addobject(PDFfontfile, path)
200 def outputPDF(self, file):
201 self.write("<<\n"
202 "/Type /FontDescriptor\n"
203 "/FontName /%s\n"
204 "/Flags 4\n" # FIXME
205 "/FontBBox [-10 -10 1000 1000]\n" # FIXME
206 "/ItalicAngle 0\n" # FIXME
207 "/Ascent 20\n" # FIXME
208 "/Descent -5\n" # FIXME
209 "/CapHeight 15\n" # FIXME
210 "/StemV 3\n" # FIXME
211 "/FontFile %d 0 R\n" # FIXME
212 # "/CharSet \n" # fill in when stripping
213 ">>\n" % (self.font.getbasepsname(), self.fontfile.refno))
215 class PDFfontfile(PDFobject):
217 def __init__(self, writer, refno, path):
218 PDFobject.__init__(self, writer, refno)
219 self.path = path
221 def outputPDF(self, file):
222 fontfile = open(self.path)
223 fontdata = fontfile.read()
224 fontfile.close()
225 if fontdata[0:2] != fullfont._PFB_ASCII:
226 raise RuntimeError("PFB_ASCII mark expected")
227 length1 = fullfont.pfblength(fontdata[2:6])
228 if fontdata[6+length1:8+length1] != fullfont._PFB_BIN:
229 raise RuntimeError("PFB_BIN mark expected")
230 length2 = fullfont.pfblength(fontdata[8+length1:12+length1])
231 if fontdata[12+length1+length2:14+length1+length2] != fullfont._PFB_ASCII:
232 raise RuntimeError("PFB_ASCII mark expected")
233 length3 = fullfont.pfblength(fontdata[14+length1+length2:18+length1+length2])
234 if fontdata[18+length1+length2+length3:20+length1+length2+length3] != fullfont._PFB_DONE:
235 raise RuntimeError("PFB_DONE mark expected")
236 if len(fontdata) != 20 + length1 + length2 + length3:
237 raise RuntimeError("end of pfb file expected")
239 # we might be allowed to skip the third part ...
240 if fontdata[18+length1+length2:18+length1+length2+length3].replace("\n", "").replace("\r", "").replace("\t", "").replace(" ", "") == "0"*512 + "cleartomark":
241 length3 = 0
243 uncompresseddata = fontdata[6:6+length1] + fontdata[12+length1:12+length1+length2] + fontdata[18+length1+length2:18+length1+length2+length3]
244 compresseddata = zlib.compress(uncompresseddata)
246 self.write("<<\n"
247 "/Length %d\n"
248 "/Length1 %d\n"
249 "/Length2 %d\n"
250 "/Length3 %d\n"
251 "/Filter /FlateDecode\n"
252 ">>\n"
253 "stream\n" % (len(compresseddata), length1, length2, length3))
254 #file.write(fontdata[6:6+length1])
255 #file.write(fontdata[12+length1:12+length1+length2])
256 #file.write(fontdata[18+length1+length2:18+length1+length2+length3])
257 self.write(compresseddata)
258 self.write("endstream\n")
261 class PDFwriter:
263 def __init__(self, document, filename, compress=1, compresslevel=6):
264 sys.stderr.write("*** PyX Warning: writePDFfile is experimental and supports only a subset of PyX's features\n")
266 if filename[-4:] != ".pdf":
267 filename = filename + ".pdf"
268 try:
269 self.file = open(filename, "wb")
270 except IOError:
271 raise IOError("cannot open output file")
273 if compress and not haszlib:
274 compress = 0
275 sys.stderr.write("*** PyX Warning: compression disabled due to missing zlib module\n")
276 self.compress = compress
277 self.compresslevel = compresslevel
279 self.pdfobjects = []
280 self.pdfobjectcount = 0
282 self.file.write("%%PDF-1.4\n%%%s%s%s%s\n" % (chr(195), chr(182), chr(195), chr(169)))
284 # the PDFcatalog class automatically builds up the pdfobjects from a document
285 catalog = self.addobject(PDFcatalog, document)
287 # by recursively appending to the list of pdfobjects, we create this list in reverse
288 # order and thus need to reverse it before writing it in the output file
289 self.pdfobjects.reverse()
290 for pdfobject in self.pdfobjects:
291 pdfobject.outputPDFobject(self.file)
293 # create xref list
294 xrefpos = self.file.tell()
295 self.file.write("xref\n"
296 "0 %d\n"
297 "0000000000 65535 f \n" % (len(self.pdfobjects)+1))
298 for pdfobject in self.pdfobjects:
299 self.file.write("%010i 00000 n \n" % pdfobject.filepos)
301 # trailer
302 self.file.write("trailer\n"
303 "<<\n"
304 "/Size %i\n"
305 "/Root %i 0 R\n"
306 ">>\n"
307 "startxref\n"
308 "%i\n"
309 "%%%%EOF\n" % (len(self.pdfobjects)+1, catalog.refno, xrefpos))
310 self.file.close()
312 def addobject(self, pdfobjectclass, *args, **kwargs):
313 """ create an instance of pdfobjectclass, append it to the list of pdfobjects
314 and return it to the caller.
316 The constructor of pdfobjectclass gets as arguments the PDFwriter and the
317 refno of the new object well as further arguments *args and **kwargs."""
319 self.pdfobjectcount += 1
320 pdfobject = pdfobjectclass(self, self.pdfobjectcount, *args, **kwargs)
321 self.pdfobjects.append(pdfobject)
322 return pdfobject
323 # # During the creating of object other objects may already have been added.
324 # # We have to make sure that we add the object at the right place in the
325 # # object list:
326 # i = len(self.pdfobjects)
327 # while i > 0 and self.pdfobjects[i-1].refno > pdfobject.refno:
328 # i -= 1
329 # self.pdfobjects.insert(i, pdfobject)
330 # return pdfobject
333 def page(self, abbox, canvas, mergedprolog, ctrafo):
334 # insert resources
335 for pritem in mergedprolog:
336 if isinstance(pritem, prolog.fontdefinition):
337 if pritem.filename:
338 pass
339 # fontfile
341 # resources
342 self.beginobj(PDFresources % self.pages)
343 self.write("<<\n")
344 if len([pritem for pritem in mergedprolog if isinstance(pritem, prolog.fontdefinition)]):
345 self.write("/Font\n"
346 "<<\n")
347 for pritem in mergedprolog:
348 if isinstance(pritem, prolog.fontdefinition):
349 self.write("/%s %d 0 R\n" % (pritem.font.getpsname(),
350 self.refdict[PDFfont % pritem.font.getpsname()]))
351 self.write(">>\n")
352 self.write(">>\n")
353 self.endobj()
356 # some ideas...
358 class context:
360 def __init__(self):
361 self.colorspace = None
362 self.linewidth = None
364 class contextstack:
366 def __init__(self):
367 self.stack = []
368 self.context = context()
370 def save(self):
371 self.stack.append(copy.copy(self.context))
373 def restore(self):
374 self.context = self.stack.pop()