[Image-SIG] Problems with XpmImagePlugin.py
Richard Oudkerk
oudkerk at maths.man.ac.uk
Tue Jul 6 12:32:31 CEST 2004
I have found a few problems related to the XpmImagePlugin filter. (A
patched version is included in the attachment.)
(1) self.tile is defined as
self.tile = [("raw", (0, 0)+self.size, self.fp.tell(),
("P", 0, 1))]
This assumes that the pixel data will have no padding --- but this is
only true if the fall-back XpmImagePlugin.load_read is used because
PIL was unable to use Image.core.map or Image.core.map_buffer.
Therefore the resulting pictures are badly mangled (except when the
fall-back is used).
I think the correct statement (assuming no C-style comments) should be
self.tile = [("raw", (0, 0)+self.size, self.fp.tell()+1,
("P", stride, 1))]
where 'stride' is the length of a full line of pixel data including
the end-of-line character(s).
With this change there is no need to define XpmImagePlugin.load_read
because the default definition of load_read (in ImageFile) works fine.
(2) XpmImagePlugin expects all colours to be specified in the form
"#rrggbb". However the form "#rrrrggggbbbb" also seems to be very
common. Also, the XPM files with actual colour names like "red" or
"SandyBrown" are not accepted.
(3) XpmImagePlugin cannot deal with any C-style comments like
"/* colors */" or "/* pixels */" which appear in most XPM files
(certainly those produced by netpbm and ImageMagick).
(4) In the function PyImaging_MapBuffer() in map.c there is a check to
make sure that we don't have
offset + ysize * stride > bytes
where "bytes" is the length of the buffer. However if there is not
enough padding at the end of the file then this can fail. I have a
couple of XPM files which end with '"};' (with no final '\n' or
'\r\n') where this fails and produces the error "ValueError: buffer is
not large enough".
I think it is sufficient to check is that we don't have
offset + (ysize-1) * stride + xsize > bytes
This will ensure that if "0 <= y < ysize" and "0 <= x < xsize" then
"im->image[y][x]" will always be a point in the buffer.
Making this change seems to solve the problem. (A similar check also
appears in map.c/mapping_readimage() --- maybe a change is needed
there as well.)
(5) If I load a GIF or XPM file where one colour is supposed to be
transparent and then save it as a PNG file, the transparent colour
becomes solid. This is not something that happens if I use
ImageMagick.
(6) ImageColor.colormap misspells 'lightgray' as 'lightgrey'. (I
assume this is a misspelling since it contains 'gray', 'dimgray',
'slategray',... but not 'grey', 'dimgrey', 'slategrey',...)
A patched version of XpmImagePlugin.py which deals with problems (1),
(2) and (3) is in the attachment. (ImageColor.colormap and a dict of
ten or so exceptions is used to deal with colour names like "red" or
"firebrick".)
I have tested PIL on the 2000+ XPM files on my linux partition. With
the old version of XpmImagePlugin.py only a third could be
successfully converted to PNG by PIL (and even then the results were
wrong because of problem (1)).
The patched version can deal with over 90% (although with the
transparency problem mentioned above). Of the remainder a further 7%
use 2 chars/pixel (and so can't be dealt with using the "raw"
decoder), with the rest either being broken in some way or using X11
colours ending in a number such as 'peachpuff4' (which seems to be
deprecated for use XPM files).
I have tested it under under cygwin/python2.3, win32/python2.2 and
linux/python1.5.
Thanks, Richard
-------------- next part --------------
#
# The Python Imaging Library.
# $Id: //modules/pil/PIL/XpmImagePlugin.py#3 $
#
# XPM File handling
#
# History:
# 1996-12-29 fl Created
# 2001-02-17 fl Use 're' instead of 'regex' (Python 2.1) (0.7)
#
# Copyright (c) Secret Labs AB 1997-2001.
# Copyright (c) Fredrik Lundh 1996-2001.
#
# See the README file for information on usage and redistribution.
#
__version__ = "0.2"
import re, string
import Image, ImageFile, ImagePalette
# XPM header
xpm_head = re.compile('\s*"\s*([0-9]*)\s+([0-9]*)\s+([0-9]*)\s+([0-9]*)')
palette_line = re.compile('\s*"(.)([^"]+)"')
def _accept(prefix):
return prefix[:9] == "/* XPM */"
##
# Image plugin for X11 pixel maps.
class XpmImageFile(ImageFile.ImageFile):
format = "XPM"
format_description = "X11 Pixel Map"
def _open(self):
if not _accept(self.fp.read(9)):
raise SyntaxError, "not an XPM file"
# skip forward to next string
while 1:
s = self.fp.readline()
if not s:
raise SyntaxError, "broken XPM file"
m = xpm_head.match(s)
if m:
break
self.size = int(m.group(1)), int(m.group(2))
pal = int(m.group(3))
bpp = int(m.group(4))
if pal > 256 or bpp != 1:
raise ValueError, "cannot read this XPM file"
#
# load palette description
palette = ["\0\0\0"] * 256
for i in range(pal):
# skip forward to next palette entry
while 1:
line = self.fp.readline()
if not line:
raise SyntaxError, "broken XPM file"
result = palette_line.match(line)
if result:
break
# determine position in palette and colour name for this entry
# (if colour name is more than one word then concatenate)
pos = ord(result.group(1))
s = string.split(result.group(2))
try:
beg = end = s.index("c") + 1
except ValueError:
raise ValueError, "failed to parse a palette entry"
while end < len(s) and s[end] not in ("m", "s", "g", "g4"):
end = end + 1
name = string.lower( string.join( s[beg:end], "" ) )
# set the correct value of 'palette[pos]'
if name in ("none", "#background", "#transparent"):
self.info["transparency"] = pos
name = "gray"
r,g,b = getrgb(name)
palette[pos] = chr(r) + chr(g) + chr(b)
# set 'offset' to point at the first byte of the pixel data
# and 'stride' to be the length of the first line of pixel data
while 1:
offset = self.fp.tell()
s = self.fp.readline()
if not s:
raise SyntaxError, "broken XPM file"
if string.lstrip(s)[:1] == '"':
break
offset = offset + string.index(s, '"') + 1
stride = len(s)
self.mode = "P"
self.palette = ImagePalette.raw("RGB", string.join(palette, ""))
self.tile = [("raw", (0, 0)+self.size, offset, ("P", stride, 1))]
#
# Registry
Image.register_open("XPM", XpmImageFile, _accept)
Image.register_extension("XPM", ".xpm")
Image.register_mime("XPM", "image/xpm")
import ImageColor
# ImageColor.colormap uses colour definitions from the CSS3/SVG
# standard which sometimes differs from the XPM standard. Here we
# list the differences (according to
# "lib/ImageMagick-5.5.7/colors.mgk"). Note also that the XPM format
# also allows names of the form 'grayN' where 0 <= N <= 100.
xpmmap = {
# The following have different values in ImageColor.colormap
'gray' : '#bebebe',
'green' : '#00ff00',
'maroon' : '#b03060',
'purple' : '#a020f0',
# The following do not appear in ImageColor.colormap
'lightgoldenrod' : '#eedd82',
'lightslateblue' : '#8470ff',
'mediumforestgreen' : '#32814b',
'mediumgoldenrod' : '#d1c166',
'navyblue' : '#000080',
'violetred' : '#d02090',
# The following is misspelt 'lightgrey' in ImageColor.colormap
'lightgray' : '#d3d3d3'
}
def getrgb(name):
"converts an XPM colour name to a tuple (r,g,b) where 0 <= r,g,b < 256"
name = string.lower(name)
name = string.replace(name, " ", "")
name = string.replace(name, "grey", "gray")
if name[0] == "#":
if len(name)-1 in (3,6,12):
rgb = name[1:]
else:
raise ValueError, "unrecognized color " + name
elif name[0:4] == "gray" and len(name) > 4 and \
'0' <= name[4] <= '9':
temp = int( string.atoi(name[4:]) * (255.0/100.0) + 0.5 )
rgb = "%02x%02x%02x" % (temp,temp,temp)
elif name in xpmmap.keys():
rgb = xpmmap[name][1:]
else:
try:
rgb = ImageColor.colormap[name][1:]
except KeyError:
raise ValueError, "unrecognized color " + name
d = len(rgb) / 3
fact = 255.0 / (16**d-1)
return ( int( string.atoi(rgb[0:d], 16) * fact + 0.5 ),
int( string.atoi(rgb[d:2*d], 16) * fact + 0.5 ),
int( string.atoi(rgb[2*d:3*d], 16) * fact + 0.5 ) )
More information about the Image-SIG
mailing list