2 # -*- coding: ISO-8859-1 -*-
5 # Copyright (C) 2004-2005 André Wobst <wobsta@users.sourceforge.net>
7 # This file is part of PyX (http://pyx.sourceforge.net/).
9 # PyX is free software; you can redistribute it and/or modify
10 # it under the terms of the GNU General Public License as published by
11 # the Free Software Foundation; either version 2 of the License, or
12 # (at your option) any later version.
14 # PyX is distributed in the hope that it will be useful,
15 # but WITHOUT ANY WARRANTY; without even the implied warranty of
16 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 # GNU General Public License for more details.
19 # You should have received a copy of the GNU General Public License
20 # along with PyX; if not, write to the Free Software
21 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
23 import struct
, warnings
30 import bbox
, canvas
, pswriter
, pdfwriter
, trafo
, unit
32 def ascii85lines(datalen
):
35 return (datalen
+ 56)/60
37 def ascii85stream(file, data
):
38 """Encodes the string data in ASCII85 and writes it to
39 the stream file. The number of lines written to the stream
40 is known just from the length of the data by means of the
41 ascii85lines function. Note that the tailing newline character
42 of the last line is not added by this function, but it is taken
43 into account in the ascii85lines function."""
44 i
= 3 # go on smoothly in case of data length equals zero
46 l
= [None, None, None, None]
47 for i
in range(len(data
)):
51 if i
%60 == 3 and i
!= 3:
55 # l[3], c5 = divmod(256*256*256*l[0]+256*256*l[1]+256*l[2]+l[3], 85)
56 # l[2], c4 = divmod(l[3], 85)
57 # we have to avoid number > 2**31 by
58 l
[3], c5
= divmod(256*256*l
[0]+256*256*l
[1]+256*l
[2]+l
[3], 85)
59 l
[2], c4
= divmod(256*256*3*l
[0]+l
[3], 85)
60 l
[1], c3
= divmod(l
[2], 85)
61 c1
, c2
= divmod(l
[1], 85)
62 file.write(struct
.pack('BBBBB', c1
+33, c2
+33, c3
+33, c4
+33, c5
+33))
66 for j
in range((i
%4) + 1, 4):
68 l
[3], c5
= divmod(256*256*l
[0]+256*256*l
[1]+256*l
[2]+l
[3], 85)
69 l
[2], c4
= divmod(256*256*3*l
[0]+l
[3], 85)
70 l
[1], c3
= divmod(l
[2], 85)
71 c1
, c2
= divmod(l
[1], 85)
72 file.write(struct
.pack('BBBB', c1
+33, c2
+33, c3
+33, c4
+33)[:(i
%4)+2])
77 def __init__(self
, width
, height
, mode
, data
, compressed
=None):
78 if width
<= 0 or height
<= 0:
79 raise ValueError("valid image size")
80 if mode
not in ["L", "RGB", "CMYK"]:
81 raise ValueError("invalid mode")
82 if compressed
is None and len(mode
)*width
*height
!= len(data
):
83 raise ValueError("wrong size of uncompressed data")
84 self
.size
= width
, height
87 self
.compressed
= compressed
89 def tostring(self
, *args
):
91 raise RuntimeError("encoding not supported in this implementation")
94 def convert(self
, model
):
95 raise RuntimeError("color model conversion not supported in this implementation")
98 class jpegimage(image
):
100 def __init__(self
, file):
104 data
= open(file, "rb").read()
109 if data
[pos
] == "\377" and data
[pos
+1] not in ["\0", "\377"]:
110 # print "marker: 0x%02x \\%03o" % (ord(data[pos+1]), ord(data[pos+1]))
111 if data
[pos
+1] == "\330":
115 elif not nestinglevel
:
116 raise ValueError("begin marker expected")
117 elif data
[pos
+1] == "\331":
122 elif data
[pos
+1] in ["\300", "\301"]:
123 l
, bits
, height
, width
, components
= struct
.unpack(">HBHHB", data
[pos
+2:pos
+10])
125 raise ValueError("implementation limited to 8 bit per component only")
127 mode
= {1: "L", 3: "RGB", 4: "CMYK"}[components
]
129 raise ValueError("invalid number of components")
131 elif data
[pos
+1] == "\340":
132 l
, id, major
, minor
, dpikind
, xdpi
, ydpi
= struct
.unpack(">H5sBBBHH", data
[pos
+2:pos
+16])
134 self
.info
= {"dpi": (xdpi
, ydpi
)}
136 self
.info
= {"dpi": (xdpi
*2.54, ydpi
*2.45)}
137 # else do not provide dpi information
141 raise ValueError("end marker expected")
142 image
.__init
__(self
, width
, height
, mode
, data
[begin
:end
], compressed
="DCT")
145 class PSimagedata(pswriter
.PSresource
):
147 def __init__(self
, name
, data
, singlestring
, maxstrlen
):
148 pswriter
.PSresource
.__init
__(self
, "imagedata", name
)
150 self
.singlestring
= singlestring
151 self
.maxstrlen
= maxstrlen
153 def outputPS(self
, file, writer
, registry
):
154 # TODO resource data could be written directly on the output stream
155 # after proper code reorganization
156 file.write("%%%%BeginRessource: %s\n" % self
.id)
157 if self
.singlestring
:
158 file.write("%%%%BeginData: %i ASCII Lines\n"
159 "<~" % ascii85lines(len(self
.data
)))
160 ascii85stream(file, self
.data
)
164 datalen
= len(self
.data
)
165 tailpos
= datalen
- datalen
% self
.maxstrlen
166 file.write("%%%%BeginData: %i ASCII Lines\n" %
167 ((tailpos
/self
.maxstrlen
) * ascii85lines(self
.maxstrlen
) +
168 ascii85lines(datalen
-tailpos
)))
170 for i
in xrange(0, tailpos
, self
.maxstrlen
):
172 ascii85stream(file, self
.data
[i
: i
+self
.maxstrlen
])
174 if datalen
!= tailpos
:
176 ascii85stream(file, self
.data
[tailpos
:])
180 file.write("/%s exch def\n" % self
.id)
181 file.write("%%EndRessource\n")
184 class PDFimagepalettedata(pdfwriter
.PDFobject
):
186 def __init__(self
, name
, data
):
187 pdfwriter
.PDFobject
.__init
__(self
, "imagepalettedata", name
)
190 def output(self
, file, writer
, registry
):
192 "/Length %d\n" % len(self
.data
))
195 file.write(self
.data
)
200 class PDFimage(pdfwriter
.PDFobject
):
202 def __init__(self
, name
, width
, height
, palettecolorspace
, palettedata
, colorspace
,
203 bitspercomponent
, compressmode
, data
, registry
):
204 if palettedata
is not None:
206 elif colorspace
== "/DeviceGray":
210 pdfwriter
.PDFobject
.__init
__(self
, "image", name
, "XObject", procset
)
211 if palettedata
is not None:
212 # acrobat wants a palette to be an object
213 self
.PDFpalettedata
= PDFimagepalettedata(name
, palettedata
)
214 registry
.add(self
.PDFpalettedata
)
218 self
.palettecolorspace
= palettecolorspace
219 self
.palettedata
= palettedata
220 self
.colorspace
= colorspace
221 self
.bitspercomponent
= bitspercomponent
222 self
.compressmode
= compressmode
225 def output(self
, file, writer
, registry
):
229 "/Width %d\n" % self
.width
)
230 file.write("/Height %d\n" % self
.height
)
231 if self
.palettedata
is not None:
232 file.write("/ColorSpace [ /Indexed %s %i\n" % (self
.palettecolorspace
, len(self
.palettedata
)/3-1))
233 file.write("%d 0 R\n" % registry
.getrefno(self
.PDFpalettedata
))
236 file.write("/ColorSpace %s\n" % self
.colorspace
)
237 file.write("/BitsPerComponent %d\n" % self
.bitspercomponent
)
238 file.write("/Length %d\n" % len(self
.data
))
239 if self
.compressmode
:
240 file.write("/Filter /%sDecode\n" % self
.compressmode
)
243 file.write(self
.data
)
248 class bitmap(canvas
.canvasitem
):
250 def __init__(self
, xpos
, ypos
, image
, width
=None, height
=None, ratio
=None,
251 PSstoreimage
=0, PSmaxstrlen
=4093,
252 compressmode
="Flate", flatecompresslevel
=6,
253 dctquality
=75, dctoptimize
=0, dctprogression
=0):
256 self
.imagewidth
, self
.imageheight
= image
.size
257 self
.PSstoreimage
= PSstoreimage
258 self
.PSmaxstrlen
= PSmaxstrlen
260 if width
is not None or height
is not None:
263 if self
.width
is None:
265 self
.width
= self
.height
* self
.imagewidth
/ float(self
.imageheight
)
267 self
.width
= ratio
* self
.height
268 elif self
.height
is None:
270 self
.height
= self
.width
* self
.imageheight
/ float(self
.imagewidth
)
272 self
.height
= (1.0/ratio
) * self
.width
273 elif ratio
is not None:
274 raise ValueError("can't specify a ratio when setting width and height")
276 if ratio
is not None:
277 raise ValueError("must specify width or height to set a ratio")
278 widthdpi
, heightdpi
= image
.info
["dpi"] # fails when no dpi information available
279 self
.width
= self
.imagewidth
/ float(widthdpi
) * unit
.t_inch
280 self
.height
= self
.imageheight
/ float(heightdpi
) * unit
.t_inch
282 self
.xpos_pt
= unit
.topt(self
.xpos
)
283 self
.ypos_pt
= unit
.topt(self
.ypos
)
284 self
.width_pt
= unit
.topt(self
.width
)
285 self
.height_pt
= unit
.topt(self
.height
)
287 # create decode and colorspace
288 self
.colorspace
= self
.palettecolorspace
= self
.palettedata
= None
289 if image
.mode
== "P":
290 palettemode
, self
.palettedata
= image
.palette
.getdata()
291 self
.decode
= "[0 255]"
293 self
.palettecolorspace
= {"L": "/DeviceGray",
295 "CMYK": "/DeviceCMYK"}[palettemode
]
297 warnings
.warn("image with unknown palette mode '%s' converted to rgb image" % palettemode
)
298 image
= image
.convert("RGB")
299 self
.decode
= "[0 1 0 1 0 1]"
300 self
.palettedata
= None
301 self
.colorspace
= "/DeviceRGB"
302 elif len(image
.mode
) == 1:
303 if image
.mode
!= "L":
304 image
= image
.convert("L")
305 warnings
.warn("specific single channel image mode not natively supported, converted to regular grayscale")
306 self
.decode
= "[0 1]"
307 self
.colorspace
= "/DeviceGray"
308 elif image
.mode
== "CMYK":
309 self
.decode
= "[0 1 0 1 0 1 0 1]"
310 self
.colorspace
= "/DeviceCMYK"
312 if image
.mode
!= "RGB":
313 image
= image
.convert("RGB")
314 warnings
.warn("image with unknown mode converted to rgb")
315 self
.decode
= "[0 1 0 1 0 1]"
316 self
.colorspace
= "/DeviceRGB"
319 self
.imagematrixPS
= (trafo
.mirror(0)
320 .translated_pt(-self
.xpos_pt
, self
.ypos_pt
+self
.height_pt
)
321 .scaled_pt(self
.imagewidth
/self
.width_pt
, self
.imageheight
/self
.height_pt
))
322 self
.imagematrixPDF
= (trafo
.scale_pt(self
.width_pt
, self
.height_pt
)
323 .translated_pt(self
.xpos_pt
, self
.ypos_pt
))
325 # check whether imagedata is compressed or not
327 imagecompressed
= image
.compressed
329 imagecompressed
= None
330 if compressmode
!= None and imagecompressed
!= None:
331 raise ValueError("compression of a compressed image not supported")
332 self
.compressmode
= compressmode
333 if compressmode
is not None and compressmode
not in ["Flate", "DCT"]:
334 raise ValueError("invalid compressmode '%s'" % compressmode
)
335 if imagecompressed
is not None:
336 self
.compressmode
= imagecompressed
337 if imagecompressed
not in ["Flate", "DCT"]:
338 raise ValueError("invalid compressed image '%s'" % imagecompressed
)
339 if not haszlib
and compressmode
== "Flate":
340 warnings
.warn("zlib module not available, disable compression")
341 self
.compressmode
= compressmode
= None
344 if compressmode
== "Flate":
345 self
.data
= zlib
.compress(image
.tostring(), flatecompresslevel
)
346 elif compressmode
== "DCT":
347 self
.data
= image
.tostring("jpeg", image
.mode
,
348 dctquality
, dctoptimize
, dctprogression
)
350 self
.data
= image
.tostring()
352 self
.PSsinglestring
= self
.PSstoreimage
and len(self
.data
) < self
.PSmaxstrlen
353 if self
.PSsinglestring
:
354 self
.PSimagename
= "image-%d-%s-singlestring" % (id(image
), compressmode
)
356 self
.PSimagename
= "image-%d-%s-stringarray" % (id(image
), compressmode
)
357 self
.PDFimagename
= "image-%d-%s" % (id(image
), compressmode
)
360 return bbox
.bbox_pt(self
.xpos_pt
, self
.ypos_pt
,
361 self
.xpos_pt
+self
.width_pt
, self
.ypos_pt
+self
.height_pt
)
363 def outputPS(self
, file, writer
, context
, registry
):
364 if self
.PSstoreimage
and not self
.PSsinglestring
:
365 registry
.add(pswriter
.PSdefinition("imagedataaccess",
366 "{ /imagedataindex load " # get list index
367 "dup 1 add /imagedataindex exch store " # store increased index
368 "/imagedataid load exch get }")) # select string from array
369 if self
.PSstoreimage
:
370 registry
.add(PSimagedata(self
.PSimagename
, self
.data
, self
.PSsinglestring
, self
.PSmaxstrlen
))
372 file.write("gsave\n")
373 if self
.palettedata
is not None:
374 file.write("[ /Indexed %s %i\n" % (self
.palettecolorspace
, len(self
.palettedata
)/3-1))
375 file.write("%%%%BeginData: %i ASCII Lines\n" % ascii85lines(len(self
.palettedata
)))
377 ascii85stream(file, self
.palettedata
)
380 file.write("] setcolorspace\n")
382 file.write("%s setcolorspace\n" % self
.colorspace
)
384 if self
.PSstoreimage
and not self
.PSsinglestring
:
385 file.write("/imagedataindex 0 store\n" # not use the stack since interpreters differ in their stack usage
386 "/imagedataid %s store\n" % self
.PSimagename
)
390 "/Width %i\n" % self
.imagewidth
)
391 file.write("/Height %i\n" % self
.imageheight
)
392 file.write("/BitsPerComponent 8\n"
393 "/ImageMatrix %s\n" % self
.imagematrixPS
)
394 file.write("/Decode %s\n" % self
.decode
)
396 file.write("/DataSource ")
397 if self
.PSstoreimage
:
398 if self
.PSsinglestring
:
399 file.write("/%s load" % self
.PSimagename
)
401 file.write("/imagedataaccess load") # some printers do not allow for inline code here -> we store it in a resource
403 file.write("currentfile /ASCII85Decode filter")
404 if self
.compressmode
:
405 file.write(" /%sDecode filter" % self
.compressmode
)
410 if self
.PSstoreimage
:
411 file.write("image\n")
413 # the datasource is currentstream (plus some filters)
414 file.write("%%%%BeginData: %i ASCII Lines\n"
415 "image\n" % (ascii85lines(len(self
.data
)) + 1))
416 ascii85stream(file, self
.data
)
417 file.write("~>\n%%EndData\n")
419 file.write("grestore\n")
421 def outputPDF(self
, file, writer
, context
, registry
):
422 registry
.add(PDFimage(self
.PDFimagename
, self
.imagewidth
, self
.imageheight
,
423 self
.palettecolorspace
, self
.palettedata
, self
.colorspace
,
424 8, self
.compressmode
, self
.data
, registry
))
427 self
.imagematrixPDF
.outputPDF(file, writer
, context
, registry
)
428 file.write("/%s Do\n" % self
.PDFimagename
)