[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