correct the self-intersection in parallel
[PyX/mjg.git] / pyx / bitmap.py
blob3d3540f5c9017e2d7d71393931d6515d002101cc
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 outputPS(self, file, writer, registry):
154 # TODO resource data could be written directly on the output stream
155 # after proper code reorganization
156 file.write("%%%%BeginRessource: %s\n" % self.id)
157 if self.singlestring:
158 file.write("%%%%BeginData: %i ASCII Lines\n"
159 "<~" % ascii85lines(len(self.data)))
160 ascii85stream(file, self.data)
161 file.write("~>\n"
162 "%%EndData\n")
163 else:
164 datalen = len(self.data)
165 tailpos = datalen - datalen % self.maxstrlen
166 file.write("%%%%BeginData: %i ASCII Lines\n" %
167 ((tailpos/self.maxstrlen) * ascii85lines(self.maxstrlen) +
168 ascii85lines(datalen-tailpos)))
169 file.write("[ ")
170 for i in xrange(0, tailpos, self.maxstrlen):
171 file.write("<~")
172 ascii85stream(file, self.data[i: i+self.maxstrlen])
173 file.write("~>\n")
174 if datalen != tailpos:
175 file.write("<~")
176 ascii85stream(file, self.data[tailpos:])
177 file.write("~>")
178 file.write("]\n"
179 "%%EndData\n")
180 file.write("/%s exch def\n" % self.id)
181 file.write("%%EndRessource\n")
184 class PDFimagepalettedata(pdfwriter.PDFobject):
186 def __init__(self, name, data):
187 pdfwriter.PDFobject.__init__(self, "imagepalettedata", name)
188 self.data = data
190 def outputPDF(self, file, writer, registry):
191 file.write("<<\n"
192 "/Length %d\n" % len(self.data))
193 file.write(">>\n"
194 "stream\n")
195 file.write(self.data)
196 file.write("\n"
197 "endstream\n")
200 class PDFimage(pdfwriter.PDFobject):
202 def __init__(self, name, width, height, palettecolorspace, palettedata, colorspace,
203 bitspercomponent, compressmode, data, registry):
204 if palettedata is not None:
205 procset = "ImageI"
206 elif colorspace == "/DeviceGray":
207 procset = "ImageB"
208 else:
209 procset = "ImageC"
210 pdfwriter.PDFobject.__init__(self, "image", name, "XObject", procset)
211 if palettedata is not None:
212 # acrobat wants a palette to be an object
213 self.PDFpalettedata = PDFimagepalettedata(name, palettedata)
214 registry.add(self.PDFpalettedata)
215 self.name = name
216 self.width = width
217 self.height = height
218 self.palettecolorspace = palettecolorspace
219 self.palettedata = palettedata
220 self.colorspace = colorspace
221 self.bitspercomponent = bitspercomponent
222 self.compressmode = compressmode
223 self.data = data
225 def outputPDF(self, file, writer, registry):
226 file.write("<<\n"
227 "/Type /XObject\n"
228 "/Subtype /Image\n"
229 "/Width %d\n" % self.width)
230 file.write("/Height %d\n" % self.height)
231 if self.palettedata is not None:
232 file.write("/ColorSpace [ /Indexed %s %i\n" % (self.palettecolorspace, len(self.palettedata)/3-1))
233 file.write("%d 0 R\n" % registry.getrefno(self.PDFpalettedata))
234 file.write("]\n")
235 else:
236 file.write("/ColorSpace %s\n" % self.colorspace)
237 file.write("/BitsPerComponent %d\n" % self.bitspercomponent)
238 file.write("/Length %d\n" % len(self.data))
239 if self.compressmode:
240 file.write("/Filter /%sDecode\n" % self.compressmode)
241 file.write(">>\n"
242 "stream\n")
243 file.write(self.data)
244 file.write("\n"
245 "endstream\n")
248 class bitmap(canvas.canvasitem):
250 def __init__(self, xpos, ypos, image, width=None, height=None, ratio=None,
251 PSstoreimage=0, PSmaxstrlen=4093,
252 compressmode="Flate", flatecompresslevel=6,
253 dctquality=75, dctoptimize=0, dctprogression=0):
254 self.xpos = xpos
255 self.ypos = ypos
256 self.imagewidth, self.imageheight = image.size
257 self.PSstoreimage = PSstoreimage
258 self.PSmaxstrlen = PSmaxstrlen
260 if width is not None or height is not None:
261 self.width = width
262 self.height = height
263 if self.width is None:
264 if ratio is None:
265 self.width = self.height * self.imagewidth / float(self.imageheight)
266 else:
267 self.width = ratio * self.height
268 elif self.height is None:
269 if ratio is None:
270 self.height = self.width * self.imageheight / float(self.imagewidth)
271 else:
272 self.height = (1.0/ratio) * self.width
273 elif ratio is not None:
274 raise ValueError("can't specify a ratio when setting width and height")
275 else:
276 if ratio is not None:
277 raise ValueError("must specify width or height to set a ratio")
278 widthdpi, heightdpi = image.info["dpi"] # fails when no dpi information available
279 self.width = self.imagewidth / float(widthdpi) * unit.t_inch
280 self.height = self.imageheight / float(heightdpi) * unit.t_inch
282 self.xpos_pt = unit.topt(self.xpos)
283 self.ypos_pt = unit.topt(self.ypos)
284 self.width_pt = unit.topt(self.width)
285 self.height_pt = unit.topt(self.height)
287 # create decode and colorspace
288 self.colorspace = self.palettecolorspace = self.palettedata = None
289 if image.mode == "P":
290 palettemode, self.palettedata = image.palette.getdata()
291 self.decode = "[0 255]"
292 try:
293 self.palettecolorspace = {"L": "/DeviceGray",
294 "RGB": "/DeviceRGB",
295 "CMYK": "/DeviceCMYK"}[palettemode]
296 except KeyError:
297 warnings.warn("image with unknown palette mode '%s' converted to rgb image" % palettemode)
298 image = image.convert("RGB")
299 self.decode = "[0 1 0 1 0 1]"
300 self.palettedata = None
301 self.colorspace = "/DeviceRGB"
302 elif len(image.mode) == 1:
303 if image.mode != "L":
304 image = image.convert("L")
305 warnings.warn("specific single channel image mode not natively supported, converted to regular grayscale")
306 self.decode = "[0 1]"
307 self.colorspace = "/DeviceGray"
308 elif image.mode == "CMYK":
309 self.decode = "[0 1 0 1 0 1 0 1]"
310 self.colorspace = "/DeviceCMYK"
311 else:
312 if image.mode != "RGB":
313 image = image.convert("RGB")
314 warnings.warn("image with unknown mode converted to rgb")
315 self.decode = "[0 1 0 1 0 1]"
316 self.colorspace = "/DeviceRGB"
318 # create imagematrix
319 self.imagematrixPS = (trafo.mirror(0)
320 .translated_pt(-self.xpos_pt, self.ypos_pt+self.height_pt)
321 .scaled_pt(self.imagewidth/self.width_pt, self.imageheight/self.height_pt))
322 self.imagematrixPDF = (trafo.scale_pt(self.width_pt, self.height_pt)
323 .translated_pt(self.xpos_pt, self.ypos_pt))
325 # check whether imagedata is compressed or not
326 try:
327 imagecompressed = image.compressed
328 except:
329 imagecompressed = None
330 if compressmode != None and imagecompressed != None:
331 raise ValueError("compression of a compressed image not supported")
332 self.compressmode = compressmode
333 if compressmode is not None and compressmode not in ["Flate", "DCT"]:
334 raise ValueError("invalid compressmode '%s'" % compressmode)
335 if imagecompressed is not None:
336 self.compressmode = imagecompressed
337 if imagecompressed not in ["Flate", "DCT"]:
338 raise ValueError("invalid compressed image '%s'" % imagecompressed)
339 if not haszlib and compressmode == "Flate":
340 warnings.warn("zlib module not available, disable compression")
341 self.compressmode = compressmode = None
343 # create data
344 if compressmode == "Flate":
345 self.data = zlib.compress(image.tostring(), flatecompresslevel)
346 elif compressmode == "DCT":
347 self.data = image.tostring("jpeg", image.mode,
348 dctquality, dctoptimize, dctprogression)
349 else:
350 self.data = image.tostring()
352 self.PSsinglestring = self.PSstoreimage and len(self.data) < self.PSmaxstrlen
353 if self.PSsinglestring:
354 self.PSimagename = "image-%d-%s-singlestring" % (id(image), compressmode)
355 else:
356 self.PSimagename = "image-%d-%s-stringarray" % (id(image), compressmode)
357 self.PDFimagename = "image-%d-%s" % (id(image), compressmode)
359 def bbox(self):
360 return bbox.bbox_pt(self.xpos_pt, self.ypos_pt,
361 self.xpos_pt+self.width_pt, self.ypos_pt+self.height_pt)
363 def registerPS(self, registry):
364 if self.PSstoreimage and not self.PSsinglestring:
365 registry.add(pswriter.PSdefinition("imagedataaccess",
366 "{ /imagedataindex load " # get list index
367 "dup 1 add /imagedataindex exch store " # store increased index
368 "/imagedataid load exch get }")) # select string from array
369 if self.PSstoreimage:
370 registry.add(PSimagedata(self.PSimagename, self.data, self.PSsinglestring, self.PSmaxstrlen))
372 def registerPDF(self, registry):
373 registry.add(PDFimage(self.PDFimagename, self.imagewidth, self.imageheight,
374 self.palettecolorspace, self.palettedata, self.colorspace,
375 8, self.compressmode, self.data, registry))
377 def outputPS(self, file, writer, context):
378 file.write("gsave\n")
379 if self.palettedata is not None:
380 file.write("[ /Indexed %s %i\n" % (self.palettecolorspace, len(self.palettedata)/3-1))
381 file.write("%%%%BeginData: %i ASCII Lines\n" % ascii85lines(len(self.data)))
382 file.write("<~")
383 ascii85stream(file, self.palettedata)
384 file.write("~>\n"
385 "%%EndData\n")
386 file.write("] setcolorspace\n")
387 else:
388 file.write("%s setcolorspace\n" % self.colorspace)
390 if self.PSstoreimage and not self.PSsinglestring:
391 file.write("/imagedataindex 0 store\n" # not use the stack since interpreters differ in their stack usage
392 "/imagedataid %s store\n" % self.PSimagename)
394 file.write("<<\n"
395 "/ImageType 1\n"
396 "/Width %i\n" % self.imagewidth)
397 file.write("/Height %i\n" % self.imageheight)
398 file.write("/BitsPerComponent 8\n"
399 "/ImageMatrix %s\n" % self.imagematrixPS)
400 file.write("/Decode %s\n" % self.decode)
402 file.write("/DataSource ")
403 if self.PSstoreimage:
404 if self.PSsinglestring:
405 file.write("/%s load" % self.PSimagename)
406 else:
407 file.write("/imagedataaccess load") # some printers do not allow for inline code here -> we store it in a resource
408 else:
409 file.write("currentfile /ASCII85Decode filter")
410 if self.compressmode:
411 file.write(" /%sDecode filter" % self.compressmode)
412 file.write("\n")
414 file.write(">>\n")
416 if self.PSstoreimage:
417 file.write("image\n")
418 else:
419 # the datasource is currentstream (plus some filters)
420 file.write("%%%%BeginData: %i ASCII Lines\n"
421 "image\n" % (ascii85lines(len(self.data)) + 1))
422 ascii85stream(file, self.data)
423 file.write("~>\n%%EndData\n")
425 file.write("grestore\n")
427 def outputPDF(self, file, writer, context):
428 file.write("q\n")
429 self.imagematrixPDF.outputPDF(file, writer, context)
430 file.write("/%s Do\n" % self.PDFimagename)
431 file.write("Q\n")