Introduction and Background
Since beginning programming a couple of years ago, I have always made it my intention to create an image viewer. Although I started out in C and C++,
I have moved onto programming in C# and was frustrated by the lack of information on reading image formats in that language. After learning more about programming
and working on a separate project to read vCards, I have come back to creating an image viewer and this time with far more success than my earlier attempts.
Here, I am showing you my own implementation of a Portable Bitmap viewer, written solely in C# without third-party libraries, and in what I hope is a simple,
fairly easy-to-follow way.
Using the Code
Firstly, it is necessary for me to explain that there are two types of portable bitmaps: ASCII and Binary. The ASCII files are much larger
than the binary files and are older, but apparently better for being protected against corruption. Wikipedia has a good article on the format as does the online File Format Encyclopedia.
Both types have a magic number; a two byte sequence at the start of the file. We need to check this first to determine the file type:
public int readMagicNumber(string fname)
{
FileStream pbm_name = new FileStream(
fname,
FileMode.Open,
FileAccess.Read,
FileShare.None);
using (BinaryReader r = new BinaryReader(pbm_name))
{
Int16 test = r.ReadInt16();
if (test == 0x3150) {
return 1;
}
else if (test == 0x3450) {
return 0;
}
else {
return -1;
}
}
}
Our next step is to read the files. Let’s look at the ASCII type first.
Unlike JPEGs and BMPs, PBM files aren’t straightforward. There’s no offset to tell us where the data begins and no set order of bytes. There can even be text
comments interspersed through the data, anywhere. This seemingly-relaxed approach was intentional and was meant to protect data and preserve the actual
bitmap over networks (the format is from a ‘family’ called Network Portable Bitmaps or NetPBMs). It doesn’t make it simple for us, though. N.B.: The arguments
for readData
are an array that will store the pixel data as char and the rest should be self-explanatory from the code.
public void readData(
string fname,
out int Height,
out int Width,
out char[] byteAndCharArray)
{
string input = File.ReadAllText(fname);
input = input.Replace("\r", "");
string[] StringArray = input.Split(
new string[] { "\n" },
StringSplitOptions.RemoveEmptyEntries);
int mNumberCounter = 0, dimCounter = 0, result;
Width = 0;
Height = 0;
bool result1;
byteAndCharArray = null;
for (int i = 0; i < StringArray.Length; i++)
{
if (StringArray[i].StartsWith("# "))
{
continue; }
else if (mNumberCounter == 0)
{
string[] BrokenUp = StringArray[i].Split(
new string[] { " " },
StringSplitOptions.RemoveEmptyEntries);
switch (BrokenUp.Length)
{
case 1:
mNumberCounter = 1;
break;
case 2:
mNumberCounter = 1;
result1 = int.TryParse(
BrokenUp[1],
NumberStyles.Integer,
CultureInfo.CurrentCulture,
out result);
if (result1)
{
Width = result;
dimCounter++;
}
else
{
continue;
}
break;
case 3:
mNumberCounter = 1;
result1 = int.TryParse(
BrokenUp[1],
NumberStyles.Integer,
CultureInfo.CurrentCulture,
out result);
if (result1)
{
Width = result;
dimCounter++;
}
else
{
continue;
}
result1 = int.TryParse(
BrokenUp[2],
NumberStyles.Integer,
CultureInfo.CurrentCulture,
out result);
if (result1)
{
Height = result;
dimCounter++;
}
else
{
continue;
}
break;
}
}
Okay, so we've now asserted that the only piece of data we've found is the magic number, it's time to find the rest. Firstly, we will need to check dimCounter
.
Remember, this is a counter that tells us how many dimensions we've found; the PBM format specifies that the width comes first, so if dimCounter
is only 1,
then we've only found the width.
else if (dimCounter == 0)
{
string[] BrokenUp = StringArray[i].Split(
new string[] { " " },
StringSplitOptions.RemoveEmptyEntries);
switch (BrokenUp.Length)
{
case 1:
result1 = int.TryParse(
BrokenUp[0],
NumberStyles.Integer,
CultureInfo.CurrentCulture,
out result);
if (result1)
{
Width = result;
dimCounter++;
}
else
{
continue;
}
break;
case 2:
mNumberCounter = 1;
result1 = int.TryParse(
BrokenUp[0],
NumberStyles.Integer,
CultureInfo.CurrentCulture,
out result);
if (result1)
{
Width = result;
dimCounter++;
}
else
{
continue;
}
result1 = int.TryParse(
BrokenUp[1],
NumberStyles.Integer,
CultureInfo.CurrentCulture,
out result);
if (result1)
{
Height = result;
dimCounter++;
}
else
{
continue;
}
break;
}
}
else if (dimCounter == 1)
{
string[] BrokenUp = StringArray[i].Split(
new string[] { " " },
StringSplitOptions.RemoveEmptyEntries);
result1 = int.TryParse(
BrokenUp[0],
NumberStyles.Integer,
CultureInfo.CurrentCulture,
out result);
if (result1)
{
Height = result;
dimCounter++;
}
else
{
continue;
}
}
Okay, so through a process of elimination, we have found all the dimensions and the magic number. After that comes the data itself - the bitmap. We've even checked
to see whether or not the lines are comments, so if it's not a comment and everything's been found, then this must be the data.
We read through the remaining data, character by character, and add the 1s and 0s to a char array.
The trick is that these files can contain comments throughout them, but they are in-line and once written, a comment will last until a new line is started.
The comment can include 1s and 0s, but these shouldn't be read by the program as pixel data. So that the program knows when it's in the middle of a comment,
we set a boolean flag to true when we encounter the beginning of a comment ('#'). When this flag is set and the character we're reading is not a newline character,
then we just continue without adding anything to our pixel array. If it is a newline, then that marks the end of that comment and we set the flag to false.
else
{
string main = ConvertStringArrayToString(StringArray, i);
bool booleanFlag = false;
byteAndCharArray = new char[Width*Height];
int j = 0;
foreach (char c in main)
{
if (booleanFlag && c != '\n')
continue;
else if (booleanFlag && c == '\n')
booleanFlag = false;
else if (c == '0' || c == '1')
{
byteAndCharArray[j] = c;
j++;
}
else if (c == '#')
booleanFlag = true;
else
continue;
}
break;
}
Okay, so we’ve got the pixels. They have been checked and we can now only work with what we’ve been given. The next step is to turn this data into something useful.
In my case, I’m turning this data into a System.Drawing.Bitmap
, which is then saved as BitmapImage
. This can be used to display the data
in a WPF Image
element. For Windows Forms, I think you can omit the BitmapImage
part and just return the Bitmap
, cast it as an Image
,
and then display it in a PictureBox
, but I don’t use WinForms, so forgive me if I’m wrong.
My code is simple, but slow. Using ‘unsafe’ code (i.e., pointers) would be much faster than this, but I’m not an expert on it and it took me some time to work it
out with the Binary (see below). Basically, I pass the program the chars from the above source and then the program sets each pixel to white if the char
is ‘1’ and black if it is ‘0’ (well, anything that’s not ‘1’, which should be ‘0’).
public System.Windows.Media.Imaging.BitmapImage bmpFromPBM(
char[] pixels,
int width,
int height)
{
int Width = width;
int Height = height;
using (Bitmap B = new Bitmap(Width, Height))
{
int X = 0;
int Y = 0;
for (int i = 0; i < pixels.Length; i++)
{
B.SetPixel(X, Y, pixels[i] == '0' ?
System.Drawing.Color.White :
System.Drawing.Color.Black );
X += 1;
if (X >= Width)
{
X = 0; Y += 1; }
}
MemoryStream ms = new MemoryStream();
B.Save(ms, System.Drawing.Imaging.ImageFormat.Bmp);
ms.Position = 0;
System.Windows.Media.Imaging.BitmapImage bi =
new System.Windows.Media.Imaging.BitmapImage();
bi.BeginInit();
bi.StreamSource = ms;
bi.EndInit();
return bi;
}
}
Just to note, sometimes a ‘Generic Error’ (really helpful, I know) can occur in GDI+ when saving the bitmap. A simple fix is to copy it to a new bitmap
again (Bitmap bitmap1 = new Bitmap(B);
) and save bitmap1
instead. I don’t know why this happens and neither
do the people at MSDN and Stack Overflow, but it just does.
Anyway, that’s a wrap for the ASCII PBMs. The Binary files are parsed in a similar way, but I replicate the second code example, with the switch
statements,
and then insert the following code in the else
conditional rather than stuff that’s there for parsing the ASCII files. In that function, a byte array called
stringBytes
is given as an out
argument instead of the char array in example 2.
int nCount = i;
BinarySearcher b = new BinarySearcher();
FileStream fs = new FileStream(
fname,
FileMode.Open,
FileAccess.Read);
int[] offsets = b.SearchToArray(0x0a, fname, nCount);
int stride = ((Width * ((1 + 7) / 8)) + 4 -
((Width * ((1 + 7) / 8)) % 4));
using (BinaryReader e = new BinaryReader(fs))
{
e.BaseStream.Position = offsets[nCount-1] + 1;
long bufferSize = e.BaseStream.Length;
stringBytes = e.ReadBytes((int)bufferSize -
offsets[nCount-1] +
((stride - Width) * Height));
stringBytes = ConvertBytes(stringBytes);
}
break;
Lastly, we can now convert this into an image. Here, I use pointers to speed things up. It gets a bit complicated, but it does work.
public System.Windows.Media.Imaging.BitmapImage bmpFromBinaryPBM(
byte[] pixels,
int width,
int height)
{
int stride = ((width * ((1 + 7) / 8)) + 4 -
((width * ((1 + 7) / 8)) % 4));
Bitmap B = new Bitmap(
width,
height,
System.Drawing.Imaging.PixelFormat.Format8bppIndexed);
unsafe
{
System.Drawing.Imaging.BitmapData bmd = B.LockBits(
new Rectangle(0, 0, B.Width, B.Height),
System.Drawing.Imaging.ImageLockMode.ReadWrite,
B.PixelFormat);
int j = 0;
int y = 0;
IntPtr ptr = bmd.Scan0;
int bytes = Math.Abs(pixels.Length);
byte[] rgbValues = new byte[bytes];
System.Runtime.InteropServices.Marshal.Copy(
ptr,
rgbValues,
0,
bytes);
for (int counter = 0;
counter < rgbValues.Length
&& j < pixels.Length; )
{
int k = 1;
for (int i = 0; i < 8; i++, counter++, j++)
{
if (bmd.Stride - width == 0 & y == B.Width)
{
y = 0;
break;
}
else if (!(bmd.Stride - width == 0) & y == B.Width)
{
y = 0;
counter += bmd.Stride - width;
j += (bmd.Stride - width) +
(pixels.Length - rgbValues.Length) /
height;
break;
}
byte colour = pixels[j];
rgbValues[counter] = colour;
y++;
}
}
System.Runtime.InteropServices.Marshal.Copy(
rgbValues,
0,
ptr,
bytes);
B.UnlockBits(bmd);
}
MemoryStream ms = new MemoryStream();
B.Save(ms, System.Drawing.Imaging.ImageFormat.Bmp);
ms.Position = 0;
System.Windows.Media.Imaging.BitmapImage bi = new
System.Windows.Media.Imaging.BitmapImage();
bi.BeginInit();
bi.StreamSource = ms;
bi.EndInit();
return bi;
}
Okay, finally, implementing. To use this code, I wrote a series of calls from outside the class. All of my functions are public, so they can be called from anywhere
around the program. You may want to add a ‘superfunction’ to the class that calls all the other functions – I’ve not done this, though. The example below shows how
I’ve called my code and take the file name from an OpenFileDialog
called openDialog
.
pbm pbm = new pbm();
int pbm_result =
pbm.readMagicNumber(openDialog.FileName);
int height, width;
if (pbm_result == 1)
{
char[] pixels;
pbm.readData(openDialog.FileName, out height, out width, out pixels);
if (Height > 0)
{
if (Width > 0)
{
try
{
image1.Source =
pbm.bmpFromPBM(
pixels,
pbmWidth,
pbmHeight);
label6.Content = Width;
label7.Content = Height;
label8.Content = "1";
}
catch (Exception eee)
{
MessageBox.Show(
"There was an error
processing this file. It could
not be read properly" +
eee);
}
}
else
MessageBox.Show("There was an error
reading the Width of this file. There
could be a comment obstructing
it.");
}
else
MessageBox.Show("There was an error
reading the Height of this file. There could
be a comment obstructing it.");
}
else if (pbm_result == 0)
{
byte[] pixels;
pbm.readBinaryData(
openDialog.FileName,
out height,
out width,
out pixels);
label6.Content = width;
label7.Content = height;
label8.Content = "1";
image1.Source = pbm.bmpFromBinaryPBM(
pixels,
width,
height);
}
else if (pbm_result == -1)
{
MessageBox.Show("This file is not a valid
PBM File");
}
#endregion
Points of Interest
Like I say in the code, using pointers to manipulate the Bitmap is far more efficient and definitely quicker than using SetPixel
,
but I include the SetPixel
method here to offer both sides and because I often find that coming up with the algorithm using unsafe code can take some time.
I mention a couple of functions that aren't explained. One is ConvertStringArrayToString()
. It basically uses
a StringBuilder
to join together the strings in a string array after a certain index and returns that concatenated string. The next is
ConvertBytes()
. This turns a byte into 8 bytes that are either 0x00 or 0xFF depending on the individual bits of a given byte.
E.g., if the byte 0xC0 were passed as part of the byte array in the argument, the result for that byte would be a byte of 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00.
History
- First uploaded: Sat. 17 Dec. 2011 (17:40 GMT).