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