2 # -*- coding: ISO-8859-1 -*-
5 # Copyright (C) 2005-2006 Jörg Lehmann <joergl@users.sourceforge.net>
6 # Copyright (C) 2005-2006 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
24 import cStringIO
, copy
, warnings
, time
31 import bbox
, unit
, style
, type1font
, version
36 # fallback implementation for Python 2.2 and below
38 return zip(xrange(len(list)), list)
45 # we need to keep the original order of the resources (for PDFcontentlength)
49 def add(self
, resource
):
50 """ register resource, merging it with an already registered resource of the same type and id"""
51 resources
= self
.types
.setdefault(resource
.type, {})
52 if resources
.has_key(resource
.id):
53 resources
[resource
.id].merge(resource
)
55 self
.resources
.append(resource
)
56 resources
[resource
.id] = resource
58 def getrefno(self
, resource
):
59 return self
.types
[resource
.type][resource
.id].refno
61 def mergeregistry(self
, registry
):
62 for resource
in registry
.resources
:
65 def write(self
, file, writer
, catalog
):
66 # first we set all refnos
68 for resource
in self
.resources
:
69 resource
.refno
= refno
72 # second, all objects are written, keeping the positions in the output file
74 for resource
in self
.resources
:
75 fileposes
.append(file.tell())
76 file.write("%i 0 obj\n" % resource
.refno
)
77 resource
.output(file, writer
, self
)
78 file.write("endobj\n")
84 "0000000000 65535 f \n" % refno
)
86 for filepos
in fileposes
:
87 file.write("%010i 00000 n \n" % filepos
)
90 file.write("trailer\n"
93 file.write("/Root %i 0 R\n" % self
.getrefno(catalog
))
94 file.write("/Info %i 0 R\n" % self
.getrefno(catalog
.PDFinfo
))
103 def __init__(self
, type, _id
=None, pageresource
=None, pageprocset
=None):
104 """create a PDFobject
105 - type has to be a string describing the type of the object
106 - _id is a unique identification used for the object if it is not None.
107 Otherwise id(self) is used
108 - If pageresource is not None, it has to be a string describing the name
109 of the resource to be included in the resource dictionary of the pages
110 including the PDFobject.
111 - If pageprocset is not None, it has to be a string describing the name
112 to be used in the ProcSet list of the pages including the PDFObject.
119 self
.pageresource
= pageresource
120 self
.pageprocset
= pageprocset
123 def merge(self
, other
):
126 def output(self
, file, writer
, registry
):
127 raise NotImplementedError("output method has to be provided by PDFobject subclass")
130 class PDFcatalog(PDFobject
):
132 def __init__(self
, document
, writer
, registry
):
133 PDFobject
.__init
__(self
, "catalog")
134 self
.PDFpages
= PDFpages(document
, writer
, registry
)
135 registry
.add(self
.PDFpages
)
136 self
.PDFinfo
= PDFinfo(writer
, registry
)
137 registry
.add(self
.PDFinfo
)
139 def output(self
, file, writer
, registry
):
142 "/Pages %i 0 R\n" % registry
.getrefno(self
.PDFpages
))
143 if writer
.fullscreen
:
144 file.write("/PageMode /FullScreen\n")
148 class PDFinfo(PDFobject
):
150 def __init__(self
, writer
, registry
):
151 PDFobject
.__init
__(self
, "info")
153 def output(self
, file, writer
, registry
):
154 if time
.timezone
< 0:
155 # divmod on positive numbers, otherwise the minutes have a different sign from the hours
156 timezone
= "-%02i'%02i'" % divmod(-time
.timezone
/60, 60)
157 elif time
.timezone
> 0:
158 timezone
= "+%02i'%02i'" % divmod(time
.timezone
/60, 60)
165 if 32 <= ord(c
) <= 127 and c
not in "()[]<>\\":
168 r
+= "\\%03o" % ord(c
)
173 file.write("/Title (%s)\n" % pdfstring(writer
.title
))
175 file.write("/Author (%s)\n" % pdfstring(writer
.author
))
177 file.write("/Subject (%s)\n" % pdfstring(writer
.subject
))
179 file.write("/Keywords (%s)\n" % pdfstring(writer
.keywords
))
180 file.write("/Creator (PyX %s)\n" % version
.version
)
181 file.write("/CreationDate (D:%s%s)\n" % (time
.strftime("%Y%m%d%H%M"), timezone
))
185 class PDFpages(PDFobject
):
187 def __init__(self
, document
, writer
, registry
):
188 PDFobject
.__init
__(self
, "pages")
189 self
.PDFpagelist
= []
190 for pageno
, page
in enumerate(document
.pages
):
191 page
= PDFpage(page
, pageno
, self
, writer
, registry
)
192 self
.PDFpagelist
.append(page
)
193 for i
in range(len(self
.PDFpagelist
), 0, -1):
194 registry
.add(self
.PDFpagelist
[i
-1])
196 def output(self
, file, writer
, registry
):
201 ">>\n" % (" ".join(["%i 0 R" % registry
.getrefno(page
)
202 for page
in self
.PDFpagelist
]),
203 len(self
.PDFpagelist
)))
206 class PDFpage(PDFobject
):
208 def __init__(self
, page
, pageno
, PDFpages
, writer
, registry
):
209 PDFobject
.__init
__(self
, "page", pageno
)
210 self
.PDFpages
= PDFpages
213 # every page uses its own registry in order to find out which
214 # resources are used within the page. However, the
215 # pageregistry is also merged in the global registry
216 self
.pageregistry
= PDFregistry()
217 self
.bbox
= bbox
.empty()
219 self
.PDFcontent
= PDFcontent(self
, writer
, self
.pageregistry
, self
.bbox
)
220 self
.pageregistry
.add(self
.PDFcontent
)
221 registry
.mergeregistry(self
.pageregistry
)
223 self
.pagetrafo
= page
.pagetrafo(self
.bbox
)
225 self
.transformedbbox
= self
.bbox
.transformed(self
.pagetrafo
)
227 self
.transformedbbox
= self
.bbox
229 def output(self
, file, writer
, registry
):
232 "/Parent %i 0 R\n" % registry
.getrefno(self
.PDFpages
))
233 paperformat
= self
.page
.paperformat
235 file.write("/MediaBox [0 0 %f %f]\n" % (unit
.topt(paperformat
.width
), unit
.topt(paperformat
.height
)))
237 file.write("/MediaBox [%f %f %f %f]\n" % self
.transformedbbox
.highrestuple_pt())
238 if self
.transformedbbox
and writer
.writebbox
:
239 file.write("/CropBox [%f %f %f %f]\n" % self
.transformedbbox
.highrestuple_pt())
242 for type in self
.pageregistry
.types
.keys():
243 for resource
in self
.pageregistry
.types
[type].values():
244 if resource
.pageprocset
is not None and resource
.pageprocset
not in procset
:
245 procset
.append(resource
.pageprocset
)
246 if resource
.pageresource
is not None:
247 resources
.setdefault(resource
.pageresource
, []).append(resource
)
248 file.write("/Resources <<\n"
249 "/ProcSet [ %s ]\n" % " ".join(["/%s" % p
for p
in procset
]))
250 for pageresource
, resources
in resources
.items():
251 file.write("/%s <<\n%s\n>>\n" % (pageresource
, "\n".join(["/%s %i 0 R" % (resource
.name
, registry
.getrefno(resource
))
252 for resource
in resources
])))
254 file.write("/Contents %i 0 R\n"
255 ">>\n" % registry
.getrefno(self
.PDFcontent
))
258 class PDFcontent(PDFobject
):
260 def __init__(self
, PDFpage
, writer
, registry
, bbox
):
261 PDFobject
.__init
__(self
, "content")
262 self
.PDFpage
= PDFpage
264 self
.contentfile
= cStringIO
.StringIO()
265 # XXX this should maybe be handled by the page since removing
266 # this code would allow us to (nearly, since we also need to
267 # set more info in the content dict) reuse PDFcontent for
270 style
.linewidth
.normal
.processPDF(self
.contentfile
, writer
, acontext
, registry
, bbox
)
272 self
.PDFpage
.page
.canvas
.processPDF(self
.contentfile
, writer
, acontext
, registry
, bbox
)
274 def output(self
, file, writer
, registry
):
275 # apply a possible global transformation
276 if self
.PDFpage
.pagetrafo
:
277 pagetrafofile
= cStringIO
.StringIO()
278 self
.PDFpage
.pagetrafo
.processPDF(pagetrafofile
, writer
, context(), registry
, bbox
.empty())
279 content
= pagetrafofile
.getvalue() + self
.contentfile
.getvalue()
281 content
= self
.contentfile
.getvalue()
284 "/Length %i\n" % len(content
))
285 # if writer.compress:
286 # file.write("/Filter /FlateDecode\n")
289 beginstreampos
= file.tell()
292 # stream = compressedstream(file, writer.compresslevel)
303 file.write("endstream\n")
306 class PDFfont(PDFobject
):
308 def __init__(self
, font
, chars
, writer
, registry
):
309 PDFobject
.__init
__(self
, "font", font
.name
, "Font", "Text")
311 self
.fontdescriptor
= PDFfontdescriptor(font
, chars
, writer
, registry
)
312 registry
.add(self
.fontdescriptor
)
315 self
.encoding
= PDFencoding(font
.encoding
, writer
, registry
)
316 registry
.add(self
.encoding
)
320 self
.name
= font
.name
321 self
.basefontname
= font
.basefontname
322 self
.metric
= font
.metric
324 def output(self
, file, writer
, registry
):
328 file.write("/Name /%s\n" % self
.name
)
329 file.write("/BaseFont /%s\n" % self
.basefontname
)
330 if self
.fontdescriptor
.fontfile
is not None and self
.fontdescriptor
.fontfile
.usedchars
is not None:
331 usedchars
= self
.fontdescriptor
.fontfile
.usedchars
332 firstchar
= min(usedchars
.keys())
333 lastchar
= max(usedchars
.keys())
334 file.write("/FirstChar %d\n" % firstchar
)
335 file.write("/LastChar %d\n" % lastchar
)
336 file.write("/Widths\n"
338 for i
in range(firstchar
, lastchar
+1):
339 if i
and not (i
% 8):
343 if usedchars
.has_key(i
):
344 file.write("%f" % self
.metric
.getwidth_ds(i
))
349 file.write("/FirstChar 0\n"
354 if i
and not (i
% 8):
359 width
= self
.metric
.getwidth_ds(i
)
360 except (IndexError, AttributeError):
362 file.write("%f" % width
)
364 file.write("/FontDescriptor %d 0 R\n" % registry
.getrefno(self
.fontdescriptor
))
366 file.write("/Encoding %d 0 R\n" % registry
.getrefno(self
.encoding
))
370 class PDFfontdescriptor(PDFobject
):
372 def __init__(self
, font
, chars
, writer
, registry
):
373 PDFobject
.__init
__(self
, "fontdescriptor", font
.basefontname
)
375 if font
.filename
is None:
378 self
.fontfile
= PDFfontfile(font
.basefontname
, font
.filename
, font
.encoding
, chars
, writer
, registry
)
379 registry
.add(self
.fontfile
)
381 self
.name
= font
.basefontname
382 self
.fontinfo
= font
.metric
.fontinfo()
384 def output(self
, file, writer
, registry
):
386 "/Type /FontDescriptor\n"
387 "/FontName /%s\n" % self
.name
)
388 if self
.fontfile
is None:
389 file.write("/Flags 32\n")
391 file.write("/Flags %d\n" % self
.fontfile
.getflags())
392 file.write("/FontBBox [%d %d %d %d]\n" % self
.fontinfo
.fontbbox
)
393 file.write("/ItalicAngle %d\n" % self
.fontinfo
.italicangle
)
394 file.write("/Ascent %d\n" % self
.fontinfo
.ascent
)
395 file.write("/Descent %d\n" % self
.fontinfo
.descent
)
396 file.write("/CapHeight %d\n" % self
.fontinfo
.capheight
)
397 file.write("/StemV %d\n" % self
.fontinfo
.vstem
)
398 if self
.fontfile
is not None:
399 file.write("/FontFile %d 0 R\n" % registry
.getrefno(self
.fontfile
))
403 class PDFfontfile(PDFobject
):
405 def __init__(self
, name
, filename
, encoding
, chars
, writer
, registry
):
406 PDFobject
.__init
__(self
, "fontfile", filename
)
408 self
.filename
= filename
410 self
.encodingfilename
= None
412 self
.encodingfilename
= encoding
.filename
415 self
.usedchars
[char
] = 1
420 def merge(self
, other
):
421 if self
.encodingfilename
== other
.encodingfilename
:
422 self
.usedchars
.update(other
.usedchars
)
424 # TODO: need to resolve the encoding when several encodings are in the play
427 def mkfontfile(self
):
429 self
.font
= font
.t1font
.T1pfbfont(self
.filename
)
432 if self
.font
is None:
434 return self
.font
.getflags()
436 def output(self
, file, writer
, registry
):
437 if self
.font
is None:
440 # XXX: access to the encoding file
441 if self
.encodingfilename
:
442 encodingfile
= type1font
.encodingfile(self
.encodingfilename
, self
.encodingfilename
)
443 usedglyphs
= [encodingfile
.decode(char
)[1:] for char
in self
.usedchars
.keys()]
445 self
.font
._encoding
()
446 usedglyphs
= [self
.font
.encoding
.decode(char
) for char
in self
.usedchars
.keys()]
447 strippedfont
= self
.font
.getstrippedfont(usedglyphs
)
449 strippedfont
= self
.font
450 strippedfont
.outputPDF(file, writer
)
453 class PDFencoding(PDFobject
):
455 def __init__(self
, encoding
, writer
, registry
):
456 PDFobject
.__init
__(self
, "encoding", encoding
.name
)
457 self
.encoding
= encoding
459 def output(self
, file, writer
, registry
):
460 encodingfile
= type1font
.encodingfile(self
.encoding
.name
, self
.encoding
.filename
)
461 encodingfile
.outputPDF(file, writer
)
466 def __init__(self
, document
, filename
,
467 title
=None, author
=None, subject
=None, keywords
=None,
468 fullscreen
=0, writebbox
=0, compress
=1, compresslevel
=6):
469 if not filename
.endswith(".pdf"):
470 filename
= filename
+ ".pdf"
472 file = open(filename
, "wb")
474 raise IOError("cannot open output file")
478 self
.subject
= subject
479 self
.keywords
= keywords
480 self
.fullscreen
= fullscreen
481 self
.writebbox
= writebbox
482 if compress
and not haszlib
:
484 warnings
.warn("compression disabled due to missing zlib module")
485 self
.compress
= compress
486 self
.compresslevel
= compresslevel
488 # the PDFcatalog class automatically builds up the pdfobjects from a document
489 registry
= PDFregistry()
490 catalog
= PDFcatalog(document
, self
, registry
)
491 registry
.add(catalog
)
493 file.write("%%PDF-1.4\n%%%s%s%s%s\n" % (chr(195), chr(182), chr(195), chr(169)))
494 registry
.write(file, self
, catalog
)
498 class compressedstream
:
500 def __init__(self
, file, compresslevel
):
502 self
.compressobj
= zlib
.compressobj(compresslevel
)
504 def write(self
, string
):
505 self
.file.write(self
.compressobj
.compress(string
))
508 self
.file.write(self
.compressobj
.flush())
514 self
.linewidth_pt
= None
515 # XXX there are both stroke and fill color spaces
516 self
.colorspace
= None
522 def __call__(self
, **kwargs
):
523 newcontext
= copy
.copy(self
)
524 for key
, value
in kwargs
.items():
525 setattr(newcontext
, key
, value
)