Loading the color palette from a BBM/LBM image file using C#
I took a break from arguing with our GIF decoder to take a quick look at the BBM format as I have a few files in that format containing colour palettes I wished to extract. When I looked into this, I found a BBM file is essentially an LBM file without any image data, so I set to work at writing a new palette serializer for reading and writing the palette files. This article describes how to read the palettes from BBM and LBM files.
Note: I only cover loading of color palette data in this article. The image data I don't even look at - this article does not represent a full LBM decoder.
Caveat Emptor
The sample code presented in this article took all of an hour to write and has been tested on a pretty small selection of images. It only handles 8bit LBM files (possibly lower depths too, but I have not tested this). And, who knows, I might be misinterpreting the specification or missing a chunk of vital information.
Overview of BBM/LBM Files
Information is sketchy so I may well be wrong in particulars, but a BBM file essentially seems to be a LBM without a full image - in the files I've experimented with, there are header chunks describing a bitmap, but no real data. A LBM file is of course a full graphic, most popular (I think) by DeluxePaint on both the Amiga and MS DOS. As I said though, this information is my understanding and might be totally wrong. Luckily enough, or our purposes it doesn't matter. However, to keep things simple, for the rest of this article I'm going to refer to LBM, but you can consider this interchangeable with BBM.
The LBM format is more formally known as ILBM - IFF Interleaved Bitmap. It is built upon "EA IFF 85" Standard for Interchange Format Files. Both formats were devised by Electronic Arts as a standard means of sharing data between systems, their example of writing a theme song with a Macintosh score editor and incorporating it into an Amiga game summing it up neatly.
More information on these formats can be found in specification documents here and here.
Reading an LBM file
An IFF file is comprised of chunks of data. Each chunk is prefixed with a four character ID, the size of the data in the chunk, and then the chunk data itself.
There is one oddity in that if the size of the chunk is odd, an extra padding byte is added to the chunk data to make it even. This padding byte is not included in the size field, so if it's odd you must make sure you read (and discard) the padding byte.
Oh yes, and there's one other important detail. I don't know if this is specific to all IFF format files, or just LBM, but integers and longs are in big-endian format, so we need to convert these when we read them.
The CMAP chunk
The only section of the LBM file we are interested in is the
CMAP
chunk, which describes an 8bit colour palette. According
to the specification however, it's optional so it's entirely
possible that not all LBM files contain a CMAP
. Also, only
8bit (or lower?) LBM files will contain a CMAP
, as they only
support RGB channels. 24bit or 32bit images won't have one as
there's no scope for storing alpha channels.
The data section of a CMAP
chunk is as simple as it gets - one
set of 3 bytes for each colour describing the red, green and
blue channels. The size attribute is the number of colours * 3.
Other Chunks
Although I'm not reading other chunks, I still have to pay attention to some of them.
Firstly, the FORM
chunk describes an IFF document. So, if the
file we read doesn't start with this, it's not a valid IFF file
and we shouldn't continue reading.
The second chunk we at least want to identify is the chunk that
states if this is an actual image. The specification states that
this should be ILBM
, but the sample images I've worked with
use a different header which is PBM
for Planar BitMap. Note
there's a trailing space on this ID as the specification states
ID's are four ASCII characters long. As in both cases the CMAP
section is the same, I look for either of these.
Anything else will be discarded.
Reading the file
After opening the file, the first thing we do is read the first
four bytes and convert these to an ASCII string. If the string
reads FORM
, we know we have an IFF document and continue
reading. Otherwise, we throw an InvalidDataException
exception.
using (FileStream stream = File.OpenRead(fileName))
{
byte[] buffer;
string header;
// read the FORM header that identifies the document as an IFF file
buffer = new byte[4];
stream.Read(buffer, 0, buffer.Length);
if (Encoding.ASCII.GetString(buffer) != "FORM")
throw new InvalidDataException("Form header not found.");
Next we read the size of the data contained within the FORM
chunk. As we aren't checking for nested chunks nor reading all
the data, we can safely ignore this.
// the next value is the size of all the data in the FORM chunk
// We don't actually need this value, but we have to read it
// regardless to advance the stream
this.ReadInt(stream);
Time for another sanity check, this time to verify we are
reading an image, be it Planar (PBM
) or Interleaved (ILBM
).
For some reason this chunk doesn't include a size, so we don't attempt to read any more bytes as the next byte is the start of a new chunk.
// read either the PBM or ILBM header that identifies this document as an image file
stream.Read(buffer, 0, buffer.Length);
header = Encoding.ASCII.GetString(buffer);
if (header != "PBM " && header != "ILBM")
throw new InvalidDataException("Bitmap header not found.");
The reset of the routine is going to load one chunk of data from the file at a time, and either discard it or process it.
First, we read 4 bytes that will be the ID of the chunk. We also need the size of the chunk, regardless of whether we use it or not, so we'll read that too.
while (stream.Read(buffer, 0, buffer.Length) == buffer.Length)
{
int chunkLength;
chunkLength = this.ReadInt(stream);
As we are only interested in CMAP
chunks, if the pending chunk
has any other type of ID, we skip the remainder of the chunk, as
identified by chunkLength
read earlier. If we can, we just
move the current position in the stream ahead, but if we can't
(can't think why not!) then we just read and discard bytes until
done.
if (Encoding.ASCII.GetString(buffer) != "CMAP")
{
// some other LBM chunk, skip it
if (stream.CanSeek)
stream.Seek(chunkLength, SeekOrigin.Current);
else
{
for (int i = 0; i < chunkLength; i++)
stream.ReadByte();
}
}
Aha! We finally found the CMAP
chunk. Now it's just a
straightforward reading of colours. chunkLength
is the number
of colours / 3 (as each colour is represented by 3 bytes), so a
simple loop to read each triplet and add them to our results
collection is all we need.
Then, we exit out of the while
loop - no pointing reading the
entire file now that we have what we wanted.
else
{
// color map chunk!
for (int i = 0; i < chunkLength / 3; i++)
{
int r;
int g;
int b;
r = stream.ReadByte();
g = stream.ReadByte();
b = stream.ReadByte();
colorPalette.Add(Color.FromArgb(r, g, b));
}
// all done so stop reading the rest of the file
break;
}
If we are still in the loop however, then we need to check our
chunkLength
value. If it's odd, we read and discard the
padding byte - otherwise you'll be out of alignment and won't
hit any more chunk headers, except by accident.
// chunks always contain an even number of bytes even if the recorded length is odd
// if the length is odd, then there's a padding byte in the file - just read and discard
if (chunkLength % 2 != 0)
stream.ReadByte();
}
}
return colorPalette;
}
Converting big-endian to little-endian
At the start of the article I mentioned that the numeric data types in an LBM image are stored as big-endian. On the Windows platform, we use little-endian. So when we try to read the chunk length from the file... well, it's just not going to work.
As bit shifting still jellies my brain, I took to Stack Overflow which provided me with a function for converting four bytes of big-endian data into a little-endian integer.
private int ReadInt(Stream stream)
{
byte[] buffer;
// big endian conversion: http://stackoverflow.com/a/14401341/148962
buffer = new byte[4];
stream.Read(buffer, 0, buffer.Length);
return (buffer[0] << 24) | (buffer[1] << 16) | (buffer[2] << 8) | buffer[3];
}
So in our sample, we read our 4 bytes, shift their bits around and return the result.
That was easy
That was fairly straightforward! Well, if I ignore the endian
conversion. And I'm sure if I decided to read the BODY
chunk
and actually start decoding the image itself I'd be tearing out
my hair, but the bit I actually wanted could hardly have been
easier.
As usual, a fully working sample is attached to this post.
Further Thoughts
As I wrap up this post it occurred to me I forgot to add
anything in for if it's a valid LBM image, but doesn't contain a
CMAP
section. Although the clue would be in the empty list
that's returned.
I also didn't even begin to look at writing a BBM file... this will probably be the next thing I take a look at. Unless I get distracted by Microsoft's (old?) palette format which I discovered is also a variant of IFF and should be just as easy to read.
Update History
- 2014-01-11 - First published
- 2020-11-21 - Updated formatting
Downloads
Filename | Description | Version | Release Date | |
---|---|---|---|---|
BbmPaletteLoader.zip
|
Sample project for the article loading the color palette from a BBM/LBM image file using C# |
1.0.0.0 | 11/01/2014 | Download |
Leave a Comment
While we appreciate comments from our users, please follow our posting guidelines. Have you tried the Cyotek Forums for support from Cyotek and the community?