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
, binascii
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])
74 _asciihexlinelength
= 64
75 def asciihexlines(datalen
):
76 return (datalen
*2 + _asciihexlinelength
- 1) / _asciihexlinelength
78 def asciihexstream(file, data
):
79 hexdata
= binascii
.b2a_hex(data
)
80 for i
in range((len(hexdata
)-1)/_asciihexlinelength
+ 1):
81 file.write(hexdata
[i
*_asciihexlinelength
: i
*_asciihexlinelength
+_asciihexlinelength
])
87 def __init__(self
, width
, height
, mode
, data
, compressed
=None):
88 if width
<= 0 or height
<= 0:
89 raise ValueError("valid image size")
90 if mode
not in ["L", "RGB", "CMYK"]:
91 raise ValueError("invalid mode")
92 if compressed
is None and len(mode
)*width
*height
!= len(data
):
93 raise ValueError("wrong size of uncompressed data")
94 self
.size
= width
, height
97 self
.compressed
= compressed
99 def tostring(self
, *args
):
101 raise RuntimeError("encoding not supported in this implementation")
104 def convert(self
, model
):
105 raise RuntimeError("color model conversion not supported in this implementation")
108 class jpegimage(image
):
110 def __init__(self
, file):
114 data
= open(file, "rb").read()
119 if data
[pos
] == "\377" and data
[pos
+1] not in ["\0", "\377"]:
120 # print "marker: 0x%02x \\%03o" % (ord(data[pos+1]), ord(data[pos+1]))
121 if data
[pos
+1] == "\330":
125 elif not nestinglevel
:
126 raise ValueError("begin marker expected")
127 elif data
[pos
+1] == "\331":
132 elif data
[pos
+1] in ["\300", "\301"]:
133 l
, bits
, height
, width
, components
= struct
.unpack(">HBHHB", data
[pos
+2:pos
+10])
135 raise ValueError("implementation limited to 8 bit per component only")
137 mode
= {1: "L", 3: "RGB", 4: "CMYK"}[components
]
139 raise ValueError("invalid number of components")
141 elif data
[pos
+1] == "\340":
142 l
, id, major
, minor
, dpikind
, xdpi
, ydpi
= struct
.unpack(">H5sBBBHH", data
[pos
+2:pos
+16])
144 self
.info
= {"dpi": (xdpi
, ydpi
)}
146 self
.info
= {"dpi": (xdpi
*2.54, ydpi
*2.45)}
147 # else do not provide dpi information
151 raise ValueError("end marker expected")
152 image
.__init
__(self
, width
, height
, mode
, data
[begin
:end
], compressed
="DCT")
155 class PSimagedata(pswriter
.PSresource
):
157 def __init__(self
, name
, data
, singlestring
, maxstrlen
):
158 pswriter
.PSresource
.__init
__(self
, "imagedata", name
)
160 self
.singlestring
= singlestring
161 self
.maxstrlen
= maxstrlen
163 def output(self
, file, writer
, registry
):
164 file.write("%%%%BeginRessource: %s\n" % self
.id)
165 if self
.singlestring
:
166 file.write("%%%%BeginData: %i ASCII Lines\n"
167 "<~" % ascii85lines(len(self
.data
)))
168 ascii85stream(file, self
.data
)
172 datalen
= len(self
.data
)
173 tailpos
= datalen
- datalen
% self
.maxstrlen
174 file.write("%%%%BeginData: %i ASCII Lines\n" %
175 ((tailpos
/self
.maxstrlen
) * ascii85lines(self
.maxstrlen
) +
176 ascii85lines(datalen
-tailpos
)))
178 for i
in xrange(0, tailpos
, self
.maxstrlen
):
180 ascii85stream(file, self
.data
[i
: i
+self
.maxstrlen
])
182 if datalen
!= tailpos
:
184 ascii85stream(file, self
.data
[tailpos
:])
188 file.write("/%s exch def\n" % self
.id)
189 file.write("%%EndRessource\n")
192 class PDFimagepalettedata(pdfwriter
.PDFobject
):
194 def __init__(self
, name
, data
):
195 pdfwriter
.PDFobject
.__init
__(self
, "imagepalettedata", name
)
198 def write(self
, file, writer
, registry
):
200 "/Length %d\n" % len(self
.data
))
203 file.write(self
.data
)
208 class PDFimage(pdfwriter
.PDFobject
):
210 def __init__(self
, name
, width
, height
, palettecolorspace
, palettedata
, colorspace
,
211 bitspercomponent
, compressmode
, data
, registry
):
212 if palettedata
is not None:
214 elif colorspace
== "/DeviceGray":
218 pdfwriter
.PDFobject
.__init
__(self
, "image", name
)
219 registry
.addresource("XObject", name
, self
, procset
=procset
)
220 if palettedata
is not None:
221 # acrobat wants a palette to be an object
222 self
.PDFpalettedata
= PDFimagepalettedata(name
, palettedata
)
223 registry
.add(self
.PDFpalettedata
)
227 self
.palettecolorspace
= palettecolorspace
228 self
.palettedata
= palettedata
229 self
.colorspace
= colorspace
230 self
.bitspercomponent
= bitspercomponent
231 self
.compressmode
= compressmode
234 def write(self
, file, writer
, registry
):
238 "/Width %d\n" % self
.width
)
239 file.write("/Height %d\n" % self
.height
)
240 if self
.palettedata
is not None:
241 file.write("/ColorSpace [ /Indexed %s %i\n" % (self
.palettecolorspace
, len(self
.palettedata
)/3-1))
242 file.write("%d 0 R\n" % registry
.getrefno(self
.PDFpalettedata
))
245 file.write("/ColorSpace %s\n" % self
.colorspace
)
246 file.write("/BitsPerComponent %d\n" % self
.bitspercomponent
)
247 file.write("/Length %d\n" % len(self
.data
))
248 if self
.compressmode
:
249 file.write("/Filter /%sDecode\n" % self
.compressmode
)
252 file.write(self
.data
)
257 class bitmap(canvas
.canvasitem
):
259 def __init__(self
, xpos
, ypos
, image
, width
=None, height
=None, ratio
=None,
260 PSstoreimage
=0, PSmaxstrlen
=4093, PSbinexpand
=1,
261 compressmode
="Flate", flatecompresslevel
=6,
262 dctquality
=75, dctoptimize
=0, dctprogression
=0):
265 self
.imagewidth
, self
.imageheight
= image
.size
266 self
.PSstoreimage
= PSstoreimage
267 self
.PSmaxstrlen
= PSmaxstrlen
268 self
.PSbinexpand
= PSbinexpand
270 if width
is not None or height
is not None:
273 if self
.width
is None:
275 self
.width
= self
.height
* self
.imagewidth
/ float(self
.imageheight
)
277 self
.width
= ratio
* self
.height
278 elif self
.height
is None:
280 self
.height
= self
.width
* self
.imageheight
/ float(self
.imagewidth
)
282 self
.height
= (1.0/ratio
) * self
.width
283 elif ratio
is not None:
284 raise ValueError("can't specify a ratio when setting width and height")
286 if ratio
is not None:
287 raise ValueError("must specify width or height to set a ratio")
288 widthdpi
, heightdpi
= image
.info
["dpi"] # fails when no dpi information available
289 self
.width
= self
.imagewidth
/ float(widthdpi
) * unit
.t_inch
290 self
.height
= self
.imageheight
/ float(heightdpi
) * unit
.t_inch
292 self
.xpos_pt
= unit
.topt(self
.xpos
)
293 self
.ypos_pt
= unit
.topt(self
.ypos
)
294 self
.width_pt
= unit
.topt(self
.width
)
295 self
.height_pt
= unit
.topt(self
.height
)
297 # create decode and colorspace
298 self
.colorspace
= self
.palettecolorspace
= self
.palettedata
= None
299 if image
.mode
== "P":
300 palettemode
, self
.palettedata
= image
.palette
.getdata()
301 self
.decode
= "[0 255]"
303 self
.palettecolorspace
= {"L": "/DeviceGray",
305 "CMYK": "/DeviceCMYK"}[palettemode
]
307 warnings
.warn("image with unknown palette mode '%s' converted to rgb image" % palettemode
)
308 image
= image
.convert("RGB")
309 self
.decode
= "[0 1 0 1 0 1]"
310 self
.palettedata
= None
311 self
.colorspace
= "/DeviceRGB"
312 elif len(image
.mode
) == 1:
313 if image
.mode
!= "L":
314 image
= image
.convert("L")
315 warnings
.warn("specific single channel image mode not natively supported, converted to regular grayscale")
316 self
.decode
= "[0 1]"
317 self
.colorspace
= "/DeviceGray"
318 elif image
.mode
== "CMYK":
319 self
.decode
= "[0 1 0 1 0 1 0 1]"
320 self
.colorspace
= "/DeviceCMYK"
322 if image
.mode
!= "RGB":
323 image
= image
.convert("RGB")
324 warnings
.warn("image with unknown mode converted to rgb")
325 self
.decode
= "[0 1 0 1 0 1]"
326 self
.colorspace
= "/DeviceRGB"
329 self
.imagematrixPS
= (trafo
.mirror(0)
330 .translated_pt(-self
.xpos_pt
, self
.ypos_pt
+self
.height_pt
)
331 .scaled_pt(self
.imagewidth
/self
.width_pt
, self
.imageheight
/self
.height_pt
))
332 self
.imagematrixPDF
= (trafo
.scale_pt(self
.width_pt
, self
.height_pt
)
333 .translated_pt(self
.xpos_pt
, self
.ypos_pt
))
335 # check whether imagedata is compressed or not
337 imagecompressed
= image
.compressed
339 imagecompressed
= None
340 if compressmode
!= None and imagecompressed
!= None:
341 raise ValueError("compression of a compressed image not supported")
342 self
.compressmode
= compressmode
343 if compressmode
is not None and compressmode
not in ["Flate", "DCT"]:
344 raise ValueError("invalid compressmode '%s'" % compressmode
)
345 if imagecompressed
is not None:
346 self
.compressmode
= imagecompressed
347 if imagecompressed
not in ["Flate", "DCT"]:
348 raise ValueError("invalid compressed image '%s'" % imagecompressed
)
349 if not haszlib
and compressmode
== "Flate":
350 warnings
.warn("zlib module not available, disable compression")
351 self
.compressmode
= compressmode
= None
354 if compressmode
== "Flate":
355 self
.data
= zlib
.compress(image
.tostring(), flatecompresslevel
)
356 elif compressmode
== "DCT":
357 self
.data
= image
.tostring("jpeg", image
.mode
,
358 dctquality
, dctoptimize
, dctprogression
)
360 self
.data
= image
.tostring()
362 self
.PSsinglestring
= self
.PSstoreimage
and len(self
.data
) < self
.PSmaxstrlen
363 if self
.PSsinglestring
:
364 self
.PSimagename
= "image-%d-%s-singlestring" % (id(image
), compressmode
)
366 self
.PSimagename
= "image-%d-%s-stringarray" % (id(image
), compressmode
)
367 self
.PDFimagename
= "image-%d-%s" % (id(image
), compressmode
)
370 return bbox
.bbox_pt(self
.xpos_pt
, self
.ypos_pt
,
371 self
.xpos_pt
+self
.width_pt
, self
.ypos_pt
+self
.height_pt
)
373 def processPS(self
, file, writer
, context
, registry
, bbox
):
374 if self
.PSstoreimage
and not self
.PSsinglestring
:
375 registry
.add(pswriter
.PSdefinition("imagedataaccess",
376 "{ /imagedataindex load " # get list index
377 "dup 1 add /imagedataindex exch store " # store increased index
378 "/imagedataid load exch get }")) # select string from array
379 if self
.PSstoreimage
:
380 registry
.add(PSimagedata(self
.PSimagename
, self
.data
, self
.PSsinglestring
, self
.PSmaxstrlen
))
383 file.write("gsave\n")
384 if self
.palettedata
is not None:
385 file.write("[ /Indexed %s %i\n" % (self
.palettecolorspace
, len(self
.palettedata
)/3-1))
386 file.write("%%%%BeginData: %i ASCII Lines\n" % ascii85lines(len(self
.palettedata
)))
388 ascii85stream(file, self
.palettedata
)
391 file.write("] setcolorspace\n")
393 file.write("%s setcolorspace\n" % self
.colorspace
)
395 if self
.PSstoreimage
and not self
.PSsinglestring
:
396 file.write("/imagedataindex 0 store\n" # not use the stack since interpreters differ in their stack usage
397 "/imagedataid %s store\n" % self
.PSimagename
)
401 "/Width %i\n" % self
.imagewidth
)
402 file.write("/Height %i\n" % self
.imageheight
)
403 file.write("/BitsPerComponent 8\n"
404 "/ImageMatrix %s\n" % self
.imagematrixPS
)
405 file.write("/Decode %s\n" % self
.decode
)
407 file.write("/DataSource ")
408 if self
.PSstoreimage
:
409 if self
.PSsinglestring
:
410 file.write("/%s load" % self
.PSimagename
)
412 file.write("/imagedataaccess load") # some printers do not allow for inline code here -> we store it in a resource
414 if self
.PSbinexpand
== 2:
415 file.write("currentfile /ASCIIHexDecode filter")
417 file.write("currentfile /ASCII85Decode filter")
418 if self
.compressmode
:
419 file.write(" /%sDecode filter" % self
.compressmode
)
424 if self
.PSstoreimage
:
425 file.write("image\n")
427 if self
.PSbinexpand
== 2:
428 file.write("%%%%BeginData: %i ASCII Lines\n"
429 "image\n" % (asciihexlines(len(self
.data
)) + 1))
430 asciihexstream(file, self
.data
)
432 # the datasource is currentstream (plus some filters)
433 file.write("%%%%BeginData: %i ASCII Lines\n"
434 "image\n" % (ascii85lines(len(self
.data
)) + 1))
435 ascii85stream(file, self
.data
)
437 file.write("%%EndData\n")
439 file.write("grestore\n")
441 def processPDF(self
, file, writer
, context
, registry
, bbox
):
442 registry
.add(PDFimage(self
.PDFimagename
, self
.imagewidth
, self
.imageheight
,
443 self
.palettecolorspace
, self
.palettedata
, self
.colorspace
,
444 8, self
.compressmode
, self
.data
, registry
))
448 self
.imagematrixPDF
.processPDF(file, writer
, context
, registry
, bbox
)
449 file.write("/%s Do\n" % self
.PDFimagename
)