generate PDF, as well
[PyX/mjg.git] / pyx / bitmap.py
blob8bb09e78b84e6db2fe6f953dc6b4578935bb58ec
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 pdfwriter.PDFobject.__init__(self, "image", name)
205 if palettedata is not None:
206 # acrobat wants a palette to be an object
207 self.PDFpalettedata = PDFimagepalettedata(name, palettedata)
208 registry.add(self.PDFpalettedata)
209 self.name = name
210 self.width = width
211 self.height = height
212 self.palettecolorspace = palettecolorspace
213 self.palettedata = palettedata
214 self.colorspace = colorspace
215 self.bitspercomponent = bitspercomponent
216 self.compressmode = compressmode
217 self.data = data
219 def outputPDF(self, file, writer, registry):
220 file.write("<<\n"
221 "/Type /XObject\n"
222 "/Subtype /Image\n"
223 "/Width %d\n" % self.width)
224 file.write("/Height %d\n" % self.height)
225 if self.palettedata is not None:
226 file.write("/ColorSpace [ /Indexed %s %i\n" % (self.palettecolorspace, len(self.palettedata)/3-1))
227 file.write("%d 0 R\n" % registry.getrefno(self.PDFpalettedata))
228 file.write("]\n")
229 else:
230 file.write("/ColorSpace %s\n" % self.colorspace)
231 file.write("/BitsPerComponent %d\n" % self.bitspercomponent)
232 file.write("/Length %d\n" % len(self.data))
233 if self.compressmode:
234 file.write("/Filter /%sDecode\n" % self.compressmode)
235 file.write(">>\n"
236 "stream\n")
237 file.write(self.data)
238 file.write("\n"
239 "endstream\n")
242 class bitmap(canvas.canvasitem):
244 def __init__(self, xpos, ypos, image, width=None, height=None, ratio=None,
245 PSstoreimage=0, PSmaxstrlen=4093,
246 compressmode="Flate", flatecompresslevel=6,
247 dctquality=75, dctoptimize=0, dctprogression=0):
248 self.xpos = xpos
249 self.ypos = ypos
250 self.imagewidth, self.imageheight = image.size
251 self.PSstoreimage = PSstoreimage
252 self.PSmaxstrlen = PSmaxstrlen
254 if width is not None or height is not None:
255 self.width = width
256 self.height = height
257 if self.width is None:
258 if ratio is None:
259 self.width = self.height * self.imagewidth / float(self.imageheight)
260 else:
261 self.width = ratio * self.height
262 elif self.height is None:
263 if ratio is None:
264 self.height = self.width * self.imageheight / float(self.imagewidth)
265 else:
266 self.height = (1.0/ratio) * self.width
267 elif ratio is not None:
268 raise ValueError("can't specify a ratio when setting width and height")
269 else:
270 if ratio is not None:
271 raise ValueError("must specify width or height to set a ratio")
272 widthdpi, heightdpi = image.info["dpi"] # fails when no dpi information available
273 self.width = self.imagewidth / float(widthdpi) * unit.t_inch
274 self.height = self.imageheight / float(heightdpi) * unit.t_inch
276 self.xpos_pt = unit.topt(self.xpos)
277 self.ypos_pt = unit.topt(self.ypos)
278 self.width_pt = unit.topt(self.width)
279 self.height_pt = unit.topt(self.height)
281 # create decode and colorspace
282 self.colorspace = self.palettecolorspace = self.palettedata = None
283 if image.mode == "P":
284 palettemode, self.palettedata = image.palette.getdata()
285 self.decode = "[0 255]"
286 try:
287 self.palettecolorspace = {"L": "/DeviceGray",
288 "RGB": "/DeviceRGB",
289 "CMYK": "/DeviceCMYK"}[palettemode]
290 except KeyError:
291 warnings.warn("image with unknown palette mode '%s' converted to rgb image" % palettemode)
292 image = image.convert("RGB")
293 self.decode = "[0 1 0 1 0 1]"
294 self.palettedata = None
295 self.colorspace = "/DeviceRGB"
296 elif len(image.mode) == 1:
297 if image.mode != "L":
298 image = image.convert("L")
299 warnings.warn("specific single channel image mode not natively supported, converted to regular grayscale")
300 self.decode = "[0 1]"
301 self.colorspace = "/DeviceGray"
302 elif image.mode == "CMYK":
303 self.decode = "[0 1 0 1 0 1 0 1]"
304 self.colorspace = "/DeviceCMYK"
305 else:
306 if image.mode != "RGB":
307 image = image.convert("RGB")
308 warnings.warn("image with unknown mode converted to rgb")
309 self.decode = "[0 1 0 1 0 1]"
310 self.colorspace = "/DeviceRGB"
312 # create imagematrix
313 self.imagematrixPS = (trafo.mirror(0)
314 .translated_pt(-self.xpos_pt, self.ypos_pt+self.height_pt)
315 .scaled_pt(self.imagewidth/self.width_pt, self.imageheight/self.height_pt))
316 self.imagematrixPDF = (trafo.scale_pt(self.width_pt, self.height_pt)
317 .translated_pt(self.xpos_pt, self.ypos_pt))
319 # check whether imagedata is compressed or not
320 try:
321 imagecompressed = image.compressed
322 except:
323 imagecompressed = None
324 if compressmode != None and imagecompressed != None:
325 raise ValueError("compression of a compressed image not supported")
326 self.compressmode = compressmode
327 if compressmode is not None and compressmode not in ["Flate", "DCT"]:
328 raise ValueError("invalid compressmode '%s'" % compressmode)
329 if imagecompressed is not None:
330 self.compressmode = imagecompressed
331 if imagecompressed not in ["Flate", "DCT"]:
332 raise ValueError("invalid compressed image '%s'" % imagecompressed)
333 if not haszlib and compressmode == "Flate":
334 warnings.warn("zlib module not available, disable compression")
335 self.compressmode = compressmode = None
337 # create data
338 if compressmode == "Flate":
339 self.data = zlib.compress(image.tostring(), flatecompresslevel)
340 elif compressmode == "DCT":
341 self.data = image.tostring("jpeg", image.mode,
342 dctquality, dctoptimize, dctprogression)
343 else:
344 self.data = image.tostring()
346 self.PSsinglestring = self.PSstoreimage and len(self.data) < self.PSmaxstrlen
347 if self.PSsinglestring:
348 self.PSimagename = "image-%d-%s-singlestring" % (id(image), compressmode)
349 else:
350 self.PSimagename = "image-%d-%s-stringarray" % (id(image), compressmode)
351 self.PDFimagename = "image-%d-%s" % (id(image), compressmode)
353 def bbox(self):
354 return bbox.bbox_pt(self.xpos_pt, self.ypos_pt,
355 self.xpos_pt+self.width_pt, self.ypos_pt+self.height_pt)
357 def registerPS(self, registry):
358 if self.PSstoreimage and not self.PSsinglestring:
359 registry.add(pswriter.PSdefinition("imagedataaccess",
360 "{ /imagedataindex load " # get list index
361 "dup 1 add /imagedataindex exch store " # store increased index
362 "/imagedataid load exch get }")) # select string from array
363 if self.PSstoreimage:
364 registry.add(PSimagedata(self.PSimagename, self.data, self.PSsinglestring, self.PSmaxstrlen))
366 def registerPDF(self, registry):
367 registry.add(PDFimage(self.PDFimagename, self.imagewidth, self.imageheight,
368 self.palettecolorspace, self.palettedata, self.colorspace,
369 8, self.compressmode, self.data, registry))
371 def outputPS(self, file, writer, context):
372 file.write("gsave\n")
373 if self.palettedata is not None:
374 file.write("[ /Indexed %s %i\n" % (self.palettecolorspace, len(self.palettedata)/3-1))
375 file.write("%%%%BeginData: %i ASCII Lines\n" % ascii85lines(len(self.data)))
376 file.write("<~")
377 ascii85stream(file, self.palettedata)
378 file.write("~>\n"
379 "%%EndData\n")
380 file.write("] setcolorspace\n")
381 else:
382 file.write("%s setcolorspace\n" % self.colorspace)
384 if self.PSstoreimage and not self.PSsinglestring:
385 file.write("/imagedataindex 0 store\n" # not use the stack since interpreters differ in their stack usage
386 "/imagedataid %s store\n" % self.PSimagename)
388 file.write("<<\n"
389 "/ImageType 1\n"
390 "/Width %i\n" % self.imagewidth)
391 file.write("/Height %i\n" % self.imageheight)
392 file.write("/BitsPerComponent 8\n"
393 "/ImageMatrix %s\n" % self.imagematrixPS)
394 file.write("/Decode %s\n" % self.decode)
396 file.write("/DataSource ")
397 if self.PSstoreimage:
398 if self.PSsinglestring:
399 file.write("/%s load" % self.PSimagename)
400 else:
401 file.write("/imagedataaccess load") # some printers do not allow for inline code here -> we store it in a resource
402 else:
403 file.write("currentfile /ASCII85Decode filter")
404 if self.compressmode:
405 file.write(" /%sDecode filter" % self.compressmode)
406 file.write("\n")
408 file.write(">>\n")
410 if self.PSstoreimage:
411 file.write("image\n")
412 else:
413 # the datasource is currentstream (plus some filters)
414 file.write("%%%%BeginData: %i ASCII Lines\n"
415 "image\n" % (ascii85lines(len(self.data)) + 1))
416 ascii85stream(file, self.data)
417 file.write("~>\n%%EndData\n")
419 file.write("grestore\n")
421 def outputPDF(self, file, writer, context):
422 file.write("q\n")
423 self.imagematrixPDF.outputPDF(file, writer, context)
424 file.write("/%s Do\n" % self.PDFimagename)
425 file.write("Q\n")