Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Decoding Portable Bitmap (PBM) files in C# and displaying them in WPF

0.00/5 (No votes)
19 Dec 2011 4  
A tutorial aimed at intermediate coders on decoding PBM (Portable Bitmap) files using C#.

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)//P1
        {
            return 1;
        }

        else if (test == 0x3450)//P4
        {
            return 0;
        }

        else //Get in a tantrum
        {
            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);
    //Array of all values in the file

    int mNumberCounter = 0, dimCounter = 0, result; 
    //counter for the magic number, dimensions and an empty integer to 
    //hold the result of a search that we’ll do later on.

    Width = 0;
    Height = 0;

    bool result1;

    byteAndCharArray = null;

    for (int i = 0; i < StringArray.Length; i++)
    {
        //Check to see whether or not the line starts with "# "

        if (StringArray[i].StartsWith("# "))
        {
            continue; //It’s a comment – we need to ignore it.
        }

        //This line is not a comment.
        
        else if (mNumberCounter == 0)
        //If we’ve not encountered the magic number, 
        //then this should really be an error, 
        //but I’m trying to be flexible.
        {
            string[] BrokenUp = StringArray[i].Split(
               new string[] { " " },
               StringSplitOptions.RemoveEmptyEntries); 
            //Break up the string by whitespace characters.
            switch (BrokenUp.Length)
            {
                case 1: 
                //If we’ve found something, but only one thing,
                //then this must be the magic number, but there 
                //are no dimensions

                    //we have only got the magic number
                    mNumberCounter = 1;
                    break;
                case 2:
                    //we have only got the Width (the width is given  
                    //BEFORE the height
                    
                    mNumberCounter = 1;
                    //We must have the magic number

                    result1 = int.TryParse(
                       BrokenUp[1],
                       NumberStyles.Integer, 
                       CultureInfo.CurrentCulture, 
                       out result);
                    //Try to parse the width as an integer

                    if (result1)
                    //If it works
                    {
                        Width = result;
                        //set the width
                        dimCounter++;
                    }

                    else
                    {
                        continue;
                        //Well, it wasn’t the width after all
                    }
                   
                    break;

                case 3:
                    //we have everything, the width the height and the 
                    //magic no.
                    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; 
                        //okay, so maybe we don’t, we’ll keep looking
                    }

                    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)
//If we’ve only found the magic no., but not any dimensions
{
    string[] BrokenUp = StringArray[i].Split(
      new string[] { " " }, 
      StringSplitOptions.RemoveEmptyEntries);

    switch (BrokenUp.Length)
    {
        case 1:
            //we have only got the Width
            result1 = int.TryParse(
               BrokenUp[0], 
               NumberStyles.Integer, 
               CultureInfo.CurrentCulture, 
               out result);
            
            if (result1)
            {
                Width = result;
                dimCounter++;
            }

            else
            {
                continue;
            }
            break;

        case 2:
            //we have only got the Height
            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)
//Height will be found hopefully
{
    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 
//We’ve got the data and there isn’t a comment - take it that  
//this is the start of the pixel data.
{
    string main = ConvertStringArrayToString(StringArray, i);

    bool booleanFlag = false; 
    //This will be used to tell the program whether we’re 
    //in a comment.

    byteAndCharArray = new char[Width*Height];
    int j = 0;

    foreach (char c in main)
    {
        if (booleanFlag && c != '\n')
        //If we're in a comment, just carry on
            continue;

        else if (booleanFlag && c == '\n')
        //A newline character can mark the end of a comment. 
        //If we've met one, then set the flag to false, 
        //so that at the next loop, we read the data
            booleanFlag = false;

        else if (c == '0' || c == '1')
        //If we've met a 1 or 0 and we're not in a comment, 
        //then add it to the array.
        {
            byteAndCharArray[j] = c;
            j++;
        }

        else if (c == '#')
        //This marks the start of a comment.
            booleanFlag = true; 
            //Well, we’ve found a comment, so we need to make       
            //sure the program knows in future.

        else
        //If it's not met any of the above conditions, 
        //it's just junk and we can get rid of (i.e. ignore) it.
            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)
{
    //Remember that pixels is simply a string of "0"s and 
    //"1"s. Width and Height are integers.
    
    int Width = width;
    int Height = height;

    //Create our bitmap
    using (Bitmap B = new Bitmap(Width, Height))
    {              
        int X = 0;
        int Y = 0;
        //Current X,Y co-ordinates of the Bitmap object
        
        //Loop through all of the bits
        for (int i = 0; i < pixels.Length; i++)
        {
            //Below, we're comparing the value with "0". 
            //If it is a zero, then we change the pixel white, 
            //else make it black.

            B.SetPixel(X, Y, pixels[i] == '0' ? 
                 System.Drawing.Color.White :
                 System.Drawing.Color.Black );
                
            //Increment our X position
            
            X += 1;//Move along the right

            //If we're passed the right boundry, 
            //reset the X and move the Y to the next line
            
            if (X >= Width)
            {
                X = 0;//reset
                Y += 1;//Add another row
            }
        }

        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;
//This is the number of newlines that we've encountered.

BinarySearcher b = new BinarySearcher();
FileStream fs = new FileStream(
  fname, 
  FileMode.Open, 
  FileAccess.Read);

int[] offsets = b.SearchToArray(0x0a, fname, nCount);
//This gives us the offset location where all of 
//the newlines in the file are (see source).

int stride = ((Width * ((1 + 7) / 8)) + 4 - 
             ((Width * ((1 + 7) / 8)) % 4));

//calculates the stride


using (BinaryReader e = new BinaryReader(fs))
{
    e.BaseStream.Position = offsets[nCount-1] + 1;
    //This gives the location of our current newline 
    //in the actual filestream

    long bufferSize = e.BaseStream.Length;
    //Gives the total size of the buffer

    stringBytes = e.ReadBytes((int)bufferSize - 
                              offsets[nCount-1] + 
                              ((stride - Width) * Height));
    //read the bytes from our location to the end of the stream.

    stringBytes = ConvertBytes(stringBytes);
    //Next I turn the bits into bytes for ease later – you 
    //don’t need to do this, but it’s much easier to deal 
    //with mentally when working with the code. (see the 
    //source code for this)
}

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); 
        //unlock the bytes of the bitmap in memory
        
        int j = 0;
        int y = 0;
        
        IntPtr ptr = bmd.Scan0;
        //find the beginning of the data

        int bytes = Math.Abs(pixels.Length);
        //This gives us the bitmap data size
        
        byte[] rgbValues = new byte[bytes];

        System.Runtime.InteropServices.Marshal.Copy(
           ptr, 
           rgbValues,
           0,
           bytes);
        //copy all of the bytes in the bitmap’s memory to the 
        //rgbValues array.

        for (int counter = 0; 
               counter < rgbValues.Length 
               && j < pixels.Length; )
        {
            int k = 1;
            //bring k back to 1

            for (int i = 0; i < 8; i++, counter++, j++) 
            //Let’s go forward in 8 bytes. 
            //It makes it more simple when we have to deal with padding.
            {
                if (bmd.Stride - width == 0 & y == B.Width)
                {
                    y = 0;
                    //reset y and then break. There is no padding, 
                    //so we don’t need to skip anything.
                    break;
                }

                else if (!(bmd.Stride - width == 0) & y == B.Width)
                {
                    y = 0;
                    counter += bmd.Stride - width;
                    //When we reach the width, we skip the padding
                    
                    j += (bmd.Stride - width) + 
                         (pixels.Length - rgbValues.Length) / 
                         height;
                    
                    //reset y and move j along, but remember to skip the 
                    //padding or everything will look skewed. 
                    //PBMs don’t do padding, so we have to leave it. 
                    //Some other formats like BMPs and PCXs do and you 
                    //need to consider that if you’re planning on 
                    //implementing this elsewhere.
                    
                    break;
                }

                byte colour = pixels[j];
                //set colour (sorry Americans) to equal the pixel value
                //from our file that we parsed earlier.

                rgbValues[counter] = colour;
                //set the pixel array from our BitmapImage to contain 
                //this value.

                y++; 
                //Move y along (that is, move forward by a pixel)
            }
           
            //We don't move along until all the values are sorted.
        }

        System.Runtime.InteropServices.Marshal.Copy(
           rgbValues, 
           0,
           ptr,
           bytes); 

        //put rgbValues back into the BitmapData memory area

        B.UnlockBits(bmd);
        //Unlock the bytes and end the unsafe stuff.
    }

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

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here