jpeg reader on bytes
[PyX.git] / bitmap.py
blob663553e51db9e91a0f260bed4a1138dc9a653ba5
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 data = open(file, "rb").read()
134 pos = 0
135 nestinglevel = 0
136 try:
137 while True:
138 if data[pos] == 0o377 and data[pos+1] not in [0, 0o377]:
139 # print("marker: 0x%02x \\%03o" % (data[pos+1], data[pos+1]))
140 if data[pos+1] == 0o330:
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] == 0o331:
147 nestinglevel -= 1
148 if not nestinglevel:
149 end = pos + 2
150 break
151 elif data[pos+1] in [0o300, 0o301]:
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] == 0o340:
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 range(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_bytes(self.data)
278 file.write("\n"
279 "endstream\n")
281 class bitmap_trafo(baseclasses.canvasitem):
283 def __init__(self, trafo, image,
284 PSstoreimage=0, PSmaxstrlen=4093, PSbinexpand=1,
285 compressmode="Flate", flatecompresslevel=6,
286 dctquality=75, dctoptimize=0, dctprogression=0):
287 self.pdftrafo = trafo
288 self.image = image
289 self.imagewidth, self.imageheight = image.size
291 self.PSstoreimage = PSstoreimage
292 self.PSmaxstrlen = PSmaxstrlen
293 self.PSbinexpand = PSbinexpand
294 self.compressmode = compressmode
295 self.flatecompresslevel = flatecompresslevel
296 self.dctquality = dctquality
297 self.dctoptimize = dctoptimize
298 self.dctprogression = dctprogression
300 try:
301 self.imagecompressed = image.compressed
302 except:
303 self.imagecompressed = None
304 if self.compressmode not in [None, "Flate", "DCT"]:
305 raise ValueError("invalid compressmode '%s'" % self.compressmode)
306 if self.imagecompressed not in [None, "Flate", "DCT"]:
307 raise ValueError("invalid compressed image '%s'" % self.imagecompressed)
308 if self.compressmode is not None and self.imagecompressed is not None:
309 raise ValueError("compression of a compressed image not supported")
310 if not haszlib and self.compressmode == "Flate":
311 warnings.warn("zlib module not available, disable compression")
312 self.compressmode = None
314 def imagedata(self, interleavealpha):
315 """internal function
317 returns a tuple (mode, data, alpha, palettemode, palettedata)
318 where mode does not contain antialiasing anymore
321 alpha = palettemode = palettedata = None
322 data = self.image
323 mode = data.mode
324 if mode.startswith("A"):
325 mode = mode[1:]
326 if interleavealpha:
327 alpha = True
328 else:
329 bands = data.split()
330 alpha = bands[0]
331 data = image(self.imagewidth, self.imageheight, mode,
332 "".join(["".join(values)
333 for values in zip(*[band.tobytes()
334 for band in bands[1:]])]), palette=data.palette)
335 if mode.endswith("A"):
336 bands = data.split()
337 bands = list(bands[-1:]) + list(bands[:-1])
338 mode = mode[:-1]
339 if interleavealpha:
340 alpha = True
341 # 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
342 data = image(self.imagewidth, self.imageheight, "A%s" % mode,
343 "".join(["".join(values)
344 for values in zip(*[band.tobytes()
345 for band in bands])]), palette=data.palette)
346 else:
347 alpha = bands[0]
348 data = image(self.imagewidth, self.imageheight, mode,
349 "".join(["".join(values)
350 for values in zip(*[band.tobytes()
351 for band in bands[1:]])]), palette=data.palette)
353 if mode == "P":
354 palettemode, palettedata = data.palette.getdata()
355 if palettemode not in ["L", "RGB", "CMYK"]:
356 warnings.warn("image with unknown palette mode '%s' converted to rgb image" % palettemode)
357 data = data.convert("RGB")
358 mode = "RGB"
359 palettemode = None
360 palettedata = None
361 elif len(mode) == 1:
362 if mode != "L":
363 warnings.warn("specific single channel image mode not natively supported, converted to regular grayscale")
364 data = data.convert("L")
365 mode = "L"
366 elif mode not in ["CMYK", "RGB"]:
367 warnings.warn("image with unknown mode converted to rgb")
368 data = data.convert("RGB")
369 mode = "RGB"
371 if self.compressmode == "Flate":
372 data = zlib.compress(data.tobytes(), self.flatecompresslevel)
373 elif self.compressmode == "DCT":
374 data = data.tobytes("jpeg", mode, self.dctquality, self.dctoptimize, self.dctprogression)
375 else:
376 data = data.tobytes()
377 if alpha and not interleavealpha:
378 if self.compressmode == "Flate":
379 alpha = zlib.compress(alpha.tobytes(), self.flatecompresslevel)
380 elif self.compressmode == "DCT":
381 # well, this here is strange, we might want a alphacompressmode ...
382 alpha = alpha.tobytes("jpeg", mode, self.dctquality, self.dctoptimize, self.dctprogression)
383 else:
384 alpha = alpha.tobytes()
386 return mode, data, alpha, palettemode, palettedata
388 def bbox(self):
389 bb = bbox.empty()
390 bb.includepoint_pt(*self.pdftrafo.apply_pt(0.0, 0.0))
391 bb.includepoint_pt(*self.pdftrafo.apply_pt(0.0, 1.0))
392 bb.includepoint_pt(*self.pdftrafo.apply_pt(1.0, 0.0))
393 bb.includepoint_pt(*self.pdftrafo.apply_pt(1.0, 1.0))
394 return bb
396 def processPS(self, file, writer, context, registry, bbox):
397 mode, data, alpha, palettemode, palettedata = self.imagedata(True)
398 pstrafo = trafo.translate_pt(0, -1.0).scaled(self.imagewidth, -self.imageheight)*self.pdftrafo.inverse()
400 PSsinglestring = self.PSstoreimage and len(data) < self.PSmaxstrlen
401 if PSsinglestring:
402 PSimagename = "image-%d-%s-singlestring" % (id(self.image), self.compressmode)
403 else:
404 PSimagename = "image-%d-%s-stringarray" % (id(self.image), self.compressmode)
406 if self.PSstoreimage and not PSsinglestring:
407 registry.add(pswriter.PSdefinition("imagedataaccess",
408 b"{ /imagedataindex load " # get list index
409 b"dup 1 add /imagedataindex exch store " # store increased index
410 b"/imagedataid load exch get }")) # select string from array
411 if self.PSstoreimage:
412 registry.add(PSimagedata(PSimagename, data, PSsinglestring, self.PSmaxstrlen))
413 bbox += self.bbox()
415 file.write("gsave\n")
416 if palettedata is not None:
417 file.write("[ /Indexed %s %i\n" % (devicenames[palettemode], len(palettedata)/3-1))
418 file.write("%%%%BeginData: %i ASCII Lines\n" % ascii85lines(len(palettedata)))
419 file.write("<~")
420 ascii85stream(file, palettedata)
421 file.write("~>\n"
422 "%%EndData\n")
423 file.write("] setcolorspace\n")
424 else:
425 file.write("%s setcolorspace\n" % devicenames[mode])
427 if self.PSstoreimage and not PSsinglestring:
428 file.write("/imagedataindex 0 store\n" # not use the stack since interpreters differ in their stack usage
429 "/imagedataid %s store\n" % PSimagename)
431 file.write("<<\n")
432 if alpha:
433 file.write("/ImageType 3\n"
434 "/DataDict\n"
435 "<<\n")
436 file.write("/ImageType 1\n"
437 "/Width %i\n" % self.imagewidth)
438 file.write("/Height %i\n" % self.imageheight)
439 file.write("/BitsPerComponent 8\n"
440 "/ImageMatrix %s\n" % pstrafo)
441 file.write("/Decode %s\n" % decodestrings[mode])
443 file.write("/DataSource ")
444 if self.PSstoreimage:
445 if PSsinglestring:
446 file.write("/%s load" % PSimagename)
447 else:
448 file.write("/imagedataaccess load") # some printers do not allow for inline code here -> we store it in a resource
449 else:
450 if self.PSbinexpand == 2:
451 file.write("currentfile /ASCIIHexDecode filter")
452 else:
453 file.write("currentfile /ASCII85Decode filter")
454 if self.compressmode or self.imagecompressed:
455 file.write(" /%sDecode filter" % (self.compressmode or self.imagecompressed))
456 file.write("\n")
458 file.write(">>\n")
460 if alpha:
461 file.write("/MaskDict\n"
462 "<<\n"
463 "/ImageType 1\n"
464 "/Width %i\n" % self.imagewidth)
465 file.write("/Height %i\n" % self.imageheight)
466 file.write("/BitsPerComponent 8\n"
467 "/ImageMatrix %s\n" % pstrafo)
468 file.write("/Decode [1 0]\n"
469 ">>\n"
470 "/InterleaveType 1\n"
471 ">>\n")
473 if self.PSstoreimage:
474 file.write("image\n")
475 else:
476 if self.PSbinexpand == 2:
477 file.write("%%%%BeginData: %i ASCII Lines\n"
478 "image\n" % (asciihexlines(len(data)) + 1))
479 asciihexstream(file, data)
480 file.write(">\n")
481 else:
482 # the datasource is currentstream (plus some filters)
483 file.write("%%%%BeginData: %i ASCII Lines\n"
484 "image\n" % (ascii85lines(len(data)) + 1))
485 ascii85stream(file, data)
486 file.write("~>\n")
487 file.write("%%EndData\n")
489 file.write("grestore\n")
491 def processPDF(self, file, writer, context, registry, bbox):
492 mode, data, alpha, palettemode, palettedata = self.imagedata(False)
494 name = "image-%d-%s" % (id(self.image), self.compressmode or self.imagecompressed)
495 if alpha:
496 alpha = PDFimage("%s-smask" % name, self.imagewidth, self.imageheight,
497 None, None, "L", 8,
498 self.compressmode, alpha, None, registry, addresource=False)
499 registry.add(alpha)
500 registry.add(PDFimage(name, self.imagewidth, self.imageheight,
501 palettemode, palettedata, mode, 8,
502 self.compressmode or self.imagecompressed, data, alpha, registry))
504 bbox += self.bbox()
506 file.write("q\n")
507 self.pdftrafo.processPDF(file, writer, context, registry)
508 file.write("/%s Do\n" % name)
509 file.write("Q\n")
512 class bitmap_pt(bitmap_trafo):
514 def __init__(self, xpos_pt, ypos_pt, image, width_pt=None, height_pt=None, ratio=None, **kwargs):
515 imagewidth, imageheight = image.size
516 if width_pt is not None or height_pt is not None:
517 if width_pt is None:
518 if ratio is None:
519 width_pt = height_pt * imagewidth / float(imageheight)
520 else:
521 width_pt = ratio * height_pt
522 elif height_pt is None:
523 if ratio is None:
524 height_pt = width_pt * imageheight / float(imagewidth)
525 else:
526 height_pt = (1.0/ratio) * width_pt
527 elif ratio is not None:
528 raise ValueError("can't specify a ratio when setting width_pt and height_pt")
529 else:
530 if ratio is not None:
531 raise ValueError("must specify width_pt or height_pt to set a ratio")
532 widthdpi, heightdpi = image.info["dpi"] # fails when no dpi information available
533 width_pt = 72.0 * imagewidth / float(widthdpi)
534 height_pt = 72.0 * imageheight / float(heightdpi)
536 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)
539 class bitmap(bitmap_pt):
541 def __init__(self, xpos, ypos, image, width=None, height=None, **kwargs):
542 xpos_pt = unit.topt(xpos)
543 ypos_pt = unit.topt(ypos)
544 if width is not None:
545 width_pt = unit.topt(width)
546 else:
547 width_pt = None
548 if height is not None:
549 height_pt = unit.topt(height)
550 else:
551 height_pt = None
553 bitmap_pt.__init__(self, xpos_pt, ypos_pt, image, width_pt=width_pt, height_pt=height_pt, **kwargs)