make the input type used in pipeGS selectable
[PyX/mjg.git] / pyx / bitmap.py
blobae522061e30e889233c3d7bd93f55bf4028a4e1c
1 # -*- coding: ISO-8859-1 -*-
4 # Copyright (C) 2004-2006 André Wobst <wobsta@users.sourceforge.net>
6 # This file is part of PyX (http://pyx.sourceforge.net/).
8 # PyX is free software; you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation; either version 2 of the License, or
11 # (at your option) any later version.
13 # PyX is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
18 # You should have received a copy of the GNU General Public License
19 # along with PyX; if not, write to the Free Software
20 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
22 import struct, warnings, binascii
23 try:
24 import zlib
25 haszlib = 1
26 except:
27 haszlib = 0
29 import bbox, canvasitem, pswriter, pdfwriter, trafo, unit
31 def ascii85lines(datalen):
32 if datalen < 4:
33 return 1
34 return (datalen + 56)/60
36 def ascii85stream(file, data):
37 """Encodes the string data in ASCII85 and writes it to
38 the stream file. The number of lines written to the stream
39 is known just from the length of the data by means of the
40 ascii85lines function. Note that the tailing newline character
41 of the last line is not added by this function, but it is taken
42 into account in the ascii85lines function."""
43 i = 3 # go on smoothly in case of data length equals zero
44 l = 0
45 l = [None, None, None, None]
46 for i in range(len(data)):
47 c = data[i]
48 l[i%4] = ord(c)
49 if i%4 == 3:
50 if i%60 == 3 and i != 3:
51 file.write("\n")
52 if l:
53 # instead of
54 # l[3], c5 = divmod(256*256*256*l[0]+256*256*l[1]+256*l[2]+l[3], 85)
55 # l[2], c4 = divmod(l[3], 85)
56 # we have to avoid number > 2**31 by
57 l[3], c5 = divmod(256*256*l[0]+256*256*l[1]+256*l[2]+l[3], 85)
58 l[2], c4 = divmod(256*256*3*l[0]+l[3], 85)
59 l[1], c3 = divmod(l[2], 85)
60 c1 , c2 = divmod(l[1], 85)
61 file.write(struct.pack('BBBBB', c1+33, c2+33, c3+33, c4+33, c5+33))
62 else:
63 file.write("z")
64 if i%4 != 3:
65 for j in range((i%4) + 1, 4):
66 l[j] = 0
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(struct.pack('BBBB', c1+33, c2+33, c3+33, c4+33)[:(i%4)+2])
73 _asciihexlinelength = 64
74 def asciihexlines(datalen):
75 return (datalen*2 + _asciihexlinelength - 1) / _asciihexlinelength
77 def asciihexstream(file, data):
78 hexdata = binascii.b2a_hex(data)
79 for i in range((len(hexdata)-1)/_asciihexlinelength + 1):
80 file.write(hexdata[i*_asciihexlinelength: i*_asciihexlinelength+_asciihexlinelength])
81 file.write("\n")
84 class image:
86 def __init__(self, width, height, mode, data, compressed=None):
87 if width <= 0 or height <= 0:
88 raise ValueError("valid image size")
89 if mode not in ["L", "RGB", "CMYK"]:
90 raise ValueError("invalid mode")
91 if compressed is None and len(mode)*width*height != len(data):
92 raise ValueError("wrong size of uncompressed data")
93 self.size = width, height
94 self.mode = mode
95 self.data = data
96 self.compressed = compressed
98 def tostring(self, *args):
99 if len(args):
100 raise RuntimeError("encoding not supported in this implementation")
101 return self.data
103 def convert(self, model):
104 raise RuntimeError("color model conversion not supported in this implementation")
107 class jpegimage(image):
109 def __init__(self, file):
110 try:
111 data = file.read()
112 except:
113 data = open(file, "rb").read()
114 pos = 0
115 nestinglevel = 0
116 try:
117 while 1:
118 if data[pos] == "\377" and data[pos+1] not in ["\0", "\377"]:
119 # print "marker: 0x%02x \\%03o" % (ord(data[pos+1]), ord(data[pos+1]))
120 if data[pos+1] == "\330":
121 if not nestinglevel:
122 begin = pos
123 nestinglevel += 1
124 elif not nestinglevel:
125 raise ValueError("begin marker expected")
126 elif data[pos+1] == "\331":
127 nestinglevel -= 1
128 if not nestinglevel:
129 end = pos + 2
130 break
131 elif data[pos+1] in ["\300", "\301"]:
132 l, bits, height, width, components = struct.unpack(">HBHHB", data[pos+2:pos+10])
133 if bits != 8:
134 raise ValueError("implementation limited to 8 bit per component only")
135 try:
136 mode = {1: "L", 3: "RGB", 4: "CMYK"}[components]
137 except KeyError:
138 raise ValueError("invalid number of components")
139 pos += l+1
140 elif data[pos+1] == "\340":
141 l, id, major, minor, dpikind, xdpi, ydpi = struct.unpack(">H5sBBBHH", data[pos+2:pos+16])
142 if dpikind == 1:
143 self.info = {"dpi": (xdpi, ydpi)}
144 elif dpikind == 2:
145 self.info = {"dpi": (xdpi*2.54, ydpi*2.45)}
146 # else do not provide dpi information
147 pos += l+1
148 pos += 1
149 except IndexError:
150 raise ValueError("end marker expected")
151 image.__init__(self, width, height, mode, data[begin:end], compressed="DCT")
154 class PSimagedata(pswriter.PSresource):
156 def __init__(self, name, data, singlestring, maxstrlen):
157 pswriter.PSresource.__init__(self, "imagedata", name)
158 self.data = data
159 self.singlestring = singlestring
160 self.maxstrlen = maxstrlen
162 def output(self, file, writer, registry):
163 file.write("%%%%BeginRessource: %s\n" % self.id)
164 if self.singlestring:
165 file.write("%%%%BeginData: %i ASCII Lines\n"
166 "<~" % ascii85lines(len(self.data)))
167 ascii85stream(file, self.data)
168 file.write("~>\n"
169 "%%EndData\n")
170 else:
171 datalen = len(self.data)
172 tailpos = datalen - datalen % self.maxstrlen
173 file.write("%%%%BeginData: %i ASCII Lines\n" %
174 ((tailpos/self.maxstrlen) * ascii85lines(self.maxstrlen) +
175 ascii85lines(datalen-tailpos)))
176 file.write("[ ")
177 for i in xrange(0, tailpos, self.maxstrlen):
178 file.write("<~")
179 ascii85stream(file, self.data[i: i+self.maxstrlen])
180 file.write("~>\n")
181 if datalen != tailpos:
182 file.write("<~")
183 ascii85stream(file, self.data[tailpos:])
184 file.write("~>")
185 file.write("]\n"
186 "%%EndData\n")
187 file.write("/%s exch def\n" % self.id)
188 file.write("%%EndRessource\n")
191 class PDFimagepalettedata(pdfwriter.PDFobject):
193 def __init__(self, name, data):
194 pdfwriter.PDFobject.__init__(self, "imagepalettedata", name)
195 self.data = data
197 def write(self, file, writer, registry):
198 file.write("<<\n"
199 "/Length %d\n" % len(self.data))
200 file.write(">>\n"
201 "stream\n")
202 file.write(self.data)
203 file.write("\n"
204 "endstream\n")
207 class PDFimage(pdfwriter.PDFobject):
209 def __init__(self, name, width, height, palettecolorspace, palettedata, colorspace,
210 bitspercomponent, compressmode, data, registry):
211 if palettedata is not None:
212 procset = "ImageI"
213 elif colorspace == "/DeviceGray":
214 procset = "ImageB"
215 else:
216 procset = "ImageC"
217 pdfwriter.PDFobject.__init__(self, "image", name)
218 registry.addresource("XObject", name, self, procset=procset)
219 if palettedata is not None:
220 # acrobat wants a palette to be an object
221 self.PDFpalettedata = PDFimagepalettedata(name, palettedata)
222 registry.add(self.PDFpalettedata)
223 self.name = name
224 self.width = width
225 self.height = height
226 self.palettecolorspace = palettecolorspace
227 self.palettedata = palettedata
228 self.colorspace = colorspace
229 self.bitspercomponent = bitspercomponent
230 self.compressmode = compressmode
231 self.data = data
233 def write(self, file, writer, registry):
234 file.write("<<\n"
235 "/Type /XObject\n"
236 "/Subtype /Image\n"
237 "/Width %d\n" % self.width)
238 file.write("/Height %d\n" % self.height)
239 if self.palettedata is not None:
240 file.write("/ColorSpace [ /Indexed %s %i\n" % (self.palettecolorspace, len(self.palettedata)/3-1))
241 file.write("%d 0 R\n" % registry.getrefno(self.PDFpalettedata))
242 file.write("]\n")
243 else:
244 file.write("/ColorSpace %s\n" % self.colorspace)
245 file.write("/BitsPerComponent %d\n" % self.bitspercomponent)
246 file.write("/Length %d\n" % len(self.data))
247 if self.compressmode:
248 file.write("/Filter /%sDecode\n" % self.compressmode)
249 file.write(">>\n"
250 "stream\n")
251 file.write(self.data)
252 file.write("\n"
253 "endstream\n")
256 class bitmap_pt(canvasitem.canvasitem):
258 def __init__(self, xpos_pt, ypos_pt, image, width_pt=None, height_pt=None, ratio=None,
259 PSstoreimage=0, PSmaxstrlen=4093, PSbinexpand=1,
260 compressmode="Flate", flatecompresslevel=6,
261 dctquality=75, dctoptimize=0, dctprogression=0):
262 # keep a copy of the image instance to ensure different id's
263 self.image = image
265 self.xpos_pt = xpos_pt
266 self.ypos_pt = ypos_pt
267 self.imagewidth, self.imageheight = image.size
268 self.PSstoreimage = PSstoreimage
269 self.PSmaxstrlen = PSmaxstrlen
270 self.PSbinexpand = PSbinexpand
272 if width_pt is not None or height_pt is not None:
273 self.width_pt = width_pt
274 self.height_pt = height_pt
275 if self.width_pt is None:
276 if ratio is None:
277 self.width_pt = self.height_pt * self.imagewidth / float(self.imageheight)
278 else:
279 self.width_pt = ratio * self.height_pt
280 elif self.height_pt is None:
281 if ratio is None:
282 self.height_pt = self.width_pt * self.imageheight / float(self.imagewidth)
283 else:
284 self.height_pt = (1.0/ratio) * self.width_pt
285 elif ratio is not None:
286 raise ValueError("can't specify a ratio when setting width_pt and height_pt")
287 else:
288 if ratio is not None:
289 raise ValueError("must specify width_pt or height_pt to set a ratio")
290 widthdpi, heightdpi = image.info["dpi"] # fails when no dpi information available
291 self.width_pt = 72.0 * self.imagewidth / float(widthdpi)
292 self.height_pt = 72.0 * self.imageheight / float(heightdpi)
294 # create decode and colorspace
295 self.colorspace = self.palettecolorspace = self.palettedata = None
296 if image.mode == "P":
297 palettemode, self.palettedata = image.palette.getdata()
298 self.decode = "[0 255]"
299 try:
300 self.palettecolorspace = {"L": "/DeviceGray",
301 "RGB": "/DeviceRGB",
302 "CMYK": "/DeviceCMYK"}[palettemode]
303 except KeyError:
304 warnings.warn("image with unknown palette mode '%s' converted to rgb image" % palettemode)
305 image = image.convert("RGB")
306 self.decode = "[0 1 0 1 0 1]"
307 self.palettedata = None
308 self.colorspace = "/DeviceRGB"
309 elif len(image.mode) == 1:
310 if image.mode != "L":
311 image = image.convert("L")
312 warnings.warn("specific single channel image mode not natively supported, converted to regular grayscale")
313 self.decode = "[0 1]"
314 self.colorspace = "/DeviceGray"
315 elif image.mode == "CMYK":
316 self.decode = "[0 1 0 1 0 1 0 1]"
317 self.colorspace = "/DeviceCMYK"
318 else:
319 if image.mode != "RGB":
320 image = image.convert("RGB")
321 warnings.warn("image with unknown mode converted to rgb")
322 self.decode = "[0 1 0 1 0 1]"
323 self.colorspace = "/DeviceRGB"
325 # create imagematrix
326 self.imagematrixPS = (trafo.mirror(0)
327 .translated_pt(-self.xpos_pt, self.ypos_pt+self.height_pt)
328 .scaled_pt(self.imagewidth/self.width_pt, self.imageheight/self.height_pt))
329 self.imagematrixPDF = (trafo.scale_pt(self.width_pt, self.height_pt)
330 .translated_pt(self.xpos_pt, self.ypos_pt))
332 # check whether imagedata is compressed or not
333 try:
334 imagecompressed = image.compressed
335 except:
336 imagecompressed = None
337 if compressmode != None and imagecompressed != None:
338 raise ValueError("compression of a compressed image not supported")
339 self.compressmode = compressmode
340 if compressmode is not None and compressmode not in ["Flate", "DCT"]:
341 raise ValueError("invalid compressmode '%s'" % compressmode)
342 if imagecompressed is not None:
343 self.compressmode = imagecompressed
344 if imagecompressed not in ["Flate", "DCT"]:
345 raise ValueError("invalid compressed image '%s'" % imagecompressed)
346 if not haszlib and compressmode == "Flate":
347 warnings.warn("zlib module not available, disable compression")
348 self.compressmode = compressmode = None
350 # create data
351 if compressmode == "Flate":
352 self.data = zlib.compress(image.tostring(), flatecompresslevel)
353 elif compressmode == "DCT":
354 self.data = image.tostring("jpeg", image.mode,
355 dctquality, dctoptimize, dctprogression)
356 else:
357 self.data = image.tostring()
359 self.PSsinglestring = self.PSstoreimage and len(self.data) < self.PSmaxstrlen
360 if self.PSsinglestring:
361 self.PSimagename = "image-%d-%s-singlestring" % (id(image), compressmode)
362 else:
363 self.PSimagename = "image-%d-%s-stringarray" % (id(image), compressmode)
364 self.PDFimagename = "image-%d-%s" % (id(image), compressmode)
366 def bbox(self):
367 return bbox.bbox_pt(self.xpos_pt, self.ypos_pt,
368 self.xpos_pt+self.width_pt, self.ypos_pt+self.height_pt)
370 def processPS(self, file, writer, context, registry, bbox):
371 if self.PSstoreimage and not self.PSsinglestring:
372 registry.add(pswriter.PSdefinition("imagedataaccess",
373 "{ /imagedataindex load " # get list index
374 "dup 1 add /imagedataindex exch store " # store increased index
375 "/imagedataid load exch get }")) # select string from array
376 if self.PSstoreimage:
377 registry.add(PSimagedata(self.PSimagename, self.data, self.PSsinglestring, self.PSmaxstrlen))
378 bbox += self.bbox()
380 file.write("gsave\n")
381 if self.palettedata is not None:
382 file.write("[ /Indexed %s %i\n" % (self.palettecolorspace, len(self.palettedata)/3-1))
383 file.write("%%%%BeginData: %i ASCII Lines\n" % ascii85lines(len(self.palettedata)))
384 file.write("<~")
385 ascii85stream(file, self.palettedata)
386 file.write("~>\n"
387 "%%EndData\n")
388 file.write("] setcolorspace\n")
389 else:
390 file.write("%s setcolorspace\n" % self.colorspace)
392 if self.PSstoreimage and not self.PSsinglestring:
393 file.write("/imagedataindex 0 store\n" # not use the stack since interpreters differ in their stack usage
394 "/imagedataid %s store\n" % self.PSimagename)
396 file.write("<<\n"
397 "/ImageType 1\n"
398 "/Width %i\n" % self.imagewidth)
399 file.write("/Height %i\n" % self.imageheight)
400 file.write("/BitsPerComponent 8\n"
401 "/ImageMatrix %s\n" % self.imagematrixPS)
402 file.write("/Decode %s\n" % self.decode)
404 file.write("/DataSource ")
405 if self.PSstoreimage:
406 if self.PSsinglestring:
407 file.write("/%s load" % self.PSimagename)
408 else:
409 file.write("/imagedataaccess load") # some printers do not allow for inline code here -> we store it in a resource
410 else:
411 if self.PSbinexpand == 2:
412 file.write("currentfile /ASCIIHexDecode filter")
413 else:
414 file.write("currentfile /ASCII85Decode filter")
415 if self.compressmode:
416 file.write(" /%sDecode filter" % self.compressmode)
417 file.write("\n")
419 file.write(">>\n")
421 if self.PSstoreimage:
422 file.write("image\n")
423 else:
424 if self.PSbinexpand == 2:
425 file.write("%%%%BeginData: %i ASCII Lines\n"
426 "image\n" % (asciihexlines(len(self.data)) + 1))
427 asciihexstream(file, self.data)
428 else:
429 # the datasource is currentstream (plus some filters)
430 file.write("%%%%BeginData: %i ASCII Lines\n"
431 "image\n" % (ascii85lines(len(self.data)) + 1))
432 ascii85stream(file, self.data)
433 file.write("~>\n")
434 file.write("%%EndData\n")
436 file.write("grestore\n")
438 def processPDF(self, file, writer, context, registry, bbox):
439 registry.add(PDFimage(self.PDFimagename, self.imagewidth, self.imageheight,
440 self.palettecolorspace, self.palettedata, self.colorspace,
441 8, self.compressmode, self.data, registry))
442 bbox += self.bbox()
444 file.write("q\n")
445 self.imagematrixPDF.processPDF(file, writer, context, registry, bbox)
446 file.write("/%s Do\n" % self.PDFimagename)
447 file.write("Q\n")
450 class bitmap(bitmap_pt):
452 def __init__(self, xpos, ypos, image, width=None, height=None, **kwargs):
453 xpos_pt = unit.topt(xpos)
454 ypos_pt = unit.topt(ypos)
455 if width is not None:
456 width_pt = unit.topt(width)
457 else:
458 width_pt = None
459 if height is not None:
460 height_pt = unit.topt(height)
461 else:
462 heigth_pt = None
464 self.__init__(xpos_pt, ypos_pt, image, width_pt=width_pt, height_pt=height_pt, **kwargs)