fix file opening for bbox reading when using filelocator (reported by Michael J Gruber)
[PyX/mjg.git] / pyx / bitmap.py
blob42dffc5ca02acc7fd369124e778ed3ef70c3893b
1 # -*- encoding: utf-8 -*-
4 # Copyright (C) 2004-2011 André Wobst <wobsta@users.sourceforge.net>
5 # Copyright (C) 2011 Michael Schindler<m-schindler@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
24 try:
25 import zlib
26 haszlib = 1
27 except:
28 haszlib = 0
30 import bbox, canvasitem, pswriter, pdfwriter, trafo, unit
32 devicenames = {"L": "/DeviceGray",
33 "RGB": "/DeviceRGB",
34 "CMYK": "/DeviceCMYK"}
35 decodestrings = {"L": "[0 1]",
36 "RGB": "[0 1 0 1 0 1]",
37 "CMYK": "[0 1 0 1 0 1 0 1]",
38 "P": "[0 255]"}
41 def ascii85lines(datalen):
42 if datalen < 4:
43 return 1
44 return (datalen + 56)/60
46 def ascii85stream(file, data):
47 """Encodes the string data in ASCII85 and writes it to
48 the stream file. The number of lines written to the stream
49 is known just from the length of the data by means of the
50 ascii85lines function. Note that the tailing newline character
51 of the last line is not added by this function, but it is taken
52 into account in the ascii85lines function."""
53 i = 3 # go on smoothly in case of data length equals zero
54 l = 0
55 l = [None, None, None, None]
56 for i in range(len(data)):
57 c = data[i]
58 l[i%4] = ord(c)
59 if i%4 == 3:
60 if i%60 == 3 and i != 3:
61 file.write("\n")
62 if l:
63 # instead of
64 # l[3], c5 = divmod(256*256*256*l[0]+256*256*l[1]+256*l[2]+l[3], 85)
65 # l[2], c4 = divmod(l[3], 85)
66 # we have to avoid number > 2**31 by
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('BBBBB', c1+33, c2+33, c3+33, c4+33, c5+33))
72 else:
73 file.write("z")
74 if i%4 != 3:
75 for j in range((i%4) + 1, 4):
76 l[j] = 0
77 l[3], c5 = divmod(256*256*l[0]+256*256*l[1]+256*l[2]+l[3], 85)
78 l[2], c4 = divmod(256*256*3*l[0]+l[3], 85)
79 l[1], c3 = divmod(l[2], 85)
80 c1 , c2 = divmod(l[1], 85)
81 file.write(struct.pack('BBBB', c1+33, c2+33, c3+33, c4+33)[:(i%4)+2])
83 _asciihexlinelength = 64
84 def asciihexlines(datalen):
85 return (datalen*2 + _asciihexlinelength - 1) / _asciihexlinelength
87 def asciihexstream(file, data):
88 hexdata = binascii.b2a_hex(data)
89 for i in range((len(hexdata)-1)/_asciihexlinelength + 1):
90 file.write(hexdata[i*_asciihexlinelength: i*_asciihexlinelength+_asciihexlinelength])
91 file.write("\n")
94 class image:
96 def __init__(self, width, height, mode, data, compressed=None, palette=None):
97 if width <= 0 or height <= 0:
98 raise ValueError("valid image size")
99 if mode not in ["L", "RGB", "CMYK", "LA", "RGBA", "CMYKA", "AL", "ARGB", "ACMYK"]:
100 raise ValueError("invalid mode")
101 if compressed is None and len(mode)*width*height != len(data):
102 raise ValueError("wrong size of uncompressed data")
103 self.size = width, height
104 self.mode = mode
105 self.data = data
106 self.compressed = compressed
107 self.palette = palette
109 def split(self):
110 if self.compressed is not None:
111 raise RuntimeError("cannot extract bands from compressed image")
112 bands = len(self.mode)
113 width, height = self.size
114 return [image(width, height, "L", "".join([self.data[i*bands+band]
115 for i in range(width*height)]))
116 for band in range(bands)]
118 def tostring(self, *args):
119 if len(args):
120 raise RuntimeError("encoding not supported in this implementation")
121 return self.data
123 def convert(self, model):
124 raise RuntimeError("color model conversion not supported in this implementation")
127 class jpegimage(image):
129 def __init__(self, file):
130 try:
131 data = file.read()
132 except:
133 data = open(file, "rb").read()
134 pos = 0
135 nestinglevel = 0
136 try:
137 while 1:
138 if data[pos] == "\377" and data[pos+1] not in ["\0", "\377"]:
139 # print "marker: 0x%02x \\%03o" % (ord(data[pos+1]), ord(data[pos+1]))
140 if data[pos+1] == "\330":
141 if not nestinglevel:
142 begin = pos
143 nestinglevel += 1
144 elif not nestinglevel:
145 raise ValueError("begin marker expected")
146 elif data[pos+1] == "\331":
147 nestinglevel -= 1
148 if not nestinglevel:
149 end = pos + 2
150 break
151 elif data[pos+1] in ["\300", "\301"]:
152 l, bits, height, width, components = struct.unpack(">HBHHB", data[pos+2:pos+10])
153 if bits != 8:
154 raise ValueError("implementation limited to 8 bit per component only")
155 try:
156 mode = {1: "L", 3: "RGB", 4: "CMYK"}[components]
157 except KeyError:
158 raise ValueError("invalid number of components")
159 pos += l+1
160 elif data[pos+1] == "\340":
161 l, id, major, minor, dpikind, xdpi, ydpi = struct.unpack(">H5sBBBHH", data[pos+2:pos+16])
162 if dpikind == 1:
163 self.info = {"dpi": (xdpi, ydpi)}
164 elif dpikind == 2:
165 self.info = {"dpi": (xdpi*2.54, ydpi*2.45)}
166 # else do not provide dpi information
167 pos += l+1
168 pos += 1
169 except IndexError:
170 raise ValueError("end marker expected")
171 image.__init__(self, width, height, mode, data[begin:end], compressed="DCT")
174 class PSimagedata(pswriter.PSresource):
176 def __init__(self, name, data, singlestring, maxstrlen):
177 pswriter.PSresource.__init__(self, "imagedata", name)
178 self.data = data
179 self.singlestring = singlestring
180 self.maxstrlen = maxstrlen
182 def output(self, file, writer, registry):
183 file.write("%%%%BeginRessource: %s\n" % self.id)
184 if self.singlestring:
185 file.write("%%%%BeginData: %i ASCII Lines\n"
186 "<~" % ascii85lines(len(self.data)))
187 ascii85stream(file, self.data)
188 file.write("~>\n"
189 "%%EndData\n")
190 else:
191 datalen = len(self.data)
192 tailpos = datalen - datalen % self.maxstrlen
193 file.write("%%%%BeginData: %i ASCII Lines\n" %
194 ((tailpos/self.maxstrlen) * ascii85lines(self.maxstrlen) +
195 ascii85lines(datalen-tailpos)))
196 file.write("[ ")
197 for i in xrange(0, tailpos, self.maxstrlen):
198 file.write("<~")
199 ascii85stream(file, self.data[i: i+self.maxstrlen])
200 file.write("~>\n")
201 if datalen != tailpos:
202 file.write("<~")
203 ascii85stream(file, self.data[tailpos:])
204 file.write("~>")
205 file.write("]\n"
206 "%%EndData\n")
207 file.write("/%s exch def\n" % self.id)
208 file.write("%%EndRessource\n")
211 class PDFimagepalettedata(pdfwriter.PDFobject):
213 def __init__(self, name, data):
214 pdfwriter.PDFobject.__init__(self, "imagepalettedata", name)
215 self.data = data
217 def write(self, file, writer, registry):
218 file.write("<<\n"
219 "/Length %d\n" % len(self.data))
220 file.write(">>\n"
221 "stream\n")
222 file.write(self.data)
223 file.write("\n"
224 "endstream\n")
227 class PDFimage(pdfwriter.PDFobject):
229 def __init__(self, name, width, height, palettemode, palettedata, mode,
230 bitspercomponent, compressmode, data, smask, registry, addresource=True):
231 pdfwriter.PDFobject.__init__(self, "image", name)
233 if addresource:
234 if palettedata is not None:
235 procset = "ImageI"
236 elif mode == "L":
237 procset = "ImageB"
238 else:
239 procset = "ImageC"
240 registry.addresource("XObject", name, self, procset=procset)
241 if palettedata is not None:
242 # note that acrobat wants a palette to be an object (which clearly is a bug)
243 self.PDFpalettedata = PDFimagepalettedata(name, palettedata)
244 registry.add(self.PDFpalettedata)
246 self.name = name
247 self.width = width
248 self.height = height
249 self.palettemode = palettemode
250 self.palettedata = palettedata
251 self.mode = mode
252 self.bitspercomponent = bitspercomponent
253 self.compressmode = compressmode
254 self.data = data
255 self.smask = smask
257 def write(self, file, writer, registry):
258 file.write("<<\n"
259 "/Type /XObject\n"
260 "/Subtype /Image\n"
261 "/Width %d\n" % self.width)
262 file.write("/Height %d\n" % self.height)
263 if self.palettedata is not None:
264 file.write("/ColorSpace [ /Indexed %s %i\n" % (devicenames[self.palettemode], len(self.palettedata)/3-1))
265 file.write("%d 0 R\n" % registry.getrefno(self.PDFpalettedata))
266 file.write("]\n")
267 else:
268 file.write("/ColorSpace %s\n" % devicenames[self.mode])
269 if self.smask:
270 file.write("/SMask %d 0 R\n" % registry.getrefno(self.smask))
271 file.write("/BitsPerComponent %d\n" % self.bitspercomponent)
272 file.write("/Length %d\n" % len(self.data))
273 if self.compressmode:
274 file.write("/Filter /%sDecode\n" % self.compressmode)
275 file.write(">>\n"
276 "stream\n")
277 file.write(self.data)
278 file.write("\n"
279 "endstream\n")
282 class bitmap_pt(canvasitem.canvasitem):
284 def __init__(self, xpos_pt, ypos_pt, image, width_pt=None, height_pt=None, ratio=None,
285 PSstoreimage=0, PSmaxstrlen=4093, PSbinexpand=1,
286 compressmode="Flate", flatecompresslevel=6,
287 dctquality=75, dctoptimize=0, dctprogression=0):
288 self.xpos_pt = xpos_pt
289 self.ypos_pt = ypos_pt
290 self.image = image
292 self.imagewidth, self.imageheight = image.size
294 if width_pt is not None or height_pt is not None:
295 self.width_pt = width_pt
296 self.height_pt = height_pt
297 if self.width_pt is None:
298 if ratio is None:
299 self.width_pt = self.height_pt * self.imagewidth / float(self.imageheight)
300 else:
301 self.width_pt = ratio * self.height_pt
302 elif self.height_pt is None:
303 if ratio is None:
304 self.height_pt = self.width_pt * self.imageheight / float(self.imagewidth)
305 else:
306 self.height_pt = (1.0/ratio) * self.width_pt
307 elif ratio is not None:
308 raise ValueError("can't specify a ratio when setting width_pt and height_pt")
309 else:
310 if ratio is not None:
311 raise ValueError("must specify width_pt or height_pt to set a ratio")
312 widthdpi, heightdpi = image.info["dpi"] # fails when no dpi information available
313 self.width_pt = 72.0 * self.imagewidth / float(widthdpi)
314 self.height_pt = 72.0 * self.imageheight / float(heightdpi)
316 self.PSstoreimage = PSstoreimage
317 self.PSmaxstrlen = PSmaxstrlen
318 self.PSbinexpand = PSbinexpand
319 self.compressmode = compressmode
320 self.flatecompresslevel = flatecompresslevel
321 self.dctquality = dctquality
322 self.dctoptimize = dctoptimize
323 self.dctprogression = dctprogression
325 try:
326 self.imagecompressed = image.compressed
327 except:
328 self.imagecompressed = None
329 if self.compressmode not in [None, "Flate", "DCT"]:
330 raise ValueError("invalid compressmode '%s'" % self.compressmode)
331 if self.imagecompressed not in [None, "Flate", "DCT"]:
332 raise ValueError("invalid compressed image '%s'" % self.imagecompressed)
333 if self.compressmode is not None and self.imagecompressed is not None:
334 raise ValueError("compression of a compressed image not supported")
335 if not haszlib and self.compressmode == "Flate":
336 warnings.warn("zlib module not available, disable compression")
337 self.compressmode = None
339 def imagedata(self, interleavealpha):
340 """internal function
342 returns a tuple (mode, data, alpha, palettemode, palettedata)
343 where mode does not contain antialiasing anymore
346 alpha = palettemode = palettedata = None
347 data = self.image
348 mode = data.mode
349 if mode.startswith("A"):
350 mode = mode[1:]
351 if interleavealpha:
352 alpha = True
353 else:
354 bands = data.split()
355 alpha = band[0]
356 data = image(self.imagewidth, self.imageheight, mode,
357 "".join(["".join(values)
358 for values in zip(*[band.tostring()
359 for band in bands[1:]])]), palette=data.palette)
360 if mode.endswith("A"):
361 bands = data.split()
362 bands = list(bands[-1:]) + list(bands[:-1])
363 mode = mode[:-1]
364 if interleavealpha:
365 alpha = True
366 # 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
367 data = image(self.imagewidth, self.imageheight, "A%s" % mode,
368 "".join(["".join(values)
369 for values in zip(*[band.tostring()
370 for band in bands])]), palette=data.palette)
371 else:
372 alpha = bands[0]
373 data = image(self.imagewidth, self.imageheight, mode,
374 "".join(["".join(values)
375 for values in zip(*[band.tostring()
376 for band in bands[1:]])]), palette=data.palette)
378 if mode == "P":
379 palettemode, palettedata = data.palette.getdata()
380 if palettemode not in ["L", "RGB", "CMYK"]:
381 warnings.warn("image with unknown palette mode '%s' converted to rgb image" % palettemode)
382 data = data.convert("RGB")
383 mode = "RGB"
384 palettemode = None
385 palettedata = None
386 elif len(mode) == 1:
387 if mode != "L":
388 warnings.warn("specific single channel image mode not natively supported, converted to regular grayscale")
389 data = data.convert("L")
390 mode = "L"
391 elif mode not in ["CMYK", "RGB"]:
392 warnings.warn("image with unknown mode converted to rgb")
393 data = data.convert("RGB")
394 mode = "RGB"
396 if self.compressmode == "Flate":
397 data = zlib.compress(data.tostring(), self.flatecompresslevel)
398 elif self.compressmode == "DCT":
399 data = data.tostring("jpeg", mode, self.dctquality, self.dctoptimize, self.dctprogression)
400 else:
401 data = data.tostring()
402 if alpha and not interleavealpha:
403 if self.compressmode == "Flate":
404 alpha = zlib.compress(alpha.tostring(), self.flatecompresslevel)
405 elif self.compressmode == "DCT":
406 # well, this here is strange, we might want a alphacompressmode ...
407 alpha = alpha.tostring("jpeg", mode, self.dctquality, self.dctoptimize, self.dctprogression)
408 else:
409 alpha = alpha.tostring()
411 return mode, data, alpha, palettemode, palettedata
413 def bbox(self):
414 return bbox.bbox_pt(self.xpos_pt, self.ypos_pt,
415 self.xpos_pt+self.width_pt, self.ypos_pt+self.height_pt)
417 def processPS(self, file, writer, context, registry, bbox):
418 mode, data, alpha, palettemode, palettedata = self.imagedata(True)
419 imagematrixPS = (trafo.mirror(0)
420 .translated_pt(-self.xpos_pt, self.ypos_pt+self.height_pt)
421 .scaled_pt(self.imagewidth/self.width_pt, self.imageheight/self.height_pt))
424 PSsinglestring = self.PSstoreimage and len(data) < self.PSmaxstrlen
425 if PSsinglestring:
426 PSimagename = "image-%d-%s-singlestring" % (id(self.image), self.compressmode)
427 else:
428 PSimagename = "image-%d-%s-stringarray" % (id(self.image), self.compressmode)
430 if self.PSstoreimage and not PSsinglestring:
431 registry.add(pswriter.PSdefinition("imagedataaccess",
432 "{ /imagedataindex load " # get list index
433 "dup 1 add /imagedataindex exch store " # store increased index
434 "/imagedataid load exch get }")) # select string from array
435 if self.PSstoreimage:
436 registry.add(PSimagedata(PSimagename, data, PSsinglestring, self.PSmaxstrlen))
437 bbox += self.bbox()
439 file.write("gsave\n")
440 if palettedata is not None:
441 file.write("[ /Indexed %s %i\n" % (devicenames[palettemode], len(palettedata)/3-1))
442 file.write("%%%%BeginData: %i ASCII Lines\n" % ascii85lines(len(palettedata)))
443 file.write("<~")
444 ascii85stream(file, palettedata)
445 file.write("~>\n"
446 "%%EndData\n")
447 file.write("] setcolorspace\n")
448 else:
449 file.write("%s setcolorspace\n" % devicenames[mode])
451 if self.PSstoreimage and not PSsinglestring:
452 file.write("/imagedataindex 0 store\n" # not use the stack since interpreters differ in their stack usage
453 "/imagedataid %s store\n" % PSimagename)
455 file.write("<<\n")
456 if alpha:
457 file.write("/ImageType 3\n"
458 "/DataDict\n"
459 "<<\n")
460 file.write("/ImageType 1\n"
461 "/Width %i\n" % self.imagewidth)
462 file.write("/Height %i\n" % self.imageheight)
463 file.write("/BitsPerComponent 8\n"
464 "/ImageMatrix %s\n" % imagematrixPS)
465 file.write("/Decode %s\n" % decodestrings[mode])
467 file.write("/DataSource ")
468 if self.PSstoreimage:
469 if PSsinglestring:
470 file.write("/%s load" % PSimagename)
471 else:
472 file.write("/imagedataaccess load") # some printers do not allow for inline code here -> we store it in a resource
473 else:
474 if self.PSbinexpand == 2:
475 file.write("currentfile /ASCIIHexDecode filter")
476 else:
477 file.write("currentfile /ASCII85Decode filter")
478 if self.compressmode or self.imagecompressed:
479 file.write(" /%sDecode filter" % (self.compressmode or self.imagecompressed))
480 file.write("\n")
482 file.write(">>\n")
484 if alpha:
485 file.write("/MaskDict\n"
486 "<<\n"
487 "/ImageType 1\n"
488 "/Width %i\n" % self.imagewidth)
489 file.write("/Height %i\n" % self.imageheight)
490 file.write("/BitsPerComponent 8\n"
491 "/ImageMatrix %s\n" % imagematrixPS)
492 file.write("/Decode [1 0]\n"
493 ">>\n"
494 "/InterleaveType 1\n"
495 ">>\n")
497 if self.PSstoreimage:
498 file.write("image\n")
499 else:
500 if self.PSbinexpand == 2:
501 file.write("%%%%BeginData: %i ASCII Lines\n"
502 "image\n" % (asciihexlines(len(data)) + 1))
503 asciihexstream(file, data)
504 else:
505 # the datasource is currentstream (plus some filters)
506 file.write("%%%%BeginData: %i ASCII Lines\n"
507 "image\n" % (ascii85lines(len(data)) + 1))
508 ascii85stream(file, data)
509 file.write("~>\n")
510 file.write("%%EndData\n")
512 file.write("grestore\n")
514 def processPDF(self, file, writer, context, registry, bbox):
515 mode, data, alpha, palettemode, palettedata = self.imagedata(False)
517 name = "image-%d-%s" % (id(self.image), self.compressmode or self.imagecompressed)
518 if alpha:
519 alpha = PDFimage("%s-smask" % name, self.imagewidth, self.imageheight,
520 None, None, "L", 8,
521 self.compressmode, alpha, None, registry, addresource=False)
522 registry.add(alpha)
523 registry.add(PDFimage(name, self.imagewidth, self.imageheight,
524 palettemode, palettedata, mode, 8,
525 self.compressmode or self.imagecompressed, data, alpha, registry))
527 bbox += self.bbox()
528 imagematrixPDF = (trafo.scale_pt(self.width_pt, self.height_pt)
529 .translated_pt(self.xpos_pt, self.ypos_pt))
531 file.write("q\n")
532 imagematrixPDF.processPDF(file, writer, context, registry, bbox)
533 file.write("/%s Do\n" % name)
534 file.write("Q\n")
537 class bitmap(bitmap_pt):
539 def __init__(self, xpos, ypos, image, width=None, height=None, **kwargs):
540 xpos_pt = unit.topt(xpos)
541 ypos_pt = unit.topt(ypos)
542 if width is not None:
543 width_pt = unit.topt(width)
544 else:
545 width_pt = None
546 if height is not None:
547 height_pt = unit.topt(height)
548 else:
549 height_pt = None
551 bitmap_pt.__init__(self, xpos_pt, ypos_pt, image, width_pt=width_pt, height_pt=height_pt, **kwargs)