65.9K
CodeProject 正在变化。 阅读更多。
Home

在 C# 中解码 Portable Bitmap (PBM) 文件并在 WPF 中显示它们

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.33/5 (6投票s)

2011年12月18日

CPOL

6分钟阅读

viewsIcon

33440

downloadIcon

750

本教程面向中级程序员,介绍如何使用 C# 解码 PBM (Portable Bitmap) 文件。

引言和背景

自从几年前开始编程以来,我一直以创建图像查看器为目标。虽然我最初使用的是 C 和 C++,但我已经转向使用 C# 编程,并且对在这门语言中读取图像格式的信息匮乏感到沮丧。在学习了更多编程知识并完成了一个读取 vCard 的独立项目后,我回到了创建图像查看器的项目,这次比我早期的尝试取得了更大的成功。

在这里,我将向您展示我用纯 C#(不使用第三方库)编写的 Portable Bitmap 查看器的实现,并希望以一种简单、易于理解的方式展示。

Using the Code

首先,有必要解释一下,便携式位图有两种类型:ASCII 和二进制。ASCII 文件比二进制文件大得多,而且是较旧的格式,但据说更不容易损坏。维基百科和在线文件格式百科全书都有关于该格式的优秀文章。

这两种类型都有一个魔术数字;文件开头是一个两个字节的序列。我们需要先检查这个来确定文件类型。

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;
        }
    }
}

下一步是读取文件。我们先来看 ASCII 类型。

与 JPEG 和 BMP 不同,PBM 文件并不直接。没有偏移量告诉我们数据从哪里开始,也没有固定的字节顺序。数据中甚至可以随意穿插文本注释。这种看似宽松的方法是故意的,旨在保护数据并在网络上传输(该格式属于一个名为 Network Portable Bitmaps 或 NetPBMs 的“家族”)位图。但这并没有让我们的工作变得简单。注意:readData 的参数是一个将存储像素数据的 char 数组,其余部分应从代码中自行解释。

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;
            }
        }

好的,现在我们已经确认找到的唯一数据是魔术数字,是时候寻找其余部分了。首先,我们需要检查 dimCounter。请记住,这是一个计数器,告诉我们找到了多少个维度;PBM 格式规定宽度在前,所以如果 dimCounter 只有 1,那么我们只找到了宽度。

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;
    }
}

好的,通过排除法,我们已经找到了所有维度和魔术数字。之后就是数据本身——位图。我们甚至检查了行是否是注释,所以如果不是注释并且所有东西都已找到,那么这一定是数据。

我们逐个字符地读取剩余数据,并将 1 和 0 添加到 char 数组中。

诀窍在于,这些文件可以在其中包含注释,但它们是行内注释,一旦写入,注释将持续到新行开始。注释可以包含 1 和 0,但不应被程序读取为像素数据。为了让程序知道何时处于注释的中间,我们在遇到注释的开头('#')时将一个布尔标志设置为 true。当此标志被设置并且我们读取的字符不是换行符时,我们将继续而不将任何内容添加到我们的像素数组中。如果它是换行符,那么它标志着该注释的结束,我们将标志设置为 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;
}

好的,我们已经获取了像素。它们已经被检查过,我们现在只能处理我们获得的数据。下一步是将这些数据转换为有用的东西。在我的例子中,我将这些数据转换为 System.Drawing.Bitmap,然后保存为 BitmapImage。这可用于在 WPF 的 Image 元素中显示数据。对于 Windows Forms,我认为您可以省略 BitmapImage 部分,只返回 Bitmap,将其强制转换为 Image,然后在 PictureBox 中显示,但我不用 WinForms,所以如果我说错了请原谅。

我的代码很简单,但速度很慢。使用“不安全”代码(即指针)会比这快得多,但我不是这方面的专家,而且我花了一些时间才弄清楚二进制(见下文)。基本上,我将上面的源字符传递给程序,然后程序将像素设置为白色(如果字符是 '1')和黑色(如果字符是 '0')(好吧,任何不是 '1' 的,应该是 '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;
    }
}

需要注意的是,有时在 GDI+ 中保存位图时会发生“通用错误”(我知道这很有帮助)。一个简单的解决方法是将其再次复制到一个新的位图中(Bitmap bitmap1 = new Bitmap(B);),然后保存 bitmap1 而不是原始位图。我不知道为什么会发生这种情况,MSDN 和 Stack Overflow 的人也不知道,但它就是会发生。

总之,ASCII PBM 就说到这里。二进制文件以类似的方式解析,但我会复制第二个代码示例,其中包含 switch 语句,然后将以下代码插入 else 条件句中,而不是用于解析 ASCII 文件的代码。在该函数中,一个名为 stringBytes 的字节数组被作为 out 参数给出,而不是示例 2 中的 char 数组。

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;

最后,我们可以将其转换为图像。在这里,我使用指针来加快速度。这有点复杂,但确实有效。

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;

}

好的,最后是实现。要使用此代码,我编写了一系列从类外部调用的语句。我的所有函数都是公共的,因此可以在程序的任何地方调用它们。您可能希望在类中添加一个“超级函数”来调用所有其他函数——不过我没有这样做。下面的示例显示了我如何调用我的代码,并从名为 openDialogOpenFileDialog 获取文件名。

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 

关注点

正如我在代码中所说,使用指针操作 Bitmap 比使用 SetPixel 更高效,也肯定更快,但我在此包含 SetPixel 方法是为了提供两种选择,并且因为我发现使用不安全代码来编写算法有时需要一些时间。

我提到了几个未解释的函数。一个是 ConvertStringArrayToString()。它基本上使用 StringBuilder 将字符串数组中的字符串连接起来,并在某个索引之后返回该连接的字符串。下一个是 ConvertBytes()。这会将一个字节转换为 8 个字节,这些字节是 0x00 或 0xFF,具体取决于给定字节的各个位。例如,如果字节 0xC0 作为参数中的字节数组的一部分传递,则该字节的结果将是一个字节 0xFF、0xFF、0x00、0x00、0x00、0x00、0x00、0x00。

历史

  • 首次上传:2011 年 12 月 17 日星期六(格林威治标准时间 17:40)。
© . All rights reserved.