reduce length of pattern lines by one order of magnitude to prevent problems with...
[PyX/mjg.git] / pyx / bitmap.py
blobd7d39ab152bd34dfe090ef0eacfc741507006056
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, binascii
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])
74 _asciihexlinelength = 64
75 def asciihexlines(datalen):
76 return (datalen*2 + _asciihexlinelength - 1) / _asciihexlinelength
78 def asciihexstream(file, data):
79 hexdata = binascii.b2a_hex(data)
80 for i in range((len(hexdata)-1)/_asciihexlinelength + 1):
81 file.write(hexdata[i*_asciihexlinelength: i*_asciihexlinelength+_asciihexlinelength])
82 file.write("\n")
85 class image:
87 def __init__(self, width, height, mode, data, compressed=None):
88 if width <= 0 or height <= 0:
89 raise ValueError("valid image size")
90 if mode not in ["L", "RGB", "CMYK"]:
91 raise ValueError("invalid mode")
92 if compressed is None and len(mode)*width*height != len(data):
93 raise ValueError("wrong size of uncompressed data")
94 self.size = width, height
95 self.mode = mode
96 self.data = data
97 self.compressed = compressed
99 def tostring(self, *args):
100 if len(args):
101 raise RuntimeError("encoding not supported in this implementation")
102 return self.data
104 def convert(self, model):
105 raise RuntimeError("color model conversion not supported in this implementation")
108 class jpegimage(image):
110 def __init__(self, file):
111 try:
112 data = file.read()
113 except:
114 data = open(file, "rb").read()
115 pos = 0
116 nestinglevel = 0
117 try:
118 while 1:
119 if data[pos] == "\377" and data[pos+1] not in ["\0", "\377"]:
120 # print "marker: 0x%02x \\%03o" % (ord(data[pos+1]), ord(data[pos+1]))
121 if data[pos+1] == "\330":
122 if not nestinglevel:
123 begin = pos
124 nestinglevel += 1
125 elif not nestinglevel:
126 raise ValueError("begin marker expected")
127 elif data[pos+1] == "\331":
128 nestinglevel -= 1
129 if not nestinglevel:
130 end = pos + 2
131 break
132 elif data[pos+1] in ["\300", "\301"]:
133 l, bits, height, width, components = struct.unpack(">HBHHB", data[pos+2:pos+10])
134 if bits != 8:
135 raise ValueError("implementation limited to 8 bit per component only")
136 try:
137 mode = {1: "L", 3: "RGB", 4: "CMYK"}[components]
138 except KeyError:
139 raise ValueError("invalid number of components")
140 pos += l+1
141 elif data[pos+1] == "\340":
142 l, id, major, minor, dpikind, xdpi, ydpi = struct.unpack(">H5sBBBHH", data[pos+2:pos+16])
143 if dpikind == 1:
144 self.info = {"dpi": (xdpi, ydpi)}
145 elif dpikind == 2:
146 self.info = {"dpi": (xdpi*2.54, ydpi*2.45)}
147 # else do not provide dpi information
148 pos += l+1
149 pos += 1
150 except IndexError:
151 raise ValueError("end marker expected")
152 image.__init__(self, width, height, mode, data[begin:end], compressed="DCT")
155 class PSimagedata(pswriter.PSresource):
157 def __init__(self, name, data, singlestring, maxstrlen):
158 pswriter.PSresource.__init__(self, "imagedata", name)
159 self.data = data
160 self.singlestring = singlestring
161 self.maxstrlen = maxstrlen
163 def output(self, file, writer, registry):
164 file.write("%%%%BeginRessource: %s\n" % self.id)
165 if self.singlestring:
166 file.write("%%%%BeginData: %i ASCII Lines\n"
167 "<~" % ascii85lines(len(self.data)))
168 ascii85stream(file, self.data)
169 file.write("~>\n"
170 "%%EndData\n")
171 else:
172 datalen = len(self.data)
173 tailpos = datalen - datalen % self.maxstrlen
174 file.write("%%%%BeginData: %i ASCII Lines\n" %
175 ((tailpos/self.maxstrlen) * ascii85lines(self.maxstrlen) +
176 ascii85lines(datalen-tailpos)))
177 file.write("[ ")
178 for i in xrange(0, tailpos, self.maxstrlen):
179 file.write("<~")
180 ascii85stream(file, self.data[i: i+self.maxstrlen])
181 file.write("~>\n")
182 if datalen != tailpos:
183 file.write("<~")
184 ascii85stream(file, self.data[tailpos:])
185 file.write("~>")
186 file.write("]\n"
187 "%%EndData\n")
188 file.write("/%s exch def\n" % self.id)
189 file.write("%%EndRessource\n")
192 class PDFimagepalettedata(pdfwriter.PDFobject):
194 def __init__(self, name, data):
195 pdfwriter.PDFobject.__init__(self, "imagepalettedata", name)
196 self.data = data
198 def write(self, file, writer, registry):
199 file.write("<<\n"
200 "/Length %d\n" % len(self.data))
201 file.write(">>\n"
202 "stream\n")
203 file.write(self.data)
204 file.write("\n"
205 "endstream\n")
208 class PDFimage(pdfwriter.PDFobject):
210 def __init__(self, name, width, height, palettecolorspace, palettedata, colorspace,
211 bitspercomponent, compressmode, data, registry):
212 if palettedata is not None:
213 procset = "ImageI"
214 elif colorspace == "/DeviceGray":
215 procset = "ImageB"
216 else:
217 procset = "ImageC"
218 pdfwriter.PDFobject.__init__(self, "image", name)
219 registry.addresource("XObject", name, self, procset=procset)
220 if palettedata is not None:
221 # acrobat wants a palette to be an object
222 self.PDFpalettedata = PDFimagepalettedata(name, palettedata)
223 registry.add(self.PDFpalettedata)
224 self.name = name
225 self.width = width
226 self.height = height
227 self.palettecolorspace = palettecolorspace
228 self.palettedata = palettedata
229 self.colorspace = colorspace
230 self.bitspercomponent = bitspercomponent
231 self.compressmode = compressmode
232 self.data = data
234 def write(self, file, writer, registry):
235 file.write("<<\n"
236 "/Type /XObject\n"
237 "/Subtype /Image\n"
238 "/Width %d\n" % self.width)
239 file.write("/Height %d\n" % self.height)
240 if self.palettedata is not None:
241 file.write("/ColorSpace [ /Indexed %s %i\n" % (self.palettecolorspace, len(self.palettedata)/3-1))
242 file.write("%d 0 R\n" % registry.getrefno(self.PDFpalettedata))
243 file.write("]\n")
244 else:
245 file.write("/ColorSpace %s\n" % self.colorspace)
246 file.write("/BitsPerComponent %d\n" % self.bitspercomponent)
247 file.write("/Length %d\n" % len(self.data))
248 if self.compressmode:
249 file.write("/Filter /%sDecode\n" % self.compressmode)
250 file.write(">>\n"
251 "stream\n")
252 file.write(self.data)
253 file.write("\n"
254 "endstream\n")
257 class bitmap(canvas.canvasitem):
259 def __init__(self, xpos, ypos, image, width=None, height=None, ratio=None,
260 PSstoreimage=0, PSmaxstrlen=4093, PSbinexpand=1,
261 compressmode="Flate", flatecompresslevel=6,
262 dctquality=75, dctoptimize=0, dctprogression=0):
263 self.xpos = xpos
264 self.ypos = ypos
265 self.imagewidth, self.imageheight = image.size
266 self.PSstoreimage = PSstoreimage
267 self.PSmaxstrlen = PSmaxstrlen
268 self.PSbinexpand = PSbinexpand
270 if width is not None or height is not None:
271 self.width = width
272 self.height = height
273 if self.width is None:
274 if ratio is None:
275 self.width = self.height * self.imagewidth / float(self.imageheight)
276 else:
277 self.width = ratio * self.height
278 elif self.height is None:
279 if ratio is None:
280 self.height = self.width * self.imageheight / float(self.imagewidth)
281 else:
282 self.height = (1.0/ratio) * self.width
283 elif ratio is not None:
284 raise ValueError("can't specify a ratio when setting width and height")
285 else:
286 if ratio is not None:
287 raise ValueError("must specify width or height to set a ratio")
288 widthdpi, heightdpi = image.info["dpi"] # fails when no dpi information available
289 self.width = self.imagewidth / float(widthdpi) * unit.t_inch
290 self.height = self.imageheight / float(heightdpi) * unit.t_inch
292 self.xpos_pt = unit.topt(self.xpos)
293 self.ypos_pt = unit.topt(self.ypos)
294 self.width_pt = unit.topt(self.width)
295 self.height_pt = unit.topt(self.height)
297 # create decode and colorspace
298 self.colorspace = self.palettecolorspace = self.palettedata = None
299 if image.mode == "P":
300 palettemode, self.palettedata = image.palette.getdata()
301 self.decode = "[0 255]"
302 try:
303 self.palettecolorspace = {"L": "/DeviceGray",
304 "RGB": "/DeviceRGB",
305 "CMYK": "/DeviceCMYK"}[palettemode]
306 except KeyError:
307 warnings.warn("image with unknown palette mode '%s' converted to rgb image" % palettemode)
308 image = image.convert("RGB")
309 self.decode = "[0 1 0 1 0 1]"
310 self.palettedata = None
311 self.colorspace = "/DeviceRGB"
312 elif len(image.mode) == 1:
313 if image.mode != "L":
314 image = image.convert("L")
315 warnings.warn("specific single channel image mode not natively supported, converted to regular grayscale")
316 self.decode = "[0 1]"
317 self.colorspace = "/DeviceGray"
318 elif image.mode == "CMYK":
319 self.decode = "[0 1 0 1 0 1 0 1]"
320 self.colorspace = "/DeviceCMYK"
321 else:
322 if image.mode != "RGB":
323 image = image.convert("RGB")
324 warnings.warn("image with unknown mode converted to rgb")
325 self.decode = "[0 1 0 1 0 1]"
326 self.colorspace = "/DeviceRGB"
328 # create imagematrix
329 self.imagematrixPS = (trafo.mirror(0)
330 .translated_pt(-self.xpos_pt, self.ypos_pt+self.height_pt)
331 .scaled_pt(self.imagewidth/self.width_pt, self.imageheight/self.height_pt))
332 self.imagematrixPDF = (trafo.scale_pt(self.width_pt, self.height_pt)
333 .translated_pt(self.xpos_pt, self.ypos_pt))
335 # check whether imagedata is compressed or not
336 try:
337 imagecompressed = image.compressed
338 except:
339 imagecompressed = None
340 if compressmode != None and imagecompressed != None:
341 raise ValueError("compression of a compressed image not supported")
342 self.compressmode = compressmode
343 if compressmode is not None and compressmode not in ["Flate", "DCT"]:
344 raise ValueError("invalid compressmode '%s'" % compressmode)
345 if imagecompressed is not None:
346 self.compressmode = imagecompressed
347 if imagecompressed not in ["Flate", "DCT"]:
348 raise ValueError("invalid compressed image '%s'" % imagecompressed)
349 if not haszlib and compressmode == "Flate":
350 warnings.warn("zlib module not available, disable compression")
351 self.compressmode = compressmode = None
353 # create data
354 if compressmode == "Flate":
355 self.data = zlib.compress(image.tostring(), flatecompresslevel)
356 elif compressmode == "DCT":
357 self.data = image.tostring("jpeg", image.mode,
358 dctquality, dctoptimize, dctprogression)
359 else:
360 self.data = image.tostring()
362 self.PSsinglestring = self.PSstoreimage and len(self.data) < self.PSmaxstrlen
363 if self.PSsinglestring:
364 self.PSimagename = "image-%d-%s-singlestring" % (id(image), compressmode)
365 else:
366 self.PSimagename = "image-%d-%s-stringarray" % (id(image), compressmode)
367 self.PDFimagename = "image-%d-%s" % (id(image), compressmode)
369 def bbox(self):
370 return bbox.bbox_pt(self.xpos_pt, self.ypos_pt,
371 self.xpos_pt+self.width_pt, self.ypos_pt+self.height_pt)
373 def processPS(self, file, writer, context, registry, bbox):
374 if self.PSstoreimage and not self.PSsinglestring:
375 registry.add(pswriter.PSdefinition("imagedataaccess",
376 "{ /imagedataindex load " # get list index
377 "dup 1 add /imagedataindex exch store " # store increased index
378 "/imagedataid load exch get }")) # select string from array
379 if self.PSstoreimage:
380 registry.add(PSimagedata(self.PSimagename, self.data, self.PSsinglestring, self.PSmaxstrlen))
381 bbox += self.bbox()
383 file.write("gsave\n")
384 if self.palettedata is not None:
385 file.write("[ /Indexed %s %i\n" % (self.palettecolorspace, len(self.palettedata)/3-1))
386 file.write("%%%%BeginData: %i ASCII Lines\n" % ascii85lines(len(self.palettedata)))
387 file.write("<~")
388 ascii85stream(file, self.palettedata)
389 file.write("~>\n"
390 "%%EndData\n")
391 file.write("] setcolorspace\n")
392 else:
393 file.write("%s setcolorspace\n" % self.colorspace)
395 if self.PSstoreimage and not self.PSsinglestring:
396 file.write("/imagedataindex 0 store\n" # not use the stack since interpreters differ in their stack usage
397 "/imagedataid %s store\n" % self.PSimagename)
399 file.write("<<\n"
400 "/ImageType 1\n"
401 "/Width %i\n" % self.imagewidth)
402 file.write("/Height %i\n" % self.imageheight)
403 file.write("/BitsPerComponent 8\n"
404 "/ImageMatrix %s\n" % self.imagematrixPS)
405 file.write("/Decode %s\n" % self.decode)
407 file.write("/DataSource ")
408 if self.PSstoreimage:
409 if self.PSsinglestring:
410 file.write("/%s load" % self.PSimagename)
411 else:
412 file.write("/imagedataaccess load") # some printers do not allow for inline code here -> we store it in a resource
413 else:
414 if self.PSbinexpand == 2:
415 file.write("currentfile /ASCIIHexDecode filter")
416 else:
417 file.write("currentfile /ASCII85Decode filter")
418 if self.compressmode:
419 file.write(" /%sDecode filter" % self.compressmode)
420 file.write("\n")
422 file.write(">>\n")
424 if self.PSstoreimage:
425 file.write("image\n")
426 else:
427 if self.PSbinexpand == 2:
428 file.write("%%%%BeginData: %i ASCII Lines\n"
429 "image\n" % (asciihexlines(len(self.data)) + 1))
430 asciihexstream(file, self.data)
431 else:
432 # the datasource is currentstream (plus some filters)
433 file.write("%%%%BeginData: %i ASCII Lines\n"
434 "image\n" % (ascii85lines(len(self.data)) + 1))
435 ascii85stream(file, self.data)
436 file.write("~>\n")
437 file.write("%%EndData\n")
439 file.write("grestore\n")
441 def processPDF(self, file, writer, context, registry, bbox):
442 registry.add(PDFimage(self.PDFimagename, self.imagewidth, self.imageheight,
443 self.palettecolorspace, self.palettedata, self.colorspace,
444 8, self.compressmode, self.data, registry))
445 bbox += self.bbox()
447 file.write("q\n")
448 self.imagematrixPDF.processPDF(file, writer, context, registry, bbox)
449 file.write("/%s Do\n" % self.PDFimagename)
450 file.write("Q\n")