Cyotek Development Bloghttps://devblog.cyotek.com/tag/bbm/atom.xml2014-01-11T15:26:50ZLoading the color palette from a BBM/LBM image file using C#urn:uuid:71168b3e-e6fa-4939-aff9-d8ad469c95e72014-01-11T15:26:50Z2014-01-11T15:26:50Z<p>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.</p>
<blockquote>
<p>Note: I <em>only</em> 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.</p>
</blockquote>
<figure class="screenshot" ><a href="https://images.cyotek.com/image/devblog/lbmpaletteloader.png" class="gallery" title="A sample application showing loading of palettes from BBM/LBM files" ><img src="https://images.cyotek.com/image/thumbnail/devblog/lbmpaletteloader.png" alt="A sample application showing loading of palettes from BBM/LBM files" decoding="async" loading="lazy" /></a><figcaption>A sample application showing loading of palettes from BBM/LBM files</figcaption></figure><h2 id="caveat-emptor">Caveat Emptor</h2>
<p>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.</p>
<h2 id="overview-of-bbmlbm-files">Overview of BBM/LBM Files</h2>
<p>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.</p>
<p>The LBM format is more formally known as <em>ILBM - IFF Interleaved
Bitmap</em>. It is built upon <em>&quot;EA IFF 85&quot; Standard for Interchange
Format Files</em>. 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.</p>
<p>More information on these formats can be found in specification
documents <a href="http://home.comcast.net/%7Eerniew/lwsdk/docs/filefmts/eaiff85.html" rel="external nofollow noopener">here</a> and <a href="http://home.comcast.net/%7Eerniew/lwsdk/docs/filefmts/ilbm.html" rel="external nofollow noopener">here</a>.</p>
<h2 id="reading-an-lbm-file">Reading an LBM file</h2>
<p>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.</p>
<p>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.</p>
<p>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 <a href="http://en.wikipedia.org/wiki/Endianness#Big-endian" rel="external nofollow noopener">big-endian</a> format, so we need to
convert these when we read them.</p>
<h2 id="the-cmap-chunk">The CMAP chunk</h2>
<p>The only section of the LBM file we are interested in is the
<code>CMAP</code> 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 <code>CMAP</code>. Also, only
8bit (or lower?) LBM files will contain a <code>CMAP</code>, as they only
support RGB channels. 24bit or 32bit images won't have one as
there's no scope for storing alpha channels.</p>
<p>The data section of a <code>CMAP</code> 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.</p>
<h2 id="other-chunks">Other Chunks</h2>
<p>Although I'm not reading other chunks, I still have to pay
attention to some of them.</p>
<p>Firstly, the <code>FORM</code> 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.</p>
<p>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 <code>ILBM</code>, but the sample images I've worked with
use a different header which is <code>PBM</code> for <em>Planar BitMap</em>. 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 <code>CMAP</code>
section is the same, I look for either of these.</p>
<p>Anything else will be discarded.</p>
<h2 id="reading-the-file">Reading the file</h2>
<p>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 <code>FORM</code>, we know we have an IFF document and continue
reading. Otherwise, we throw an <code>InvalidDataException</code>
exception.</p>
<figure class="lang-csharp highlight"><figcaption><span>csharp</span></figcaption><pre class="code">
<span class="keyword">using</span> <span class="symbol">(</span>FileStream stream <span class="symbol">=</span> File<span class="symbol">.</span>OpenRead<span class="symbol">(</span>fileName<span class="symbol">)</span><span class="symbol">)</span>
<span class="symbol">{</span>
 <span class="keyword">byte</span><span class="symbol">[</span><span class="symbol">]</span> buffer<span class="symbol">;</span>
 <span class="keyword">string</span> header<span class="symbol">;</span>

 <span class="comment">// read the FORM header that identifies the document as an IFF file</span>
 buffer <span class="symbol">=</span> <span class="keyword">new</span> <span class="keyword">byte</span><span class="symbol">[</span><span class="number">4</span><span class="symbol">]</span><span class="symbol">;</span>
 stream<span class="symbol">.</span>Read<span class="symbol">(</span>buffer<span class="symbol">,</span> <span class="number">0</span><span class="symbol">,</span> buffer<span class="symbol">.</span>Length<span class="symbol">)</span><span class="symbol">;</span>
 <span class="keyword">if</span> <span class="symbol">(</span>Encoding<span class="symbol">.</span>ASCII<span class="symbol">.</span>GetString<span class="symbol">(</span>buffer<span class="symbol">)</span> <span class="symbol">!=</span> <span class="string">&quot;FORM&quot;</span><span class="symbol">)</span>
 <span class="keyword">throw</span> <span class="keyword">new</span> InvalidDataException<span class="symbol">(</span><span class="string">&quot;Form header not found.&quot;</span><span class="symbol">)</span><span class="symbol">;</span>
</pre>
</figure>
<p>Next we read the size of the data contained within the <code>FORM</code>
chunk. As we aren't checking for nested chunks nor reading all
the data, we can safely ignore this.</p>
<figure class="lang-csharp highlight"><figcaption><span>csharp</span></figcaption><pre class="code">
 <span class="comment">// the next value is the size of all the data in the FORM chunk</span>
 <span class="comment">// We don&#39;t actually need this value, but we have to read it</span>
 <span class="comment">// regardless to advance the stream</span>
 <span class="keyword">this</span><span class="symbol">.</span>ReadInt<span class="symbol">(</span>stream<span class="symbol">)</span><span class="symbol">;</span>
</pre>
</figure>
<p>Time for another sanity check, this time to verify we are
reading an image, be it Planar (<code>PBM </code>) or Interleaved (<code>ILBM</code>).</p>
<p>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.</p>
<figure class="lang-csharp highlight"><figcaption><span>csharp</span></figcaption><pre class="code">
 <span class="comment">// read either the PBM or ILBM header that identifies this document as an image file</span>
 stream<span class="symbol">.</span>Read<span class="symbol">(</span>buffer<span class="symbol">,</span> <span class="number">0</span><span class="symbol">,</span> buffer<span class="symbol">.</span>Length<span class="symbol">)</span><span class="symbol">;</span>
 header <span class="symbol">=</span> Encoding<span class="symbol">.</span>ASCII<span class="symbol">.</span>GetString<span class="symbol">(</span>buffer<span class="symbol">)</span><span class="symbol">;</span>
 <span class="keyword">if</span> <span class="symbol">(</span>header <span class="symbol">!=</span> <span class="string">&quot;PBM &quot;</span> <span class="symbol">&amp;&amp;</span> header <span class="symbol">!=</span> <span class="string">&quot;ILBM&quot;</span><span class="symbol">)</span>
 <span class="keyword">throw</span> <span class="keyword">new</span> InvalidDataException<span class="symbol">(</span><span class="string">&quot;Bitmap header not found.&quot;</span><span class="symbol">)</span><span class="symbol">;</span>
</pre>
</figure>
<p>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.</p>
<p>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.</p>
<figure class="lang-csharp highlight"><figcaption><span>csharp</span></figcaption><pre class="code">
 <span class="keyword">while</span> <span class="symbol">(</span>stream<span class="symbol">.</span>Read<span class="symbol">(</span>buffer<span class="symbol">,</span> <span class="number">0</span><span class="symbol">,</span> buffer<span class="symbol">.</span>Length<span class="symbol">)</span> <span class="symbol">==</span> buffer<span class="symbol">.</span>Length<span class="symbol">)</span>
 <span class="symbol">{</span>
 <span class="keyword">int</span> chunkLength<span class="symbol">;</span>

 chunkLength <span class="symbol">=</span> <span class="keyword">this</span><span class="symbol">.</span>ReadInt<span class="symbol">(</span>stream<span class="symbol">)</span><span class="symbol">;</span>
</pre>
</figure>
<p>As we are only interested in <code>CMAP</code> chunks, if the pending chunk
has any other type of ID, we skip the remainder of the chunk, as
identified by <code>chunkLength</code> 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.</p>
<figure class="lang-csharp highlight"><figcaption><span>csharp</span></figcaption><pre class="code">
 <span class="keyword">if</span> <span class="symbol">(</span>Encoding<span class="symbol">.</span>ASCII<span class="symbol">.</span>GetString<span class="symbol">(</span>buffer<span class="symbol">)</span> <span class="symbol">!=</span> <span class="string">&quot;CMAP&quot;</span><span class="symbol">)</span>
 <span class="symbol">{</span>
 <span class="comment">// some other LBM chunk, skip it</span>
 <span class="keyword">if</span> <span class="symbol">(</span>stream<span class="symbol">.</span>CanSeek<span class="symbol">)</span>
 stream<span class="symbol">.</span>Seek<span class="symbol">(</span>chunkLength<span class="symbol">,</span> SeekOrigin<span class="symbol">.</span>Current<span class="symbol">)</span><span class="symbol">;</span>
 <span class="keyword">else</span>
 <span class="symbol">{</span>
 <span class="keyword">for</span> <span class="symbol">(</span><span class="keyword">int</span> i <span class="symbol">=</span> <span class="number">0</span><span class="symbol">;</span> i <span class="symbol">&lt;</span> chunkLength<span class="symbol">;</span> i<span class="symbol">++</span><span class="symbol">)</span>
 stream<span class="symbol">.</span>ReadByte<span class="symbol">(</span><span class="symbol">)</span><span class="symbol">;</span>
 <span class="symbol">}</span>
 <span class="symbol">}</span>
</pre>
</figure>
<p>Aha! We finally found the <code>CMAP</code> chunk. Now it's just a
straightforward reading of colours. <code>chunkLength</code> 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.</p>
<p>Then, we exit out of the <code>while</code> loop - no pointing reading the
entire file now that we have what we wanted.</p>
<figure class="lang-csharp highlight"><figcaption><span>csharp</span></figcaption><pre class="code">
 <span class="keyword">else</span>
 <span class="symbol">{</span>
 <span class="comment">// color map chunk!</span>
 <span class="keyword">for</span> <span class="symbol">(</span><span class="keyword">int</span> i <span class="symbol">=</span> <span class="number">0</span><span class="symbol">;</span> i <span class="symbol">&lt;</span> chunkLength <span class="symbol">/</span> <span class="number">3</span><span class="symbol">;</span> i<span class="symbol">++</span><span class="symbol">)</span>
 <span class="symbol">{</span>
 <span class="keyword">int</span> r<span class="symbol">;</span>
 <span class="keyword">int</span> g<span class="symbol">;</span>
 <span class="keyword">int</span> b<span class="symbol">;</span>

 r <span class="symbol">=</span> stream<span class="symbol">.</span>ReadByte<span class="symbol">(</span><span class="symbol">)</span><span class="symbol">;</span>
 g <span class="symbol">=</span> stream<span class="symbol">.</span>ReadByte<span class="symbol">(</span><span class="symbol">)</span><span class="symbol">;</span>
 b <span class="symbol">=</span> stream<span class="symbol">.</span>ReadByte<span class="symbol">(</span><span class="symbol">)</span><span class="symbol">;</span>

 colorPalette<span class="symbol">.</span>Add<span class="symbol">(</span>Color<span class="symbol">.</span>FromArgb<span class="symbol">(</span>r<span class="symbol">,</span> g<span class="symbol">,</span> b<span class="symbol">)</span><span class="symbol">)</span><span class="symbol">;</span>
 <span class="symbol">}</span>

 <span class="comment">// all done so stop reading the rest of the file</span>
 <span class="keyword">break</span><span class="symbol">;</span>
 <span class="symbol">}</span>
</pre>
</figure>
<p>If we are still in the loop however, then we need to check our
<code>chunkLength</code> 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.</p>
<figure class="lang-csharp highlight"><figcaption><span>csharp</span></figcaption><pre class="code">
 <span class="comment">// chunks always contain an even number of bytes even if the recorded length is odd</span>
 <span class="comment">// if the length is odd, then there&#39;s a padding byte in the file - just read and discard</span>
 <span class="keyword">if</span> <span class="symbol">(</span>chunkLength <span class="symbol">%</span> <span class="number">2</span> <span class="symbol">!=</span> <span class="number">0</span><span class="symbol">)</span>
 stream<span class="symbol">.</span>ReadByte<span class="symbol">(</span><span class="symbol">)</span><span class="symbol">;</span>
 <span class="symbol">}</span>
 <span class="symbol">}</span>

 <span class="keyword">return</span> colorPalette<span class="symbol">;</span>
