Attached is an updated ImagePlugin for PIL to read (some) RLE-compressed
SGI images. I needed to load RLE-compressed images for a program of mine
and didn't like that I had to use PythonMagick for this. William Baxter
asked about this on the list about half a year ago as well, I believe.
The former SGI ImagePlugin used the wrong orientation when loading SGI
images, too.
It took me quite some time to come up with a way to extend the image
loading mechanism of PIL. What I came up with is possibly quite a hack,
but the best I could do by reading the source code for ImageFile.py,
TiffImagePlugin.py and Unpack.c as reference. I didn't find out how to
implement a custom decoder instead of the default "raw" encoder, as
there seemed to be a lack of documentation for this?
You'll find a couple of FIXME tags in the code where I didn't know how
to tell PIL what I wanted to do, for example when it comes to loading
some 16 bit channel data. Proper options for the tile list are still a
mystery to me, since Unpack.c has some modes like "xxx;L" and "xxx;16B"
which don't seem to be supported by the raw decoder yet, or at least I
couldn't find the right combination of self.mode and "raw" read mode.
I tested this against a couple of *.sgi and *.rgba images that I found
via filewatcher.com and it worked fine on those that it supported. Some
nasty unsupported images can be found at Toby Thain's Telegraphics site
where he keeps a SGI format plugin for Photoshop. For example, only
"normal" Colormaps are supported at the moment, although things like
dithered images wouldn't be too hard to add (though obsolete). I didn't
have the chance to test greyscale images, but they should load okay.
I don't know what the requirements are for using this code, as this is
just my second program in Python and I have no idea how portable my code
is towards older versions of Python. I have developed it under Python
2.4.4 myself and would expect that one would need at least Python 2.0.
Any comments on this, new testdata for monochrome or dithered images, or
tips for to address some of my FIXME's are highly welcome.
In case this gets added to the official PIL codebase, I have found a
couple of other ImagePlugins while searching for reference code on the
net that I'd propose to add:
- Bob Ippolito made plugins for the Mac OS X icns file format and for
the SoftImage format, however it seems like only the IcnsImagePlugin.py
was added. The other one can be found at
https://svn.red-bean.com/bob/SoftimageImage/trunk/
If you Google for the filename you'll find that some others have found
interest in it, too.
- John Wright has code for a plugin online which can read and write
Nokia operator logos in the OTB and GMS format:
http://www.dryfish.org/projects/NokiaImagePlugin.py.html
- Oliver Jowett developed a plugin to read a variety of DDS texture
files. The original page is gone, but can still be observed in
archive.org, the code has been picked up and improved by two separate
parties for some "Dark Age of Camelot" projects. Here are some links:
http://web.archive.org/web/20031205022205/http://www.randomly.org/projects/mapper/index.html
(archived version of the original site for the code)
http://www.ceejbot.com/DAoC/ (a fixed version of the DAoC Mapper)
http://www.ceejbot.com/DAoC/tools/DdsImageFile.py (download there)
http://nathrach.republicofnewhome.org/mappergui.html#reqs (download
MapperGUI 2.4)
The two versions of this plugin differ in only six lines, it seems that
in the original source code some color channels were switched.
Cheers,
Karsten Hiddemann
#
# The Python Imaging Library.
# $Id: SgiImagePlugin.py 2134 2004-10-06 08:55:20Z fredrik $
#
# SGI image file handling
#
# See "SGI RGB Image Format", Paul Haeberli.
# <http://local.wasp.uwa.edu.au/~pbourke/dataformats/sgirgb/>
# <http://www.fileformat.info/format/sgiimage/>
#
# History:
# 1995-09-10 fl Created
# 2008-04-30 kh Added support for RLE compression, fixed orientation
#
# Copyright (c) 2008 by Karsten Hiddemann.
# Copyright (c) 1997 by Secret Labs AB.
# Copyright (c) 1995 by Fredrik Lundh.
#
# See the README file for information on usage and redistribution.
#
__version__ = "0.2"
import string
import Image, ImageFile
try:
xrange(0)
except NameError:
xrange = range
#
# detect system's endianess
#
# FIXME: wouldn't it be faster to combine this with byteswap() and read
# in a whole array with fromfile() instead of i16/i32 reads everywhere?
# Or isn't the itemsize of 'L' necessarily 4, which could be a problem?
try:
from sys import byteorder
def little_endian():
return byteorder == "little"
def big_endian():
return byteorder == "big"
except ImportError:
from struct import pack
def little_endian():
return pack('<h', 1) == pack('=h',1)
def big_endian():
return pack('>h', 1) == pack('=h',1)
#
# read big endian short
def i16(c):
return ord(c[1]) + (ord(c[0])<<8)
#
# read big endian long
def i32(c):
return ord(c[3]) + (ord(c[2])<<8) + (ord(c[1])<<16) + (ord(c[0])<<24)
MODES = {
# (bytes per channel, dimension, zsize) => rawmode
(1, 1, 1): 'L',
(1, 2, 1): 'L',
(1, 3, 3): 'RGB',
(1, 3, 4): 'RGBA',
# FIXME: how to tell the raw decoder that there are separate 16 bit
channels?
# (2, 1, 1): 'L;16',
# (2, 2, 1): 'L;16',
# (2, 3, 3): 'RGB;16',
# (2, 3, 4): 'RGBA;16',
}
def _accept(prefix):
return i16(prefix) == 474
def _rle_decode(s):
i = 0
o = ''
while 1:
pixel = ord(s[i])
count = pixel & 0x7F
if not count:
break
i += 1
if (pixel & 0x80):
o += s[i:i+count]
i += count
else:
o += s[i]*count
i += 1
return o
##
# Image plugin for SGI images.
# implemented according to file format information from:
# http://www.fileformat.info/format/sgiimage/
# ftp://ftp.sgi.com/sgi/graphics/grafica/sgiimage.html
# http://local.wasp.uwa.edu.au/~pbourke/dataformats/sgirgb/
# testdata from:
# http://www.telegraphics.com.au/svn/sgiformat/testdata/
# http://www.telegraphics.com.au/svn/sgiformat/trunk/adobebugs/
class SgiImageFile(ImageFile.ImageFile):
format = 'SGI'
format_description = 'SGI Image File Format'
def _open(self):
# HEAD
s = self.fp.read(110)
if not _accept(s):
raise SyntaxError, 'not an SGI image file'
# relevant header entries
compression = ord(s[2])
bpc = ord(s[3])
dim = i16(s[4:])
xsize = i16(s[6:])
ysize = i16(s[8:])
zsize = i16(s[10:])
colormap = i32(s[105:])
# print ("compression: %d, bpc: %d, dim: %d, xsize: %d, ysize:
%d, zsize: %d, colormap: %d") % (compression, bpc, dim, xsize, ysize, zsize,
colormap)
# colormap isn't even used in the former rgbimgmodule
implementation
if colormap != 0:
raise SyntaxError, 'only normal colormaps are supported'
# bytes, dimension, zsize
layout = (bpc, dim, zsize)
# single scanline images, probably redundant
if dim == 1:
ysize = 1
# size
self.size = (xsize, ysize)
# decoder info
try:
self.mode = MODES[layout]
except KeyError:
raise SyntaxError, 'unsupported SGI image mode'
# decoder info
self.compression = compression
self.channelsize = bpc*xsize*ysize
self.tile = []
for i in xrange(zsize):
rawmode = self.mode[i] # one out of "LRGBA"
# FIXME: change rawmode for 16 bit, what's the right
way to do it?
# if bpc == 2:
# rawmode = 'I;16B'
self.tile.append((
'raw',
(0,0)+self.size,
512+self.channelsize*i,
(rawmode, 0, -1)
))
# FIXME: the memory mapping code in ImageFile could cause
decompression to fail?
# self.mode == "L" would trigger memory mapping and ignore our
seek/read wrappers
if compression == 1:
self.tile.append(("", (), 0, ()))
def load_prepare(self):
ImageFile.ImageFile.load_prepare(self)
if self.compression == 1:
# FIXME: remove the bogus tile again, see above
self.tile.pop()
# decode one channel at a time
self.decodermaxblock = self.channelsize
# prepare tables
self._rle_load_tables()
# file offset => channel map for the RLE decoding
self.seek2channel = {}
for (i, channel) in enumerate(self.tile):
self.seek2channel[channel[2]] = i
# make seek/read wrappers accessible
self.load_seek = self._rle_seek
self.load_read = self._rle_read
def _rle_load_tables(self):
self.fp.seek(512)
self.offsets = []
for channel in xrange(len(self.tile)):
o = []
s = self.fp.read(4*self.size[1])
for y in xrange(self.size[1]):
o.append(i32(s[4*y:]))
self.offsets.append(o)
self.counts = []
for channel in xrange(len(self.tile)):
c = []
s = self.fp.read(4*self.size[1])
for y in xrange(self.size[1]):
c.append(i32(s[4*y:]))
self.counts.append(c)
def _rle_seek(self, pos):
self.readingchannel = self.seek2channel[pos]
return self.fp.seek(pos)
def _rle_read(self, bytes):
if bytes != self.channelsize:
raise SyntaxError, 'invalid read operation, must read
the full channel data'
r = ''
# we know which channel the decoder wants to read, decode it
for y in xrange(self.size[1]):
# lots of seeks/reads are a waste of time, we should
better
# buffer the whole file in memory if possible. maybe in
0.3
self.fp.seek(self.offsets[self.readingchannel][y])
s = self.fp.read(self.counts[self.readingchannel][y])
# FIXME: we can try decoding what we have or force an
IOError here
# if (len(s) != self.counts[self.readingchannel][y])
# return None
r += _rle_decode(s)
return r
#
# registry
Image.register_open(SgiImageFile.format, SgiImageFile, _accept)
Image.register_extension(SgiImageFile.format, '.bw')
Image.register_extension(SgiImageFile.format, '.rgb')
Image.register_extension(SgiImageFile.format, '.rgba')
Image.register_extension(SgiImageFile.format, '.sgi')
_______________________________________________
Image-SIG maillist - Image-SIG@python.org
http://mail.python.org/mailman/listinfo/image-sig