improve the sorting of controldists a bit
[PyX/mjg.git] / pyx / bitmap.py
blobcd7cf5c0eeca871383d24d9ad8f542f826af5e6d
1 #!/usr/bin/env python
2 # -*- coding: ISO-8859-1 -*-
5 # Copyright (C) 2004-2005 André Wobst <wobsta@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
24 try:
25 import zlib
26 haszlib = 1
27 except:
28 haszlib = 0
30 import bbox, canvas, pswriter, pdfwriter, trafo, unit
32 def ascii85lines(datalen):
33 if datalen < 4:
34 return 1
35 return (datalen + 56)/60
37 def ascii85stream(file, data):
38 """Encodes the string data in ASCII85 and writes it to
39 the stream file. The number of lines written to the stream
40 is known just from the length of the data by means of the
41 ascii85lines function. Note that the tailing newline character
42 of the last line is not added by this function, but it is taken
43 into account in the ascii85lines function."""
44 i = 3 # go on smoothly in case of data length equals zero
45 l = 0
46 l = [None, None, None, None]
47 for i in range(len(data)):
48 c = data[i]
49 l[i%4] = ord(c)
50 if i%4 == 3:
51 if i%60 == 3 and i != 3:
52 file.write("\n")
53 if l:
54 # instead of
55 # l[3], c5 = divmod(256*256*256*l[0]+256*256*l[1]+256*l[2]+l[3], 85)
56 # l[2], c4 = divmod(l[3], 85)
57 # we have to avoid number > 2**31 by
58 l[3], c5 = divmod(256*256*l[0]+256*256*l[1]+256*l[2]+l[3], 85)
59 l[2], c4 = divmod(256*256*3*l[0]+l[3], 85)
60 l[1], c3 = divmod(l[2], 85)
61 c1 , c2 = divmod(l[1], 85)
62 file.write(struct.pack('BBBBB', c1+33, c2+33, c3+33, c4+33, c5+33))
63 else:
64 file.write("z")
65 if i%4 != 3:
66 for j in range((i%4) + 1, 4):
67 l[j] = 0
68 l[3], c5 = divmod(256*256*l[0]+256*256*l[1]+256*l[2]+l[3], 85)
69 l[2], c4 = divmod(256*256*3*l[0]+l[3], 85)
70 l[1], c3 = divmod(l[2], 85)
71 c1 , c2 = divmod(l[1], 85)
72 file.write(struct.pack('BBBB', c1+33, c2+33, c3+33, c4+33)[:(i%4)+2])
75 class image:
77 def __init__(self, width, height, mode, data, compressed=None):
78 if width <= 0 or height <= 0:
79 raise ValueError("valid image size")
80 if mode not in ["L", "RGB", "CMYK"]:
81 raise ValueError("invalid mode")
82 if compressed is None and len(mode)*width*height != len(data):
83 raise ValueError("wrong size of uncompressed data")
84 self.size = width, height
85 self.mode = mode
86 self.data = data
87 self.compressed = compressed
89 def tostring(self, *args):
90 if len(args):
91 raise RuntimeError("encoding not supported in this implementation")
92 return self.data
94 def convert(self, model):
95 raise RuntimeError("color model conversion not supported in this implementation")
98 class jpegimage(image):
100 def __init__(self, file):
101 try:
102 data = file.read()
103 except:
104 data = open(file, "rb").read()
105 pos = 0
106 nestinglevel = 0
107 try:
108 while 1:
109 if data[pos] == "\377" and data[pos+1] not in ["\0", "\377"]:
110 # print "marker: 0x%02x \\%03o" % (ord(data[pos+1]), ord(data[pos+1]))
111 if data[pos+1] == "\330":
112 if not nestinglevel:
113 begin = pos
114 nestinglevel += 1
115 elif not nestinglevel:
116 raise ValueError("begin marker expected")
117 elif data[pos+1] == "\331":
118 nestinglevel -= 1
119 if not nestinglevel:
120 end = pos + 2
121 break
122 elif data[pos+1] in ["\300", "\301"]:
123 l, bits, height, width, components = struct.unpack(">HBHHB", data[pos+2:pos+10])
124 if bits != 8:
125 raise ValueError("implementation limited to 8 bit per component only")
126 try:
127 mode = {1: "L", 3: "RGB", 4: "CMYK"}[components]
128 except KeyError:
129 raise ValueError("invalid number of components")
130 pos += l+1
131 elif data[pos+1] == "\340":
132 l, id, major, minor, dpikind, xdpi, ydpi = struct.unpack(">H5sBBBHH", data[pos+2:pos+16])
133 if dpikind == 1:
134 self.info = {"dpi": (xdpi, ydpi)}
135 elif dpikind == 2:
136 self.info = {"dpi": (xdpi*2.54, ydpi*2.45)}
137 # else do not provide dpi information
138 pos += l+1
139 pos += 1
140 except IndexError:
141 raise ValueError("end marker expected")
142 image.__init__(self, width, height, mode, data[begin:end], compressed="DCT")
145 class PSimagedata(pswriter.PSresource):
147 def __init__(self, name, data, singlestring, maxstrlen):
148 pswriter.PSresource.__init__(self, "imagedata", name)
149 self.data = data
150 self.singlestring = singlestring
151 self.maxstrlen = maxstrlen
153 def output(self, file, writer, registry):
154 file.write("%%%%BeginRessource: %s\n" % self.id)
155 if self.singlestring:
156 file.write("%%%%BeginData: %i ASCII Lines\n"
157 "<~" % ascii85lines(len(self.data)))
158 ascii85stream(file, self.data)
159 file.write("~>\n"
160 "%%EndData\n")
161 else:
162 datalen = len(self.data)
163 tailpos = datalen - datalen % self.maxstrlen
164 file.write("%%%%BeginData: %i ASCII Lines\n" %
165 ((tailpos/self.maxstrlen) * ascii85lines(self.maxstrlen) +
166 ascii85lines(datalen-tailpos)))
167 file.write("[ ")
168 for i in xrange(0, tailpos, self.maxstrlen):
169 file.write("<~")
170 ascii85stream(file, self.data[i: i+self.maxstrlen])
171 file.write("~>\n")
172 if datalen != tailpos:
173 file.write("<~")
174 ascii85stream(file, self.data[tailpos:])
175 file.write("~>")
176 file.write("]\n"
177 "%%EndData\n")
178 file.write("/%s exch def\n" % self.id)
179 file.write("%%EndRessource\n")
182 class PDFimagepalettedata(pdfwriter.PDFobject):
184 def __init__(self, name, data):
185 pdfwriter.PDFobject.__init__(self, "imagepalettedata", name)
186 self.data = data
188 def write(self, file, writer, registry):
189 file.write("<<\n"
190 "/Length %d\n" % len(self.data))
191 file.write(">>\n"
192 "stream\n")
193 file.write(self.data)
194 file.write("\n"
195 "endstream\n")
198 class PDFimage(pdfwriter.PDFobject):
200 def __init__(self, name, width, height, palettecolorspace, palettedata, colorspace,
201 bitspercomponent, compressmode, data, registry):
202 if palettedata is not None:
203 procset = "ImageI"
204 elif colorspace == "/DeviceGray":
205 procset = "ImageB"
206 else:
207 procset = "ImageC"
208 pdfwriter.PDFobject.__init__(self, "image", name)
209 registry.addresource("XObject", name, self, procset=procset)
210 if palettedata is not None:
211 # acrobat wants a palette to be an object
212 self.PDFpalettedata = PDFimagepalettedata(name, palettedata)
213 registry.add(self.PDFpalettedata)
214 self.name = name
215 self.width = width
216 self.height = height
217 self.palettecolorspace = palettecolorspace
218 self.palettedata = palettedata
219 self.colorspace = colorspace
220 self.bitspercomponent = bitspercomponent
221 self.compressmode = compressmode
222 self.data = data
224 def write(self, file, writer, registry):
225 file.write("<<\n"
226 "/Type /XObject\n"
227 "/Subtype /Image\n"
228 "/Width %d\n" % self.width)
229 file.write("/Height %d\n" % self.height)
230 if self.palettedata is not None:
231 file.write("/ColorSpace [ /Indexed %s %i\n" % (self.palettecolorspace, len(self.palettedata)/3-1))
232 file.write("%d 0 R\n" % registry.getrefno(self.PDFpalettedata))
233 file.write("]\n")
234 else:
235 file.write("/ColorSpace %s\n" % self.colorspace)
236 file.write("/BitsPerComponent %d\n" % self.bitspercomponent)
237 file.write("/Length %d\n" % len(self.data))
238 if self.compressmode:
239 file.write("/Filter /%sDecode\n" % self.compressmode)
240 file.write(">>\n"
241 "stream\n")
242 file.write(self.data)
243 file.write("\n"
244 "endstream\n")
247 class bitmap(canvas.canvasitem):
249 def __init__(self, xpos, ypos, image, width=None, height=None, ratio=None,
250 PSstoreimage=0, PSmaxstrlen=4093,
251 compressmode="Flate", flatecompresslevel=6,
252 dctquality=75, dctoptimize=0, dctprogression=0):
253 self.xpos = xpos
254 self.ypos = ypos
255 self.imagewidth, self.imageheight = image.size
256 self.PSstoreimage = PSstoreimage
257 self.PSmaxstrlen = PSmaxstrlen
259 if width is not None or height is not None:
260 self.width = width
261 self.height = height
262 if self.width is None:
263 if ratio is None:
264 self.width = self.height * self.imagewidth / float(self.imageheight)
265 else:
266 self.width = ratio * self.height
267 elif self.height is None:
268 if ratio is None:
269 self.height = self.width * self.imageheight / float(self.imagewidth)
270 else:
271 self.height = (1.0/ratio) * self.width
272 elif ratio is not None:
273 raise ValueError("can't specify a ratio when setting width and height")
274 else:
275 if ratio is not None:
276 raise ValueError("must specify width or height to set a ratio")
277 widthdpi, heightdpi = image.info["dpi"] # fails when no dpi information available
278 self.width = self.imagewidth / float(widthdpi) * unit.t_inch
279 self.height = self.imageheight / float(heightdpi) * unit.t_inch
281 self.xpos_pt = unit.topt(self.xpos)
282 self.ypos_pt = unit.topt(self.ypos)
283 self.width_pt = unit.topt(self.width)
284 self.height_pt = unit.topt(self.height)
286 # create decode and colorspace
287 self.colorspace = self.palettecolorspace = self.palettedata = None
288 if image.mode == "P":
289 palettemode, self.palettedata = image.palette.getdata()
290 self.decode = "[0 255]"
291 try:
292 self.palettecolorspace = {"L": "/DeviceGray",
293 "RGB": "/DeviceRGB",
294 "CMYK": "/DeviceCMYK"}[palettemode]
295 except KeyError:
296 warnings.warn("image with unknown palette mode '%s' converted to rgb image" % palettemode)
297 image = image.convert("RGB")
298 self.decode = "[0 1 0 1 0 1]"
299 self.palettedata = None
300 self.colorspace = "/DeviceRGB"
301 elif len(image.mode) == 1:
302 if image.mode != "L":
303 image = image.convert("L")
304 warnings.warn("specific single channel image mode not natively supported, converted to regular grayscale")
305 self.decode = "[0 1]"
306 self.colorspace = "/DeviceGray"
307 elif image.mode == "CMYK":
308 self.decode = "[0 1 0 1 0 1 0 1]"
309 self.colorspace = "/DeviceCMYK"
310 else:
311 if image.mode != "RGB":
312 image = image.convert("RGB")
313 warnings.warn("image with unknown mode converted to rgb")
314 self.decode = "[0 1 0 1 0 1]"
315 self.colorspace = "/DeviceRGB"
317 # create imagematrix
318 self.imagematrixPS = (trafo.mirror(0)
319 .translated_pt(-self.xpos_pt, self.ypos_pt+self.height_pt)
320 .scaled_pt(self.imagewidth/self.width_pt, self.imageheight/self.height_pt))
321 self.imagematrixPDF = (trafo.scale_pt(self.width_pt, self.height_pt)
322 .translated_pt(self.xpos_pt, self.ypos_pt))
324 # check whether imagedata is compressed or not
325 try:
326 imagecompressed = image.compressed
327 except:
328 imagecompressed = None
329 if compressmode != None and imagecompressed != None:
330 raise ValueError("compression of a compressed image not supported")
331 self.compressmode = compressmode
332 if compressmode is not None and compressmode not in ["Flate", "DCT"]:
333 raise ValueError("invalid compressmode '%s'" % compressmode)
334 if imagecompressed is not None:
335 self.compressmode = imagecompressed
336 if imagecompressed not in ["Flate", "DCT"]:
337 raise ValueError("invalid compressed image '%s'" % imagecompressed)
338 if not haszlib and compressmode == "Flate":
339 warnings.warn("zlib module not available, disable compression")
340 self.compressmode = compressmode = None
342 # create data
343 if compressmode == "Flate":
344 self.data = zlib.compress(image.tostring(), flatecompresslevel)
345 elif compressmode == "DCT":
346 self.data = image.tostring("jpeg", image.mode,
347 dctquality, dctoptimize, dctprogression)
348 else:
349 self.data = image.tostring()
351 self.PSsinglestring = self.PSstoreimage and len(self.data) < self.PSmaxstrlen
352 if self.PSsinglestring:
353 self.PSimagename = "image-%d-%s-singlestring" % (id(image), compressmode)
354 else:
355 self.PSimagename = "image-%d-%s-stringarray" % (id(image), compressmode)
356 self.PDFimagename = "image-%d-%s" % (id(image), compressmode)
358 def bbox(self):
359 return bbox.bbox_pt(self.xpos_pt, self.ypos_pt,
360 self.xpos_pt+self.width_pt, self.ypos_pt+self.height_pt)
362 def processPS(self, file, writer, context, registry, bbox):
363 if self.PSstoreimage and not self.PSsinglestring:
364 registry.add(pswriter.PSdefinition("imagedataaccess",
365 "{ /imagedataindex load " # get list index
366 "dup 1 add /imagedataindex exch store " # store increased index
367 "/imagedataid load exch get }")) # select string from array
368 if self.PSstoreimage:
369 registry.add(PSimagedata(self.PSimagename, self.data, self.PSsinglestring, self.PSmaxstrlen))
370 bbox += self.bbox()
372 file.write("gsave\n")
373 if self.palettedata is not None:
374 file.write("[ /Indexed %s %i\n" % (self.palettecolorspace, len(self.palettedata)/3-1))
375 file.write("%%%%BeginData: %i ASCII Lines\n" % ascii85lines(len(self.palettedata)))
376 file.write("<~")
377 ascii85stream(file, self.palettedata)
378 file.write("~>\n"
379 "%%EndData\n")
380 file.write("] setcolorspace\n")
381 else:
382 file.write("%s setcolorspace\n" % self.colorspace)
384 if self.PSstoreimage and not self.PSsinglestring:
385 file.write("/imagedataindex 0 store\n" # not use the stack since interpreters differ in their stack usage
386 "/imagedataid %s store\n" % self.PSimagename)
388 file.write("<<\n"
389 "/ImageType 1\n"
390 "/Width %i\n" % self.imagewidth)
391 file.write("/Height %i\n" % self.imageheight)
392 file.write("/BitsPerComponent 8\n"
393 "/ImageMatrix %s\n" % self.imagematrixPS)
394 file.write("/Decode %s\n" % self.decode)
396 file.write("/DataSource ")
397 if self.PSstoreimage:
398 if self.PSsinglestring:
399 file.write("/%s load" % self.PSimagename)
400 else:
401 file.write("/imagedataaccess load") # some printers do not allow for inline code here -> we store it in a resource
402 else:
403 file.write("currentfile /ASCII85Decode filter")
404 if self.compressmode:
405 file.write(" /%sDecode filter" % self.compressmode)
406 file.write("\n")
408 file.write(">>\n")
410 if self.PSstoreimage:
411 file.write("image\n")
412 else:
413 # the datasource is currentstream (plus some filters)
414 file.write("%%%%BeginData: %i ASCII Lines\n"
415 "image\n" % (ascii85lines(len(self.data)) + 1))
416 ascii85stream(file, self.data)
417 file.write("~>\n%%EndData\n")
419 file.write("grestore\n")
421 def processPDF(self, file, writer, context, registry, bbox):
422 registry.add(PDFimage(self.PDFimagename, self.imagewidth, self.imageheight,
423 self.palettecolorspace, self.palettedata, self.colorspace,
424 8, self.compressmode, self.data, registry))
425 bbox += self.bbox()
427 file.write("q\n")
428 self.imagematrixPDF.processPDF(file, writer, context, registry, bbox)
429 file.write("/%s Do\n" % self.PDFimagename)
430 file.write("Q\n")