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 Stackoverflow 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.

Downloads

Filename Description Version Release Date
BbmPaletteLoader.zip
  • md5: 277a85ced16921e1e3ae9967aba61e71

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

About The Author

Gravatar

The founder of Cyotek, Richard enjoys creating new blog content for the site. Much more though, he likes to develop programs, and can often found writing reams of code. A long term gamer, he has aspirations in one day creating an epic video game. Until that time, he is mostly content with adding new bugs to WebCopy and the other Cyotek products.

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?