another unclosed file fix
[PyX.git] / pyx / bitmap.py
blob89e4951d9e249133da985522f567718335950a14
1 # -*- encoding: utf-8 -*-
4 # Copyright (C) 2004-2012 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 = True
27 except:
28 haszlib = False
30 from . import bbox, baseclasses, 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] = 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_bytes(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_bytes(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 tobytes(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 f = open(file, "rb")
134 data = f.read()
135 f.close()
136 pos = 0
137 nestinglevel = 0
138 try:
139 while True:
140 if data[pos] == 0o377 and data[pos+1] not in [0, 0o377]:
141 # print("marker: 0x%02x \\%03o" % (data[pos+1], data[pos+1]))
142 if data[pos+1] == 0o330:
143 if not nestinglevel:
144 begin = pos
145 nestinglevel += 1
146 elif not nestinglevel:
147 raise ValueError("begin marker expected")
148 elif data[pos+1] == 0o331:
149 nestinglevel -= 1
150 if not nestinglevel:
151 end = pos + 2
152 break
153 elif data[pos+1] in [0o300, 0o301]:
154 l, bits, height, width, components = struct.unpack(">HBHHB", data[pos+2:pos+10])
155 if bits != 8:
156 raise ValueError("implementation limited to 8 bit per component only")
157 try:
158 mode = {1: "L", 3: "RGB", 4: "CMYK"}[components]
159 except KeyError:
160 raise ValueError("invalid number of components")
161 pos += l+1
162 elif data[pos+1] == 0o340:
163 l, id, major, minor, dpikind, xdpi, ydpi = struct.unpack(">H5sBBBHH", data[pos+2:pos+16])
164 if dpikind == 1:
165 self.info = {"dpi": (xdpi, ydpi)}
166 elif dpikind == 2:
167 self.info = {"dpi": (xdpi*2.54, ydpi*2.45)}
168 # else do not provide dpi information
169 pos += l+1
170 pos += 1
171 except IndexError:
172 raise ValueError("end marker expected")
173 image.__init__(self, width, height, mode, data[begin:end], compressed="DCT")
176 class PSimagedata(pswriter.PSresource):
178 def __init__(self, name, data, singlestring, maxstrlen):
179 pswriter.PSresource.__init__(self, "imagedata", name)
180 self.data = data
181 self.singlestring = singlestring
182 self.maxstrlen = maxstrlen
184 def output(self, file, writer, registry):
185 file.write("%%%%BeginRessource: %s\n" % self.id)
186 if self.singlestring:
187 file.write("%%%%BeginData: %i ASCII Lines\n"
188 "<~" % ascii85lines(len(self.data)))
189 ascii85stream(file, self.data)
190 file.write("~>\n"
191 "%%EndData\n")
192 else:
193 datalen = len(self.data)
194 tailpos = datalen - datalen % self.maxstrlen
195 file.write("%%%%BeginData: %i ASCII Lines\n" %
196 ((tailpos/self.maxstrlen) * ascii85lines(self.maxstrlen) +
197 ascii85lines(datalen-tailpos)))
198 file.write("[ ")
199 for i in range(0, tailpos, self.maxstrlen):
200 file.write("<~")
201 ascii85stream(file, self.data[i: i+self.maxstrlen])
202 file.write("~>\n")
203 if datalen != tailpos:
204 file.write("<~")
205 ascii85stream(file, self.data[tailpos:])
206 file.write("~>")
207 file.write("]\n"
208 "%%EndData\n")
209 file.write("/%s exch def\n" % self.id)
210 file.write("%%EndRessource\n")
213 class PDFimagepalettedata(pdfwriter.PDFobject):
215 def __init__(self, name, data):
216 pdfwriter.PDFobject.__init__(self, "imagepalettedata", name)
217 self.data = data
219 def write(self, file, writer, registry):
220 file.write("<<\n"
221 "/Length %d\n" % len(self.data))
222 file.write(">>\n"
223 "stream\n")
224 file.write(self.data)
225 file.write("\n"
226 "endstream\n")
229 class PDFimage(pdfwriter.PDFobject):
231 def __init__(self, name, width, height, palettemode, palettedata, mode,
232 bitspercomponent, compressmode, data, smask, registry, addresource=True):
233 pdfwriter.PDFobject.__init__(self, "image", name)
235 if addresource:
236 if palettedata is not None:
237 procset = "ImageI"
238 elif mode == "L":
239 procset = "ImageB"
240 else:
241 procset = "ImageC"
242 registry.addresource("XObject", name, self, procset=procset)
243 if palettedata is not None:
244 # note that acrobat wants a palette to be an object (which clearly is a bug)
245 self.PDFpalettedata = PDFimagepalettedata(name, palettedata)
246 registry.add(self.PDFpalettedata)
248 self.name = name
249 self.width = width
250 self.height = height
251 self.palettemode = palettemode
252 self.palettedata = palettedata
253 self.mode = mode
254 self.bitspercomponent = bitspercomponent
255 self.compressmode = compressmode
256 self.data = data
257 self.smask = smask
259 def write(self, file, writer, registry):
260 file.write("<<\n"
261 "/Type /XObject\n"
262 "/Subtype /Image\n"
263 "/Width %d\n" % self.width)
264 file.write("/Height %d\n" % self.height)
265 if self.palettedata is not None:
266 file.write("/ColorSpace [ /Indexed %s %i\n" % (devicenames[self.palettemode], len(self.palettedata)/3-1))
267 file.write("%d 0 R\n" % registry.getrefno(self.PDFpalettedata))
268 file.write("]\n")
269 else:
270 file.write("/ColorSpace %s\n" % devicenames[self.mode])
271 if self.smask:
272 file.write("/SMask %d 0 R\n" % registry.getrefno(self.smask))
273 file.write("/BitsPerComponent %d\n" % self.bitspercomponent)
274 file.write("/Length %d\n" % len(self.data))
275 if self.compressmode:
276 file.write("/Filter /%sDecode\n" % self.compressmode)
277 file.write(">>\n"
278 "stream\n")
279 file.write_bytes(self.data)
280 file.write("\n"
281 "endstream\n")
283 class bitmap_trafo(baseclasses.canvasitem):
285 def __init__(self, trafo, image,
286 PSstoreimage=0, PSmaxstrlen=4093, PSbinexpand=1,
287 compressmode="Flate", flatecompresslevel=6,
288 dctquality=75, dctoptimize=0, dctprogression=0):
289 self.pdftrafo = trafo
290 self.image = image
291 self.imagewidth, self.imageheight = image.size
293 self.PSstoreimage = PSstoreimage
294 self.PSmaxstrlen = PSmaxstrlen
295 self.PSbinexpand = PSbinexpand
296 self.compressmode = compressmode
297 self.flatecompresslevel = flatecompresslevel
298 self.dctquality = dctquality
299 self.dctoptimize = dctoptimize
300 self.dctprogression = dctprogression
302 try:
303 self.imagecompressed = image.compressed
304 except:
305 self.imagecompressed = None
306 if self.compressmode not in [None, "Flate", "DCT"]:
307 raise ValueError("invalid compressmode '%s'" % self.compressmode)
308 if self.imagecompressed not in [None, "Flate", "DCT"]:
309 raise ValueError("invalid compressed image '%s'" % self.imagecompressed)
310 if self.compressmode is not None and self.imagecompressed is not None:
311 raise ValueError("compression of a compressed image not supported")
312 if not haszlib and self.compressmode == "Flate":
313 warnings.warn("zlib module not available, disable compression")
314 self.compressmode = None
316 def imagedata(self, interleavealpha):
317 """internal function
319 returns a tuple (mode, data, alpha, palettemode, palettedata)
320 where mode does not contain antialiasing anymore
323 alpha = palettemode = palettedata = None
324 data = self.image
325 mode = data.mode
326 if mode.startswith("A"):
327 mode = mode[1:]
328 if interleavealpha:
329 alpha = True
330 else:
331 bands = data.split()
332 alpha = bands[0]
333 data = image(self.imagewidth, self.imageheight, mode,
334 "".join(["".join(values)
335 for values in zip(*[band.tobytes()
336 for band in bands[1:]])]), palette=data.palette)
337 if mode.endswith("A"):
338 bands = data.split()
339 bands = list(bands[-1:]) + list(bands[:-1])
340 mode = mode[:-1]
341 if interleavealpha:
342 alpha = True
343 # 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
344 data = image(self.imagewidth, self.imageheight, "A%s" % mode,
345 "".join(["".join(values)
346 for values in zip(*[band.tobytes()
347 for band in bands])]), palette=data.palette)
348 else:
349 alpha = bands[0]
350 data = image(self.imagewidth, self.imageheight, mode,
351 "".join(["".join(values)
352 for values in zip(*[band.tobytes()
353 for band in bands[1:]])]), palette=data.palette)
355 if mode == "P":
356 palettemode, palettedata = data.palette.getdata()
357 if palettemode not in ["L", "RGB", "CMYK"]:
358 warnings.warn("image with unknown palette mode '%s' converted to rgb image" % palettemode)
359 data = data.convert("RGB")
360 mode = "RGB"
361 palettemode = None
362 palettedata = None
363 elif len(mode) == 1:
364 if mode != "L":
365 warnings.warn("specific single channel image mode not natively supported, converted to regular grayscale")
366 data = data.convert("L")
367 mode = "L"
368 elif mode not in ["CMYK", "RGB"]:
369 warnings.warn("image with unknown mode converted to rgb")
370 data = data.convert("RGB")
371 mode = "RGB"
373 if self.compressmode == "Flate":
374 data = zlib.compress(data.tobytes(), self.flatecompresslevel)
375 elif self.compressmode == "DCT":
376 data = data.tobytes("jpeg", mode, self.dctquality, self.dctoptimize, self.dctprogression)
377 else:
378 data = data.tobytes()
379 if alpha and not interleavealpha:
380 if self.compressmode == "Flate":
381 alpha = zlib.compress(alpha.tobytes(), self.flatecompresslevel)
382 elif self.compressmode == "DCT":
383 # well, this here is strange, we might want a alphacompressmode ...
384 alpha = alpha.tobytes("jpeg", mode, self.dctquality, self.dctoptimize, self.dctprogression)
385 else:
386 alpha = alpha.tobytes()
388 return mode, data, alpha, palettemode, palettedata
390 def bbox(self):
391 bb = bbox.empty()
392 bb.includepoint_pt(*self.pdftrafo.apply_pt(0.0, 0.0))
393 bb.includepoint_pt(*self.pdftrafo.apply_pt(0.0, 1.0))
394 bb.includepoint_pt(*self.pdftrafo.apply_pt(1.0, 0.0))
395 bb.includepoint_pt(*self.pdftrafo.apply_pt(1.0, 1.0))
396 return bb
398 def processPS(self, file, writer, context, registry, bbox):
399 mode, data, alpha, palettemode, palettedata = self.imagedata(True)
400 pstrafo = trafo.translate_pt(0, -1.0).scaled(self.imagewidth, -self.imageheight)*self.pdftrafo.inverse()
402 PSsinglestring = self.PSstoreimage and len(data) < self.PSmaxstrlen
403 if PSsinglestring:
404 PSimagename = "image-%d-%s-singlestring" % (id(self.image), self.compressmode)
405 else:
406 PSimagename = "image-%d-%s-stringarray" % (id(self.image), self.compressmode)
408 if self.PSstoreimage and not PSsinglestring:
409 registry.add(pswriter.PSdefinition("imagedataaccess",
410 b"{ /imagedataindex load " # get list index
411 b"dup 1 add /imagedataindex exch store " # store increased index
412 b"/imagedataid load exch get }")) # select string from array
413 if self.PSstoreimage:
414 registry.add(PSimagedata(PSimagename, data, PSsinglestring, self.PSmaxstrlen))
415 bbox += self.bbox()
417 file.write("gsave\n")
418 if palettedata is not None:
419 file.write("[ /Indexed %s %i\n" % (devicenames[palettemode], len(palettedata)/3-1))
420 file.write("%%%%BeginData: %i ASCII Lines\n" % ascii85lines(len(palettedata)))
421 file.write("<~")
422 ascii85stream(file, palettedata)
423 file.write("~>\n"
424 "%%EndData\n")
425 file.write("] setcolorspace\n")
426 else:
427 file.write("%s setcolorspace\n" % devicenames[mode])
429 if self.PSstoreimage and not PSsinglestring:
430 file.write("/imagedataindex 0 store\n" # not use the stack since interpreters differ in their stack usage
431 "/imagedataid %s store\n" % PSimagename)
433 file.write("<<\n")
434 if alpha:
435 file.write("/ImageType 3\n"
436 "/DataDict\n"
437 "<<\n")
438 file.write("/ImageType 1\n"
439 "/Width %i\n" % self.imagewidth)
440 file.write("/Height %i\n" % self.imageheight)
441 file.write("/BitsPerComponent 8\n"
442 "/ImageMatrix %s\n" % pstrafo)
443 file.write("/Decode %s\n" % decodestrings[mode])
445 file.write("/DataSource ")
446 if self.PSstoreimage:
447 if PSsinglestring:
448 file.write("/%s load" % PSimagename)
449 else:
450 file.write("/imagedataaccess load") # some printers do not allow for inline code here -> we store it in a resource
451 else:
452 if self.PSbinexpand == 2:
453 file.write("currentfile /ASCIIHexDecode filter")
454 else:
455 file.write("currentfile /ASCII85Decode filter")
456 if self.compressmode or self.imagecompressed:
457 file.write(" /%sDecode filter" % (self.compressmode or self.imagecompressed))
458 file.write("\n")
460 file.write(">>\n")
462 if alpha:
463 file.write("/MaskDict\n"
464 "<<\n"
465 "/ImageType 1\n"
466 "/Width %i\n" % self.imagewidth)
467 file.write("/Height %i\n" % self.imageheight)
468 file.write("/BitsPerComponent 8\n"
469 "/ImageMatrix %s\n" % pstrafo)
470 file.write("/Decode [1 0]\n"
471 ">>\n"
472 "/InterleaveType 1\n"
473 ">>\n")
475 if self.PSstoreimage:
476 file.write("image\n")
477 else:
478 if self.PSbinexpand == 2:
479 file.write("%%%%BeginData: %i ASCII Lines\n"
480 "image\n" % (asciihexlines(len(data)) + 1))
481 asciihexstream(file, data)
482 file.write(">\n")
483 else:
484 # the datasource is currentstream (plus some filters)
485 file.write("%%%%BeginData: %i ASCII Lines\n"
486 "image\n" % (ascii85lines(len(data)) + 1))
487 ascii85stream(file, data)
488 file.write("~>\n")
489 file.write("%%EndData\n")
491 file.write("grestore\n")
493 def processPDF(self, file, writer, context, registry, bbox):
494 mode, data, alpha, palettemode, palettedata = self.imagedata(False)
496 name = "image-%d-%s" % (id(self.image), self.compressmode or self.imagecompressed)
497 if alpha:
498 alpha = PDFimage("%s-smask" % name, self.imagewidth, self.imageheight,
499 None, None, "L", 8,
500 self.compressmode, alpha, None, registry, addresource=False)
501 registry.add(alpha)
502 registry.add(PDFimage(name, self.imagewidth, self.imageheight,
503 palettemode, palettedata, mode, 8,
504 self.compressmode or self.imagecompressed, data, alpha, registry))
506 bbox += self.bbox()
508 file.write("q\n")
509 self.pdftrafo.processPDF(file, writer, context, registry)
510 file.write("/%s Do\n" % name)
511 file.write("Q\n")
514 class bitmap_pt(bitmap_trafo):
516 def __init__(self, xpos_pt, ypos_pt, image, width_pt=None, height_pt=None, ratio=None, **kwargs):
517 imagewidth, imageheight = image.size
518 if width_pt is not None or height_pt is not None:
519 if width_pt is None:
520 if ratio is None:
521 width_pt = height_pt * imagewidth / float(imageheight)
522 else:
523 width_pt = ratio * height_pt
524 elif height_pt is None:
525 if ratio is None:
526 height_pt = width_pt * imageheight / float(imagewidth)
527 else:
528 height_pt = (1.0/ratio) * width_pt
529 elif ratio is not None:
530 raise ValueError("can't specify a ratio when setting width_pt and height_pt")
531 else:
532 if ratio is not None:
533 raise ValueError("must specify width_pt or height_pt to set a ratio")
534 widthdpi, heightdpi = image.info["dpi"] # fails when no dpi information available
535 width_pt = 72.0 * imagewidth / float(widthdpi)
536 height_pt = 72.0 * imageheight / float(heightdpi)
538 bitmap_trafo.__init__(self, trafo.trafo_pt(((float(width_pt), 0.0), (0.0, float(height_pt))), (float(xpos_pt), float(ypos_pt))), image, **kwargs)
541 class bitmap(bitmap_pt):
543 def __init__(self, xpos, ypos, image, width=None, height=None, **kwargs):
544 xpos_pt = unit.topt(xpos)
545 ypos_pt = unit.topt(ypos)
546 if width is not None:
547 width_pt = unit.topt(width)
548 else:
549 width_pt = None
550 if height is not None:
551 height_pt = unit.topt(height)
552 else:
553 height_pt = None
555 bitmap_pt.__init__(self, xpos_pt, ypos_pt, image, width_pt=width_pt, height_pt=height_pt, **kwargs)