bitmap module
[PyX/mjg.git] / pyx / bitmap.py
blob0617c703472f56345fffff15aaca9e8c64c46155
1 #!/usr/bin/env python
2 # -*- coding: ISO-8859-1 -*-
5 # Copyright (C) 2004 André Wobst <wobsta@users.sourceforge.net>
7 # This file adds bitmap functionality to PyX. Its designed for usage
8 # with PyX 0.6.x and will, in a similar form, become part of future
9 # PyX releases.
12 import cStringIO, struct, sys
13 try:
14 import zlib
15 haszlib = 1
16 except:
17 haszlib = 0
19 import base, bbox, prolog, trafo, unit
21 def ascii85stream(file, data):
22 l = 0
23 for i in range(len(data)):
24 c = data[i]
25 l = l*256 + ord(c)
26 if i%4 == 3:
27 if l:
28 l, c5 = divmod(l, 85)
29 l, c4 = divmod(l, 85)
30 l, c3 = divmod(l, 85)
31 c1, c2 = divmod(l, 85)
32 file.write(struct.pack('BBBBB', c1+33, c2+33, c3+33, c4+33, c5+33))
33 l = 0
34 else:
35 file.write("z")
36 if i%64 == 63:
37 file.write("\n")
38 if i%4 != 3:
39 for x in range(3-(i%4)):
40 l *= 256
41 l, c5 = divmod(l, 85)
42 l, c4 = divmod(l, 85)
43 l, c3 = divmod(l, 85)
44 c1, c2 = divmod(l, 85)
45 file.write(struct.pack('BBBB', c1+33, c2+33, c3+33, c4+33)[:(i%4)+2])
48 class image:
50 def __init__(self, width, height, mode, data, compressed=None):
51 if width <= 0 or height <= 0:
52 raise ValueError("valid image size")
53 if mode not in ["L", "RGB", "CMYK"]:
54 raise ValueError("invalid mode")
55 if compressed is None and len(mode)*width*height != len(data):
56 raise ValueError("wrong size of uncompressed data")
57 self.size = width, height
58 self.mode = mode
59 self.data = data
60 self.compressed = compressed
62 def tostring(self, *args):
63 if len(args):
64 raise RuntimeError("encoding not supported in this implementation")
65 return self.data
67 def convert(self, model):
68 raise RuntimeError("color model conversion not supported in this implementation")
71 class jpegimage(image):
73 def __init__(self, file):
74 try:
75 data = file.read()
76 except:
77 data = open(file, "rb").read()
78 pos = 0
79 nestinglevel = 0
80 try:
81 while 1:
82 if data[pos] == "\377" and data[pos+1] not in ["\0", "\377"]:
83 # print "marker: 0x%02x \\%03o" % (ord(data[pos+1]), ord(data[pos+1]))
84 if data[pos+1] == "\330":
85 if not nestinglevel:
86 begin = pos
87 nestinglevel += 1
88 elif not nestinglevel:
89 raise ValueError("begin marker expected")
90 elif data[pos+1] == "\331":
91 nestinglevel -= 1
92 if not nestinglevel:
93 end = pos + 2
94 break
95 elif data[pos+1] in ["\300", "\301"]:
96 l, bits, height, width, components = struct.unpack(">HBHHB", data[pos+2:pos+10])
97 if bits != 8:
98 raise ValueError("implementation limited to 8 bit per component only")
99 try:
100 mode = {1: "L", 3: "RGB", 4: "CMYK"}[components]
101 except KeyError:
102 raise ValueError("invalid number of components")
103 pos += l+1
104 elif data[pos+1] == "\340":
105 l, id, major, minor, dpikind, xdpi, ydpi = struct.unpack(">H5sBBBHH", data[pos+2:pos+16])
106 if dpikind == 1:
107 self.info = {"dpi": (xdpi, ydpi)}
108 elif dpikind == 2:
109 self.info = {"dpi": (xdpi*2.54, ydpi*2.45)}
110 # else do not provide dpi information
111 pos += l+1
112 pos += 1
113 except IndexError:
114 raise ValueError("end marker expected")
115 image.__init__(self, width, height, mode, data[begin:end], compressed="DCT")
118 class bitmap(base.PSCmd):
120 def __init__(self, xpos, ypos, image,
121 width=None, height=None, ratio=None,
122 storedata=0, maxstrlen=4093,
123 compressmode="Flate",
124 flatecompresslevel=6,
125 dctquality=75, dctoptimize=0, dctprogression=0):
126 self.xpos = xpos
127 self.ypos = ypos
128 self.imagewidth, self.imageheight = image.size
129 self.storedata = storedata
130 self.maxstrlen = maxstrlen
131 self.imagedataid = "imagedata%d" % id(self)
132 self.prologs = []
134 if width is not None or height is not None:
135 self.width = width
136 self.height = height
137 if self.width is None:
138 if ratio is None:
139 self.width = self.height * self.imagewidth / float(self.imageheight)
140 else:
141 self.width = ratio * self.height
142 elif self.height is None:
143 if ratio is None:
144 self.height = self.width * self.imageheight / float(self.imagewidth)
145 else:
146 self.height = (1.0/ratio) * self.width
147 elif ratio is not None:
148 raise ValueError("can't specify a ratio when setting width and height")
149 else:
150 if ratio is not None:
151 raise ValueError("must specify width or height to set a ratio")
152 widthdpi, heightdpi = image.info["dpi"] # XXX fails when no dpi information available
153 self.width = unit.inch(self.imagewidth / float(widthdpi))
154 self.height = unit.inch(self.imageheight / float(heightdpi))
156 self.xpos_pt = unit.topt(self.xpos)
157 self.ypos_pt = unit.topt(self.ypos)
158 self.width_pt = unit.topt(self.width)
159 self.height_pt = unit.topt(self.height)
161 # create decode and colorspace
162 self.palettedata = None
163 if image.mode == "P":
164 palettemode, self.palettedata = image.palette.getdata()
165 self.decode = "[0 255]"
166 # palettedata and closing ']' is inserted in outputPS
167 if palettemode == "L":
168 self.colorspace = "[ /Indexed /DeviceGray %i" % (len(self.palettedata)/1-1)
169 elif palettemode == "RGB":
170 self.colorspace = "[ /Indexed /DeviceRGB %i" % (len(self.palettedata)/3-1)
171 elif palettemode == "CMYK":
172 self.colorspace = "[ /Indexed /DeviceCMYK %i" % (len(self.palettedata)/4-1)
173 else:
174 image = image.convert("RGB")
175 self.decode = "[0 1 0 1 0 1]"
176 self.colorspace = "/DeviceRGB"
177 self.palettedata = None
178 sys.stderr.write("*** PyX Info: image with unknown palette mode converted to rgb image\n")
179 elif len(image.mode) == 1:
180 if image.mode != "L":
181 image = image.convert("L")
182 sys.stderr.write("*** PyX Info: specific single channel image mode not natively supported, converted to regular grayscale\n")
183 self.decode = "[0 1]"
184 self.colorspace = "/DeviceGray"
185 elif image.mode == "CMYK":
186 self.decode = "[0 1 0 1 0 1 0 1]"
187 self.colorspace = "/DeviceCMYK"
188 else:
189 if image.mode != "RGB":
190 image = image.convert("RGB")
191 sys.stderr.write("*** PyX Info: image with unknown mode converted to rgb\n")
192 self.decode = "[0 1 0 1 0 1]"
193 self.colorspace = "/DeviceRGB"
195 # create imagematrix
196 self.imagematrix = str(trafo.mirror(0)
197 .translated_pt(-self.xpos_pt, self.ypos_pt+self.height_pt)
198 .scaled_pt(self.imagewidth/self.width_pt, self.imageheight/self.height_pt))
200 # savely check whether imagedata is compressed or not
201 try:
202 imagecompressed = image.compressed
203 except:
204 imagecompressed = None
205 if compressmode != None and imagecompressed != None:
206 raise ValueError("compression of a compressed image not supported")
207 if not haszlib and compressmode == "Flate":
208 sys.stderr.write("*** PyX Info: zlib module not available, disable compression")
209 compressmode == None
211 # create data
212 if compressmode == "Flate":
213 self.data = zlib.compress(image.tostring(), flatecompresslevel)
214 elif compressmode == "DCT":
215 self.data = image.tostring("jpeg", image.mode,
216 dctquality, dctoptimize, dctprogression)
217 else:
218 self.data = image.tostring()
219 self.singlestring = self.storedata and len(self.data) < self.maxstrlen
221 # create datasource
222 if self.storedata:
223 if self.singlestring:
224 self.datasource = "/%s load" % self.imagedataid
225 else:
226 self.datasource = "/imagedataaccess load" # some printers do not allow for inline code here
227 self.prologs.append(prolog.definition("imagedataaccess",
228 "{ /imagedataindex load " # get list index
229 "dup 1 add /imagedataindex exch store " # store increased index
230 "/imagedataid load exch get }")) # select string from array
231 else:
232 self.datasource = "currentfile /ASCII85Decode filter"
233 if compressmode == "Flate" or imagecompressed == "Flate":
234 self.datasource += " /FlateDecode filter"
235 elif compressmode == "DCT" or imagecompressed == "DCT":
236 self.datasource += " /DCTDecode filter"
237 else:
238 if compressmode != None:
239 raise ValueError("invalid compressmode '%s'" % compressmode)
240 if imagecompressed != None:
241 raise ValueError("invalid compressed image '%s'" % imagecompressed)
243 # cache prolog
244 if self.storedata:
245 # TODO resource data could be written directly on the output stream
246 # after proper code reorganization
247 buffer = cStringIO.StringIO()
248 if self.singlestring:
249 buffer.write("<~")
250 ascii85stream(buffer, self.data)
251 buffer.write("~>")
252 else:
253 buffer.write("[ ")
254 datalen = len(self.data)
255 tailpos = datalen - datalen % self.maxstrlen
256 for i in xrange(0, tailpos, self.maxstrlen):
257 buffer.write("<~")
258 ascii85stream(buffer, self.data[i: i+self.maxstrlen])
259 buffer.write("~>\n")
260 if datalen != tailpos:
261 buffer.write("<~")
262 ascii85stream(buffer, self.data[tailpos:])
263 buffer.write("~> ]")
264 else:
265 buffer.write("]")
266 self.prologs.append(prolog.definition(self.imagedataid, buffer.getvalue()))
268 def bbox(self):
269 return bbox.bbox_pt(self.xpos_pt, self.ypos_pt,
270 self.xpos_pt+self.width_pt, self.ypos_pt+self.height_pt)
272 def prolog(self):
273 return self.prologs
275 def outputPS(self, file):
276 file.write("gsave\n"
277 "%s" % self.colorspace)
278 if self.palettedata is not None:
279 # insert palette data
280 file.write("<~")
281 ascii85stream(file, self.palettedata)
282 file.write("~> ]")
283 file.write(" setcolorspace\n")
285 if self.storedata and not self.singlestring:
286 file.write("/imagedataindex 0 store\n" # not use the stack since interpreters differ in their stack usage
287 "/imagedataid %s store\n" % self.imagedataid)
289 file.write("<<\n"
290 "/ImageType 1\n"
291 "/Width %i\n" # imagewidth
292 "/Height %i\n" # imageheight
293 "/BitsPerComponent 8\n"
294 "/ImageMatrix %s\n" # imagematrix
295 "/Decode %s\n" # decode
296 "/DataSource %s\n" # datasource
297 ">>\n"
298 "image\n" % (self.imagewidth, self.imageheight,
299 self.imagematrix, self.decode, self.datasource))
300 if not self.storedata:
301 ascii85stream(file, self.data)
302 file.write("~>\n")
304 file.write("grestore\n")