Applied upstream as r3028 r3025 r3024
[PyX/mjg.git] / pyx / bitmap.py
blob5c3e0d4706e294deaac504fd6c94e432b15ce8a9
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
23 try:
24 import zlib
25 haszlib = 1
26 except:
27 haszlib = 0
29 import bbox, canvasitem, pswriter, pdfwriter, trafo, unit
31 devicenames = {"L": "/DeviceGray",
32 "RGB": "/DeviceRGB",
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]",
37 "P": "[0 255]"}
40 def ascii85lines(datalen):
41 if datalen < 4:
42 return 1
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
53 l = 0
54 l = [None, None, None, None]
55 for i in range(len(data)):
56 c = data[i]
57 l[i%4] = ord(c)
58 if i%4 == 3:
59 if i%60 == 3 and i != 3:
60 file.write("\n")
61 if l:
62 # instead of
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))
71 else:
72 file.write("z")
73 if i%4 != 3:
74 for j in range((i%4) + 1, 4):
75 l[j] = 0
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])
90 file.write("\n")
93 class image:
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
103 self.mode = mode
104 self.data = data
105 self.compressed = compressed
106 self.palette = palette
108 def split(self):
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):
117 if len(args):
118 raise RuntimeError("encoding not supported in this implementation")
119 return self.data
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):
128 try:
129 data = file.read()
130 except:
131 data = open(file, "rb").read()
132 pos = 0
133 nestinglevel = 0
134 try:
135 while 1:
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":
139 if not nestinglevel:
140 begin = pos
141 nestinglevel += 1
142 elif not nestinglevel:
143 raise ValueError("begin marker expected")
144 elif data[pos+1] == "\331":
145 nestinglevel -= 1
146 if not nestinglevel:
147 end = pos + 2
148 break
149 elif data[pos+1] in ["\300", "\301"]:
150 l, bits, height, width, components = struct.unpack(">HBHHB", data[pos+2:pos+10])
151 if bits != 8:
152 raise ValueError("implementation limited to 8 bit per component only")
153 try:
154 mode = {1: "L", 3: "RGB", 4: "CMYK"}[components]
155 except KeyError:
156 raise ValueError("invalid number of components")
157 pos += l+1
158 elif data[pos+1] == "\340":
159 l, id, major, minor, dpikind, xdpi, ydpi = struct.unpack(">H5sBBBHH", data[pos+2:pos+16])
160 if dpikind == 1:
161 self.info = {"dpi": (xdpi, ydpi)}
162 elif dpikind == 2:
163 self.info = {"dpi": (xdpi*2.54, ydpi*2.45)}
164 # else do not provide dpi information
165 pos += l+1
166 pos += 1
167 except IndexError:
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)
176 self.data = data
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)
186 file.write("~>\n"
187 "%%EndData\n")
188 else:
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)))
194 file.write("[ ")
195 for i in xrange(0, tailpos, self.maxstrlen):
196 file.write("<~")
197 ascii85stream(file, self.data[i: i+self.maxstrlen])
198 file.write("~>\n")
199 if datalen != tailpos:
200 file.write("<~")
201 ascii85stream(file, self.data[tailpos:])
202 file.write("~>")
203 file.write("]\n"
204 "%%EndData\n")
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)
213 self.data = data
215 def write(self, file, writer, registry):
216 file.write("<<\n"
217 "/Length %d\n" % len(self.data))
218 file.write(">>\n"
219 "stream\n")
220 file.write(self.data)
221 file.write("\n"
222 "endstream\n")
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)
231 if addresource:
232 if palettedata is not None:
233 procset = "ImageI"
234 elif mode == "L":
235 procset = "ImageB"
236 else:
237 procset = "ImageC"
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)
244 self.name = name
245 self.width = width
246 self.height = height
247 self.palettemode = palettemode
248 self.palettedata = palettedata
249 self.mode = mode
250 self.bitspercomponent = bitspercomponent
251 self.compressmode = compressmode
252 self.data = data
253 self.smask = smask
255 def write(self, file, writer, registry):
256 file.write("<<\n"
257 "/Type /XObject\n"
258 "/Subtype /Image\n"
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))
264 file.write("]\n")
265 else:
266 file.write("/ColorSpace %s\n" % devicenames[self.mode])
267 if self.smask:
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)
273 file.write(">>\n"
274 "stream\n")
275 file.write(self.data)
276 file.write("\n"
277 "endstream\n")
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
288 self.image = image
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:
296 if ratio is None:
297 self.width_pt = self.height_pt * self.imagewidth / float(self.imageheight)
298 else:
299 self.width_pt = ratio * self.height_pt
300 elif self.height_pt is None:
301 if ratio is None:
302 self.height_pt = self.width_pt * self.imageheight / float(self.imagewidth)
303 else:
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")
307 else:
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
323 try:
324 self.imagecompressed = image.compressed
325 except:
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):
338 """internal function
340 returns a tuple (mode, data, alpha, palettemode, palettedata)
341 where mode does not contain antialiasing anymore
344 alpha = palettemode = palettedata = None
345 data = self.image
346 mode = data.mode
347 if mode.startswith("A"):
348 mode = mode[1:]
349 if interleavealpha:
350 alpha = True
351 else:
352 bands = data.split()
353 alpha = band[0]
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"):
359 bands = data.split()
360 bands = list(bands[-1:]) + list(bands[:-1])
361 mode = mode[:-1]
362 if interleavealpha:
363 alpha = True
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)
369 else:
370 alpha = bands[0]
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)
376 if mode == "P":
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")
381 mode = "RGB"
382 palettemode = None
383 palettedata = None
384 elif len(mode) == 1:
385 if mode != "L":
386 warnings.warn("specific single channel image mode not natively supported, converted to regular grayscale")
387 data = data.convert("L")
388 mode = "L"
389 elif mode not in ["CMYK", "RGB"]:
390 warnings.warn("image with unknown mode converted to rgb")
391 data = data.convert("RGB")
392 mode = "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)
398 else:
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)
406 else:
407 alpha = alpha.tostring()
409 return mode, data, alpha, palettemode, palettedata
411 def bbox(self):
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
423 if PSsinglestring:
424 PSimagename = "image-%d-%s-singlestring" % (id(self.image), self.compressmode)
425 else:
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))
435 bbox += self.bbox()
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)))
441 file.write("<~")
442 ascii85stream(file, palettedata)
443 file.write("~>\n"
444 "%%EndData\n")
445 file.write("] setcolorspace\n")
446 else:
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)
453 file.write("<<\n")
454 if alpha:
455 file.write("/ImageType 3\n"
456 "/DataDict\n"
457 "<<\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:
467 if PSsinglestring:
468 file.write("/%s load" % PSimagename)
469 else:
470 file.write("/imagedataaccess load") # some printers do not allow for inline code here -> we store it in a resource
471 else:
472 if self.PSbinexpand == 2:
473 file.write("currentfile /ASCIIHexDecode filter")
474 else:
475 file.write("currentfile /ASCII85Decode filter")
476 if self.compressmode or self.imagecompressed:
477 file.write(" /%sDecode filter" % (self.compressmode or self.imagecompressed))
478 file.write("\n")
480 file.write(">>\n")
482 if alpha:
483 file.write("/MaskDict\n"
484 "<<\n"
485 "/ImageType 1\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"
491 ">>\n"
492 "/InterleaveType 1\n"
493 ">>\n")
495 if self.PSstoreimage:
496 file.write("image\n")
497 else:
498 if self.PSbinexpand == 2:
499 file.write("%%%%BeginData: %i ASCII Lines\n"
500 "image\n" % (asciihexlines(len(data)) + 1))
501 asciihexstream(file, data)
502 else:
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)
507 file.write("~>\n")
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)
516 if alpha:
517 alpha = PDFimage("%s-smask" % name, self.imagewidth, self.imageheight,
518 None, None, "L", 8,
519 self.compressmode, alpha, None, registry, addresource=False)
520 registry.add(alpha)
521 registry.add(PDFimage(name, self.imagewidth, self.imageheight,
522 palettemode, palettedata, mode, 8,
523 self.compressmode or self.imagecompress, data, alpha, registry))
525 bbox += self.bbox()
526 imagematrixPDF = (trafo.scale_pt(self.width_pt, self.height_pt)
527 .translated_pt(self.xpos_pt, self.ypos_pt))
529 file.write("q\n")
530 imagematrixPDF.processPDF(file, writer, context, registry, bbox)
531 file.write("/%s Do\n" % name)
532 file.write("Q\n")
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)
542 else:
543 width_pt = None
544 if height is not None:
545 height_pt = unit.topt(height)
546 else:
547 height_pt = None
549 bitmap_pt.__init__(self, xpos_pt, ypos_pt, image, width_pt=width_pt, height_pt=height_pt, **kwargs)