PSCmd and PSOp are now joined in a new class canvasitem
[PyX/mjg.git] / pyx / bitmap.py
blob6e3ea7cb909fbfae7eb119dbff31ce74e8e03c6f
1 #!/usr/bin/env python
2 # -*- coding: ISO-8859-1 -*-
5 # Copyright (C) 2004 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
24 import cStringIO, struct, sys
25 try:
26 import zlib
27 haszlib = 1
28 except:
29 haszlib = 0
31 import base, bbox, prolog, trafo, unit
33 def ascii85lines(datalen):
34 if datalen < 4:
35 return 1
36 return (datalen + 56)/60
38 def ascii85stream(file, data):
39 """Encodes the string data in ASCII85 and writes it to
40 the stream file. The number of lines written to the stream
41 is known just from the length of the data by means of the
42 ascii85lines function. Note that the tailing newline character
43 of the last line is not added by this function, but it is taken
44 into account in the ascii85lines function."""
45 i = 3 # go on smoothly in case of data length equals zero
46 l = 0
47 for i in range(len(data)):
48 c = data[i]
49 l = l*256 + ord(c)
50 if i%4 == 3:
51 if i%60 == 3 and i != 3:
52 file.write("\n")
53 if l:
54 l, c5 = divmod(l, 85)
55 l, c4 = divmod(l, 85)
56 l, c3 = divmod(l, 85)
57 c1, c2 = divmod(l, 85)
58 file.write(struct.pack('BBBBB', c1+33, c2+33, c3+33, c4+33, c5+33))
59 l = 0
60 else:
61 file.write("z")
62 if i%4 != 3:
63 for x in range(3-(i%4)):
64 l *= 256
65 l, c5 = divmod(l, 85)
66 l, c4 = divmod(l, 85)
67 l, c3 = divmod(l, 85)
68 c1, c2 = divmod(l, 85)
69 file.write(struct.pack('BBBB', c1+33, c2+33, c3+33, c4+33)[:(i%4)+2])
72 class image:
74 def __init__(self, width, height, mode, data, compressed=None):
75 if width <= 0 or height <= 0:
76 raise ValueError("valid image size")
77 if mode not in ["L", "RGB", "CMYK"]:
78 raise ValueError("invalid mode")
79 if compressed is None and len(mode)*width*height != len(data):
80 raise ValueError("wrong size of uncompressed data")
81 self.size = width, height
82 self.mode = mode
83 self.data = data
84 self.compressed = compressed
86 def tostring(self, *args):
87 if len(args):
88 raise RuntimeError("encoding not supported in this implementation")
89 return self.data
91 def convert(self, model):
92 raise RuntimeError("color model conversion not supported in this implementation")
95 class jpegimage(image):
97 def __init__(self, file):
98 try:
99 data = file.read()
100 except:
101 data = open(file, "rb").read()
102 pos = 0
103 nestinglevel = 0
104 try:
105 while 1:
106 if data[pos] == "\377" and data[pos+1] not in ["\0", "\377"]:
107 # print "marker: 0x%02x \\%03o" % (ord(data[pos+1]), ord(data[pos+1]))
108 if data[pos+1] == "\330":
109 if not nestinglevel:
110 begin = pos
111 nestinglevel += 1
112 elif not nestinglevel:
113 raise ValueError("begin marker expected")
114 elif data[pos+1] == "\331":
115 nestinglevel -= 1
116 if not nestinglevel:
117 end = pos + 2
118 break
119 elif data[pos+1] in ["\300", "\301"]:
120 l, bits, height, width, components = struct.unpack(">HBHHB", data[pos+2:pos+10])
121 if bits != 8:
122 raise ValueError("implementation limited to 8 bit per component only")
123 try:
124 mode = {1: "L", 3: "RGB", 4: "CMYK"}[components]
125 except KeyError:
126 raise ValueError("invalid number of components")
127 pos += l+1
128 elif data[pos+1] == "\340":
129 l, id, major, minor, dpikind, xdpi, ydpi = struct.unpack(">H5sBBBHH", data[pos+2:pos+16])
130 if dpikind == 1:
131 self.info = {"dpi": (xdpi, ydpi)}
132 elif dpikind == 2:
133 self.info = {"dpi": (xdpi*2.54, ydpi*2.45)}
134 # else do not provide dpi information
135 pos += l+1
136 pos += 1
137 except IndexError:
138 raise ValueError("end marker expected")
139 image.__init__(self, width, height, mode, data[begin:end], compressed="DCT")
142 class bitmap(base.canvasitem):
144 def __init__(self, xpos, ypos, image,
145 width=None, height=None, ratio=None,
146 storedata=0, maxstrlen=4093,
147 compressmode="Flate",
148 flatecompresslevel=6,
149 dctquality=75, dctoptimize=0, dctprogression=0):
150 self.xpos = xpos
151 self.ypos = ypos
152 self.imagewidth, self.imageheight = image.size
153 self.storedata = storedata
154 self.maxstrlen = maxstrlen
155 self.imagedataid = "imagedata%d" % id(self)
156 self.prologs = []
158 if width is not None or height is not None:
159 self.width = width
160 self.height = height
161 if self.width is None:
162 if ratio is None:
163 self.width = self.height * self.imagewidth / float(self.imageheight)
164 else:
165 self.width = ratio * self.height
166 elif self.height is None:
167 if ratio is None:
168 self.height = self.width * self.imageheight / float(self.imagewidth)
169 else:
170 self.height = (1.0/ratio) * self.width
171 elif ratio is not None:
172 raise ValueError("can't specify a ratio when setting width and height")
173 else:
174 if ratio is not None:
175 raise ValueError("must specify width or height to set a ratio")
176 widthdpi, heightdpi = image.info["dpi"] # XXX fails when no dpi information available
177 self.width = unit.inch(self.imagewidth / float(widthdpi))
178 self.height = unit.inch(self.imageheight / float(heightdpi))
180 self.xpos_pt = unit.topt(self.xpos)
181 self.ypos_pt = unit.topt(self.ypos)
182 self.width_pt = unit.topt(self.width)
183 self.height_pt = unit.topt(self.height)
185 # create decode and colorspace
186 self.palettedata = None
187 if image.mode == "P":
188 palettemode, self.palettedata = image.palette.getdata()
189 self.decode = "[0 255]"
190 # palettedata and closing ']' is inserted in outputPS
191 if palettemode == "L":
192 self.colorspace = "[ /Indexed /DeviceGray %i" % (len(self.palettedata)/1-1)
193 elif palettemode == "RGB":
194 self.colorspace = "[ /Indexed /DeviceRGB %i" % (len(self.palettedata)/3-1)
195 elif palettemode == "CMYK":
196 self.colorspace = "[ /Indexed /DeviceCMYK %i" % (len(self.palettedata)/4-1)
197 else:
198 image = image.convert("RGB")
199 self.decode = "[0 1 0 1 0 1]"
200 self.colorspace = "/DeviceRGB"
201 self.palettedata = None
202 sys.stderr.write("*** PyX Info: image with unknown palette mode converted to rgb image\n")
203 elif len(image.mode) == 1:
204 if image.mode != "L":
205 image = image.convert("L")
206 sys.stderr.write("*** PyX Info: specific single channel image mode not natively supported, converted to regular grayscale\n")
207 self.decode = "[0 1]"
208 self.colorspace = "/DeviceGray"
209 elif image.mode == "CMYK":
210 self.decode = "[0 1 0 1 0 1 0 1]"
211 self.colorspace = "/DeviceCMYK"
212 else:
213 if image.mode != "RGB":
214 image = image.convert("RGB")
215 sys.stderr.write("*** PyX Info: image with unknown mode converted to rgb\n")
216 self.decode = "[0 1 0 1 0 1]"
217 self.colorspace = "/DeviceRGB"
219 # create imagematrix
220 self.imagematrix = str(trafo.mirror(0)
221 .translated_pt(-self.xpos_pt, self.ypos_pt+self.height_pt)
222 .scaled_pt(self.imagewidth/self.width_pt, self.imageheight/self.height_pt))
224 # savely check whether imagedata is compressed or not
225 try:
226 imagecompressed = image.compressed
227 except:
228 imagecompressed = None
229 if compressmode != None and imagecompressed != None:
230 raise ValueError("compression of a compressed image not supported")
231 if not haszlib and compressmode == "Flate":
232 sys.stderr.write("*** PyX Info: zlib module not available, disable compression")
233 compressmode == None
235 # create data
236 if compressmode == "Flate":
237 self.data = zlib.compress(image.tostring(), flatecompresslevel)
238 elif compressmode == "DCT":
239 self.data = image.tostring("jpeg", image.mode,
240 dctquality, dctoptimize, dctprogression)
241 else:
242 self.data = image.tostring()
243 self.singlestring = self.storedata and len(self.data) < self.maxstrlen
245 # create datasource
246 if self.storedata:
247 if self.singlestring:
248 self.datasource = "/%s load" % self.imagedataid
249 else:
250 self.datasource = "/imagedataaccess load" # some printers do not allow for inline code here
251 self.prologs.append(prolog.definition("imagedataaccess",
252 "{ /imagedataindex load " # get list index
253 "dup 1 add /imagedataindex exch store " # store increased index
254 "/imagedataid load exch get }")) # select string from array
255 else:
256 self.datasource = "currentfile /ASCII85Decode filter"
257 if compressmode == "Flate" or imagecompressed == "Flate":
258 self.datasource += " /FlateDecode filter"
259 elif compressmode == "DCT" or imagecompressed == "DCT":
260 self.datasource += " /DCTDecode filter"
261 else:
262 if compressmode != None:
263 raise ValueError("invalid compressmode '%s'" % compressmode)
264 if imagecompressed != None:
265 raise ValueError("invalid compressed image '%s'" % imagecompressed)
267 # cache prolog
268 if self.storedata:
269 # TODO resource data could be written directly on the output stream
270 # after proper code reorganization
271 buffer = cStringIO.StringIO()
272 if self.singlestring:
273 buffer.write("%%%%BeginData: %i ASCII Lines\n"
274 "<~" % ascii85lines(len(self.data)))
275 ascii85stream(buffer, self.data)
276 buffer.write("~>\n%%EndData\n")
277 else:
278 datalen = len(self.data)
279 tailpos = datalen - datalen % self.maxstrlen
280 buffer.write("%%%%BeginData: %i ASCII Lines\n" %
281 ((tailpos/self.maxstrlen) * ascii85lines(self.maxstrlen) + ascii85lines(datalen-tailpos)))
282 buffer.write("[ ")
283 for i in xrange(0, tailpos, self.maxstrlen):
284 buffer.write("<~")
285 ascii85stream(buffer, self.data[i: i+self.maxstrlen])
286 buffer.write("~>\n")
287 if datalen != tailpos:
288 buffer.write("<~")
289 ascii85stream(buffer, self.data[tailpos:])
290 buffer.write("~>")
291 buffer.write("]\n%%EndData\n")
292 self.prologs.append(prolog.definition(self.imagedataid, buffer.getvalue()))
294 def bbox(self):
295 return bbox.bbox_pt(self.xpos_pt, self.ypos_pt,
296 self.xpos_pt+self.width_pt, self.ypos_pt+self.height_pt)
298 def prolog(self):
299 return self.prologs
301 def outputPS(self, file):
302 file.write("gsave\n"
303 "%s" % self.colorspace)
304 if self.palettedata is not None:
305 # insert palette data
306 file.write("<~")
307 ascii85stream(file, self.palettedata)
308 file.write("~> ]")
309 file.write(" setcolorspace\n")
311 if self.storedata and not self.singlestring:
312 file.write("/imagedataindex 0 store\n" # not use the stack since interpreters differ in their stack usage
313 "/imagedataid %s store\n" % self.imagedataid)
315 file.write("<<\n"
316 "/ImageType 1\n"
317 "/Width %i\n" # imagewidth
318 "/Height %i\n" # imageheight
319 "/BitsPerComponent 8\n"
320 "/ImageMatrix %s\n" # imagematrix
321 "/Decode %s\n" # decode
322 "/DataSource %s\n" # datasource
323 ">>\n" % (self.imagewidth, self.imageheight,
324 self.imagematrix, self.decode, self.datasource))
325 if self.storedata:
326 file.write("image\n")
327 else:
328 # the datasource is currentstream (plus some filters)
329 file.write("%%%%BeginData: %i ASCII Lines\n"
330 "image\n" % (ascii85lines(len(self.data)) + 1))
331 ascii85stream(file, self.data)
332 file.write("~>\n%%EndData\n")
334 file.write("grestore\n")