<span class="symbol">}</span>
</pre>
</figure>
<h2 id="converting-big-endian-to-little-endian">Converting big-endian to little-endian</h2>
<p>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.</p>
<p>As bit shifting still jellies my brain, I took to Stack Overflow
which provided me with a <a href="https://stackoverflow.com/a/14401341/148962" rel="external nofollow noopener">function</a> for converting four bytes
of big-endian data into a little-endian integer.</p>
<figure class="lang-csharp highlight"><figcaption><span>csharp</span></figcaption><pre class="code">
<span class="keyword">private</span> <span class="keyword">int</span> ReadInt<span class="symbol">(</span>Stream stream<span class="symbol">)</span>
<span class="symbol">{</span>
 <span class="keyword">byte</span><span class="symbol">[</span><span class="symbol">]</span> buffer<span class="symbol">;</span>

 <span class="comment">// big endian conversion: http://stackoverflow.com/a/14401341/148962</span>

 buffer <span class="symbol">=</span> <span class="keyword">new</span> <span class="keyword">byte</span><span class="symbol">[</span><span class="number">4</span><span class="symbol">]</span><span class="symbol">;</span>
 stream<span class="symbol">.</span>Read<span class="symbol">(</span>buffer<span class="symbol">,</span> <span class="number">0</span><span class="symbol">,</span> buffer<span class="symbol">.</span>Length<span class="symbol">)</span><span class="symbol">;</span>

 <span class="keyword">return</span> <span class="symbol">(</span>buffer<span class="symbol">[</span><span class="number">0</span><span class="symbol">]</span> <span class="symbol">&lt;&lt;</span> <span class="number">24</span><span class="symbol">)</span> <span class="symbol">|</span> <span class="symbol">(</span>buffer<span class="symbol">[</span><span class="number">1</span><span class="symbol">]</span> <span class="symbol">&lt;&lt;</span> <span class="number">16</span><span class="symbol">)</span> <span class="symbol">|</span> <span class="symbol">(</span>buffer<span class="symbol">[</span><span class="number">2</span><span class="symbol">]</span> <span class="symbol">&lt;&lt;</span> <span class="number">8</span><span class="symbol">)</span> <span class="symbol">|</span> buffer<span class="symbol">[</span><span class="number">3</span><span class="symbol">]</span><span class="symbol">;</span>
<span class="symbol">}</span>
</pre>
</figure>
<p>So in our sample, we read our 4 bytes, shift their bits around
and return the result.</p>
<h2 id="that-was-easy">That was easy</h2>
<p>That was fairly straightforward! Well, if I ignore the endian
conversion. And I'm sure if I decided to read the <code>BODY</code> 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.</p>
<p>As usual, a fully working sample is attached to this post.</p>
<h2 id="further-thoughts">Further Thoughts</h2>
<p>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
<code>CMAP</code> section. Although the clue would be in the empty list
that's returned.</p>
<p>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.</p>
<h2 id="update-history">Update History</h2>
<ul>
<li>2014-01-11 - First published</li>
<li>2020-11-21 - Updated formatting</li>
</ul>

<p><small>
All content <a href="https://devblog.cyotek.com/copyright-and-trademarks">Copyright (c) by Cyotek Ltd</a> or its respective writers. Permission to reproduce news and web log entries and other RSS feed content in unmodified form without notice is granted provided they are not used to endorse or promote any products or opinions (other than what was expressed by the author) and without taking them out of context. Written permission from the copyright owner must be obtained for everything else.<br />Original URL of this content is https://devblog.cyotek.com/post/loading-the-color-palette-from-a-bbm-lbm-image-file-using-csharp .
</small></p>Richard Mosshttps://www.cyotek.com/richard.moss@cyotek.com