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
, canvasitem
, pswriter
, pdfwriter
, trafo
, unit
31 devicenames
= {"L": "/DeviceGray",
33 "CMYK": "/DeviceCMYK"}
34 decodestrings
= {"L": "[0 1]",
35 "RGB": "[0 1 0 1 0 1]",
36 "CMYK": "[0 1 0 1 0 1 0 1]",
40 def ascii85lines(datalen
):
43 return (datalen
+ 56)/60
45 def ascii85stream(file, data
):
46 """Encodes the string data in ASCII85 and writes it to
47 the stream file. The number of lines written to the stream
48 is known just from the length of the data by means of the
49 ascii85lines function. Note that the tailing newline character
50 of the last line is not added by this function, but it is taken
51 into account in the ascii85lines function."""
52 i
= 3 # go on smoothly in case of data length equals zero
54 l
= [None, None, None, None]
55 for i
in range(len(data
)):
59 if i
%60 == 3 and i
!= 3:
63 # l[3], c5 = divmod(256*256*256*l[0]+256*256*l[1]+256*l[2]+l[3], 85)
64 # l[2], c4 = divmod(l[3], 85)
65 # we have to avoid number > 2**31 by
66 l
[3], c5
= divmod(256*256*l
[0]+256*256*l
[1]+256*l
[2]+l
[3], 85)
67 l
[2], c4
= divmod(256*256*3*l
[0]+l
[3], 85)
68 l
[1], c3
= divmod(l
[2], 85)
69 c1
, c2
= divmod(l
[1], 85)
70 file.write(struct
.pack('BBBBB', c1
+33, c2
+33, c3
+33, c4
+33, c5
+33))
74 for j
in range((i
%4) + 1, 4):
76 l
[3], c5
= divmod(256*256*l
[0]+256*256*l
[1]+256*l
[2]+l
[3], 85)
77 l
[2], c4
= divmod(256*256*3*l
[0]+l
[3], 85)
78 l
[1], c3
= divmod(l
[2], 85)
79 c1
, c2
= divmod(l
[1], 85)
80 file.write(struct
.pack('BBBB', c1
+33, c2
+33, c3
+33, c4
+33)[:(i
%4)+2])
82 _asciihexlinelength
= 64
83 def asciihexlines(datalen
):
84 return (datalen
*2 + _asciihexlinelength
- 1) / _asciihexlinelength
86 def asciihexstream(file, data
):
87 hexdata
= binascii
.b2a_hex(data
)
88 for i
in range((len(hexdata
)-1)/_asciihexlinelength
+ 1):
89 file.write(hexdata
[i
*_asciihexlinelength
: i
*_asciihexlinelength
+_asciihexlinelength
])
95 def __init__(self
, width
, height
, mode
, data
, compressed
=None, palette
=None):
96 if width
<= 0 or height
<= 0:
97 raise ValueError("valid image size")
98 if mode
not in ["L", "RGB", "CMYK", "LA", "RGBA", "CMYKA", "AL", "ARGB", "ACMYK"]:
99 raise ValueError("invalid mode")
100 if compressed
is None and len(mode
)*width
*height
!= len(data
):
101 raise ValueError("wrong size of uncompressed data")
102 self
.size
= width
, height
105 self
.compressed
= compressed
106 self
.palette
= palette
109 if self
.compressed
is not None:
110 raise RuntimeError("cannot extract bands from compressed image")
111 bands
= len(self
.mode
)
112 return [image(self
.width
, self
.height
, "L", "".join([self
.data
[i
*bands
+band
]
113 for i
in range(self
.width
*self
.height
)]))
114 for band
in range(bands
)]
116 def tostring(self
, *args
):
118 raise RuntimeError("encoding not supported in this implementation")
121 def convert(self
, model
):
122 raise RuntimeError("color model conversion not supported in this implementation")
125 class jpegimage(image
):
127 def __init__(self
, file):
131 data
= open(file, "rb").read()
136 if data
[pos
] == "\377" and data
[pos
+1] not in ["\0", "\377"]:
137 # print "marker: 0x%02x \\%03o" % (ord(data[pos+1]), ord(data[pos+1]))
138 if data
[pos
+1] == "\330":
142 elif not nestinglevel
:
143 raise ValueError("begin marker expected")
144 elif data
[pos
+1] == "\331":
149 elif data
[pos
+1] in ["\300", "\301"]:
150 l
, bits
, height
, width
, components
= struct
.unpack(">HBHHB", data
[pos
+2:pos
+10])
152 raise ValueError("implementation limited to 8 bit per component only")
154 mode
= {1: "L", 3: "RGB", 4: "CMYK"}[components
]
156 raise ValueError("invalid number of components")
158 elif data
[pos
+1] == "\340":
159 l
, id, major
, minor
, dpikind
, xdpi
, ydpi
= struct
.unpack(">H5sBBBHH", data
[pos
+2:pos
+16])
161 self
.info
= {"dpi": (xdpi
, ydpi
)}
163 self
.info
= {"dpi": (xdpi
*2.54, ydpi
*2.45)}
164 # else do not provide dpi information
168 raise ValueError("end marker expected")
169 image
.__init
__(self
, width
, height
, mode
, data
[begin
:end
], compressed
="DCT")
172 class PSimagedata(pswriter
.PSresource
):
174 def __init__(self
, name
, data
, singlestring
, maxstrlen
):
175 pswriter
.PSresource
.__init
__(self
, "imagedata", name
)
177 self
.singlestring
= singlestring
178 self
.maxstrlen
= maxstrlen
180 def output(self
, file, writer
, registry
):
181 file.write("%%%%BeginRessource: %s\n" % self
.id)
182 if self
.singlestring
:
183 file.write("%%%%BeginData: %i ASCII Lines\n"
184 "<~" % ascii85lines(len(self
.data
)))
185 ascii85stream(file, self
.data
)
189 datalen
= len(self
.data
)
190 tailpos
= datalen
- datalen
% self
.maxstrlen
191 file.write("%%%%BeginData: %i ASCII Lines\n" %
192 ((tailpos
/self
.maxstrlen
) * ascii85lines(self
.maxstrlen
) +
193 ascii85lines(datalen
-tailpos
)))
195 for i
in xrange(0, tailpos
, self
.maxstrlen
):
197 ascii85stream(file, self
.data
[i
: i
+self
.maxstrlen
])
199 if datalen
!= tailpos
:
201 ascii85stream(file, self
.data
[tailpos
:])
205 file.write("/%s exch def\n" % self
.id)
206 file.write("%%EndRessource\n")
209 class PDFimagepalettedata(pdfwriter
.PDFobject
):
211 def __init__(self
, name
, data
):
212 pdfwriter
.PDFobject
.__init
__(self
, "imagepalettedata", name
)
215 def write(self
, file, writer
, registry
):
217 "/Length %d\n" % len(self
.data
))
220 file.write(self
.data
)
225 class PDFimage(pdfwriter
.PDFobject
):
227 def __init__(self
, name
, width
, height
, palettemode
, palettedata
, mode
,
228 bitspercomponent
, compressmode
, data
, smask
, registry
, addresource
=True):
229 pdfwriter
.PDFobject
.__init
__(self
, "image", name
)
232 if palettedata
is not None:
238 registry
.addresource("XObject", name
, self
, procset
=procset
)
239 if palettedata
is not None:
240 # note that acrobat wants a palette to be an object (which clearly is a bug)
241 self
.PDFpalettedata
= PDFimagepalettedata(name
, palettedata
)
242 registry
.add(self
.PDFpalettedata
)
247 self
.palettemode
= palettemode
248 self
.palettedata
= palettedata
250 self
.bitspercomponent
= bitspercomponent
251 self
.compressmode
= compressmode
255 def write(self
, file, writer
, registry
):
259 "/Width %d\n" % self
.width
)
260 file.write("/Height %d\n" % self
.height
)
261 if self
.palettedata
is not None:
262 file.write("/ColorSpace [ /Indexed %s %i\n" % (devicenames
[self
.palettemode
], len(self
.palettedata
)/3-1))
263 file.write("%d 0 R\n" % registry
.getrefno(self
.PDFpalettedata
))
266 file.write("/ColorSpace %s\n" % devicenames
[self
.mode
])
268 file.write("/SMask %d 0 R\n" % registry
.getrefno(self
.smask
))
269 file.write("/BitsPerComponent %d\n" % self
.bitspercomponent
)
270 file.write("/Length %d\n" % len(self
.data
))
271 if self
.compressmode
:
272 file.write("/Filter /%sDecode\n" % self
.compressmode
)
275 file.write(self
.data
)
280 class bitmap_pt(canvasitem
.canvasitem
):
282 def __init__(self
, xpos_pt
, ypos_pt
, image
, width_pt
=None, height_pt
=None, ratio
=None,
283 PSstoreimage
=0, PSmaxstrlen
=4093, PSbinexpand
=1,
284 compressmode
="Flate", flatecompresslevel
=6,
285 dctquality
=75, dctoptimize
=0, dctprogression
=0):
286 self
.xpos_pt
= xpos_pt
287 self
.ypos_pt
= ypos_pt
290 self
.imagewidth
, self
.imageheight
= image
.size
292 if width_pt
is not None or height_pt
is not None:
293 self
.width_pt
= width_pt
294 self
.height_pt
= height_pt
295 if self
.width_pt
is None:
297 self
.width_pt
= self
.height_pt
* self
.imagewidth
/ float(self
.imageheight
)
299 self
.width_pt
= ratio
* self
.height_pt
300 elif self
.height_pt
is None:
302 self
.height_pt
= self
.width_pt
* self
.imageheight
/ float(self
.imagewidth
)
304 self
.height_pt
= (1.0/ratio
) * self
.width_pt
305 elif ratio
is not None:
306 raise ValueError("can't specify a ratio when setting width_pt and height_pt")
308 if ratio
is not None:
309 raise ValueError("must specify width_pt or height_pt to set a ratio")
310 widthdpi
, heightdpi
= image
.info
["dpi"] # fails when no dpi information available
311 self
.width_pt
= 72.0 * self
.imagewidth
/ float(widthdpi
)
312 self
.height_pt
= 72.0 * self
.imageheight
/ float(heightdpi
)
314 self
.PSstoreimage
= PSstoreimage
315 self
.PSmaxstrlen
= PSmaxstrlen
316 self
.PSbinexpand
= PSbinexpand
317 self
.compressmode
= compressmode
318 self
.flatecompresslevel
= flatecompresslevel
319 self
.dctquality
= dctquality
320 self
.dctoptimize
= dctoptimize
321 self
.dctprogression
= dctprogression
324 self
.imagecompressed
= image
.compressed
326 self
.imagecompressed
= None
327 if self
.compressmode
not in [None, "Flate", "DCT"]:
328 raise ValueError("invalid compressmode '%s'" % self
.compressmode
)
329 if self
.imagecompressed
not in [None, "Flate", "DCT"]:
330 raise ValueError("invalid compressed image '%s'" % self
.imagecompressed
)
331 if self
.compressmode
is not None and self
.imagecompressed
is not None:
332 raise ValueError("compression of a compressed image not supported")
333 if not haszlib
and self
.compressmode
== "Flate":
334 warnings
.warn("zlib module not available, disable compression")
335 self
.compressmode
= None
337 def imagedata(self
, interleavealpha
):
340 returns a tuple (mode, data, alpha, palettemode, palettedata)
341 where mode does not contain antialiasing anymore
344 alpha
= palettemode
= palettedata
= None
347 if mode
.startswith("A"):
354 data
= image(self
.imagewidth
, self
.imageheight
, mode
,
355 "".join(["".join(values
)
356 for values
in zip(*[band
.tostring()
357 for band
in bands
[1:]])]), palette
=data
.palette
)
358 if mode
.endswith("A"):
360 bands
= list(bands
[-1:]) + list(bands
[:-1])
364 # TODO: this is slow, but we don't want to depend on PIL or anything ... still, its incredibly slow to do it with lists and joins
365 data
= image(self
.imagewidth
, self
.imageheight
, "A%s" % mode
,
366 "".join(["".join(values
)
367 for values
in zip(*[band
.tostring()
368 for band
in bands
])]), palette
=data
.palette
)
371 data
= image(self
.imagewidth
, self
.imageheight
, mode
,
372 "".join(["".join(values
)
373 for values
in zip(*[band
.tostring()
374 for band
in bands
[1:]])]), palette
=data
.palette
)
377 palettemode
, palettedata
= data
.palette
.getdata()
378 if palettemode
not in ["L", "RGB", "CMYK"]:
379 warnings
.warn("image with unknown palette mode '%s' converted to rgb image" % palettemode
)
380 data
= data
.convert("RGB")
386 warnings
.warn("specific single channel image mode not natively supported, converted to regular grayscale")
387 data
= data
.convert("L")
389 elif mode
not in ["CMYK", "RGB"]:
390 warnings
.warn("image with unknown mode converted to rgb")
391 data
= data
.convert("RGB")
394 if self
.compressmode
== "Flate":
395 data
= zlib
.compress(data
.tostring(), self
.flatecompresslevel
)
396 elif self
.compressmode
== "DCT":
397 data
= data
.tostring("jpeg", mode
, self
.dctquality
, self
.dctoptimize
, self
.dctprogression
)
399 data
= data
.tostring()
400 if alpha
and not interleavealpha
:
401 if self
.compressmode
== "Flate":
402 alpha
= zlib
.compress(alpha
.tostring(), self
.flatecompresslevel
)
403 elif self
.compressmode
== "DCT":
404 # well, this here is strange, we might want a alphacompressmode ...
405 alpha
= alpha
.tostring("jpeg", mode
, self
.dctquality
, self
.dctoptimize
, self
.dctprogression
)
407 alpha
= alpha
.tostring()
409 return mode
, data
, alpha
, palettemode
, palettedata
412 return bbox
.bbox_pt(self
.xpos_pt
, self
.ypos_pt
,
413 self
.xpos_pt
+self
.width_pt
, self
.ypos_pt
+self
.height_pt
)
415 def processPS(self
, file, writer
, context
, registry
, bbox
):
416 mode
, data
, alpha
, palettemode
, palettedata
= self
.imagedata(True)
417 imagematrixPS
= (trafo
.mirror(0)
418 .translated_pt(-self
.xpos_pt
, self
.ypos_pt
+self
.height_pt
)
419 .scaled_pt(self
.imagewidth
/self
.width_pt
, self
.imageheight
/self
.height_pt
))
422 PSsinglestring
= self
.PSstoreimage
and len(data
) < self
.PSmaxstrlen
424 PSimagename
= "image-%d-%s-singlestring" % (id(self
.image
), self
.compressmode
)
426 PSimagename
= "image-%d-%s-stringarray" % (id(self
.image
), self
.compressmode
)
428 if self
.PSstoreimage
and not PSsinglestring
:
429 registry
.add(pswriter
.PSdefinition("imagedataaccess",
430 "{ /imagedataindex load " # get list index
431 "dup 1 add /imagedataindex exch store " # store increased index
432 "/imagedataid load exch get }")) # select string from array
433 if self
.PSstoreimage
:
434 registry
.add(PSimagedata(PSimagename
, data
, PSsinglestring
, self
.PSmaxstrlen
))
437 file.write("gsave\n")
438 if palettedata
is not None:
439 file.write("[ /Indexed %s %i\n" % (devicenames
[palettemode
], len(palettedata
)/3-1))
440 file.write("%%%%BeginData: %i ASCII Lines\n" % ascii85lines(len(palettedata
)))
442 ascii85stream(file, palettedata
)
445 file.write("] setcolorspace\n")
447 file.write("%s setcolorspace\n" % devicenames
[mode
])
449 if self
.PSstoreimage
and not PSsinglestring
:
450 file.write("/imagedataindex 0 store\n" # not use the stack since interpreters differ in their stack usage
451 "/imagedataid %s store\n" % PSimagename
)
455 file.write("/ImageType 3\n"
458 file.write("/ImageType 1\n"
459 "/Width %i\n" % self
.imagewidth
)
460 file.write("/Height %i\n" % self
.imageheight
)
461 file.write("/BitsPerComponent 8\n"
462 "/ImageMatrix %s\n" % imagematrixPS
)
463 file.write("/Decode %s\n" % decodestrings
[mode
])
465 file.write("/DataSource ")
466 if self
.PSstoreimage
:
468 file.write("/%s load" % PSimagename
)
470 file.write("/imagedataaccess load") # some printers do not allow for inline code here -> we store it in a resource
472 if self
.PSbinexpand
== 2:
473 file.write("currentfile /ASCIIHexDecode filter")
475 file.write("currentfile /ASCII85Decode filter")
476 if self
.compressmode
or self
.imagecompressed
:
477 file.write(" /%sDecode filter" % (self
.compressmode
or self
.imagecompressed
))
483 file.write("/MaskDict\n"
486 "/Width %i\n" % self
.imagewidth
)
487 file.write("/Height %i\n" % self
.imageheight
)
488 file.write("/BitsPerComponent 8\n"
489 "/ImageMatrix %s\n" % imagematrixPS
)
490 file.write("/Decode [1 0]\n"
492 "/InterleaveType 1\n"
495 if self
.PSstoreimage
:
496 file.write("image\n")
498 if self
.PSbinexpand
== 2:
499 file.write("%%%%BeginData: %i ASCII Lines\n"
500 "image\n" % (asciihexlines(len(data
)) + 1))
501 asciihexstream(file, data
)
503 # the datasource is currentstream (plus some filters)
504 file.write("%%%%BeginData: %i ASCII Lines\n"
505 "image\n" % (ascii85lines(len(data
)) + 1))
506 ascii85stream(file, data
)
508 file.write("%%EndData\n")
510 file.write("grestore\n")
512 def processPDF(self
, file, writer
, context
, registry
, bbox
):
513 mode
, data
, alpha
, palettemode
, palettedata
= self
.imagedata(False)
515 name
= "image-%d-%s" % (id(self
.image
), self
.compressmode
or self
.imagecompress
)
517 alpha
= PDFimage("%s-smask" % name
, self
.imagewidth
, self
.imageheight
,
519 self
.compressmode
, alpha
, None, registry
, addresource
=False)
521 registry
.add(PDFimage(name
, self
.imagewidth
, self
.imageheight
,
522 palettemode
, palettedata
, mode
, 8,
523 self
.compressmode
or self
.imagecompress
, data
, alpha
, registry
))
526 imagematrixPDF
= (trafo
.scale_pt(self
.width_pt
, self
.height_pt
)
527 .translated_pt(self
.xpos_pt
, self
.ypos_pt
))
530 imagematrixPDF
.processPDF(file, writer
, context
, registry
, bbox
)
531 file.write("/%s Do\n" % name
)
535 class bitmap(bitmap_pt
):
537 def __init__(self
, xpos
, ypos
, image
, width
=None, height
=None, **kwargs
):
538 xpos_pt
= unit
.topt(xpos
)
539 ypos_pt
= unit
.topt(ypos
)
540 if width
is not None:
541 width_pt
= unit
.topt(width
)
544 if height
is not None:
545 height_pt
= unit
.topt(height
)
549 bitmap_pt
.__init
__(self
, xpos_pt
, ypos_pt
, image
, width_pt
=width_pt
, height_pt
=height_pt
, **kwargs
)