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 outputPDF(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 pdfwriter
.PDFobject
.__init
__(self
, "image", name
)
205 if palettedata
is not None:
206 # acrobat wants a palette to be an object
207 self
.PDFpalettedata
= PDFimagepalettedata(name
, palettedata
)
208 registry
.add(self
.PDFpalettedata
)
212 self
.palettecolorspace
= palettecolorspace
213 self
.palettedata
= palettedata
214 self
.colorspace
= colorspace
215 self
.bitspercomponent
= bitspercomponent
216 self
.compressmode
= compressmode
219 def outputPDF(self
, file, writer
, registry
):
223 "/Width %d\n" % self
.width
)
224 file.write("/Height %d\n" % self
.height
)
225 if self
.palettedata
is not None:
226 file.write("/ColorSpace [ /Indexed %s %i\n" % (self
.palettecolorspace
, len(self
.palettedata
)/3-1))
227 file.write("%d 0 R\n" % registry
.getrefno(self
.PDFpalettedata
))
230 file.write("/ColorSpace %s\n" % self
.colorspace
)
231 file.write("/BitsPerComponent %d\n" % self
.bitspercomponent
)
232 file.write("/Length %d\n" % len(self
.data
))
233 if self
.compressmode
:
234 file.write("/Filter /%sDecode\n" % self
.compressmode
)
237 file.write(self
.data
)
242 class bitmap(canvas
.canvasitem
):
244 def __init__(self
, xpos
, ypos
, image
, width
=None, height
=None, ratio
=None,
245 PSstoreimage
=0, PSmaxstrlen
=4093,
246 compressmode
="Flate", flatecompresslevel
=6,
247 dctquality
=75, dctoptimize
=0, dctprogression
=0):
250 self
.imagewidth
, self
.imageheight
= image
.size
251 self
.PSstoreimage
= PSstoreimage
252 self
.PSmaxstrlen
= PSmaxstrlen
254 if width
is not None or height
is not None:
257 if self
.width
is None:
259 self
.width
= self
.height
* self
.imagewidth
/ float(self
.imageheight
)
261 self
.width
= ratio
* self
.height
262 elif self
.height
is None:
264 self
.height
= self
.width
* self
.imageheight
/ float(self
.imagewidth
)
266 self
.height
= (1.0/ratio
) * self
.width
267 elif ratio
is not None:
268 raise ValueError("can't specify a ratio when setting width and height")
270 if ratio
is not None:
271 raise ValueError("must specify width or height to set a ratio")
272 widthdpi
, heightdpi
= image
.info
["dpi"] # fails when no dpi information available
273 self
.width
= self
.imagewidth
/ float(widthdpi
) * unit
.t_inch
274 self
.height
= self
.imageheight
/ float(heightdpi
) * unit
.t_inch
276 self
.xpos_pt
= unit
.topt(self
.xpos
)
277 self
.ypos_pt
= unit
.topt(self
.ypos
)
278 self
.width_pt
= unit
.topt(self
.width
)
279 self
.height_pt
= unit
.topt(self
.height
)
281 # create decode and colorspace
282 self
.colorspace
= self
.palettecolorspace
= self
.palettedata
= None
283 if image
.mode
== "P":
284 palettemode
, self
.palettedata
= image
.palette
.getdata()
285 self
.decode
= "[0 255]"
287 self
.palettecolorspace
= {"L": "/DeviceGray",
289 "CMYK": "/DeviceCMYK"}[palettemode
]
291 warnings
.warn("image with unknown palette mode '%s' converted to rgb image" % palettemode
)
292 image
= image
.convert("RGB")
293 self
.decode
= "[0 1 0 1 0 1]"
294 self
.palettedata
= None
295 self
.colorspace
= "/DeviceRGB"
296 elif len(image
.mode
) == 1:
297 if image
.mode
!= "L":
298 image
= image
.convert("L")
299 warnings
.warn("specific single channel image mode not natively supported, converted to regular grayscale")
300 self
.decode
= "[0 1]"
301 self
.colorspace
= "/DeviceGray"
302 elif image
.mode
== "CMYK":
303 self
.decode
= "[0 1 0 1 0 1 0 1]"
304 self
.colorspace
= "/DeviceCMYK"
306 if image
.mode
!= "RGB":
307 image
= image
.convert("RGB")
308 warnings
.warn("image with unknown mode converted to rgb")
309 self
.decode
= "[0 1 0 1 0 1]"
310 self
.colorspace
= "/DeviceRGB"
313 self
.imagematrixPS
= (trafo
.mirror(0)
314 .translated_pt(-self
.xpos_pt
, self
.ypos_pt
+self
.height_pt
)
315 .scaled_pt(self
.imagewidth
/self
.width_pt
, self
.imageheight
/self
.height_pt
))
316 self
.imagematrixPDF
= (trafo
.scale_pt(self
.width_pt
, self
.height_pt
)
317 .translated_pt(self
.xpos_pt
, self
.ypos_pt
))
319 # check whether imagedata is compressed or not
321 imagecompressed
= image
.compressed
323 imagecompressed
= None
324 if compressmode
!= None and imagecompressed
!= None:
325 raise ValueError("compression of a compressed image not supported")
326 self
.compressmode
= compressmode
327 if compressmode
is not None and compressmode
not in ["Flate", "DCT"]:
328 raise ValueError("invalid compressmode '%s'" % compressmode
)
329 if imagecompressed
is not None:
330 self
.compressmode
= imagecompressed
331 if imagecompressed
not in ["Flate", "DCT"]:
332 raise ValueError("invalid compressed image '%s'" % imagecompressed
)
333 if not haszlib
and compressmode
== "Flate":
334 warnings
.warn("zlib module not available, disable compression")
335 self
.compressmode
= compressmode
= None
338 if compressmode
== "Flate":
339 self
.data
= zlib
.compress(image
.tostring(), flatecompresslevel
)
340 elif compressmode
== "DCT":
341 self
.data
= image
.tostring("jpeg", image
.mode
,
342 dctquality
, dctoptimize
, dctprogression
)
344 self
.data
= image
.tostring()
346 self
.PSsinglestring
= self
.PSstoreimage
and len(self
.data
) < self
.PSmaxstrlen
347 if self
.PSsinglestring
:
348 self
.PSimagename
= "image-%d-%s-singlestring" % (id(image
), compressmode
)
350 self
.PSimagename
= "image-%d-%s-stringarray" % (id(image
), compressmode
)
351 self
.PDFimagename
= "image-%d-%s" % (id(image
), compressmode
)
354 return bbox
.bbox_pt(self
.xpos_pt
, self
.ypos_pt
,
355 self
.xpos_pt
+self
.width_pt
, self
.ypos_pt
+self
.height_pt
)
357 def registerPS(self
, registry
):
358 if self
.PSstoreimage
and not self
.PSsinglestring
:
359 registry
.add(pswriter
.PSdefinition("imagedataaccess",
360 "{ /imagedataindex load " # get list index
361 "dup 1 add /imagedataindex exch store " # store increased index
362 "/imagedataid load exch get }")) # select string from array
363 if self
.PSstoreimage
:
364 registry
.add(PSimagedata(self
.PSimagename
, self
.data
, self
.PSsinglestring
, self
.PSmaxstrlen
))
366 def registerPDF(self
, registry
):
367 registry
.add(PDFimage(self
.PDFimagename
, self
.imagewidth
, self
.imageheight
,
368 self
.palettecolorspace
, self
.palettedata
, self
.colorspace
,
369 8, self
.compressmode
, self
.data
, registry
))
371 def outputPS(self
, file, writer
, context
):
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
.data
)))
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
):
423 self
.imagematrixPDF
.outputPDF(file, writer
, context
)
424 file.write("/%s Do\n" % self
.PDFimagename
)