1 # -*- coding: ISO-8859-1 -*-
4 # Copyright (C) 2004-2006 André Wobst <wobsta@users.sourceforge.net>
6 # This file is part of PyX (http://pyx.sourceforge.net/).
8 # PyX is free software; you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation; either version 2 of the License, or
11 # (at your option) any later version.
13 # PyX is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
18 # You should have received a copy of the GNU General Public License
19 # along with PyX; if not, write to the Free Software
20 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
22 import struct
, warnings
, binascii
29 import bbox
, canvas
, pswriter
, pdfwriter
, trafo
, unit
31 def ascii85lines(datalen
):
34 return (datalen
+ 56)/60
36 def ascii85stream(file, data
):
37 """Encodes the string data in ASCII85 and writes it to
38 the stream file. The number of lines written to the stream
39 is known just from the length of the data by means of the
40 ascii85lines function. Note that the tailing newline character
41 of the last line is not added by this function, but it is taken
42 into account in the ascii85lines function."""
43 i
= 3 # go on smoothly in case of data length equals zero
45 l
= [None, None, None, None]
46 for i
in range(len(data
)):
50 if i
%60 == 3 and i
!= 3:
54 # l[3], c5 = divmod(256*256*256*l[0]+256*256*l[1]+256*l[2]+l[3], 85)
55 # l[2], c4 = divmod(l[3], 85)
56 # we have to avoid number > 2**31 by
57 l
[3], c5
= divmod(256*256*l
[0]+256*256*l
[1]+256*l
[2]+l
[3], 85)
58 l
[2], c4
= divmod(256*256*3*l
[0]+l
[3], 85)
59 l
[1], c3
= divmod(l
[2], 85)
60 c1
, c2
= divmod(l
[1], 85)
61 file.write(struct
.pack('BBBBB', c1
+33, c2
+33, c3
+33, c4
+33, c5
+33))
65 for j
in range((i
%4) + 1, 4):
67 l
[3], c5
= divmod(256*256*l
[0]+256*256*l
[1]+256*l
[2]+l
[3], 85)
68 l
[2], c4
= divmod(256*256*3*l
[0]+l
[3], 85)
69 l
[1], c3
= divmod(l
[2], 85)
70 c1
, c2
= divmod(l
[1], 85)
71 file.write(struct
.pack('BBBB', c1
+33, c2
+33, c3
+33, c4
+33)[:(i
%4)+2])
73 _asciihexlinelength
= 64
74 def asciihexlines(datalen
):
75 return (datalen
*2 + _asciihexlinelength
- 1) / _asciihexlinelength
77 def asciihexstream(file, data
):
78 hexdata
= binascii
.b2a_hex(data
)
79 for i
in range((len(hexdata
)-1)/_asciihexlinelength
+ 1):
80 file.write(hexdata
[i
*_asciihexlinelength
: i
*_asciihexlinelength
+_asciihexlinelength
])
86 def __init__(self
, width
, height
, mode
, data
, compressed
=None):
87 if width
<= 0 or height
<= 0:
88 raise ValueError("valid image size")
89 if mode
not in ["L", "RGB", "CMYK"]:
90 raise ValueError("invalid mode")
91 if compressed
is None and len(mode
)*width
*height
!= len(data
):
92 raise ValueError("wrong size of uncompressed data")
93 self
.size
= width
, height
96 self
.compressed
= compressed
98 def tostring(self
, *args
):
100 raise RuntimeError("encoding not supported in this implementation")
103 def convert(self
, model
):
104 raise RuntimeError("color model conversion not supported in this implementation")
107 class jpegimage(image
):
109 def __init__(self
, file):
113 data
= open(file, "rb").read()
118 if data
[pos
] == "\377" and data
[pos
+1] not in ["\0", "\377"]:
119 # print "marker: 0x%02x \\%03o" % (ord(data[pos+1]), ord(data[pos+1]))
120 if data
[pos
+1] == "\330":
124 elif not nestinglevel
:
125 raise ValueError("begin marker expected")
126 elif data
[pos
+1] == "\331":
131 elif data
[pos
+1] in ["\300", "\301"]:
132 l
, bits
, height
, width
, components
= struct
.unpack(">HBHHB", data
[pos
+2:pos
+10])
134 raise ValueError("implementation limited to 8 bit per component only")
136 mode
= {1: "L", 3: "RGB", 4: "CMYK"}[components
]
138 raise ValueError("invalid number of components")
140 elif data
[pos
+1] == "\340":
141 l
, id, major
, minor
, dpikind
, xdpi
, ydpi
= struct
.unpack(">H5sBBBHH", data
[pos
+2:pos
+16])
143 self
.info
= {"dpi": (xdpi
, ydpi
)}
145 self
.info
= {"dpi": (xdpi
*2.54, ydpi
*2.45)}
146 # else do not provide dpi information
150 raise ValueError("end marker expected")
151 image
.__init
__(self
, width
, height
, mode
, data
[begin
:end
], compressed
="DCT")
154 class PSimagedata(pswriter
.PSresource
):
156 def __init__(self
, name
, data
, singlestring
, maxstrlen
):
157 pswriter
.PSresource
.__init
__(self
, "imagedata", name
)
159 self
.singlestring
= singlestring
160 self
.maxstrlen
= maxstrlen
162 def output(self
, file, writer
, registry
):
163 file.write("%%%%BeginRessource: %s\n" % self
.id)
164 if self
.singlestring
:
165 file.write("%%%%BeginData: %i ASCII Lines\n"
166 "<~" % ascii85lines(len(self
.data
)))
167 ascii85stream(file, self
.data
)
171 datalen
= len(self
.data
)
172 tailpos
= datalen
- datalen
% self
.maxstrlen
173 file.write("%%%%BeginData: %i ASCII Lines\n" %
174 ((tailpos
/self
.maxstrlen
) * ascii85lines(self
.maxstrlen
) +
175 ascii85lines(datalen
-tailpos
)))
177 for i
in xrange(0, tailpos
, self
.maxstrlen
):
179 ascii85stream(file, self
.data
[i
: i
+self
.maxstrlen
])
181 if datalen
!= tailpos
:
183 ascii85stream(file, self
.data
[tailpos
:])
187 file.write("/%s exch def\n" % self
.id)
188 file.write("%%EndRessource\n")
191 class PDFimagepalettedata(pdfwriter
.PDFobject
):
193 def __init__(self
, name
, data
):
194 pdfwriter
.PDFobject
.__init
__(self
, "imagepalettedata", name
)
197 def write(self
, file, writer
, registry
):
199 "/Length %d\n" % len(self
.data
))
202 file.write(self
.data
)
207 class PDFimage(pdfwriter
.PDFobject
):
209 def __init__(self
, name
, width
, height
, palettecolorspace
, palettedata
, colorspace
,
210 bitspercomponent
, compressmode
, data
, registry
):
211 if palettedata
is not None:
213 elif colorspace
== "/DeviceGray":
217 pdfwriter
.PDFobject
.__init
__(self
, "image", name
)
218 registry
.addresource("XObject", name
, self
, procset
=procset
)
219 if palettedata
is not None:
220 # acrobat wants a palette to be an object
221 self
.PDFpalettedata
= PDFimagepalettedata(name
, palettedata
)
222 registry
.add(self
.PDFpalettedata
)
226 self
.palettecolorspace
= palettecolorspace
227 self
.palettedata
= palettedata
228 self
.colorspace
= colorspace
229 self
.bitspercomponent
= bitspercomponent
230 self
.compressmode
= compressmode
233 def write(self
, file, writer
, registry
):
237 "/Width %d\n" % self
.width
)
238 file.write("/Height %d\n" % self
.height
)
239 if self
.palettedata
is not None:
240 file.write("/ColorSpace [ /Indexed %s %i\n" % (self
.palettecolorspace
, len(self
.palettedata
)/3-1))
241 file.write("%d 0 R\n" % registry
.getrefno(self
.PDFpalettedata
))
244 file.write("/ColorSpace %s\n" % self
.colorspace
)
245 file.write("/BitsPerComponent %d\n" % self
.bitspercomponent
)
246 file.write("/Length %d\n" % len(self
.data
))
247 if self
.compressmode
:
248 file.write("/Filter /%sDecode\n" % self
.compressmode
)
251 file.write(self
.data
)
256 class bitmap(canvas
.canvasitem
):
258 def __init__(self
, xpos
, ypos
, image
, width
=None, height
=None, ratio
=None,
259 PSstoreimage
=0, PSmaxstrlen
=4093, PSbinexpand
=1,
260 compressmode
="Flate", flatecompresslevel
=6,
261 dctquality
=75, dctoptimize
=0, dctprogression
=0):
262 # keep a copy of the image instance to ensure different id's
267 self
.imagewidth
, self
.imageheight
= image
.size
268 self
.PSstoreimage
= PSstoreimage
269 self
.PSmaxstrlen
= PSmaxstrlen
270 self
.PSbinexpand
= PSbinexpand
272 if width
is not None or height
is not None:
275 if self
.width
is None:
277 self
.width
= self
.height
* self
.imagewidth
/ float(self
.imageheight
)
279 self
.width
= ratio
* self
.height
280 elif self
.height
is None:
282 self
.height
= self
.width
* self
.imageheight
/ float(self
.imagewidth
)
284 self
.height
= (1.0/ratio
) * self
.width
285 elif ratio
is not None:
286 raise ValueError("can't specify a ratio when setting width and height")
288 if ratio
is not None:
289 raise ValueError("must specify width or height to set a ratio")
290 widthdpi
, heightdpi
= image
.info
["dpi"] # fails when no dpi information available
291 self
.width
= self
.imagewidth
/ float(widthdpi
) * unit
.t_inch
292 self
.height
= self
.imageheight
/ float(heightdpi
) * unit
.t_inch
294 self
.xpos_pt
= unit
.topt(self
.xpos
)
295 self
.ypos_pt
= unit
.topt(self
.ypos
)
296 self
.width_pt
= unit
.topt(self
.width
)
297 self
.height_pt
= unit
.topt(self
.height
)
299 # create decode and colorspace
300 self
.colorspace
= self
.palettecolorspace
= self
.palettedata
= None
301 if image
.mode
== "P":
302 palettemode
, self
.palettedata
= image
.palette
.getdata()
303 self
.decode
= "[0 255]"
305 self
.palettecolorspace
= {"L": "/DeviceGray",
307 "CMYK": "/DeviceCMYK"}[palettemode
]
309 warnings
.warn("image with unknown palette mode '%s' converted to rgb image" % palettemode
)
310 image
= image
.convert("RGB")
311 self
.decode
= "[0 1 0 1 0 1]"
312 self
.palettedata
= None
313 self
.colorspace
= "/DeviceRGB"
314 elif len(image
.mode
) == 1:
315 if image
.mode
!= "L":
316 image
= image
.convert("L")
317 warnings
.warn("specific single channel image mode not natively supported, converted to regular grayscale")
318 self
.decode
= "[0 1]"
319 self
.colorspace
= "/DeviceGray"
320 elif image
.mode
== "CMYK":
321 self
.decode
= "[0 1 0 1 0 1 0 1]"
322 self
.colorspace
= "/DeviceCMYK"
324 if image
.mode
!= "RGB":
325 image
= image
.convert("RGB")
326 warnings
.warn("image with unknown mode converted to rgb")
327 self
.decode
= "[0 1 0 1 0 1]"
328 self
.colorspace
= "/DeviceRGB"
331 self
.imagematrixPS
= (trafo
.mirror(0)
332 .translated_pt(-self
.xpos_pt
, self
.ypos_pt
+self
.height_pt
)
333 .scaled_pt(self
.imagewidth
/self
.width_pt
, self
.imageheight
/self
.height_pt
))
334 self
.imagematrixPDF
= (trafo
.scale_pt(self
.width_pt
, self
.height_pt
)
335 .translated_pt(self
.xpos_pt
, self
.ypos_pt
))
337 # check whether imagedata is compressed or not
339 imagecompressed
= image
.compressed
341 imagecompressed
= None
342 if compressmode
!= None and imagecompressed
!= None:
343 raise ValueError("compression of a compressed image not supported")
344 self
.compressmode
= compressmode
345 if compressmode
is not None and compressmode
not in ["Flate", "DCT"]:
346 raise ValueError("invalid compressmode '%s'" % compressmode
)
347 if imagecompressed
is not None:
348 self
.compressmode
= imagecompressed
349 if imagecompressed
not in ["Flate", "DCT"]:
350 raise ValueError("invalid compressed image '%s'" % imagecompressed
)
351 if not haszlib
and compressmode
== "Flate":
352 warnings
.warn("zlib module not available, disable compression")
353 self
.compressmode
= compressmode
= None
356 if compressmode
== "Flate":
357 self
.data
= zlib
.compress(image
.tostring(), flatecompresslevel
)
358 elif compressmode
== "DCT":
359 self
.data
= image
.tostring("jpeg", image
.mode
,
360 dctquality
, dctoptimize
, dctprogression
)
362 self
.data
= image
.tostring()
364 self
.PSsinglestring
= self
.PSstoreimage
and len(self
.data
) < self
.PSmaxstrlen
365 if self
.PSsinglestring
:
366 self
.PSimagename
= "image-%d-%s-singlestring" % (id(image
), compressmode
)
368 self
.PSimagename
= "image-%d-%s-stringarray" % (id(image
), compressmode
)
369 self
.PDFimagename
= "image-%d-%s" % (id(image
), compressmode
)
372 return bbox
.bbox_pt(self
.xpos_pt
, self
.ypos_pt
,
373 self
.xpos_pt
+self
.width_pt
, self
.ypos_pt
+self
.height_pt
)
375 def processPS(self
, file, writer
, context
, registry
, bbox
):
376 if self
.PSstoreimage
and not self
.PSsinglestring
:
377 registry
.add(pswriter
.PSdefinition("imagedataaccess",
378 "{ /imagedataindex load " # get list index
379 "dup 1 add /imagedataindex exch store " # store increased index
380 "/imagedataid load exch get }")) # select string from array
381 if self
.PSstoreimage
:
382 registry
.add(PSimagedata(self
.PSimagename
, self
.data
, self
.PSsinglestring
, self
.PSmaxstrlen
))
385 file.write("gsave\n")
386 if self
.palettedata
is not None:
387 file.write("[ /Indexed %s %i\n" % (self
.palettecolorspace
, len(self
.palettedata
)/3-1))
388 file.write("%%%%BeginData: %i ASCII Lines\n" % ascii85lines(len(self
.palettedata
)))
390 ascii85stream(file, self
.palettedata
)
393 file.write("] setcolorspace\n")
395 file.write("%s setcolorspace\n" % self
.colorspace
)
397 if self
.PSstoreimage
and not self
.PSsinglestring
:
398 file.write("/imagedataindex 0 store\n" # not use the stack since interpreters differ in their stack usage
399 "/imagedataid %s store\n" % self
.PSimagename
)
403 "/Width %i\n" % self
.imagewidth
)
404 file.write("/Height %i\n" % self
.imageheight
)
405 file.write("/BitsPerComponent 8\n"
406 "/ImageMatrix %s\n" % self
.imagematrixPS
)
407 file.write("/Decode %s\n" % self
.decode
)
409 file.write("/DataSource ")
410 if self
.PSstoreimage
:
411 if self
.PSsinglestring
:
412 file.write("/%s load" % self
.PSimagename
)
414 file.write("/imagedataaccess load") # some printers do not allow for inline code here -> we store it in a resource
416 if self
.PSbinexpand
== 2:
417 file.write("currentfile /ASCIIHexDecode filter")
419 file.write("currentfile /ASCII85Decode filter")
420 if self
.compressmode
:
421 file.write(" /%sDecode filter" % self
.compressmode
)
426 if self
.PSstoreimage
:
427 file.write("image\n")
429 if self
.PSbinexpand
== 2:
430 file.write("%%%%BeginData: %i ASCII Lines\n"
431 "image\n" % (asciihexlines(len(self
.data
)) + 1))
432 asciihexstream(file, self
.data
)
434 # the datasource is currentstream (plus some filters)
435 file.write("%%%%BeginData: %i ASCII Lines\n"
436 "image\n" % (ascii85lines(len(self
.data
)) + 1))
437 ascii85stream(file, self
.data
)
439 file.write("%%EndData\n")
441 file.write("grestore\n")
443 def processPDF(self
, file, writer
, context
, registry
, bbox
):
444 registry
.add(PDFimage(self
.PDFimagename
, self
.imagewidth
, self
.imageheight
,
445 self
.palettecolorspace
, self
.palettedata
, self
.colorspace
,
446 8, self
.compressmode
, self
.data
, registry
))
450 self
.imagematrixPDF
.processPDF(file, writer
, context
, registry
, bbox
)
451 file.write("/%s Do\n" % self
.PDFimagename
)