bump version number
[PyX.git] / bitmap.py
blob9307af5446f2e79461d62eea91793bd6d8fa5231
1 # -*- encoding: utf-8 -*-
4 # Copyright (C) 2004-2015 André Wobst <wobsta@users.sourceforge.net>
5 # Copyright (C) 2011 Michael Schindler<m-schindler@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 binascii, logging, struct, io
24 try:
25 import zlib
26 haszlib = True
27 except:
28 haszlib = False
30 from . import bbox, baseclasses, pswriter, pdfwriter, trafo, unit
32 logger = logging.getLogger("pyx")
34 devicenames = {"L": "/DeviceGray",
35 "RGB": "/DeviceRGB",
36 "CMYK": "/DeviceCMYK"}
37 decodestrings = {"L": "[0 1]",
38 "RGB": "[0 1 0 1 0 1]",
39 "CMYK": "[0 1 0 1 0 1 0 1]",
40 "P": "[0 255]"}
43 def ascii85lines(datalen):
44 if datalen < 4:
45 return 1
46 return (datalen + 56)/60
48 def ascii85stream(file, data):
49 """Encodes the string data in ASCII85 and writes it to
50 the stream file. The number of lines written to the stream
51 is known just from the length of the data by means of the
52 ascii85lines function. Note that the tailing newline character
53 of the last line is not added by this function, but it is taken
54 into account in the ascii85lines function."""
55 i = 3 # go on smoothly in case of data length equals zero
56 l = 0
57 l = [None, None, None, None]
58 for i in range(len(data)):
59 c = data[i]
60 l[i%4] = c
61 if i%4 == 3:
62 if i%60 == 3 and i != 3:
63 file.write("\n")
64 if l:
65 # instead of
66 # l[3], c5 = divmod(256*256*256*l[0]+256*256*l[1]+256*l[2]+l[3], 85)
67 # l[2], c4 = divmod(l[3], 85)
68 # we have to avoid number > 2**31 by
69 l[3], c5 = divmod(256*256*l[0]+256*256*l[1]+256*l[2]+l[3], 85)
70 l[2], c4 = divmod(256*256*3*l[0]+l[3], 85)
71 l[1], c3 = divmod(l[2], 85)
72 c1 , c2 = divmod(l[1], 85)
73 file.write_bytes(struct.pack("BBBBB", c1+33, c2+33, c3+33, c4+33, c5+33))
74 else:
75 file.write("z")
76 if i%4 != 3:
77 for j in range((i%4) + 1, 4):
78 l[j] = 0
79 l[3], c5 = divmod(256*256*l[0]+256*256*l[1]+256*l[2]+l[3], 85)
80 l[2], c4 = divmod(256*256*3*l[0]+l[3], 85)
81 l[1], c3 = divmod(l[2], 85)
82 c1 , c2 = divmod(l[1], 85)
83 file.write_bytes(struct.pack("BBBB", c1+33, c2+33, c3+33, c4+33)[:(i%4)+2])
85 _asciihexlinelength = 64
86 def asciihexlines(datalen):
87 return (datalen*2 + _asciihexlinelength - 1) / _asciihexlinelength
89 def asciihexstream(file, data):
90 hexdata = binascii.b2a_hex(data)
91 for i in range((len(hexdata)-1)/_asciihexlinelength + 1):
92 file.write(hexdata[i*_asciihexlinelength: i*_asciihexlinelength+_asciihexlinelength])
93 file.write("\n")
96 class palette:
98 def __init__(self, mode, data):
99 self.mode = mode
100 self.data = data
102 def getdata(self):
103 return self.mode, self.data
106 class image:
108 def __init__(self, width, height, mode, data, compressed=None, palette=None):
109 if width <= 0 or height <= 0:
110 raise ValueError("valid image size")
111 if mode not in ["L", "RGB", "CMYK", "LA", "RGBA", "CMYKA", "AL", "ARGB", "ACMYK", "P"]:
112 raise ValueError("invalid mode")
113 if compressed is None and len(mode)*width*height != len(data):
114 raise ValueError("wrong size of uncompressed data")
115 self.size = width, height
116 self.mode = mode
117 self.data = data
118 self.compressed = compressed
119 self.palette = palette
121 def split(self):
122 if self.compressed is not None:
123 raise RuntimeError("cannot extract bands from compressed image")
124 bands = len(self.mode)
125 width, height = self.size
126 return [image(width, height, "L", bytes(self.data[band::bands])) for band in range(bands)]
128 def tobytes(self, *args):
129 if len(args):
130 raise RuntimeError("encoding not supported in this implementation")
131 return self.data
133 def convert(self, model):
134 raise RuntimeError("color model conversion not supported in this implementation")
136 def save(self, file, format=None, **attrs):
137 if format != "png":
138 raise RuntimeError("Uncompressed image can be output as PNG only.", format, file)
139 if not haszlib:
140 raise ValueError("PNG output not available due to missing zlib module.")
141 try:
142 pngmode, bytesperpixel = {"L": (0, 1), "LA": (4, 2), "RGB": (2, 3), "RGBA": (6, 4), "P": (3, 1)}[self.mode]
143 except KeyError:
144 raise RuntimeError("Unsupported mode '%s' for PNG output." % self.mode)
145 width, height = self.size
146 assert len(self.data) == width*height*bytesperpixel
147 # inject filter byte to each scanline
148 data = b"".join(b"\x00" + self.data[bytesperpixel*width*pos:bytesperpixel*width*(pos+1)] for pos in range(0, height))
149 chunk=lambda name, data=b"": struct.pack("!I", len(data)) + name + data + struct.pack("!I", 0xFFFFFFFF & zlib.crc32(name + data))
150 file.write(b"\x89PNG\r\n\x1a\n")
151 file.write(chunk(b"IHDR", struct.pack("!2I5B", width, height, 8, pngmode, 0, 0, 0)))
152 if self.mode == "P":
153 palettemode, palettedata = self.palette.getdata()
154 if palettemode == "L":
155 palettemode = "RGB"
156 palettedata = b"".join(bytes([x, x, x]) for x in palettedata)
157 if palettemode != "RGB":
158 raise RuntimeError("Unsupported palette mode '%s' for PNG output." % palettemode)
159 file.write(chunk(b"PLTE", palettedata))
160 file.write(chunk(b"IDAT", zlib.compress(data, 9)))
161 file.write(chunk(b"IEND"))
164 class jpegimage(image):
166 def __init__(self, file):
167 try:
168 data = file.read()
169 except:
170 with open(file, "rb") as f:
171 data = f.read()
172 pos = 0
173 nestinglevel = 0
174 try:
175 while True:
176 if data[pos] == 0o377 and data[pos+1] not in [0, 0o377]:
177 # print("marker: 0x%02x \\%03o" % (data[pos+1], data[pos+1]))
178 if data[pos+1] == 0o330:
179 if not nestinglevel:
180 begin = pos
181 nestinglevel += 1
182 elif not nestinglevel:
183 raise ValueError("begin marker expected")
184 elif data[pos+1] == 0o331:
185 nestinglevel -= 1
186 if not nestinglevel:
187 end = pos + 2
188 break
189 elif data[pos+1] in [0o300, 0o302]:
190 l, bits, height, width, components = struct.unpack(">HBHHB", data[pos+2:pos+10])
191 if bits != 8:
192 raise ValueError("implementation limited to 8 bit per component only")
193 try:
194 mode = {1: "L", 3: "RGB", 4: "CMYK"}[components]
195 except KeyError:
196 raise ValueError("invalid number of components")
197 pos += l+1
198 elif data[pos+1] == 0o340:
199 l, id, major, minor, dpikind, xdpi, ydpi = struct.unpack(">H5sBBBHH", data[pos+2:pos+16])
200 if dpikind == 1:
201 self.info = {"dpi": (xdpi, ydpi)}
202 elif dpikind == 2:
203 self.info = {"dpi": (xdpi*2.54, ydpi*2.45)}
204 # else do not provide dpi information
205 pos += l+1
206 pos += 1
207 except IndexError:
208 raise ValueError("end marker expected")
209 image.__init__(self, width, height, mode, data[begin:end], compressed="DCT")
211 def save(self, file, format=None, **attrs):
212 if format != "jpeg":
213 raise RuntimeError("JPG image can be output as JPG only.")
214 file.write(self.data)
217 class PSimagedata(pswriter.PSresource):
219 def __init__(self, name, data, singlestring, maxstrlen):
220 pswriter.PSresource.__init__(self, "imagedata", name)
221 self.data = data
222 self.singlestring = singlestring
223 self.maxstrlen = maxstrlen
225 def output(self, file, writer, registry):
226 file.write("%%%%BeginRessource: %s\n" % self.id)
227 if self.singlestring:
228 file.write("%%%%BeginData: %i ASCII Lines\n"
229 "<~" % ascii85lines(len(self.data)))
230 ascii85stream(file, self.data)
231 file.write("~>\n"
232 "%%EndData\n")
233 else:
234 datalen = len(self.data)
235 tailpos = datalen - datalen % self.maxstrlen
236 file.write("%%%%BeginData: %i ASCII Lines\n" %
237 ((tailpos/self.maxstrlen) * ascii85lines(self.maxstrlen) +
238 ascii85lines(datalen-tailpos)))
239 file.write("[ ")
240 for i in range(0, tailpos, self.maxstrlen):
241 file.write("<~")
242 ascii85stream(file, self.data[i: i+self.maxstrlen])
243 file.write("~>\n")
244 if datalen != tailpos:
245 file.write("<~")
246 ascii85stream(file, self.data[tailpos:])
247 file.write("~>")
248 file.write("]\n"
249 "%%EndData\n")
250 file.write("/%s exch def\n" % self.id)
251 file.write("%%EndRessource\n")
254 class PDFimagepalettedata(pdfwriter.PDFobject):
256 def __init__(self, name, data):
257 pdfwriter.PDFobject.__init__(self, "imagepalettedata", name)
258 self.data = data
260 def write(self, file, writer, registry):
261 file.write("<<\n"
262 "/Length %d\n" % len(self.data))
263 file.write(">>\n"
264 "stream\n")
265 file.write(self.data)
266 file.write("\n"
267 "endstream\n")
270 class PDFimage(pdfwriter.PDFobject):
272 def __init__(self, name, width, height, palettemode, palettedata, mode,
273 bitspercomponent, compressmode, data, smask, registry, addresource=True):
274 pdfwriter.PDFobject.__init__(self, "image", name)
276 if addresource:
277 if palettedata is not None:
278 procset = "ImageI"
279 elif mode == "L":
280 procset = "ImageB"
281 else:
282 procset = "ImageC"
283 registry.addresource("XObject", name, self, procset=procset)
284 if palettedata is not None:
285 # note that acrobat wants a palette to be an object (which clearly is a bug)
286 self.PDFpalettedata = PDFimagepalettedata(name, palettedata)
287 registry.add(self.PDFpalettedata)
289 self.name = name
290 self.width = width
291 self.height = height
292 self.palettemode = palettemode
293 self.palettedata = palettedata
294 self.mode = mode
295 self.bitspercomponent = bitspercomponent
296 self.compressmode = compressmode
297 self.data = data
298 self.smask = smask
300 def write(self, file, writer, registry):
301 file.write("<<\n"
302 "/Type /XObject\n"
303 "/Subtype /Image\n"
304 "/Width %d\n" % self.width)
305 file.write("/Height %d\n" % self.height)
306 if self.palettedata is not None:
307 file.write("/ColorSpace [ /Indexed %s %i\n" % (devicenames[self.palettemode], len(self.palettedata)/3-1))
308 file.write("%d 0 R\n" % registry.getrefno(self.PDFpalettedata))
309 file.write("]\n")
310 else:
311 file.write("/ColorSpace %s\n" % devicenames[self.mode])
312 if self.smask:
313 file.write("/SMask %d 0 R\n" % registry.getrefno(self.smask))
314 file.write("/BitsPerComponent %d\n" % self.bitspercomponent)
315 file.write("/Length %d\n" % len(self.data))
316 if self.compressmode:
317 file.write("/Filter /%sDecode\n" % self.compressmode)
318 file.write(">>\n"
319 "stream\n")
320 file.write_bytes(self.data)
321 file.write("\n"
322 "endstream\n")
326 class bitmap_trafo(baseclasses.canvasitem):
328 def __init__(self, trafo, image,
329 PSstoreimage=0, PSmaxstrlen=4093, PSbinexpand=1,
330 compressmode="Flate", flatecompresslevel=6,
331 dctquality=75, dctoptimize=0, dctprogression=0):
332 self.pdftrafo = trafo
333 self.image = image
334 self.imagewidth, self.imageheight = image.size
336 self.PSstoreimage = PSstoreimage
337 self.PSmaxstrlen = PSmaxstrlen
338 self.PSbinexpand = PSbinexpand
339 self.compressmode = compressmode
340 self.flatecompresslevel = flatecompresslevel
341 self.dctquality = dctquality
342 self.dctoptimize = dctoptimize
343 self.dctprogression = dctprogression
345 try:
346 self.imagecompressed = image.compressed
347 except:
348 self.imagecompressed = None
349 if self.compressmode not in [None, "Flate", "DCT"]:
350 raise ValueError("invalid compressmode '%s'" % self.compressmode)
351 if self.imagecompressed not in [None, "Flate", "DCT"]:
352 raise ValueError("invalid compressed image '%s'" % self.imagecompressed)
353 if self.compressmode is not None and self.imagecompressed is not None:
354 raise ValueError("compression of a compressed image not supported")
355 if not haszlib and self.compressmode == "Flate":
356 logger.warning("zlib module not available, disable compression")
357 self.compressmode = None
359 def imagedata(self, interleavealpha):
360 """ Returns a tuple (mode, data, alpha, palettemode, palettedata)
361 where mode does not contain the alpha channel anymore.
363 If there is an alpha channel, for interleavealpha == False it is
364 returned as a band in alpha itself. For interleavealpha == True
365 alpha will be True and the channel is interleaved in front of each
366 pixel in data.
369 alpha = palettemode = palettedata = None
370 data = self.image
371 mode = data.mode
372 if mode.startswith("A"):
373 mode = mode[1:]
374 if interleavealpha:
375 alpha = True
376 else:
377 bands = data.split()
378 alpha = bands[0]
379 data = image(self.imagewidth, self.imageheight, mode,
380 b"".join([bytes(values)
381 for values in zip(*[band.tobytes()
382 for band in bands[1:]])]), palette=data.palette)
383 if mode.endswith("A"):
384 bands = data.split()
385 mode = mode[:-1]
386 if interleavealpha:
387 alpha = True
388 bands = list(bands[-1:]) + list(bands[:-1])
389 data = image(self.imagewidth, self.imageheight, "A%s" % mode,
390 b"".join([bytes(values)
391 for values in zip(*[band.tobytes()
392 for band in bands])]), palette=data.palette)
393 else:
394 alpha = bands[-1]
395 data = image(self.imagewidth, self.imageheight, mode,
396 b"".join([bytes(values)
397 for values in zip(*[band.tobytes()
398 for band in bands[:-1]])]), palette=data.palette)
400 if mode == "P":
401 palettemode, palettedata = data.palette.getdata()
402 if palettemode not in ["L", "RGB", "CMYK"]:
403 logger.warning("image with invalid palette mode '%s' converted to rgb image" % palettemode)
404 data = data.convert("RGB")
405 mode = "RGB"
406 palettemode = None
407 palettedata = None
408 elif len(mode) == 1:
409 if mode != "L":
410 logger.warning("specific single channel image mode not natively supported, converted to regular grayscale")
411 data = data.convert("L")
412 mode = "L"
413 elif mode not in ["CMYK", "RGB"]:
414 logger.warning("image with invalid mode converted to rgb")
415 data = data.convert("RGB")
416 mode = "RGB"
418 if self.compressmode == "Flate":
419 data = zlib.compress(data.tobytes(), self.flatecompresslevel)
420 elif self.compressmode == "DCT":
421 data = data.tobytes("jpeg", mode, self.dctquality, self.dctoptimize, self.dctprogression)
422 else:
423 data = data.tobytes()
424 if alpha and not interleavealpha:
425 # we might want a separate alphacompressmode
426 if self.compressmode == "Flate":
427 alpha = zlib.compress(alpha.tobytes(), self.flatecompresslevel)
428 elif self.compressmode == "DCT":
429 alpha = alpha.tobytes("jpeg", mode, self.dctquality, self.dctoptimize, self.dctprogression)
430 else:
431 alpha = alpha.tobytes()
433 return mode, data, alpha, palettemode, palettedata
435 def bbox(self):
436 bb = bbox.empty()
437 bb.includepoint_pt(*self.pdftrafo.apply_pt(0.0, 0.0))
438 bb.includepoint_pt(*self.pdftrafo.apply_pt(0.0, 1.0))
439 bb.includepoint_pt(*self.pdftrafo.apply_pt(1.0, 0.0))
440 bb.includepoint_pt(*self.pdftrafo.apply_pt(1.0, 1.0))
441 return bb
443 def processPS(self, file, writer, context, registry, bbox):
444 mode, data, alpha, palettemode, palettedata = self.imagedata(True)
445 pstrafo = trafo.translate_pt(0, -1.0).scaled(self.imagewidth, -self.imageheight)*self.pdftrafo.inverse()
447 PSsinglestring = self.PSstoreimage and len(data) < self.PSmaxstrlen
448 if PSsinglestring:
449 PSimagename = "image-%d-%s-singlestring" % (id(self.image), self.compressmode)
450 else:
451 PSimagename = "image-%d-%s-stringarray" % (id(self.image), self.compressmode)
453 if self.PSstoreimage and not PSsinglestring:
454 registry.add(pswriter.PSdefinition("imagedataaccess",
455 b"{ /imagedataindex load " # get list index
456 b"dup 1 add /imagedataindex exch store " # store increased index
457 b"/imagedataid load exch get }")) # select string from array
458 if self.PSstoreimage:
459 registry.add(PSimagedata(PSimagename, data, PSsinglestring, self.PSmaxstrlen))
460 bbox += self.bbox()
462 file.write("gsave\n")
463 if palettedata is not None:
464 file.write("[ /Indexed %s %i\n" % (devicenames[palettemode], len(palettedata)/3-1))
465 file.write("%%%%BeginData: %i ASCII Lines\n" % ascii85lines(len(palettedata)))
466 file.write("<~")
467 ascii85stream(file, palettedata)
468 file.write("~>\n"
469 "%%EndData\n")
470 file.write("] setcolorspace\n")
471 else:
472 file.write("%s setcolorspace\n" % devicenames[mode])
474 if self.PSstoreimage and not PSsinglestring:
475 file.write("/imagedataindex 0 store\n" # not use the stack since interpreters differ in their stack usage
476 "/imagedataid %s store\n" % PSimagename)
478 file.write("<<\n")
479 if alpha:
480 file.write("/ImageType 3\n"
481 "/DataDict\n"
482 "<<\n")
483 file.write("/ImageType 1\n"
484 "/Width %i\n" % self.imagewidth)
485 file.write("/Height %i\n" % self.imageheight)
486 file.write("/BitsPerComponent 8\n"
487 "/ImageMatrix %s\n" % pstrafo)
488 file.write("/Decode %s\n" % decodestrings[mode])
490 file.write("/DataSource ")
491 if self.PSstoreimage:
492 if PSsinglestring:
493 file.write("/%s load" % PSimagename)
494 else:
495 file.write("/imagedataaccess load") # some printers do not allow for inline code here -> we store it in a resource
496 else:
497 if self.PSbinexpand == 2:
498 file.write("currentfile /ASCIIHexDecode filter")
499 else:
500 file.write("currentfile /ASCII85Decode filter")
501 if self.compressmode or self.imagecompressed:
502 file.write(" /%sDecode filter" % (self.compressmode or self.imagecompressed))
503 file.write("\n")
505 file.write(">>\n")
507 if alpha:
508 file.write("/MaskDict\n"
509 "<<\n"
510 "/ImageType 1\n"
511 "/Width %i\n" % self.imagewidth)
512 file.write("/Height %i\n" % self.imageheight)
513 file.write("/BitsPerComponent 8\n"
514 "/ImageMatrix %s\n" % pstrafo)
515 file.write("/Decode [1 0]\n"
516 ">>\n"
517 "/InterleaveType 1\n"
518 ">>\n")
520 if self.PSstoreimage:
521 file.write("image\n")
522 else:
523 if self.PSbinexpand == 2:
524 file.write("%%%%BeginData: %i ASCII Lines\n"
525 "image\n" % (asciihexlines(len(data)) + 1))
526 asciihexstream(file, data)
527 file.write(">\n")
528 else:
529 # the datasource is currentstream (plus some filters)
530 file.write("%%%%BeginData: %i ASCII Lines\n"
531 "image\n" % (ascii85lines(len(data)) + 1))
532 ascii85stream(file, data)
533 file.write("~>\n")
534 file.write("%%EndData\n")
536 file.write("grestore\n")
538 def processPDF(self, file, writer, context, registry, bbox):
539 mode, data, alpha, palettemode, palettedata = self.imagedata(False)
541 name = "image-%d-%s" % (id(self.image), self.compressmode or self.imagecompressed)
542 if alpha:
543 alpha = PDFimage("%s-smask" % name, self.imagewidth, self.imageheight,
544 None, None, "L", 8,
545 self.compressmode, alpha, None, registry, addresource=False)
546 registry.add(alpha)
547 registry.add(PDFimage(name, self.imagewidth, self.imageheight,
548 palettemode, palettedata, mode, 8,
549 self.compressmode or self.imagecompressed, data, alpha, registry))
551 bbox += self.bbox()
553 file.write("q\n")
554 self.pdftrafo.processPDF(file, writer, context, registry)
555 file.write("/%s Do\n" % name)
556 file.write("Q\n")
558 def processSVG(self, xml, writer, context, registry, bbox):
559 if self.compressmode == "Flate":
560 f = io.BytesIO()
561 self.image.save(f, "png")
562 inlinedata = "data:image/png;base64," + binascii.b2a_base64(f.getvalue()).decode('ascii').replace("\n", "")
563 elif self.compressmode == "DCT" or self.imagecompressed == "DCT":
564 f = io.BytesIO()
565 self.image.save(f, "jpeg")
566 inlinedata = "data:image/jpeg;base64," + binascii.b2a_base64(f.getvalue()).decode('ascii').replace("\n", "")
567 else:
568 raise ValueError("SVG cannot store uncompressed image data.")
569 attrs = {"preserveAspectRatio": "none", "x": "0", "y": "-1", "width": "1", "height": "1", "xlink:href": inlinedata}
570 self.pdftrafo.processSVGattrs(attrs, writer, context, registry)
571 xml.startSVGElement("image", attrs)
572 xml.endSVGElement("image")
575 class bitmap_pt(bitmap_trafo):
577 def __init__(self, xpos_pt, ypos_pt, image, width_pt=None, height_pt=None, ratio=None, **kwargs):
578 imagewidth, imageheight = image.size
579 if width_pt is not None or height_pt is not None:
580 if width_pt is None:
581 if ratio is None:
582 width_pt = height_pt * imagewidth / float(imageheight)
583 else:
584 width_pt = ratio * height_pt
585 elif height_pt is None:
586 if ratio is None:
587 height_pt = width_pt * imageheight / float(imagewidth)
588 else:
589 height_pt = (1.0/ratio) * width_pt
590 elif ratio is not None:
591 raise ValueError("can't specify a ratio when setting width_pt and height_pt")
592 else:
593 if ratio is not None:
594 raise ValueError("must specify width_pt or height_pt to set a ratio")
595 widthdpi, heightdpi = image.info["dpi"] # fails when no dpi information available
596 width_pt = 72.0 * imagewidth / float(widthdpi)
597 height_pt = 72.0 * imageheight / float(heightdpi)
599 bitmap_trafo.__init__(self, trafo.trafo_pt(((float(width_pt), 0.0), (0.0, float(height_pt))), (float(xpos_pt), float(ypos_pt))), image, **kwargs)
602 class bitmap(bitmap_pt):
604 def __init__(self, xpos, ypos, image, width=None, height=None, **kwargs):
605 xpos_pt = unit.topt(xpos)
606 ypos_pt = unit.topt(ypos)
607 if width is not None:
608 width_pt = unit.topt(width)
609 else:
610 width_pt = None
611 if height is not None:
612 height_pt = unit.topt(height)
613 else:
614 height_pt = None
616 bitmap_pt.__init__(self, xpos_pt, ypos_pt, image, width_pt=width_pt, height_pt=height_pt, **kwargs)